From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- editor/libeditor/HTMLEditUtils.h | 2788 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 2788 insertions(+) create mode 100644 editor/libeditor/HTMLEditUtils.h (limited to 'editor/libeditor/HTMLEditUtils.h') diff --git a/editor/libeditor/HTMLEditUtils.h b/editor/libeditor/HTMLEditUtils.h new file mode 100644 index 0000000000..115a329247 --- /dev/null +++ b/editor/libeditor/HTMLEditUtils.h @@ -0,0 +1,2788 @@ +/* -*- 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., `