/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef HTMLEditUtils_h #define HTMLEditUtils_h /** * This header declares/defines static helper methods as members of * HTMLEditUtils. If you want to create or look for helper trivial classes for * HTMLEditor, see HTMLEditHelpers.h. */ #include "EditorBase.h" #include "EditorDOMPoint.h" #include "EditorForwards.h" #include "EditorUtils.h" #include "HTMLEditHelpers.h" #include "mozilla/Attributes.h" #include "mozilla/EnumSet.h" #include "mozilla/IntegerRange.h" #include "mozilla/Maybe.h" #include "mozilla/Result.h" #include "mozilla/dom/AbstractRange.h" #include "mozilla/dom/AncestorIterator.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/HTMLBRElement.h" #include "mozilla/dom/Selection.h" #include "mozilla/dom/Text.h" #include "nsContentUtils.h" #include "nsCRT.h" #include "nsGkAtoms.h" #include "nsHTMLTags.h" #include "nsTArray.h" class nsAtom; class nsPresContext; namespace mozilla { enum class CollectChildrenOption { // Ignore non-editable nodes IgnoreNonEditableChildren, // Ignore invisible text nodes IgnoreInvisibleTextNodes, // Collect list children too. CollectListChildren, // Collect table children too. CollectTableChildren, }; class HTMLEditUtils final { using AbstractRange = dom::AbstractRange; using Element = dom::Element; using Selection = dom::Selection; using Text = dom::Text; public: static constexpr char16_t kNewLine = '\n'; static constexpr char16_t kCarriageReturn = '\r'; static constexpr char16_t kTab = '\t'; static constexpr char16_t kSpace = ' '; static constexpr char16_t kNBSP = 0x00A0; static constexpr char16_t kGreaterThan = '>'; /** * IsSimplyEditableNode() returns true when aNode is simply editable. * This does NOT means that aNode can be removed from current parent nor * aNode's data is editable. */ static bool IsSimplyEditableNode(const nsINode& aNode) { return aNode.IsEditable(); } /** * Return true if inclusive flat tree ancestor has `inert` state. */ static bool ContentIsInert(const nsIContent& aContent); /** * IsNeverContentEditableElementByUser() returns true if the element's content * is never editable by user. E.g., the content is always replaced by * native anonymous node or something. */ static bool IsNeverElementContentsEditableByUser(const nsIContent& aContent) { return aContent.IsElement() && (!HTMLEditUtils::IsContainerNode(aContent) || aContent.IsAnyOfHTMLElements( nsGkAtoms::applet, nsGkAtoms::colgroup, nsGkAtoms::frameset, nsGkAtoms::head, nsGkAtoms::html, nsGkAtoms::iframe, nsGkAtoms::meter, nsGkAtoms::progress, nsGkAtoms::select, nsGkAtoms::textarea)); } /** * IsNonEditableReplacedContent() returns true when aContent is an inclusive * descendant of a replaced element whose content shouldn't be editable by * user's operation. */ static bool IsNonEditableReplacedContent(const nsIContent& aContent) { for (Element* element : aContent.InclusiveAncestorsOfType()) { if (element->IsAnyOfHTMLElements(nsGkAtoms::select, nsGkAtoms::option, nsGkAtoms::optgroup)) { return true; } } return false; } /* * IsRemovalNode() returns true when parent of aContent is editable even * if aContent isn't editable. * This is a valid method to check it if you find the content from point * of view of siblings or parents of aContent. * Note that padding `
` element for empty editor and manual native * anonymous content should be deletable even after `HTMLEditor` is destroyed * because they are owned/managed by `HTMLEditor`. */ static bool IsRemovableNode(const nsIContent& aContent) { return EditorUtils::IsPaddingBRElementForEmptyEditor(aContent) || aContent.IsRootOfNativeAnonymousSubtree() || (aContent.GetParentNode() && aContent.GetParentNode()->IsEditable() && &aContent != aContent.OwnerDoc()->GetBody() && &aContent != aContent.OwnerDoc()->GetDocumentElement()); } /** * IsRemovableFromParentNode() returns true when aContent is editable, has a * parent node and the parent node is also editable. * This is a valid method to check it if you find the content from point * of view of descendants of aContent. * Note that padding `
` element for empty editor and manual native * anonymous content should be deletable even after `HTMLEditor` is destroyed * because they are owned/managed by `HTMLEditor`. */ static bool IsRemovableFromParentNode(const nsIContent& aContent) { return EditorUtils::IsPaddingBRElementForEmptyEditor(aContent) || aContent.IsRootOfNativeAnonymousSubtree() || (aContent.IsEditable() && aContent.GetParentNode() && aContent.GetParentNode()->IsEditable() && &aContent != aContent.OwnerDoc()->GetBody() && &aContent != aContent.OwnerDoc()->GetDocumentElement()); } /** * CanContentsBeJoined() returns true if aLeftContent and aRightContent can be * joined. */ static bool CanContentsBeJoined(const nsIContent& aLeftContent, const nsIContent& aRightContent); /** * Returns true if aContent is an element and it should be treated as a block. * * @param aBlockInlineCheck * - If UseHTMLDefaultStyle or `editor.block_inline_check.use_computed_style` * pref is false, this returns true only for HTML elements which are defined * as a block by the default style. I.e., non-HTML elements are always * treated as inline. * - If UseComputedDisplayOutsideStyle, this returns true for element nodes * whose display-outside is not inline nor ruby. This is useful to get * inclusive ancestor block element. * - If UseComputedDisplayStyle, this returns true for element nodes whose * display-outside is not inline or whose display-inside is flow-root and they * do not appear as a form control. This is useful to check whether * collapsible white-spaces at the element edges are visible or invisible or * whether
element at end of the element is visible or invisible. */ [[nodiscard]] static bool IsBlockElement(const nsIContent& aContent, BlockInlineCheck aBlockInlineCheck); /** * This is designed to check elements or non-element nodes which are layed out * as inline. Therefore, inline-block etc and ruby are treated as inline. * Note that invisible non-element nodes like comment nodes are also treated * as inline. * * @param aBlockInlineCheck UseComputedDisplayOutsideStyle and * UseComputedDisplayStyle return same result for * any elements. */ [[nodiscard]] static bool IsInlineContent(const nsIContent& aContent, BlockInlineCheck aBlockInlineCheck); /** * IsVisibleElementEvenIfLeafNode() returns true if aContent is an empty block * element, a visible replaced element such as a form control. This does not * check the layout information. */ static bool IsVisibleElementEvenIfLeafNode(const nsIContent& aContent); static bool IsInlineStyle(nsINode* aNode); /** * IsDisplayOutsideInline() returns true if display-outside value is * "inside". This does NOT flush the layout. */ [[nodiscard]] static bool IsDisplayOutsideInline(const Element& aElement); /** * IsDisplayInsideFlowRoot() returns true if display-inline value of aElement * is "flow-root". This does NOT flush the layout. */ [[nodiscard]] static bool IsDisplayInsideFlowRoot(const Element& aElement); /** * Return true if aElement is a flex item or a grid item. This works only * when aElement has a primary frame. */ [[nodiscard]] static bool IsFlexOrGridItem(const Element& aElement); /** * IsRemovableInlineStyleElement() returns true if aElement is an inline * element and can be removed or split to in order to modifying inline * styles. */ static bool IsRemovableInlineStyleElement(Element& aElement); /** * Return true if aTagName is one of the format element name of * Document.execCommand("formatBlock"). */ [[nodiscard]] static bool IsFormatTagForFormatBlockCommand( const nsStaticAtom& aTagName) { return // clang-format off &aTagName == nsGkAtoms::address || &aTagName == nsGkAtoms::article || &aTagName == nsGkAtoms::aside || &aTagName == nsGkAtoms::blockquote || &aTagName == nsGkAtoms::dd || &aTagName == nsGkAtoms::div || &aTagName == nsGkAtoms::dl || &aTagName == nsGkAtoms::dt || &aTagName == nsGkAtoms::footer || &aTagName == nsGkAtoms::h1 || &aTagName == nsGkAtoms::h2 || &aTagName == nsGkAtoms::h3 || &aTagName == nsGkAtoms::h4 || &aTagName == nsGkAtoms::h5 || &aTagName == nsGkAtoms::h6 || &aTagName == nsGkAtoms::header || &aTagName == nsGkAtoms::hgroup || &aTagName == nsGkAtoms::main || &aTagName == nsGkAtoms::nav || &aTagName == nsGkAtoms::p || &aTagName == nsGkAtoms::pre || &aTagName == nsGkAtoms::section; // clang-format on } /** * Return true if aContent is a format element of * Document.execCommand("formatBlock"). */ [[nodiscard]] static bool IsFormatElementForFormatBlockCommand( const nsIContent& aContent) { if (!aContent.IsHTMLElement() || !aContent.NodeInfo()->NameAtom()->IsStatic()) { return false; } const nsStaticAtom* tagName = aContent.NodeInfo()->NameAtom()->AsStatic(); return IsFormatTagForFormatBlockCommand(*tagName); } /** * Return true if aTagName is one of the format element name of * cmd_paragraphState. */ [[nodiscard]] static bool IsFormatTagForParagraphStateCommand( const nsStaticAtom& aTagName) { return // clang-format off &aTagName == nsGkAtoms::address || &aTagName == nsGkAtoms::dd || &aTagName == nsGkAtoms::dl || &aTagName == nsGkAtoms::dt || &aTagName == nsGkAtoms::h1 || &aTagName == nsGkAtoms::h2 || &aTagName == nsGkAtoms::h3 || &aTagName == nsGkAtoms::h4 || &aTagName == nsGkAtoms::h5 || &aTagName == nsGkAtoms::h6 || &aTagName == nsGkAtoms::p || &aTagName == nsGkAtoms::pre; // clang-format on } /** * Return true if aContent is a format element of cmd_paragraphState. */ [[nodiscard]] static bool IsFormatElementForParagraphStateCommand( const nsIContent& aContent) { if (!aContent.IsHTMLElement() || !aContent.NodeInfo()->NameAtom()->IsStatic()) { return false; } const nsStaticAtom* tagName = aContent.NodeInfo()->NameAtom()->AsStatic(); return IsFormatTagForParagraphStateCommand(*tagName); } static bool IsNodeThatCanOutdent(nsINode* aNode); static bool IsHeader(nsINode& aNode); static bool IsListItem(const nsINode* aNode); static bool IsTable(nsINode* aNode); static bool IsTableRow(nsINode* aNode); static bool IsAnyTableElement(const nsINode* aNode); static bool IsAnyTableElementButNotTable(nsINode* aNode); static bool IsTableCell(const nsINode* aNode); static bool IsTableCellOrCaption(nsINode& aNode); static bool IsAnyListElement(const nsINode* aNode); static bool IsPre(const nsINode* aNode); static bool IsImage(nsINode* aNode); static bool IsLink(const nsINode* aNode); static bool IsNamedAnchor(const nsINode* aNode); static bool IsMozDiv(nsINode* aNode); static bool IsMailCite(const Element& aElement); static bool IsFormWidget(const nsINode* aNode); static bool SupportsAlignAttr(nsINode& aNode); static bool CanNodeContain(const nsINode& aParent, const nsIContent& aChild) { switch (aParent.NodeType()) { case nsINode::ELEMENT_NODE: case nsINode::DOCUMENT_FRAGMENT_NODE: return HTMLEditUtils::CanNodeContain(*aParent.NodeInfo()->NameAtom(), aChild); } return false; } static bool CanNodeContain(const nsINode& aParent, const nsAtom& aChildNodeName) { switch (aParent.NodeType()) { case nsINode::ELEMENT_NODE: case nsINode::DOCUMENT_FRAGMENT_NODE: return HTMLEditUtils::CanNodeContain(*aParent.NodeInfo()->NameAtom(), aChildNodeName); } return false; } static bool CanNodeContain(const nsAtom& aParentNodeName, const nsIContent& aChild) { switch (aChild.NodeType()) { case nsINode::TEXT_NODE: case nsINode::COMMENT_NODE: case nsINode::CDATA_SECTION_NODE: case nsINode::ELEMENT_NODE: case nsINode::DOCUMENT_FRAGMENT_NODE: return HTMLEditUtils::CanNodeContain(aParentNodeName, *aChild.NodeInfo()->NameAtom()); } return false; } // XXX Only this overload does not check the node type. Therefore, only this // handle Document and ProcessingInstructionTagName. static bool CanNodeContain(const nsAtom& aParentNodeName, const nsAtom& aChildNodeName) { nsHTMLTag childTagEnum; if (&aChildNodeName == nsGkAtoms::textTagName) { childTagEnum = eHTMLTag_text; } else if (&aChildNodeName == nsGkAtoms::commentTagName || &aChildNodeName == nsGkAtoms::cdataTagName) { childTagEnum = eHTMLTag_comment; } else { childTagEnum = nsHTMLTags::AtomTagToId(const_cast(&aChildNodeName)); } nsHTMLTag parentTagEnum = nsHTMLTags::AtomTagToId(const_cast(&aParentNodeName)); return HTMLEditUtils::CanNodeContain(parentTagEnum, childTagEnum); } /** * CanElementContainParagraph() returns true if aElement can have a

* element as its child or its descendant. */ static bool CanElementContainParagraph(const Element& aElement) { if (HTMLEditUtils::CanNodeContain(aElement, *nsGkAtoms::p)) { return true; } // Even if the element cannot have a

element as a child, it can contain //

element as a descendant if it's one of the following elements. if (aElement.IsAnyOfHTMLElements(nsGkAtoms::ol, nsGkAtoms::ul, nsGkAtoms::dl, nsGkAtoms::table, nsGkAtoms::thead, nsGkAtoms::tbody, nsGkAtoms::tfoot, nsGkAtoms::tr)) { return true; } // XXX Otherwise, Chromium checks the CSS box is a block, but we don't do it // for now. return false; } /** * Return a point which can insert a node whose name is aTagName scanning * from aPoint to its ancestor points. */ template static EditorDOMPoint GetInsertionPointInInclusiveAncestor( const nsAtom& aTagName, const EditorDOMPointType& aPoint, const Element* aAncestorLimit = nullptr) { if (MOZ_UNLIKELY(!aPoint.IsInContentNode())) { return EditorDOMPoint(); } Element* lastChild = nullptr; for (Element* containerElement : aPoint.template ContainerAs() ->template InclusiveAncestorsOfType()) { if (!HTMLEditUtils::IsSimplyEditableNode(*containerElement)) { return EditorDOMPoint(); } if (HTMLEditUtils::CanNodeContain(*containerElement, aTagName)) { return lastChild ? EditorDOMPoint(lastChild) : aPoint.template To(); } if (containerElement == aAncestorLimit) { return EditorDOMPoint(); } lastChild = containerElement; } return EditorDOMPoint(); } /** * IsContainerNode() returns true if aContent is a container node. */ static bool IsContainerNode(const nsIContent& aContent) { nsHTMLTag tagEnum; // XXX Should this handle #cdata-section too? if (aContent.IsText()) { tagEnum = eHTMLTag_text; } else { // XXX Why don't we use nsHTMLTags::AtomTagToId? Are there some // difference? tagEnum = nsHTMLTags::StringTagToId(aContent.NodeName()); } return HTMLEditUtils::IsContainerNode(tagEnum); } /** * IsSplittableNode() returns true if aContent can split. */ static bool IsSplittableNode(const nsIContent& aContent) { if (!EditorUtils::IsEditableContent(aContent, EditorUtils::EditorType::HTML) || !HTMLEditUtils::IsRemovableFromParentNode(aContent)) { return false; } if (aContent.IsElement()) { // XXX Perhaps, instead of using container, we should have "splittable" // information in the DB. E.g., `