summaryrefslogtreecommitdiffstats
path: root/editor/libeditor/HTMLEditUtils.h
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /editor/libeditor/HTMLEditUtils.h
parentInitial commit. (diff)
downloadfirefox-esr-upstream.tar.xz
firefox-esr-upstream.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--editor/libeditor/HTMLEditUtils.h2559
1 files changed, 2559 insertions, 0 deletions
diff --git a/editor/libeditor/HTMLEditUtils.h b/editor/libeditor/HTMLEditUtils.h
new file mode 100644
index 0000000000..e07fa79095
--- /dev/null
+++ b/editor/libeditor/HTMLEditUtils.h
@@ -0,0 +1,2559 @@
+/* -*- 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 "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::picture, 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<Element>()) {
+ 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 `<br>` 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 `<br>` 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);
+
+ /**
+ * IsBlockElement() returns true if aContent is an element and it should
+ * be treated as a block. (This does not refer style information.)
+ */
+ static bool IsBlockElement(const nsIContent& aContent);
+ /**
+ * IsInlineElement() returns true if aElement is an element node but
+ * shouldn't be treated as a block or aElement is not an element.
+ * XXX This name is wrong. Must be renamed to IsInlineContent() or something.
+ */
+ static bool IsInlineElement(const nsIContent& aContent) {
+ return !IsBlockElement(aContent);
+ }
+
+ /**
+ * 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);
+
+ /**
+ * 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);
+ static bool IsFormatNode(const nsINode* aNode);
+ 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, 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(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(nsAtom& aParentNodeName, 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(&aChildNodeName);
+ }
+
+ nsHTMLTag parentTagEnum = nsHTMLTags::AtomTagToId(&aParentNodeName);
+ return HTMLEditUtils::CanNodeContain(parentTagEnum, childTagEnum);
+ }
+
+ /**
+ * CanElementContainParagraph() returns true if aElement can have a <p>
+ * 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 <p> element as a child, it can contain
+ // <p> 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;
+ }
+
+ /**
+ * 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., `<template>`, `<script>` elements
+ // can have children, but shouldn't be split.
+ return HTMLEditUtils::IsContainerNode(aContent) &&
+ !aContent.IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::button,
+ nsGkAtoms::caption, nsGkAtoms::table,
+ nsGkAtoms::tbody, nsGkAtoms::tfoot,
+ nsGkAtoms::thead, nsGkAtoms::tr) &&
+ !HTMLEditUtils::IsNeverElementContentsEditableByUser(aContent) &&
+ !HTMLEditUtils::IsNonEditableReplacedContent(aContent);
+ }
+ return aContent.IsText() && aContent.Length() > 0;
+ }
+
+ /**
+ * See execCommand spec:
+ * https://w3c.github.io/editing/execCommand.html#non-list-single-line-container
+ * https://w3c.github.io/editing/execCommand.html#single-line-container
+ */
+ static bool IsNonListSingleLineContainer(const nsINode& aNode);
+ static bool IsSingleLineContainer(const nsINode& aNode);
+
+ /**
+ * IsVisibleTextNode() returns true if aText has visible text. If it has
+ * only white-spaces and they are collapsed, returns false.
+ */
+ [[nodiscard]] static bool IsVisibleTextNode(const Text& aText);
+
+ /**
+ * IsInVisibleTextFrames() returns true if any text in aText is in visible
+ * text frames. Callers have to guarantee that there is no pending reflow.
+ */
+ static bool IsInVisibleTextFrames(nsPresContext* aPresContext,
+ const Text& aText);
+
+ /**
+ * IsVisibleBRElement() and IsInvisibleBRElement() return true if aContent is
+ * a visible HTML <br> element, i.e., not a padding <br> element for making
+ * last line in a block element visible, or an invisible <br> element.
+ */
+ static bool IsVisibleBRElement(const nsIContent& aContent) {
+ if (const dom::HTMLBRElement* brElement =
+ dom::HTMLBRElement::FromNode(&aContent)) {
+ return IsVisibleBRElement(*brElement);
+ }
+ return false;
+ }
+ static bool IsVisibleBRElement(const dom::HTMLBRElement& aBRElement) {
+ // If followed by a block boundary without visible content, it's invisible
+ // <br> element.
+ return !HTMLEditUtils::GetElementOfImmediateBlockBoundary(
+ aBRElement, WalkTreeDirection::Forward);
+ }
+ static bool IsInvisibleBRElement(const nsIContent& aContent) {
+ if (const dom::HTMLBRElement* brElement =
+ dom::HTMLBRElement::FromNode(&aContent)) {
+ return IsInvisibleBRElement(*brElement);
+ }
+ return false;
+ }
+ static bool IsInvisibleBRElement(const dom::HTMLBRElement& aBRElement) {
+ return !HTMLEditUtils::IsVisibleBRElement(aBRElement);
+ }
+
+ /**
+ * IsVisiblePreformattedNewLine() and IsInvisiblePreformattedNewLine() return
+ * true if the point is preformatted linefeed and it's visible or invisible.
+ * If linefeed is immediately before a block boundary, it's invisible.
+ *
+ * @param aFollowingBlockElement [out] If the node is followed by a block
+ * boundary, this is set to the element
+ * creating the block boundary.
+ */
+ template <typename EditorDOMPointType>
+ static bool IsVisiblePreformattedNewLine(
+ const EditorDOMPointType& aPoint,
+ Element** aFollowingBlockElement = nullptr) {
+ if (aFollowingBlockElement) {
+ *aFollowingBlockElement = nullptr;
+ }
+ if (!aPoint.IsInTextNode() || aPoint.IsEndOfContainer() ||
+ !aPoint.IsCharPreformattedNewLine()) {
+ return false;
+ }
+ // If there are some other characters in the text node, it's a visible
+ // linefeed.
+ if (!aPoint.IsAtLastContent()) {
+ if (EditorUtils::IsWhiteSpacePreformatted(
+ *aPoint.template ContainerAs<Text>())) {
+ return true;
+ }
+ const nsTextFragment& textFragment =
+ aPoint.template ContainerAs<Text>()->TextFragment();
+ for (uint32_t offset = aPoint.Offset() + 1;
+ offset < textFragment.GetLength(); ++offset) {
+ char16_t ch = textFragment.CharAt(AssertedCast<int32_t>(offset));
+ if (nsCRT::IsAsciiSpace(ch) && ch != HTMLEditUtils::kNewLine) {
+ continue; // ASCII white-space which is collapsed into the linefeed.
+ }
+ return true; // There is a visible character after it.
+ }
+ }
+ // If followed by a block boundary without visible content, it's invisible
+ // linefeed.
+ Element* followingBlockElement =
+ HTMLEditUtils::GetElementOfImmediateBlockBoundary(
+ *aPoint.template ContainerAs<Text>(), WalkTreeDirection::Forward);
+ if (aFollowingBlockElement) {
+ *aFollowingBlockElement = followingBlockElement;
+ }
+ return !followingBlockElement;
+ }
+ template <typename EditorDOMPointType>
+ static bool IsInvisiblePreformattedNewLine(
+ const EditorDOMPointType& aPoint,
+ Element** aFollowingBlockElement = nullptr) {
+ if (!aPoint.IsInTextNode() || aPoint.IsEndOfContainer() ||
+ !aPoint.IsCharPreformattedNewLine()) {
+ if (aFollowingBlockElement) {
+ *aFollowingBlockElement = nullptr;
+ }
+ return false;
+ }
+ return !IsVisiblePreformattedNewLine(aPoint, aFollowingBlockElement);
+ }
+
+ /**
+ * ShouldInsertLinefeedCharacter() returns true if the caller should insert
+ * a linefeed character instead of <br> element.
+ */
+ static bool ShouldInsertLinefeedCharacter(
+ const EditorDOMPoint& aPointToInsert, const Element& aEditingHost);
+
+ /**
+ * IsEmptyNode() returns false if aNode has some visible content nodes,
+ * list elements or table elements.
+ *
+ * @param aPresContext Must not be nullptr if
+ * EmptyCheckOption::SafeToAskLayout is set.
+ * @param aNode The node to check whether it's empty.
+ * @param aOptions You can specify which type of elements are visible
+ * and/or whether this can access layout information.
+ * @param aSeenBR [Out] Set to true if this meets an <br> element
+ * before meething visible things.
+ */
+ enum class EmptyCheckOption {
+ TreatSingleBRElementAsVisible,
+ TreatListItemAsVisible,
+ TreatTableCellAsVisible,
+ IgnoreEditableState, // TODO: Change to "TreatNonEditableContentAsVisible"
+ SafeToAskLayout,
+ };
+ using EmptyCheckOptions = EnumSet<EmptyCheckOption, uint32_t>;
+ static bool IsEmptyNode(nsPresContext* aPresContext, const nsINode& aNode,
+ const EmptyCheckOptions& aOptions = {},
+ bool* aSeenBR = nullptr);
+ static bool IsEmptyNode(const nsINode& aNode,
+ const EmptyCheckOptions& aOptions = {},
+ bool* aSeenBR = nullptr) {
+ MOZ_ASSERT(!aOptions.contains(EmptyCheckOption::SafeToAskLayout));
+ return IsEmptyNode(nullptr, aNode, aOptions, aSeenBR);
+ }
+
+ /**
+ * IsEmptyInlineContainer() returns true if aContent is an inline element
+ * which can have children and does not have meaningful content.
+ */
+ static bool IsEmptyInlineContainer(const nsIContent& aContent,
+ const EmptyCheckOptions& aOptions) {
+ return HTMLEditUtils::IsInlineElement(aContent) &&
+ HTMLEditUtils::IsContainerNode(aContent) &&
+ HTMLEditUtils::IsEmptyNode(aContent, aOptions);
+ }
+
+ /**
+ * IsEmptyBlockElement() returns true if aElement is a block level element
+ * and it doesn't have any visible content.
+ */
+ static bool IsEmptyBlockElement(const Element& aElement,
+ const EmptyCheckOptions& aOptions) {
+ return HTMLEditUtils::IsBlockElement(aElement) &&
+ HTMLEditUtils::IsEmptyNode(aElement, aOptions);
+ }
+
+ /**
+ * Return true if aListElement is completely empty or it has only one list
+ * item element which is empty.
+ */
+ [[nodiscard]] static bool IsEmptyAnyListElement(const Element& aListElement) {
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement));
+ bool foundListItem = false;
+ for (nsIContent* child = aListElement.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (HTMLEditUtils::IsListItem(child)) {
+ if (foundListItem) {
+ return false; // 2 list items found.
+ }
+ if (!IsEmptyNode(*child, {EmptyCheckOption::IgnoreEditableState})) {
+ return false; // found non-empty list item.
+ }
+ foundListItem = true;
+ continue;
+ }
+ if (child->IsElement()) {
+ return false; // found sublist or illegal child.
+ }
+ if (child->IsText() &&
+ HTMLEditUtils::IsVisibleTextNode(*child->AsText())) {
+ return false; // found illegal visible text node.
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Return true if aListElement does not have invalid child.
+ */
+ enum class TreatSubListElementAs { Invalid, Valid };
+ [[nodiscard]] static bool IsValidListElement(
+ const Element& aListElement,
+ TreatSubListElementAs aTreatSubListElementAs) {
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement));
+ for (nsIContent* child = aListElement.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (HTMLEditUtils::IsAnyListElement(child)) {
+ if (aTreatSubListElementAs == TreatSubListElementAs::Invalid) {
+ return false;
+ }
+ continue;
+ }
+ if (child->IsHTMLElement(nsGkAtoms::li)) {
+ if (MOZ_UNLIKELY(!aListElement.IsAnyOfHTMLElements(nsGkAtoms::ol,
+ nsGkAtoms::ul))) {
+ return false;
+ }
+ continue;
+ }
+ if (child->IsAnyOfHTMLElements(nsGkAtoms::dt, nsGkAtoms::dd)) {
+ if (MOZ_UNLIKELY(!aListElement.IsAnyOfHTMLElements(nsGkAtoms::dl))) {
+ return false;
+ }
+ continue;
+ }
+ if (MOZ_UNLIKELY(child->IsElement())) {
+ return false;
+ }
+ if (MOZ_LIKELY(child->IsText())) {
+ if (MOZ_UNLIKELY(HTMLEditUtils::IsVisibleTextNode(*child->AsText()))) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * IsEmptyOneHardLine() returns true if aArrayOfContents does not represent
+ * 2 or more lines and have meaningful content.
+ */
+ static bool IsEmptyOneHardLine(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents) {
+ if (NS_WARN_IF(aArrayOfContents.IsEmpty())) {
+ return true;
+ }
+
+ bool brElementHasFound = false;
+ for (OwningNonNull<nsIContent>& content : aArrayOfContents) {
+ if (!EditorUtils::IsEditableContent(content,
+ EditorUtils::EditorType::HTML)) {
+ continue;
+ }
+ if (content->IsHTMLElement(nsGkAtoms::br)) {
+ // If there are 2 or more `<br>` elements, it's not empty line since
+ // there may be only one `<br>` element in a hard line.
+ if (brElementHasFound) {
+ return false;
+ }
+ brElementHasFound = true;
+ continue;
+ }
+ if (!HTMLEditUtils::IsEmptyInlineContainer(
+ content, {EmptyCheckOption::TreatSingleBRElementAsVisible})) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * IsPointAtEdgeOfLink() returns true if aPoint is at start or end of a
+ * link.
+ */
+ template <typename PT, typename CT>
+ static bool IsPointAtEdgeOfLink(const EditorDOMPointBase<PT, CT>& aPoint,
+ Element** aFoundLinkElement = nullptr) {
+ if (aFoundLinkElement) {
+ *aFoundLinkElement = nullptr;
+ }
+ if (!aPoint.IsInContentNode()) {
+ return false;
+ }
+ if (!aPoint.IsStartOfContainer() && !aPoint.IsEndOfContainer()) {
+ return false;
+ }
+ // XXX Assuming it's not in an empty text node because it's unrealistic edge
+ // case.
+ bool maybeStartOfAnchor = aPoint.IsStartOfContainer();
+ for (EditorRawDOMPoint point(aPoint.template ContainerAs<nsIContent>());
+ point.IsSet() && (maybeStartOfAnchor ? point.IsStartOfContainer()
+ : point.IsAtLastContent());
+ point = point.ParentPoint()) {
+ if (HTMLEditUtils::IsLink(point.GetContainer())) {
+ // Now, we're at start or end of <a href>.
+ if (aFoundLinkElement) {
+ *aFoundLinkElement =
+ do_AddRef(point.template ContainerAs<Element>()).take();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * IsContentInclusiveDescendantOfLink() returns true if aContent is a
+ * descendant of a link element.
+ * Note that this returns true even if editing host of aContent is in a link
+ * element.
+ */
+ static bool IsContentInclusiveDescendantOfLink(
+ nsIContent& aContent, Element** aFoundLinkElement = nullptr) {
+ if (aFoundLinkElement) {
+ *aFoundLinkElement = nullptr;
+ }
+ for (Element* element : aContent.InclusiveAncestorsOfType<Element>()) {
+ if (HTMLEditUtils::IsLink(element)) {
+ if (aFoundLinkElement) {
+ *aFoundLinkElement = do_AddRef(element).take();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * IsRangeEntirelyInLink() returns true if aRange is entirely in a link
+ * element.
+ * Note that this returns true even if editing host of the range is in a link
+ * element.
+ */
+ template <typename EditorDOMRangeType>
+ static bool IsRangeEntirelyInLink(const EditorDOMRangeType& aRange,
+ Element** aFoundLinkElement = nullptr) {
+ MOZ_ASSERT(aRange.IsPositionedAndValid());
+ if (aFoundLinkElement) {
+ *aFoundLinkElement = nullptr;
+ }
+ nsINode* commonAncestorNode =
+ nsContentUtils::GetClosestCommonInclusiveAncestor(
+ aRange.StartRef().GetContainer(), aRange.EndRef().GetContainer());
+ if (NS_WARN_IF(!commonAncestorNode) || !commonAncestorNode->IsContent()) {
+ return false;
+ }
+ return IsContentInclusiveDescendantOfLink(*commonAncestorNode->AsContent(),
+ aFoundLinkElement);
+ }
+
+ /**
+ * Get adjacent content node of aNode if there is (even if one is in different
+ * parent element).
+ *
+ * @param aNode The node from which we start to walk the DOM
+ * tree.
+ * @param aOptions See WalkTreeOption for the detail.
+ * @param aAncestorLimiter Ancestor limiter element which these methods
+ * never cross its boundary. This is typically
+ * the editing host.
+ */
+ enum class WalkTreeOption {
+ IgnoreNonEditableNode, // Ignore non-editable nodes and their children.
+ IgnoreDataNodeExceptText, // Ignore data nodes which are not text node.
+ IgnoreWhiteSpaceOnlyText, // Ignore text nodes having only white-spaces.
+ StopAtBlockBoundary, // Stop waking the tree at a block boundary.
+ };
+ using WalkTreeOptions = EnumSet<WalkTreeOption>;
+ static nsIContent* GetPreviousContent(
+ const nsINode& aNode, const WalkTreeOptions& aOptions,
+ const Element* aAncestorLimiter = nullptr) {
+ if (&aNode == aAncestorLimiter ||
+ (aAncestorLimiter &&
+ !aNode.IsInclusiveDescendantOf(aAncestorLimiter))) {
+ return nullptr;
+ }
+ return HTMLEditUtils::GetAdjacentContent(aNode, WalkTreeDirection::Backward,
+ aOptions, aAncestorLimiter);
+ }
+ static nsIContent* GetNextContent(const nsINode& aNode,
+ const WalkTreeOptions& aOptions,
+ const Element* aAncestorLimiter = nullptr) {
+ if (&aNode == aAncestorLimiter ||
+ (aAncestorLimiter &&
+ !aNode.IsInclusiveDescendantOf(aAncestorLimiter))) {
+ return nullptr;
+ }
+ return HTMLEditUtils::GetAdjacentContent(aNode, WalkTreeDirection::Forward,
+ aOptions, aAncestorLimiter);
+ }
+
+ /**
+ * And another version that takes a point in DOM tree rather than a node.
+ */
+ template <typename PT, typename CT>
+ static nsIContent* GetPreviousContent(
+ const EditorDOMPointBase<PT, CT>& aPoint, const WalkTreeOptions& aOptions,
+ const Element* aAncestorLimiter = nullptr);
+
+ /**
+ * And another version that takes a point in DOM tree rather than a node.
+ *
+ * Note that this may return the child at the offset. E.g., following code
+ * causes infinite loop.
+ *
+ * EditorRawDOMPoint point(aEditableNode);
+ * while (nsIContent* content =
+ * GetNextContent(point, {WalkTreeOption::IgnoreNonEditableNode})) {
+ * // Do something...
+ * point.Set(content);
+ * }
+ *
+ * Following code must be you expected:
+ *
+ * while (nsIContent* content =
+ * GetNextContent(point, {WalkTreeOption::IgnoreNonEditableNode}) {
+ * // Do something...
+ * DebugOnly<bool> advanced = point.Advanced();
+ * MOZ_ASSERT(advanced);
+ * point.Set(point.GetChild());
+ * }
+ */
+ template <typename PT, typename CT>
+ static nsIContent* GetNextContent(const EditorDOMPointBase<PT, CT>& aPoint,
+ const WalkTreeOptions& aOptions,
+ const Element* aAncestorLimiter = nullptr);
+
+ /**
+ * GetPreviousSibling() and GetNextSibling() return the nearest sibling of
+ * aContent which does not match with aOption.
+ */
+ static nsIContent* GetPreviousSibling(const nsIContent& aContent,
+ const WalkTreeOptions& aOptions) {
+ for (nsIContent* sibling = aContent.GetPreviousSibling(); sibling;
+ sibling = sibling->GetPreviousSibling()) {
+ if (HTMLEditUtils::IsContentIgnored(*sibling, aOptions)) {
+ continue;
+ }
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*sibling)) {
+ return nullptr;
+ }
+ return sibling;
+ }
+ return nullptr;
+ }
+
+ static nsIContent* GetNextSibling(const nsIContent& aContent,
+ const WalkTreeOptions& aOptions) {
+ for (nsIContent* sibling = aContent.GetNextSibling(); sibling;
+ sibling = sibling->GetNextSibling()) {
+ if (HTMLEditUtils::IsContentIgnored(*sibling, aOptions)) {
+ continue;
+ }
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*sibling)) {
+ return nullptr;
+ }
+ return sibling;
+ }
+ return nullptr;
+ }
+
+ /**
+ * GetLastChild() and GetFirstChild() return the first or last child of aNode
+ * which does not match with aOption.
+ */
+ static nsIContent* GetLastChild(const nsINode& aNode,
+ const WalkTreeOptions& aOptions) {
+ for (nsIContent* child = aNode.GetLastChild(); child;
+ child = child->GetPreviousSibling()) {
+ if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) {
+ continue;
+ }
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*child)) {
+ return nullptr;
+ }
+ return child;
+ }
+ return nullptr;
+ }
+
+ static nsIContent* GetFirstChild(const nsINode& aNode,
+ const WalkTreeOptions& aOptions) {
+ for (nsIContent* child = aNode.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) {
+ continue;
+ }
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*child)) {
+ return nullptr;
+ }
+ return child;
+ }
+ return nullptr;
+ }
+
+ static bool IsLastChild(const nsIContent& aContent,
+ const WalkTreeOptions& aOptions) {
+ nsINode* parentNode = aContent.GetParentNode();
+ if (!parentNode) {
+ return false;
+ }
+ return HTMLEditUtils::GetLastChild(*parentNode, aOptions) == &aContent;
+ }
+
+ static bool IsFirstChild(const nsIContent& aContent,
+ const WalkTreeOptions& aOptions) {
+ nsINode* parentNode = aContent.GetParentNode();
+ if (!parentNode) {
+ return false;
+ }
+ return HTMLEditUtils::GetFirstChild(*parentNode, aOptions) == &aContent;
+ }
+
+ /**
+ * GetAdjacentContentToPutCaret() walks the DOM tree to find an editable node
+ * near aPoint where may be a good point to put caret and keep typing or
+ * deleting.
+ *
+ * @param aPoint The DOM point where to start to search from.
+ * @return If found, returns non-nullptr. Otherwise, nullptr.
+ * Note that if found node is in different table structure
+ * element, this returns nullptr.
+ */
+ enum class WalkTreeDirection { Forward, Backward };
+ template <typename PT, typename CT>
+ static nsIContent* GetAdjacentContentToPutCaret(
+ const EditorDOMPointBase<PT, CT>& aPoint,
+ WalkTreeDirection aWalkTreeDirection, const Element& aEditingHost) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ nsIContent* editableContent = nullptr;
+ if (aWalkTreeDirection == WalkTreeDirection::Backward) {
+ editableContent = HTMLEditUtils::GetPreviousContent(
+ aPoint, {WalkTreeOption::IgnoreNonEditableNode}, &aEditingHost);
+ if (!editableContent) {
+ return nullptr; // Not illegal.
+ }
+ } else {
+ editableContent = HTMLEditUtils::GetNextContent(
+ aPoint, {WalkTreeOption::IgnoreNonEditableNode}, &aEditingHost);
+ if (NS_WARN_IF(!editableContent)) {
+ // Perhaps, illegal because the node pointed by aPoint isn't editable
+ // and nobody of previous nodes is editable.
+ return nullptr;
+ }
+ }
+
+ // scan in the right direction until we find an eligible text node,
+ // but don't cross any breaks, images, or table elements.
+ // XXX This comment sounds odd. editableContent may have already crossed
+ // breaks and/or images if they are non-editable.
+ while (editableContent && !editableContent->IsText() &&
+ !editableContent->IsHTMLElement(nsGkAtoms::br) &&
+ !HTMLEditUtils::IsImage(editableContent)) {
+ if (aWalkTreeDirection == WalkTreeDirection::Backward) {
+ editableContent = HTMLEditUtils::GetPreviousContent(
+ *editableContent, {WalkTreeOption::IgnoreNonEditableNode},
+ &aEditingHost);
+ if (NS_WARN_IF(!editableContent)) {
+ return nullptr;
+ }
+ } else {
+ editableContent = HTMLEditUtils::GetNextContent(
+ *editableContent, {WalkTreeOption::IgnoreNonEditableNode},
+ &aEditingHost);
+ if (NS_WARN_IF(!editableContent)) {
+ return nullptr;
+ }
+ }
+ }
+
+ // don't cross any table elements
+ if ((!aPoint.IsInContentNode() &&
+ !!HTMLEditUtils::GetInclusiveAncestorAnyTableElement(
+ *editableContent)) ||
+ (HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*editableContent) !=
+ HTMLEditUtils::GetInclusiveAncestorAnyTableElement(
+ *aPoint.template ContainerAs<nsIContent>()))) {
+ return nullptr;
+ }
+
+ // otherwise, ok, we have found a good spot to put the selection
+ return editableContent;
+ }
+
+ /**
+ * GetLastLeafContent() returns rightmost leaf content in aNode. It depends
+ * on aLeafNodeTypes whether this which types of nodes are treated as leaf
+ * nodes.
+ */
+ enum class LeafNodeType {
+ // Even if there is a child block, keep scanning a leaf content in it.
+ OnlyLeafNode,
+ // If there is a child block, return it too. Note that this does not
+ // mean that block siblings are not treated as leaf nodes.
+ LeafNodeOrChildBlock,
+ // If there is a non-editable element if and only if scanning from editable
+ // node, return it too.
+ LeafNodeOrNonEditableNode,
+ // Ignore non-editable content at walking the tree.
+ OnlyEditableLeafNode,
+ };
+ using LeafNodeTypes = EnumSet<LeafNodeType>;
+ static nsIContent* GetLastLeafContent(
+ const nsINode& aNode, const LeafNodeTypes& aLeafNodeTypes,
+ const Element* aAncestorLimiter = nullptr) {
+ MOZ_ASSERT_IF(
+ aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
+ // editor shouldn't touch child nodes which are replaced with native
+ // anonymous nodes.
+ if (aNode.IsElement() &&
+ HTMLEditUtils::IsNeverElementContentsEditableByUser(
+ *aNode.AsElement())) {
+ return nullptr;
+ }
+ for (nsIContent* content = aNode.GetLastChild(); content;) {
+ if (aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode) &&
+ !EditorUtils::IsEditableContent(*content,
+ EditorUtils::EditorType::HTML)) {
+ content = HTMLEditUtils::GetPreviousContent(
+ *content, {WalkTreeOption::IgnoreNonEditableNode},
+ aAncestorLimiter);
+ continue;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) &&
+ HTMLEditUtils::IsBlockElement(*content)) {
+ return content;
+ }
+ if (!content->HasChildren() ||
+ HTMLEditUtils::IsNeverElementContentsEditableByUser(*content)) {
+ return content;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ aNode.IsEditable() && !content->IsEditable()) {
+ return content;
+ }
+ content = content->GetLastChild();
+ }
+ return nullptr;
+ }
+
+ /**
+ * GetFirstLeafContent() returns leftmost leaf content in aNode. It depends
+ * on aLeafNodeTypes whether this scans into a block child or treat block as a
+ * leaf.
+ */
+ static nsIContent* GetFirstLeafContent(
+ const nsINode& aNode, const LeafNodeTypes& aLeafNodeTypes,
+ const Element* aAncestorLimiter = nullptr) {
+ MOZ_ASSERT_IF(
+ aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
+ // editor shouldn't touch child nodes which are replaced with native
+ // anonymous nodes.
+ if (aNode.IsElement() &&
+ HTMLEditUtils::IsNeverElementContentsEditableByUser(
+ *aNode.AsElement())) {
+ return nullptr;
+ }
+ for (nsIContent* content = aNode.GetFirstChild(); content;) {
+ if (aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode) &&
+ !EditorUtils::IsEditableContent(*content,
+ EditorUtils::EditorType::HTML)) {
+ content = HTMLEditUtils::GetNextContent(
+ *content, {WalkTreeOption::IgnoreNonEditableNode},
+ aAncestorLimiter);
+ continue;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) &&
+ HTMLEditUtils::IsBlockElement(*content)) {
+ return content;
+ }
+ if (!content->HasChildren() ||
+ HTMLEditUtils::IsNeverElementContentsEditableByUser(*content)) {
+ return content;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ aNode.IsEditable() && !content->IsEditable()) {
+ return content;
+ }
+ content = content->GetFirstChild();
+ }
+ return nullptr;
+ }
+
+ /**
+ * GetNextLeafContentOrNextBlockElement() returns next leaf content or
+ * next block element of aStartContent inside aAncestorLimiter.
+ * Note that the result may be a contet outside aCurrentBlock if
+ * aStartContent equals aCurrentBlock.
+ *
+ * @param aStartContent The start content to scan next content.
+ * @param aCurrentBlock Must be ancestor of aStartContent. Dispite
+ * the name, inline content is allowed if
+ * aStartContent is in an inline editing host.
+ * @param aLeafNodeTypes See LeafNodeType.
+ * @param aAncestorLimiter Optional, setting this guarantees the
+ * result is in aAncestorLimiter unless
+ * aStartContent is not a descendant of this.
+ */
+ static nsIContent* GetNextLeafContentOrNextBlockElement(
+ const nsIContent& aStartContent, const nsIContent& aCurrentBlock,
+ const LeafNodeTypes& aLeafNodeTypes,
+ const Element* aAncestorLimiter = nullptr) {
+ MOZ_ASSERT_IF(
+ aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
+
+ if (&aStartContent == aAncestorLimiter) {
+ return nullptr;
+ }
+
+ nsIContent* nextContent = aStartContent.GetNextSibling();
+ if (!nextContent) {
+ if (!aStartContent.GetParentElement()) {
+ NS_WARNING("Reached orphan node while climbing up the DOM tree");
+ return nullptr;
+ }
+ for (Element* parentElement : aStartContent.AncestorsOfType<Element>()) {
+ if (parentElement == &aCurrentBlock) {
+ return nullptr;
+ }
+ if (parentElement == aAncestorLimiter) {
+ NS_WARNING("Reached editing host while climbing up the DOM tree");
+ return nullptr;
+ }
+ nextContent = parentElement->GetNextSibling();
+ if (nextContent) {
+ break;
+ }
+ if (!parentElement->GetParentElement()) {
+ NS_WARNING("Reached orphan node while climbing up the DOM tree");
+ return nullptr;
+ }
+ }
+ MOZ_ASSERT(nextContent);
+ }
+
+ // We have a next content. If it's a block, return it.
+ if (HTMLEditUtils::IsBlockElement(*nextContent)) {
+ return nextContent;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ aStartContent.IsEditable() && !nextContent->IsEditable()) {
+ return nextContent;
+ }
+ if (HTMLEditUtils::IsContainerNode(*nextContent)) {
+ // Else if it's a container, get deep leftmost child
+ if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent(
+ *nextContent, aLeafNodeTypes)) {
+ return child;
+ }
+ }
+ // Else return the next content itself.
+ return nextContent;
+ }
+
+ /**
+ * Similar to the above method, but take a DOM point to specify scan start
+ * point.
+ */
+ template <typename PT, typename CT>
+ static nsIContent* GetNextLeafContentOrNextBlockElement(
+ const EditorDOMPointBase<PT, CT>& aStartPoint,
+ const nsIContent& aCurrentBlock, const LeafNodeTypes& aLeafNodeTypes,
+ const Element* aAncestorLimiter = nullptr) {
+ MOZ_ASSERT(aStartPoint.IsSet());
+ MOZ_ASSERT_IF(
+ aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
+ NS_ASSERTION(!aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ "Not implemented yet");
+
+ if (!aStartPoint.IsInContentNode()) {
+ return nullptr;
+ }
+ if (aStartPoint.IsInTextNode()) {
+ return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *aStartPoint.template ContainerAs<Text>(), aCurrentBlock,
+ aLeafNodeTypes, aAncestorLimiter);
+ }
+ if (!HTMLEditUtils::IsContainerNode(
+ *aStartPoint.template ContainerAs<nsIContent>())) {
+ return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *aStartPoint.template ContainerAs<nsIContent>(), aCurrentBlock,
+ aLeafNodeTypes, aAncestorLimiter);
+ }
+
+ nsCOMPtr<nsIContent> nextContent = aStartPoint.GetChild();
+ if (!nextContent) {
+ if (aStartPoint.GetContainer() == &aCurrentBlock) {
+ // We are at end of the block.
+ return nullptr;
+ }
+
+ // We are at end of non-block container
+ return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *aStartPoint.template ContainerAs<nsIContent>(), aCurrentBlock,
+ aLeafNodeTypes, aAncestorLimiter);
+ }
+
+ // We have a next node. If it's a block, return it.
+ if (HTMLEditUtils::IsBlockElement(*nextContent)) {
+ return nextContent;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ aStartPoint.GetContainer()->IsEditable() &&
+ !nextContent->IsEditable()) {
+ return nextContent;
+ }
+ if (HTMLEditUtils::IsContainerNode(*nextContent)) {
+ // else if it's a container, get deep leftmost child
+ if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent(
+ *nextContent, aLeafNodeTypes)) {
+ return child;
+ }
+ }
+ // Else return the node itself
+ return nextContent;
+ }
+
+ /**
+ * GetPreviousLeafContentOrPreviousBlockElement() returns previous leaf
+ * content or previous block element of aStartContent inside
+ * aAncestorLimiter.
+ * Note that the result may be a contet outside aCurrentBlock if
+ * aStartContent equals aCurrentBlock.
+ *
+ * @param aStartContent The start content to scan previous content.
+ * @param aCurrentBlock Must be ancestor of aStartContent. Dispite
+ * the name, inline content is allowed if
+ * aStartContent is in an inline editing host.
+ * @param aLeafNodeTypes See LeafNodeType.
+ * @param aAncestorLimiter Optional, setting this guarantees the
+ * result is in aAncestorLimiter unless
+ * aStartContent is not a descendant of this.
+ */
+ static nsIContent* GetPreviousLeafContentOrPreviousBlockElement(
+ const nsIContent& aStartContent, const nsIContent& aCurrentBlock,
+ const LeafNodeTypes& aLeafNodeTypes,
+ const Element* aAncestorLimiter = nullptr) {
+ MOZ_ASSERT_IF(
+ aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
+ NS_ASSERTION(!aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ "Not implemented yet");
+
+ if (&aStartContent == aAncestorLimiter) {
+ return nullptr;
+ }
+
+ nsIContent* previousContent = aStartContent.GetPreviousSibling();
+ if (!previousContent) {
+ if (!aStartContent.GetParentElement()) {
+ NS_WARNING("Reached orphan node while climbing up the DOM tree");
+ return nullptr;
+ }
+ for (Element* parentElement : aStartContent.AncestorsOfType<Element>()) {
+ if (parentElement == &aCurrentBlock) {
+ return nullptr;
+ }
+ if (parentElement == aAncestorLimiter) {
+ NS_WARNING("Reached editing host while climbing up the DOM tree");
+ return nullptr;
+ }
+ previousContent = parentElement->GetPreviousSibling();
+ if (previousContent) {
+ break;
+ }
+ if (!parentElement->GetParentElement()) {
+ NS_WARNING("Reached orphan node while climbing up the DOM tree");
+ return nullptr;
+ }
+ }
+ MOZ_ASSERT(previousContent);
+ }
+
+ // We have a next content. If it's a block, return it.
+ if (HTMLEditUtils::IsBlockElement(*previousContent)) {
+ return previousContent;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ aStartContent.IsEditable() && !previousContent->IsEditable()) {
+ return previousContent;
+ }
+ if (HTMLEditUtils::IsContainerNode(*previousContent)) {
+ // Else if it's a container, get deep rightmost child
+ if (nsIContent* child = HTMLEditUtils::GetLastLeafContent(
+ *previousContent, aLeafNodeTypes)) {
+ return child;
+ }
+ }
+ // Else return the next content itself.
+ return previousContent;
+ }
+
+ /**
+ * Similar to the above method, but take a DOM point to specify scan start
+ * point.
+ */
+ template <typename PT, typename CT>
+ static nsIContent* GetPreviousLeafContentOrPreviousBlockElement(
+ const EditorDOMPointBase<PT, CT>& aStartPoint,
+ const nsIContent& aCurrentBlock, const LeafNodeTypes& aLeafNodeTypes,
+ const Element* aAncestorLimiter = nullptr) {
+ MOZ_ASSERT(aStartPoint.IsSet());
+ MOZ_ASSERT_IF(
+ aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
+ NS_ASSERTION(!aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ "Not implemented yet");
+
+ if (!aStartPoint.IsInContentNode()) {
+ return nullptr;
+ }
+ if (aStartPoint.IsInTextNode()) {
+ return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *aStartPoint.template ContainerAs<Text>(), aCurrentBlock,
+ aLeafNodeTypes, aAncestorLimiter);
+ }
+ if (!HTMLEditUtils::IsContainerNode(
+ *aStartPoint.template ContainerAs<nsIContent>())) {
+ return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *aStartPoint.template ContainerAs<nsIContent>(), aCurrentBlock,
+ aLeafNodeTypes, aAncestorLimiter);
+ }
+
+ if (aStartPoint.IsStartOfContainer()) {
+ if (aStartPoint.GetContainer() == &aCurrentBlock) {
+ // We are at start of the block.
+ return nullptr;
+ }
+
+ // We are at start of non-block container
+ return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *aStartPoint.template ContainerAs<nsIContent>(), aCurrentBlock,
+ aLeafNodeTypes, aAncestorLimiter);
+ }
+
+ nsCOMPtr<nsIContent> previousContent =
+ aStartPoint.GetPreviousSiblingOfChild();
+ if (NS_WARN_IF(!previousContent)) {
+ return nullptr;
+ }
+
+ // We have a prior node. If it's a block, return it.
+ if (HTMLEditUtils::IsBlockElement(*previousContent)) {
+ return previousContent;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ aStartPoint.GetContainer()->IsEditable() &&
+ !previousContent->IsEditable()) {
+ return previousContent;
+ }
+ if (HTMLEditUtils::IsContainerNode(*previousContent)) {
+ // Else if it's a container, get deep rightmost child
+ if (nsIContent* child = HTMLEditUtils::GetLastLeafContent(
+ *previousContent, aLeafNodeTypes)) {
+ return child;
+ }
+ }
+ // Else return the node itself
+ return previousContent;
+ }
+
+ /**
+ * Returns a content node whose inline styles should be preserved after
+ * deleting content in a range. Typically, you should set aPoint to start
+ * boundary of the range to delete.
+ */
+ template <typename EditorDOMPointType>
+ static nsIContent* GetContentToPreserveInlineStyles(
+ const EditorDOMPointType& aPoint, const Element& aEditingHost);
+
+ /**
+ * Get previous/next editable point from start or end of aContent.
+ */
+ enum class InvisibleWhiteSpaces {
+ Ignore, // Ignore invisible white-spaces, i.e., don't return middle of
+ // them.
+ Preserve, // Preserve invisible white-spaces, i.e., result may be start or
+ // end of a text node even if it begins or ends with invisible
+ // white-spaces.
+ };
+ enum class TableBoundary {
+ Ignore, // May cross any table element boundary.
+ NoCrossTableElement, // Won't cross `<table>` element boundary.
+ NoCrossAnyTableElement, // Won't cross any table element boundary.
+ };
+ template <typename EditorDOMPointType>
+ static EditorDOMPointType GetPreviousEditablePoint(
+ nsIContent& aContent, const Element* aAncestorLimiter,
+ InvisibleWhiteSpaces aInvisibleWhiteSpaces,
+ TableBoundary aHowToTreatTableBoundary);
+ template <typename EditorDOMPointType>
+ static EditorDOMPointType GetNextEditablePoint(
+ nsIContent& aContent, const Element* aAncestorLimiter,
+ InvisibleWhiteSpaces aInvisibleWhiteSpaces,
+ TableBoundary aHowToTreatTableBoundary);
+
+ /**
+ * GetAncestorElement() and GetInclusiveAncestorElement() return
+ * (inclusive) block ancestor element of aContent whose time matches
+ * aAncestorTypes.
+ */
+ enum class AncestorType {
+ ClosestBlockElement,
+ MostDistantInlineElementInBlock,
+ EditableElement,
+ IgnoreHRElement, // Ignore ancestor <hr> element since invalid structure
+ ButtonElement,
+ };
+ using AncestorTypes = EnumSet<AncestorType>;
+ constexpr static AncestorTypes
+ ClosestEditableBlockElementOrInlineEditingHost = {
+ AncestorType::ClosestBlockElement,
+ AncestorType::MostDistantInlineElementInBlock,
+ AncestorType::EditableElement};
+ constexpr static AncestorTypes ClosestBlockElement = {
+ AncestorType::ClosestBlockElement};
+ constexpr static AncestorTypes ClosestEditableBlockElement = {
+ AncestorType::ClosestBlockElement, AncestorType::EditableElement};
+ constexpr static AncestorTypes ClosestEditableBlockElementExceptHRElement = {
+ AncestorType::ClosestBlockElement, AncestorType::IgnoreHRElement,
+ AncestorType::EditableElement};
+ constexpr static AncestorTypes ClosestEditableBlockElementOrButtonElement = {
+ AncestorType::ClosestBlockElement, AncestorType::EditableElement,
+ AncestorType::ButtonElement};
+ static Element* GetAncestorElement(const nsIContent& aContent,
+ const AncestorTypes& aAncestorTypes,
+ const Element* aAncestorLimiter = nullptr);
+ static Element* GetInclusiveAncestorElement(
+ const nsIContent& aContent, const AncestorTypes& aAncestorTypes,
+ const Element* aAncestorLimiter = nullptr);
+
+ /**
+ * GetClosestAncestorTableElement() returns the nearest inclusive ancestor
+ * <table> element of aContent.
+ */
+ static Element* GetClosestAncestorTableElement(const nsIContent& aContent) {
+ // TODO: the method name and its documentation clash with the
+ // implementation. Split this method into
+ // `GetClosestAncestorTableElement` and
+ // `GetClosestInclusiveAncestorTableElement`.
+ if (!aContent.GetParent()) {
+ return nullptr;
+ }
+ for (Element* element : aContent.InclusiveAncestorsOfType<Element>()) {
+ if (HTMLEditUtils::IsTable(element)) {
+ return element;
+ }
+ }
+ return nullptr;
+ }
+
+ static Element* GetInclusiveAncestorAnyTableElement(
+ const nsIContent& aContent) {
+ for (Element* parent : aContent.InclusiveAncestorsOfType<Element>()) {
+ if (HTMLEditUtils::IsAnyTableElement(parent)) {
+ return parent;
+ }
+ }
+ return nullptr;
+ }
+
+ [[nodiscard]] static Element* GetClosestAncestorAnyListElement(
+ const nsIContent& aContent);
+ [[nodiscard]] static Element* GetClosestInclusiveAncestorAnyListElement(
+ const nsIContent& aContent);
+
+ /**
+ * GetClosestAncestorListItemElement() returns a list item element if
+ * aContent or its ancestor in editing host is one. However, this won't
+ * cross table related element.
+ */
+ static Element* GetClosestAncestorListItemElement(
+ const nsIContent& aContent, const Element* aAncestorLimit = nullptr) {
+ MOZ_ASSERT_IF(aAncestorLimit,
+ aContent.IsInclusiveDescendantOf(aAncestorLimit));
+
+ if (HTMLEditUtils::IsListItem(&aContent)) {
+ return const_cast<Element*>(aContent.AsElement());
+ }
+
+ for (Element* parentElement : aContent.AncestorsOfType<Element>()) {
+ if (HTMLEditUtils::IsAnyTableElement(parentElement)) {
+ return nullptr;
+ }
+ if (HTMLEditUtils::IsListItem(parentElement)) {
+ return parentElement;
+ }
+ if (parentElement == aAncestorLimit) {
+ return nullptr;
+ }
+ }
+ return nullptr;
+ }
+
+ /**
+ * GetRangeSelectingAllContentInAllListItems() returns a range which selects
+ * from start of the first list item to end of the last list item of
+ * aListElement. Note that the result may be in different list element if
+ * aListElement has child list element(s) directly.
+ */
+ template <typename EditorDOMRangeType>
+ static EditorDOMRangeType GetRangeSelectingAllContentInAllListItems(
+ const Element& aListElement) {
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement));
+ Element* firstListItem =
+ HTMLEditUtils::GetFirstListItemElement(aListElement);
+ Element* lastListItem = HTMLEditUtils::GetLastListItemElement(aListElement);
+ MOZ_ASSERT_IF(firstListItem, lastListItem);
+ MOZ_ASSERT_IF(!firstListItem, !lastListItem);
+ if (!firstListItem || !lastListItem) {
+ return EditorDOMRangeType();
+ }
+ return EditorDOMRangeType(
+ typename EditorDOMRangeType::PointType(firstListItem, 0u),
+ EditorDOMRangeType::PointType::AtEndOf(*lastListItem));
+ }
+
+ /**
+ * GetFirstListItemElement() returns the first list item element in the
+ * pre-order tree traversal of the DOM.
+ */
+ static Element* GetFirstListItemElement(const Element& aListElement) {
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement));
+ for (nsIContent* maybeFirstListItem = aListElement.GetFirstChild();
+ maybeFirstListItem;
+ maybeFirstListItem = maybeFirstListItem->GetNextNode(&aListElement)) {
+ if (HTMLEditUtils::IsListItem(maybeFirstListItem)) {
+ return maybeFirstListItem->AsElement();
+ }
+ }
+ return nullptr;
+ }
+
+ /**
+ * GetLastListItemElement() returns the last list item element in the
+ * post-order tree traversal of the DOM. I.e., returns the last list
+ * element whose close tag appears at last.
+ */
+ static Element* GetLastListItemElement(const Element& aListElement) {
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement));
+ for (nsIContent* maybeLastListItem = aListElement.GetLastChild();
+ maybeLastListItem;) {
+ if (HTMLEditUtils::IsListItem(maybeLastListItem)) {
+ return maybeLastListItem->AsElement();
+ }
+ if (maybeLastListItem->HasChildren()) {
+ maybeLastListItem = maybeLastListItem->GetLastChild();
+ continue;
+ }
+ if (maybeLastListItem->GetPreviousSibling()) {
+ maybeLastListItem = maybeLastListItem->GetPreviousSibling();
+ continue;
+ }
+ for (Element* parent = maybeLastListItem->GetParentElement(); parent;
+ parent = parent->GetParentElement()) {
+ maybeLastListItem = nullptr;
+ if (parent == &aListElement) {
+ return nullptr;
+ }
+ if (parent->GetPreviousSibling()) {
+ maybeLastListItem = parent->GetPreviousSibling();
+ break;
+ }
+ }
+ }
+ return nullptr;
+ }
+
+ /**
+ * GetFirstTableCellElementChild() and GetLastTableCellElementChild()
+ * return the first/last element child of <tr> element if it's a table
+ * cell element.
+ */
+ static Element* GetFirstTableCellElementChild(
+ const Element& aTableRowElement) {
+ MOZ_ASSERT(aTableRowElement.IsHTMLElement(nsGkAtoms::tr));
+ Element* firstElementChild = aTableRowElement.GetFirstElementChild();
+ return firstElementChild && HTMLEditUtils::IsTableCell(firstElementChild)
+ ? firstElementChild
+ : nullptr;
+ }
+ static Element* GetLastTableCellElementChild(
+ const Element& aTableRowElement) {
+ MOZ_ASSERT(aTableRowElement.IsHTMLElement(nsGkAtoms::tr));
+ Element* lastElementChild = aTableRowElement.GetLastElementChild();
+ return lastElementChild && HTMLEditUtils::IsTableCell(lastElementChild)
+ ? lastElementChild
+ : nullptr;
+ }
+
+ /**
+ * GetPreviousTableCellElementSibling() and GetNextTableCellElementSibling()
+ * return a table cell element of previous/next element sibling of given
+ * content node if and only if the element sibling is a table cell element.
+ */
+ static Element* GetPreviousTableCellElementSibling(
+ const nsIContent& aChildOfTableRow) {
+ MOZ_ASSERT(aChildOfTableRow.GetParentNode());
+ MOZ_ASSERT(aChildOfTableRow.GetParentNode()->IsHTMLElement(nsGkAtoms::tr));
+ Element* previousElementSibling =
+ aChildOfTableRow.GetPreviousElementSibling();
+ return previousElementSibling &&
+ HTMLEditUtils::IsTableCell(previousElementSibling)
+ ? previousElementSibling
+ : nullptr;
+ }
+ static Element* GetNextTableCellElementSibling(
+ const nsIContent& aChildOfTableRow) {
+ MOZ_ASSERT(aChildOfTableRow.GetParentNode());
+ MOZ_ASSERT(aChildOfTableRow.GetParentNode()->IsHTMLElement(nsGkAtoms::tr));
+ Element* nextElementSibling = aChildOfTableRow.GetNextElementSibling();
+ return nextElementSibling && HTMLEditUtils::IsTableCell(nextElementSibling)
+ ? nextElementSibling
+ : nullptr;
+ }
+
+ /**
+ * GetMostDistantAncestorInlineElement() returns the most distant ancestor
+ * inline element between aContent and the aEditingHost. Even if aEditingHost
+ * is an inline element, this method never returns aEditingHost as the result.
+ * Optionally, you can specify ancestor limiter content node. This guarantees
+ * that the result is a descendant of aAncestorLimiter if aContent is a
+ * descendant of aAncestorLimiter.
+ */
+ static nsIContent* GetMostDistantAncestorInlineElement(
+ const nsIContent& aContent, const Element* aEditingHost = nullptr,
+ const nsIContent* aAncestorLimiter = nullptr) {
+ if (HTMLEditUtils::IsBlockElement(aContent)) {
+ return nullptr;
+ }
+
+ // If aNode is the editing host itself, there is no modifiable inline
+ // parent.
+ if (&aContent == aEditingHost || &aContent == aAncestorLimiter) {
+ return nullptr;
+ }
+
+ // If aNode is outside of the <body> element, we don't support to edit
+ // such elements for now.
+ // XXX This should be MOZ_ASSERT after fixing bug 1413131 for avoiding
+ // calling this expensive method.
+ if (aEditingHost && !aContent.IsInclusiveDescendantOf(aEditingHost)) {
+ return nullptr;
+ }
+
+ if (!aContent.GetParent()) {
+ return const_cast<nsIContent*>(&aContent);
+ }
+
+ // Looks for the highest inline parent in the editing host.
+ nsIContent* topMostInlineContent = const_cast<nsIContent*>(&aContent);
+ for (Element* element : aContent.AncestorsOfType<Element>()) {
+ if (element == aEditingHost || element == aAncestorLimiter ||
+ !HTMLEditUtils::IsInlineElement(*element)) {
+ break;
+ }
+ topMostInlineContent = element;
+ }
+ return topMostInlineContent;
+ }
+
+ /**
+ * GetMostDistantAncestorEditableEmptyInlineElement() returns most distant
+ * ancestor which only has aEmptyContent or its ancestor, editable and
+ * inline element.
+ */
+ static Element* GetMostDistantAncestorEditableEmptyInlineElement(
+ const nsIContent& aEmptyContent, const Element* aEditingHost = nullptr,
+ const nsIContent* aAncestorLimiter = nullptr) {
+ if (&aEmptyContent == aEditingHost || &aEmptyContent == aAncestorLimiter) {
+ return nullptr;
+ }
+ nsIContent* lastEmptyContent = const_cast<nsIContent*>(&aEmptyContent);
+ for (Element* element : aEmptyContent.AncestorsOfType<Element>()) {
+ if (element == aEditingHost || element == aAncestorLimiter) {
+ break;
+ }
+ if (!HTMLEditUtils::IsInlineElement(*element) ||
+ !HTMLEditUtils::IsSimplyEditableNode(*element)) {
+ break;
+ }
+ if (element->GetChildCount() > 1) {
+ for (const nsIContent* child = element->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (child == lastEmptyContent || child->IsComment()) {
+ continue;
+ }
+ return lastEmptyContent != &aEmptyContent
+ ? lastEmptyContent->AsElement()
+ : nullptr;
+ }
+ }
+ lastEmptyContent = element;
+ }
+ return lastEmptyContent != &aEmptyContent ? lastEmptyContent->AsElement()
+ : nullptr;
+ }
+
+ /**
+ * GetElementIfOnlyOneSelected() returns an element if aRange selects only
+ * the element node (and its descendants).
+ */
+ static Element* GetElementIfOnlyOneSelected(const AbstractRange& aRange) {
+ return GetElementIfOnlyOneSelected(EditorRawDOMRange(aRange));
+ }
+ template <typename EditorDOMPointType>
+ static Element* GetElementIfOnlyOneSelected(
+ const EditorDOMRangeBase<EditorDOMPointType>& aRange) {
+ if (!aRange.IsPositioned() || aRange.Collapsed()) {
+ return nullptr;
+ }
+ const auto& start = aRange.StartRef();
+ const auto& end = aRange.EndRef();
+ if (NS_WARN_IF(!start.IsSetAndValid()) ||
+ NS_WARN_IF(!end.IsSetAndValid()) ||
+ start.GetContainer() != end.GetContainer()) {
+ return nullptr;
+ }
+ nsIContent* childAtStart = start.GetChild();
+ if (!childAtStart || !childAtStart->IsElement()) {
+ return nullptr;
+ }
+ // If start child is not the last sibling and only if end child is its
+ // next sibling, the start child is selected.
+ if (childAtStart->GetNextSibling()) {
+ return childAtStart->GetNextSibling() == end.GetChild()
+ ? childAtStart->AsElement()
+ : nullptr;
+ }
+ // If start child is the last sibling and only if no child at the end,
+ // the start child is selected.
+ return !end.GetChild() ? childAtStart->AsElement() : nullptr;
+ }
+
+ static Element* GetTableCellElementIfOnlyOneSelected(
+ const AbstractRange& aRange) {
+ Element* element = HTMLEditUtils::GetElementIfOnlyOneSelected(aRange);
+ return element && HTMLEditUtils::IsTableCell(element) ? element : nullptr;
+ }
+
+ /**
+ * GetFirstSelectedTableCellElement() returns a table cell element (i.e.,
+ * `<td>` or `<th>` if and only if first selection range selects only a
+ * table cell element.
+ */
+ static Element* GetFirstSelectedTableCellElement(
+ const Selection& aSelection) {
+ if (!aSelection.RangeCount()) {
+ return nullptr;
+ }
+ const nsRange* firstRange = aSelection.GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange) || NS_WARN_IF(!firstRange->IsPositioned())) {
+ return nullptr;
+ }
+ return GetTableCellElementIfOnlyOneSelected(*firstRange);
+ }
+
+ /**
+ * GetInclusiveFirstChildWhichHasOneChild() returns the deepest element whose
+ * tag name is one of `aFirstElementName` and `aOtherElementNames...` if and
+ * only if the elements have only one child node. In other words, when
+ * this method meets an element which does not matches any of the tag name
+ * or it has no children or 2+ children.
+ *
+ * XXX This method must be implemented without treating edge cases. So, the
+ * behavior is odd. E.g., why can we ignore non-editable node at counting
+ * each children? Why do we dig non-editable aNode or first child of its
+ * descendants?
+ */
+ template <typename FirstElementName, typename... OtherElementNames>
+ static Element* GetInclusiveDeepestFirstChildWhichHasOneChild(
+ const nsINode& aNode, const WalkTreeOptions& aOptions,
+ FirstElementName aFirstElementName,
+ OtherElementNames... aOtherElementNames) {
+ if (!aNode.IsElement()) {
+ return nullptr;
+ }
+ Element* parentElement = nullptr;
+ for (nsIContent* content = const_cast<nsIContent*>(aNode.AsContent());
+ content && content->IsElement() &&
+ content->IsAnyOfHTMLElements(aFirstElementName, aOtherElementNames...);
+ // XXX Why do we scan only the first child of every element? If it's
+ // not editable, why do we ignore it when aOptions specifies so.
+ content = content->GetFirstChild()) {
+ if (HTMLEditUtils::CountChildren(*content, aOptions) != 1) {
+ return content->AsElement();
+ }
+ parentElement = content->AsElement();
+ }
+ return parentElement;
+ }
+
+ /**
+ * Get the first <br> element in aElement. This scans only leaf nodes so
+ * if a <br> element has children illegally, it'll be ignored.
+ *
+ * @param aElement The element which may have a <br> element.
+ * @return First <br> element node in aElement if there is.
+ */
+ static dom::HTMLBRElement* GetFirstBRElement(const dom::Element& aElement) {
+ for (nsIContent* content = HTMLEditUtils::GetFirstLeafContent(
+ aElement, {LeafNodeType::OnlyLeafNode});
+ content; content = HTMLEditUtils::GetNextContent(
+ *content,
+ {WalkTreeOption::IgnoreDataNodeExceptText,
+ WalkTreeOption::IgnoreWhiteSpaceOnlyText},
+ &aElement)) {
+ if (auto* brElement = dom::HTMLBRElement::FromNode(*content)) {
+ return brElement;
+ }
+ }
+ return nullptr;
+ }
+
+ /**
+ * Return last <br> element or last text node ending with a preserved line
+ * break of/before aBlockElement.
+ */
+ enum ScanLineBreak {
+ AtEndOfBlock,
+ BeforeBlock,
+ };
+ static nsIContent* GetUnnecessaryLineBreakContent(
+ const Element& aBlockElement, ScanLineBreak aScanLineBreak);
+
+ /**
+ * IsInTableCellSelectionMode() returns true when Gecko's editor thinks that
+ * selection is in a table cell selection mode.
+ * Note that Gecko's editor traditionally treats selection as in table cell
+ * selection mode when first range selects a table cell element. I.e., even
+ * if `nsFrameSelection` is not in table cell selection mode, this may return
+ * true.
+ */
+ static bool IsInTableCellSelectionMode(const Selection& aSelection) {
+ return GetFirstSelectedTableCellElement(aSelection) != nullptr;
+ }
+
+ static EditAction GetEditActionForInsert(const nsAtom& aTagName);
+ static EditAction GetEditActionForRemoveList(const nsAtom& aTagName);
+ static EditAction GetEditActionForInsert(const Element& aElement);
+ static EditAction GetEditActionForFormatText(const nsAtom& aProperty,
+ const nsAtom* aAttribute,
+ bool aToSetStyle);
+ static EditAction GetEditActionForAlignment(const nsAString& aAlignType);
+
+ /**
+ * GetPreviousNonCollapsibleCharOffset() returns offset of previous
+ * character which is not collapsible white-space characters.
+ */
+ enum class WalkTextOption {
+ TreatNBSPsCollapsible,
+ };
+ using WalkTextOptions = EnumSet<WalkTextOption>;
+ static Maybe<uint32_t> GetPreviousNonCollapsibleCharOffset(
+ const EditorDOMPointInText& aPoint,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ return GetPreviousNonCollapsibleCharOffset(
+ *aPoint.ContainerAs<Text>(), aPoint.Offset(), aWalkTextOptions);
+ }
+ static Maybe<uint32_t> GetPreviousNonCollapsibleCharOffset(
+ const Text& aTextNode, uint32_t aOffset,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ const bool isWhiteSpaceCollapsible =
+ !EditorUtils::IsWhiteSpacePreformatted(aTextNode);
+ const bool isNewLineCollapsible =
+ !EditorUtils::IsNewLinePreformatted(aTextNode);
+ const bool isNBSPCollapsible =
+ isWhiteSpaceCollapsible &&
+ aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible);
+ const nsTextFragment& textFragment = aTextNode.TextFragment();
+ MOZ_ASSERT(aOffset <= textFragment.GetLength());
+ for (uint32_t i = aOffset; i; i--) {
+ // TODO: Perhaps, nsTextFragment should have scanner methods because
+ // the text may be in per-one-byte storage or per-two-byte storage,
+ // and `CharAt` needs to check it everytime.
+ switch (textFragment.CharAt(i - 1)) {
+ case HTMLEditUtils::kSpace:
+ case HTMLEditUtils::kCarriageReturn:
+ case HTMLEditUtils::kTab:
+ if (!isWhiteSpaceCollapsible) {
+ return Some(i - 1);
+ }
+ break;
+ case HTMLEditUtils::kNewLine:
+ if (!isNewLineCollapsible) {
+ return Some(i - 1);
+ }
+ break;
+ case HTMLEditUtils::kNBSP:
+ if (!isNBSPCollapsible) {
+ return Some(i - 1);
+ }
+ break;
+ default:
+ MOZ_ASSERT(!nsCRT::IsAsciiSpace(textFragment.CharAt(i - 1)));
+ return Some(i - 1);
+ }
+ }
+ return Nothing();
+ }
+
+ /**
+ * GetNextNonCollapsibleCharOffset() returns offset of next character which is
+ * not collapsible white-space characters.
+ */
+ static Maybe<uint32_t> GetNextNonCollapsibleCharOffset(
+ const EditorDOMPointInText& aPoint,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ return GetNextNonCollapsibleCharOffset(*aPoint.ContainerAs<Text>(),
+ aPoint.Offset(), aWalkTextOptions);
+ }
+ static Maybe<uint32_t> GetNextNonCollapsibleCharOffset(
+ const Text& aTextNode, uint32_t aOffset,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ return GetInclusiveNextNonCollapsibleCharOffset(aTextNode, aOffset + 1,
+ aWalkTextOptions);
+ }
+
+ /**
+ * GetInclusiveNextNonCollapsibleCharOffset() returns offset of inclusive next
+ * character which is not collapsible white-space characters.
+ */
+ static Maybe<uint32_t> GetInclusiveNextNonCollapsibleCharOffset(
+ const EditorDOMPointInText& aPoint,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ return GetInclusiveNextNonCollapsibleCharOffset(
+ *aPoint.ContainerAs<Text>(), aPoint.Offset(), aWalkTextOptions);
+ }
+ static Maybe<uint32_t> GetInclusiveNextNonCollapsibleCharOffset(
+ const Text& aTextNode, uint32_t aOffset,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ const bool isWhiteSpaceCollapsible =
+ !EditorUtils::IsWhiteSpacePreformatted(aTextNode);
+ const bool isNewLineCollapsible =
+ !EditorUtils::IsNewLinePreformatted(aTextNode);
+ const bool isNBSPCollapsible =
+ isWhiteSpaceCollapsible &&
+ aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible);
+ const nsTextFragment& textFragment = aTextNode.TextFragment();
+ MOZ_ASSERT(aOffset <= textFragment.GetLength());
+ for (uint32_t i = aOffset; i < textFragment.GetLength(); i++) {
+ // TODO: Perhaps, nsTextFragment should have scanner methods because
+ // the text may be in per-one-byte storage or per-two-byte storage,
+ // and `CharAt` needs to check it everytime.
+ switch (textFragment.CharAt(i)) {
+ case HTMLEditUtils::kSpace:
+ case HTMLEditUtils::kCarriageReturn:
+ case HTMLEditUtils::kTab:
+ if (!isWhiteSpaceCollapsible) {
+ return Some(i);
+ }
+ break;
+ case HTMLEditUtils::kNewLine:
+ if (!isNewLineCollapsible) {
+ return Some(i);
+ }
+ break;
+ case HTMLEditUtils::kNBSP:
+ if (!isNBSPCollapsible) {
+ return Some(i);
+ }
+ break;
+ default:
+ MOZ_ASSERT(!nsCRT::IsAsciiSpace(textFragment.CharAt(i)));
+ return Some(i);
+ }
+ }
+ return Nothing();
+ }
+
+ /**
+ * GetFirstWhiteSpaceOffsetCollapsedWith() returns first collapsible
+ * white-space offset which is collapsed with a white-space at the given
+ * position. I.e., the character at the position must be a collapsible
+ * white-space.
+ */
+ static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith(
+ const EditorDOMPointInText& aPoint,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ MOZ_ASSERT(!aPoint.IsEndOfContainer());
+ MOZ_ASSERT_IF(
+ aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible),
+ aPoint.IsCharCollapsibleASCIISpaceOrNBSP());
+ MOZ_ASSERT_IF(
+ !aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible),
+ aPoint.IsCharCollapsibleASCIISpace());
+ return GetFirstWhiteSpaceOffsetCollapsedWith(
+ *aPoint.ContainerAs<Text>(), aPoint.Offset(), aWalkTextOptions);
+ }
+ static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith(
+ const Text& aTextNode, uint32_t aOffset,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ MOZ_ASSERT(aOffset < aTextNode.TextLength());
+ MOZ_ASSERT_IF(
+ aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible),
+ EditorRawDOMPoint(&aTextNode, aOffset)
+ .IsCharCollapsibleASCIISpaceOrNBSP());
+ MOZ_ASSERT_IF(
+ !aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible),
+ EditorRawDOMPoint(&aTextNode, aOffset).IsCharCollapsibleASCIISpace());
+ if (!aOffset) {
+ return 0;
+ }
+ Maybe<uint32_t> previousVisibleCharOffset =
+ GetPreviousNonCollapsibleCharOffset(aTextNode, aOffset,
+ aWalkTextOptions);
+ return previousVisibleCharOffset.isSome()
+ ? previousVisibleCharOffset.value() + 1
+ : 0;
+ }
+
+ /**
+ * GetPreviousPreformattedNewLineInTextNode() returns a point which points
+ * previous preformatted linefeed if there is and aPoint is in a text node.
+ * If the node's linefeed characters are not preformatted or aPoint is not
+ * in a text node, this returns unset DOM point.
+ */
+ template <typename EditorDOMPointType, typename ArgEditorDOMPointType>
+ static EditorDOMPointType GetPreviousPreformattedNewLineInTextNode(
+ const ArgEditorDOMPointType& aPoint) {
+ if (!aPoint.IsInTextNode() || aPoint.IsStartOfContainer() ||
+ !EditorUtils::IsNewLinePreformatted(
+ *aPoint.template ContainerAs<Text>())) {
+ return EditorDOMPointType();
+ }
+ Text* textNode = aPoint.template ContainerAs<Text>();
+ const nsTextFragment& textFragment = textNode->TextFragment();
+ MOZ_ASSERT(aPoint.Offset() <= textFragment.GetLength());
+ for (uint32_t offset = aPoint.Offset(); offset; --offset) {
+ if (textFragment.CharAt(offset - 1) == HTMLEditUtils::kNewLine) {
+ return EditorDOMPointType(textNode, offset - 1);
+ }
+ }
+ return EditorDOMPointType();
+ }
+
+ /**
+ * GetInclusiveNextPreformattedNewLineInTextNode() returns a point which
+ * points inclusive next preformatted linefeed if there is and aPoint is in a
+ * text node. If the node's linefeed characters are not preformatted or aPoint
+ * is not in a text node, this returns unset DOM point.
+ */
+ template <typename EditorDOMPointType, typename ArgEditorDOMPointType>
+ static EditorDOMPointType GetInclusiveNextPreformattedNewLineInTextNode(
+ const ArgEditorDOMPointType& aPoint) {
+ if (!aPoint.IsInTextNode() || aPoint.IsEndOfContainer() ||
+ !EditorUtils::IsNewLinePreformatted(
+ *aPoint.template ContainerAs<Text>())) {
+ return EditorDOMPointType();
+ }
+ Text* textNode = aPoint.template ContainerAs<Text>();
+ const nsTextFragment& textFragment = textNode->TextFragment();
+ for (uint32_t offset = aPoint.Offset(); offset < textFragment.GetLength();
+ ++offset) {
+ if (textFragment.CharAt(offset) == HTMLEditUtils::kNewLine) {
+ return EditorDOMPointType(textNode, offset);
+ }
+ }
+ return EditorDOMPointType();
+ }
+
+ /**
+ * GetGoodCaretPointFor() returns a good point to collapse `Selection`
+ * after handling edit action with aDirectionAndAmount.
+ *
+ * @param aContent The content where you want to put caret
+ * around.
+ * @param aDirectionAndAmount Muse be one of eNext, eNextWord, eToEndOfLine,
+ * ePrevious, ePreviousWord and eToBeggingOfLine.
+ * Set the direction of handled edit action.
+ */
+ template <typename EditorDOMPointType>
+ static EditorDOMPointType GetGoodCaretPointFor(
+ nsIContent& aContent, nsIEditor::EDirection aDirectionAndAmount) {
+ MOZ_ASSERT(nsIEditor::EDirectionIsValidExceptNone(aDirectionAndAmount));
+
+ // XXX Why don't we check whether the candidate position is enable or not?
+ // When the result is not editable point, caret will be enclosed in
+ // the non-editable content.
+
+ // If we can put caret in aContent, return start or end in it.
+ if (aContent.IsText() || HTMLEditUtils::IsContainerNode(aContent) ||
+ NS_WARN_IF(!aContent.GetParentNode())) {
+ return EditorDOMPointType(
+ &aContent, nsIEditor::DirectionIsDelete(aDirectionAndAmount)
+ ? 0
+ : aContent.Length());
+ }
+
+ // If we are going forward, put caret at aContent itself.
+ if (nsIEditor::DirectionIsDelete(aDirectionAndAmount)) {
+ return EditorDOMPointType(&aContent);
+ }
+
+ // If we are going backward, put caret to next node unless aContent is an
+ // invisible `<br>` element.
+ // XXX Shouldn't we put caret to first leaf of the next node?
+ if (!HTMLEditUtils::IsInvisibleBRElement(aContent)) {
+ EditorDOMPointType ret(EditorDOMPointType::After(aContent));
+ NS_WARNING_ASSERTION(ret.IsSet(), "Failed to set after aContent");
+ return ret;
+ }
+
+ // Otherwise, we should put caret at the invisible `<br>` element.
+ return EditorDOMPointType(&aContent);
+ }
+
+ /**
+ * GetBetterInsertionPointFor() returns better insertion point to insert
+ * aContentToInsert.
+ *
+ * @param aContentToInsert The content to insert.
+ * @param aPointToInsert A candidate point to insert the node.
+ * @param aEditingHost The editing host containing aPointToInsert.
+ * @return Better insertion point if next visible node
+ * is a <br> element and previous visible node
+ * is neither none, another <br> element nor
+ * different block level element.
+ */
+ template <typename EditorDOMPointType, typename EditorDOMPointTypeInput>
+ static EditorDOMPointType GetBetterInsertionPointFor(
+ const nsIContent& aContentToInsert,
+ const EditorDOMPointTypeInput& aPointToInsert,
+ const Element& aEditingHost);
+
+ /**
+ * GetBetterCaretPositionToInsertText() returns better point to put caret
+ * if aPoint is near a text node or in non-container node.
+ */
+ template <typename EditorDOMPointType, typename EditorDOMPointTypeInput>
+ static EditorDOMPointType GetBetterCaretPositionToInsertText(
+ const EditorDOMPointTypeInput& aPoint, const Element& aEditingHost);
+
+ /**
+ * ComputePointToPutCaretInElementIfOutside() returns a good point in aElement
+ * to put caret if aCurrentPoint is outside of aElement.
+ *
+ * @param aElement The result is a point in aElement.
+ * @param aCurrentPoint The current (candidate) caret point. Only if this
+ * is outside aElement, returns a point in aElement.
+ */
+ template <typename EditorDOMPointType, typename EditorDOMPointTypeInput>
+ static Result<EditorDOMPointType, nsresult>
+ ComputePointToPutCaretInElementIfOutside(
+ const Element& aElement, const EditorDOMPointTypeInput& aCurrentPoint);
+
+ /**
+ * Content-based query returns true if
+ * <mHTMLProperty mAttribute=mAttributeValue> effects aContent. If there is
+ * such a element, but another element whose attribute value does not match
+ * with mAttributeValue is closer ancestor of aContent, then the distant
+ * ancestor does not effect aContent.
+ *
+ * @param aContent The target of the query
+ * @param aStyle The style which queries a representing element.
+ * @param aValue Optional, the value of aStyle.mAttribute, example: blue
+ * in <font color="blue"> May be null. Ignored if
+ * aStyle.mAttribute is null.
+ * @param aOutValue [OUT] the value of the attribute, if returns true
+ * @return true if <mHTMLProperty mAttribute=mAttributeValue>
+ * effects aContent.
+ */
+ [[nodiscard]] static bool IsInlineStyleSetByElement(
+ const nsIContent& aContent, const EditorInlineStyle& aStyle,
+ const nsAString* aValue, nsAString* aOutValue = nullptr);
+
+ /**
+ * CollectAllChildren() collects all child nodes of aParentNode.
+ */
+ static void CollectAllChildren(
+ const nsINode& aParentNode,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents) {
+ MOZ_ASSERT(aOutArrayOfContents.IsEmpty());
+ aOutArrayOfContents.SetCapacity(aParentNode.GetChildCount());
+ for (nsIContent* childContent = aParentNode.GetFirstChild(); childContent;
+ childContent = childContent->GetNextSibling()) {
+ aOutArrayOfContents.AppendElement(*childContent);
+ }
+ }
+
+ /**
+ * CollectChildren() collects child nodes of aNode (starting from
+ * first editable child, but may return non-editable children after it).
+ *
+ * @param aNode Parent node of retrieving children.
+ * @param aOutArrayOfContents [out] This method will inserts found children
+ * into this array.
+ * @param aIndexToInsertChildren Starting from this index, found
+ * children will be inserted to the array.
+ * @param aOptions Options to scan the children.
+ * @return Number of found children.
+ */
+ static size_t CollectChildren(
+ const nsINode& aNode,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
+ const CollectChildrenOptions& aOptions) {
+ return HTMLEditUtils::CollectChildren(aNode, aOutArrayOfContents, 0u,
+ aOptions);
+ }
+ static size_t CollectChildren(
+ const nsINode& aNode,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
+ size_t aIndexToInsertChildren, const CollectChildrenOptions& aOptions);
+
+ /**
+ * CollectEmptyInlineContainerDescendants() appends empty inline elements in
+ * aNode to aOutArrayOfContents. Although it's array of nsIContent, the
+ * instance will be elements.
+ *
+ * @param aNode The node whose descendants may have empty inline
+ * elements.
+ * @param aOutArrayOfContents [out] This method will append found descendants
+ * into this array.
+ * @param aOptions The option which element should be treated as
+ * empty.
+ * @return Number of found elements.
+ */
+ static size_t CollectEmptyInlineContainerDescendants(
+ const nsINode& aNode,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
+ const EmptyCheckOptions& aOptions);
+
+ /**
+ * Check whether aElement has attributes except the name aAttribute and
+ * "_moz_*" attributes.
+ */
+ [[nodiscard]] static bool ElementHasAttribute(const Element& aElement) {
+ return ElementHasAttributeExcept(aElement, *nsGkAtoms::_empty,
+ *nsGkAtoms::empty, *nsGkAtoms::_empty);
+ }
+ [[nodiscard]] static bool ElementHasAttributeExcept(
+ const Element& aElement, const nsAtom& aAttribute) {
+ return ElementHasAttributeExcept(aElement, aAttribute, *nsGkAtoms::_empty,
+ *nsGkAtoms::empty);
+ }
+ [[nodiscard]] static bool ElementHasAttributeExcept(
+ const Element& aElement, const nsAtom& aAttribute1,
+ const nsAtom& aAttribute2) {
+ return ElementHasAttributeExcept(aElement, aAttribute1, aAttribute2,
+ *nsGkAtoms::empty);
+ }
+ [[nodiscard]] static bool ElementHasAttributeExcept(
+ const Element& aElement, const nsAtom& aAttribute1,
+ const nsAtom& aAttribute2, const nsAtom& aAttribute3);
+
+ /**
+ * Returns EditorDOMPoint which points deepest editable start/end point of
+ * aNode. If a node is a container node and first/last child is editable,
+ * returns the child's start or last point recursively.
+ */
+ enum class InvisibleText { Recognize, Skip };
+ template <typename EditorDOMPointType>
+ [[nodiscard]] static EditorDOMPointType GetDeepestEditableStartPointOf(
+ const nsIContent& aContent,
+ InvisibleText aInvisibleText = InvisibleText::Recognize) {
+ if (NS_WARN_IF(!EditorUtils::IsEditableContent(
+ aContent, EditorBase::EditorType::HTML))) {
+ return EditorDOMPointType();
+ }
+ EditorDOMPointType result(&aContent, 0u);
+ while (true) {
+ nsIContent* firstChild = result.GetContainer()->GetFirstChild();
+ if (!firstChild) {
+ break;
+ }
+ // If the caller wants to skip invisible white-spaces, we should skip
+ // invisible text nodes.
+ if (aInvisibleText == InvisibleText::Skip && firstChild->IsText() &&
+ EditorUtils::IsEditableContent(*firstChild,
+ EditorBase::EditorType::HTML) &&
+ !HTMLEditUtils::IsVisibleTextNode(*firstChild->AsText())) {
+ for (nsIContent* nextSibling = firstChild->GetNextSibling();
+ nextSibling; nextSibling = nextSibling->GetNextSibling()) {
+ if (!nextSibling->IsText() ||
+ // We know its previous sibling is very start of a block.
+ // Therefore, we only need to scan the text here.
+ HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
+ *firstChild->AsText(), 0u)
+ .isSome()) {
+ firstChild = nextSibling;
+ break;
+ }
+ }
+ }
+ if ((!firstChild->IsText() &&
+ !HTMLEditUtils::IsContainerNode(*firstChild)) ||
+ !EditorUtils::IsEditableContent(*firstChild,
+ EditorBase::EditorType::HTML)) {
+ break;
+ }
+ if (aInvisibleText == InvisibleText::Skip && firstChild->IsText()) {
+ result.Set(firstChild,
+ HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
+ *firstChild->AsText(), 0u)
+ .valueOr(0u));
+ break;
+ }
+ result.Set(firstChild, 0u);
+ }
+ return result;
+ }
+ template <typename EditorDOMPointType>
+ [[nodiscard]] static EditorDOMPointType GetDeepestEditableEndPointOf(
+ const nsIContent& aContent,
+ InvisibleText aInvisibleText = InvisibleText::Recognize) {
+ if (NS_WARN_IF(!EditorUtils::IsEditableContent(
+ aContent, EditorBase::EditorType::HTML))) {
+ return EditorDOMPointType();
+ }
+ auto result = EditorDOMPointType::AtEndOf(aContent);
+ while (true) {
+ nsIContent* lastChild = result.GetContainer()->GetLastChild();
+ if (!lastChild) {
+ break;
+ }
+ // If the caller wants to skip invisible white-spaces, we should skip
+ // invisible text nodes.
+ if (aInvisibleText == InvisibleText::Skip && lastChild->IsText() &&
+ EditorUtils::IsEditableContent(*lastChild,
+ EditorBase::EditorType::HTML) &&
+ !HTMLEditUtils::IsVisibleTextNode(*lastChild->AsText())) {
+ for (nsIContent* nextSibling = lastChild->GetPreviousSibling();
+ nextSibling; nextSibling = nextSibling->GetPreviousSibling()) {
+ if (!nextSibling->IsText() ||
+ // We know its previous sibling is very start of a block.
+ // Therefore, we only need to scan the text here.
+ HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
+ *lastChild->AsText(), lastChild->AsText()->TextDataLength())
+ .isSome()) {
+ lastChild = nextSibling;
+ break;
+ }
+ }
+ }
+ if ((!lastChild->IsText() &&
+ !HTMLEditUtils::IsContainerNode(*lastChild)) ||
+ !EditorUtils::IsEditableContent(*lastChild,
+ EditorBase::EditorType::HTML)) {
+ break;
+ }
+ if (aInvisibleText == InvisibleText::Skip && lastChild->IsText()) {
+ Maybe<uint32_t> visibleCharOffset =
+ HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
+ *lastChild->AsText(), lastChild->AsText()->TextDataLength());
+ if (visibleCharOffset.isNothing()) {
+ result = EditorDOMPointType::AtEndOf(*lastChild);
+ break;
+ }
+ result.Set(lastChild, visibleCharOffset.value() + 1u);
+ break;
+ }
+ result = EditorDOMPointType::AtEndOf(*lastChild);
+ }
+ return result;
+ }
+
+ /**
+ * Get `#[0-9a-f]{6}` style HTML color value if aColorValue is valid value
+ * for color-specifying attribute. The result is useful to set attributes
+ * of HTML elements which take a color value.
+ *
+ * @param aColorValue [in] Should be one of `#[0-9a-fA-Z]{3}`,
+ * `#[0-9a-fA-Z]{3}` or a color name.
+ * @param aNormalizedValue [out] Set to `#[0-9a-f]{6}` style color code
+ * if this returns true. Otherwise, returns
+ * aColorValue as-is.
+ * @return true if aColorValue is valid. Otherwise, false.
+ */
+ static bool GetNormalizedHTMLColorValue(const nsAString& aColorValue,
+ nsAString& aNormalizedValue);
+
+ /**
+ * Return true if aColorValue may be a CSS specific color value or general
+ * keywords of CSS.
+ */
+ [[nodiscard]] static bool MaybeCSSSpecificColorValue(
+ const nsAString& aColorValue);
+
+ /**
+ * Return true if aColorValue can be specified to `color` value of <font>.
+ */
+ [[nodiscard]] static bool CanConvertToHTMLColorValue(
+ const nsAString& aColorValue);
+
+ /**
+ * Convert aColorValue to `#[0-9a-f]{6}` style HTML color value.
+ */
+ static bool ConvertToNormalizedHTMLColorValue(const nsAString& aColorValue,
+ nsAString& aNormalizedValue);
+
+ /**
+ * Get serialized color value (`rgb(...)` or `rgba(...)`) or "currentcolor"
+ * if aColorValue is valid. The result is useful to set CSS color property.
+ *
+ * @param aColorValue [in] Should be valid CSS color value.
+ * @param aZeroAlphaColor [in] If TransparentKeyword, aNormalizedValue is
+ * set to "transparent" if the alpha value is 0.
+ * Otherwise, `rgba(...)` value is set.
+ * @param aNormalizedValue [out] Serialized color value or "currentcolor".
+ * @return true if aColorValue is valid. Otherwise, false.
+ */
+ enum class ZeroAlphaColor { RGBAValue, TransparentKeyword };
+ static bool GetNormalizedCSSColorValue(const nsAString& aColorValue,
+ ZeroAlphaColor aZeroAlphaColor,
+ nsAString& aNormalizedValue);
+
+ /**
+ * Check whether aColorA and aColorB are same color.
+ *
+ * @param aTransparentKeyword Whether allow to treat "transparent" keyword
+ * as a valid value or an invalid value.
+ * @return If aColorA and aColorB are valid values and
+ * mean same color, returns true.
+ */
+ enum class TransparentKeyword { Invalid, Allowed };
+ static bool IsSameHTMLColorValue(const nsAString& aColorA,
+ const nsAString& aColorB,
+ TransparentKeyword aTransparentKeyword);
+
+ /**
+ * Check whether aColorA and aColorB are same color.
+ *
+ * @return If aColorA and aColorB are valid values and
+ * mean same color, returns true.
+ */
+ template <typename CharType>
+ static bool IsSameCSSColorValue(const nsTSubstring<CharType>& aColorA,
+ const nsTSubstring<CharType>& aColorB);
+
+ private:
+ static bool CanNodeContain(nsHTMLTag aParentTagId, nsHTMLTag aChildTagId);
+ static bool IsContainerNode(nsHTMLTag aTagId);
+
+ static bool CanCrossContentBoundary(nsIContent& aContent,
+ TableBoundary aHowToTreatTableBoundary) {
+ const bool cannotCrossBoundary =
+ (aHowToTreatTableBoundary == TableBoundary::NoCrossAnyTableElement &&
+ HTMLEditUtils::IsAnyTableElement(&aContent)) ||
+ (aHowToTreatTableBoundary == TableBoundary::NoCrossTableElement &&
+ aContent.IsHTMLElement(nsGkAtoms::table));
+ return !cannotCrossBoundary;
+ }
+
+ static bool IsContentIgnored(const nsIContent& aContent,
+ const WalkTreeOptions& aOptions) {
+ if (aOptions.contains(WalkTreeOption::IgnoreNonEditableNode) &&
+ !EditorUtils::IsEditableContent(aContent,
+ EditorUtils::EditorType::HTML)) {
+ return true;
+ }
+ if (aOptions.contains(WalkTreeOption::IgnoreDataNodeExceptText) &&
+ !EditorUtils::IsElementOrText(aContent)) {
+ return true;
+ }
+ if (aOptions.contains(WalkTreeOption::IgnoreWhiteSpaceOnlyText) &&
+ aContent.IsText() &&
+ const_cast<Text*>(aContent.AsText())->TextIsOnlyWhitespace()) {
+ return true;
+ }
+ return false;
+ }
+
+ static uint32_t CountChildren(const nsINode& aNode,
+ const WalkTreeOptions& aOptions) {
+ uint32_t count = 0;
+ for (nsIContent* child = aNode.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) {
+ continue;
+ }
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*child)) {
+ break;
+ }
+ ++count;
+ }
+ return count;
+ }
+
+ /**
+ * Helper for GetPreviousContent() and GetNextContent().
+ */
+ static nsIContent* GetAdjacentLeafContent(
+ const nsINode& aNode, WalkTreeDirection aWalkTreeDirection,
+ const WalkTreeOptions& aOptions,
+ const Element* aAncestorLimiter = nullptr);
+ static nsIContent* GetAdjacentContent(
+ const nsINode& aNode, WalkTreeDirection aWalkTreeDirection,
+ const WalkTreeOptions& aOptions,
+ const Element* aAncestorLimiter = nullptr);
+
+ /**
+ * GetElementOfImmediateBlockBoundary() returns a block element if its
+ * block boundary and aContent may be first visible thing before/after the
+ * boundary. And it may return a <br> element only when aContent is a
+ * text node and follows a <br> element because only in this case, the
+ * start white-spaces are invisible. So the <br> element works same as
+ * a block boundary.
+ */
+ static Element* GetElementOfImmediateBlockBoundary(
+ const nsIContent& aContent, const WalkTreeDirection aDirection);
+};
+
+/**
+ * DefinitionListItemScanner() scans given `<dl>` element's children.
+ * Then, you can check whether `<dt>` and/or `<dd>` elements are in it.
+ */
+class MOZ_STACK_CLASS DefinitionListItemScanner final {
+ using Element = dom::Element;
+
+ public:
+ DefinitionListItemScanner() = delete;
+ explicit DefinitionListItemScanner(Element& aDLElement) {
+ MOZ_ASSERT(aDLElement.IsHTMLElement(nsGkAtoms::dl));
+ for (nsIContent* child = aDLElement.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (child->IsHTMLElement(nsGkAtoms::dt)) {
+ mDTFound = true;
+ if (mDDFound) {
+ break;
+ }
+ continue;
+ }
+ if (child->IsHTMLElement(nsGkAtoms::dd)) {
+ mDDFound = true;
+ if (mDTFound) {
+ break;
+ }
+ continue;
+ }
+ }
+ }
+
+ bool DTElementFound() const { return mDTFound; }
+ bool DDElementFound() const { return mDDFound; }
+
+ private:
+ bool mDTFound = false;
+ bool mDDFound = false;
+};
+
+/**
+ * SelectedTableCellScanner() scans all table cell elements which are selected
+ * by each selection range. Note that if 2nd or later ranges do not select
+ * only one table cell element, the ranges are just ignored.
+ */
+class MOZ_STACK_CLASS SelectedTableCellScanner final {
+ using Element = dom::Element;
+ using Selection = dom::Selection;
+
+ public:
+ SelectedTableCellScanner() = delete;
+ explicit SelectedTableCellScanner(const Selection& aSelection) {
+ Element* firstSelectedCellElement =
+ HTMLEditUtils::GetFirstSelectedTableCellElement(aSelection);
+ if (!firstSelectedCellElement) {
+ return; // We're not in table cell selection mode.
+ }
+ mSelectedCellElements.SetCapacity(aSelection.RangeCount());
+ mSelectedCellElements.AppendElement(*firstSelectedCellElement);
+ const uint32_t rangeCount = aSelection.RangeCount();
+ for (const uint32_t i : IntegerRange(1u, rangeCount)) {
+ MOZ_ASSERT(aSelection.RangeCount() == rangeCount);
+ nsRange* range = aSelection.GetRangeAt(i);
+ if (MOZ_UNLIKELY(NS_WARN_IF(!range)) ||
+ MOZ_UNLIKELY(NS_WARN_IF(!range->IsPositioned()))) {
+ continue; // Shouldn't occur in normal conditions.
+ }
+ // Just ignore selection ranges which do not select only one table
+ // cell element. This is possible case if web apps sets multiple
+ // selections and first range selects a table cell element.
+ if (Element* selectedCellElement =
+ HTMLEditUtils::GetTableCellElementIfOnlyOneSelected(*range)) {
+ mSelectedCellElements.AppendElement(*selectedCellElement);
+ }
+ }
+ }
+
+ explicit SelectedTableCellScanner(const AutoRangeArray& aRanges);
+
+ bool IsInTableCellSelectionMode() const {
+ return !mSelectedCellElements.IsEmpty();
+ }
+
+ const nsTArray<OwningNonNull<Element>>& ElementsRef() const {
+ return mSelectedCellElements;
+ }
+
+ /**
+ * GetFirstElement() and GetNextElement() are stateful iterator methods.
+ * This is useful to port legacy code which used old `nsITableEditor` API.
+ */
+ Element* GetFirstElement() const {
+ MOZ_ASSERT(!mSelectedCellElements.IsEmpty());
+ mIndex = 0;
+ return !mSelectedCellElements.IsEmpty() ? mSelectedCellElements[0].get()
+ : nullptr;
+ }
+ Element* GetNextElement() const {
+ MOZ_ASSERT(mIndex < mSelectedCellElements.Length());
+ return ++mIndex < mSelectedCellElements.Length()
+ ? mSelectedCellElements[mIndex].get()
+ : nullptr;
+ }
+
+ private:
+ AutoTArray<OwningNonNull<Element>, 16> mSelectedCellElements;
+ mutable size_t mIndex = 0;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef HTMLEditUtils_h