This is an automated change, run as a combination of $ ./mach static-analysis check --checks='-*,modernize-type-traits' --fix -j10 $ ./mach format As --fix doesn't affect expansion within macro, an extra manual step was needed though. Differential Revision: https://phabricator.services.mozilla.com/D306715
3421 lines
141 KiB
C++
3421 lines
141 KiB
C++
/* 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 "EditorLineBreak.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/CharacterDataBuffer.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;
|
|
using WhitespaceOption = dom::CharacterDataBuffer::WhitespaceOption;
|
|
using WhitespaceOptions = dom::CharacterDataBuffer::WhitespaceOptions;
|
|
|
|
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 aNode is editable or not in a composed doc. This is useful
|
|
* if the caller may modify document fragment before inserting it into a
|
|
* Document.
|
|
*/
|
|
static bool NodeIsEditableOrNotInComposedDoc(const nsINode& aNode) {
|
|
return MOZ_UNLIKELY(!aNode.IsInComposedDoc()) || aNode.IsEditable();
|
|
}
|
|
|
|
/**
|
|
* Return true if aElement is an editing host which is either:
|
|
* - the root element
|
|
* - parent is not editable
|
|
* - the <body> element of the document
|
|
*/
|
|
[[nodiscard]] static bool ElementIsEditableRoot(const Element& aElement);
|
|
|
|
/**
|
|
* 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() && !aContent.IsHTMLElement(nsGkAtoms::button) &&
|
|
(!HTMLEditUtils::IsContainerNode(aContent) ||
|
|
HTMLEditUtils::IsReplacedElement(*aContent.AsElement()) ||
|
|
aContent.IsAnyOfHTMLElements(nsGkAtoms::applet, nsGkAtoms::colgroup,
|
|
nsGkAtoms::frameset, nsGkAtoms::head,
|
|
nsGkAtoms::html));
|
|
}
|
|
|
|
enum class ReplaceOrVoidElementOption {
|
|
LookForOnlyVoidElement,
|
|
LookForOnlyReplaceElement,
|
|
LookForOnlyNonVoidReplacedElement,
|
|
LookForReplacedOrVoidElement,
|
|
};
|
|
|
|
/**
|
|
* Return an inclusive ancestor replaced element or void element of aContent.
|
|
* I.e., if this returns non-nullptr, aContent is in a replaced element or a
|
|
* void element.
|
|
*/
|
|
[[nodiscard]] static Element* GetInclusiveAncestorReplacedOrVoidElement(
|
|
const nsIContent& aContent, ReplaceOrVoidElementOption aOption) {
|
|
const bool lookForAnyReplaceElement =
|
|
aOption == ReplaceOrVoidElementOption::LookForOnlyReplaceElement ||
|
|
aOption == ReplaceOrVoidElementOption::LookForReplacedOrVoidElement;
|
|
const bool lookForNonVoidReplacedElement =
|
|
aOption ==
|
|
ReplaceOrVoidElementOption::LookForOnlyNonVoidReplacedElement;
|
|
const bool lookForVoidElement =
|
|
aOption == ReplaceOrVoidElementOption::LookForOnlyVoidElement ||
|
|
aOption == ReplaceOrVoidElementOption::LookForReplacedOrVoidElement;
|
|
Element* lastReplacedOrVoidElement = nullptr;
|
|
for (Element* const element :
|
|
aContent.InclusiveAncestorsOfType<Element>()) {
|
|
// XXX I think we should not treat <button> contents as editable
|
|
if (lookForAnyReplaceElement &&
|
|
!element->IsHTMLElement(nsGkAtoms::button) &&
|
|
HTMLEditUtils::IsReplacedElement(*element)) {
|
|
lastReplacedOrVoidElement = element;
|
|
} else if (lookForNonVoidReplacedElement &&
|
|
!element->IsHTMLElement(nsGkAtoms::button) &&
|
|
HTMLEditUtils::IsNonVoidReplacedElement(*element)) {
|
|
lastReplacedOrVoidElement = element;
|
|
} else if (lookForVoidElement &&
|
|
!HTMLEditUtils::IsContainerNode(*element)) {
|
|
lastReplacedOrVoidElement = element;
|
|
}
|
|
}
|
|
return lastReplacedOrVoidElement;
|
|
}
|
|
|
|
/*
|
|
* 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);
|
|
|
|
/**
|
|
* Returns true if aContent is an element and it should be treated as a block.
|
|
*
|
|
* @param aBlockInlineCheck
|
|
* - If UseHTMLDefaultStyle, 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 <br> 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 laid 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 a visible
|
|
* element when aContent is empty. If aContent has a primary frame (even if
|
|
* dirty), this checks whether aContent is actually visible. Otherwise, this
|
|
* guesses it from the element type.
|
|
*/
|
|
[[nodiscard]] static bool IsVisibleElementEvenIfLeafNode(
|
|
const nsIContent& aContent);
|
|
|
|
/**
|
|
* Return true if aContent is an inline element which formats the content
|
|
* without giving any meanings. E.g., <b>, <big>, <sub>, <sup>, etc, but
|
|
* not <em>, <strong>, etc.
|
|
*/
|
|
[[nodiscard]] static bool IsInlineStyleElement(const nsIContent& aContent);
|
|
|
|
/**
|
|
* 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 aContent is a flex item or a grid item. Note that if
|
|
* aContent is the `Text` node in the following case, this returns `true`.
|
|
* <div style="display:flex"><span style="display:contents">text</span></div>
|
|
*/
|
|
[[nodiscard]] static bool IsFlexOrGridItem(const nsIContent& aContent);
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* Return true if aContent is an element which can be outdented such as
|
|
* a list element, a list-item element or a <blockquote>.
|
|
*/
|
|
[[nodiscard]] static bool IsOutdentable(const nsIContent& aContent);
|
|
|
|
/**
|
|
* Return true if aContent is one of <h1>, <h2>, <h3>, <h4>, <h5> or <h6>.
|
|
*/
|
|
[[nodiscard]] static bool IsHeadingElement(const nsIContent& aContent);
|
|
|
|
/**
|
|
* Return true if aContent is a list item element such as <li>, <dt> or <dd>.
|
|
*/
|
|
[[nodiscard]] static bool IsListItemElement(const nsIContent& aContent);
|
|
[[nodiscard]] static bool IsListItemElement(const nsIContent* aContent) {
|
|
return aContent && IsListItemElement(*aContent);
|
|
}
|
|
|
|
/**
|
|
* Return true if aContent is a <tr>.
|
|
*/
|
|
[[nodiscard]] static bool IsTableRowElement(const nsIContent& aContent);
|
|
[[nodiscard]] static bool IsTableRowElement(const nsIContent* aContent) {
|
|
return aContent && IsTableRowElement(*aContent);
|
|
}
|
|
|
|
/**
|
|
* Return true if aContent is an element which makes a table and is not a
|
|
* <col> nor a <colgroup>. So, <table>, <caption>, <tbody>, <tr>, <td>, etc.
|
|
*/
|
|
[[nodiscard]] static bool IsAnyTableElementExceptColumnElement(
|
|
const nsIContent& aContent);
|
|
|
|
/**
|
|
* Return true if aContent is an element which makes a table and nis not a
|
|
* <col>, <colgroup> nor <table>.
|
|
*/
|
|
[[nodiscard]] static bool IsAnyTableElementExceptTableElementAndColumElement(
|
|
const nsIContent& aContent);
|
|
|
|
/**
|
|
* Return true if aContent is a table cell such as <td> or <th>.
|
|
*/
|
|
[[nodiscard]] static bool IsTableCellElement(const nsIContent& aContent);
|
|
[[nodiscard]] static bool IsTableCellElement(const nsIContent* aContent) {
|
|
return aContent && IsTableCellElement(*aContent);
|
|
}
|
|
|
|
/**
|
|
* Return true if aContent is a table cell or a caption, i.e., that may
|
|
* contain visible content.
|
|
*/
|
|
[[nodiscard]] static bool IsTableCellOrCaptionElement(
|
|
const nsIContent& aContent);
|
|
|
|
/**
|
|
* Return true if aContent is a list element such as <ul>, <ol> or <dl>.
|
|
*/
|
|
[[nodiscard]] static bool IsListElement(const nsIContent& aContent);
|
|
[[nodiscard]] static bool IsListElement(const nsIContent* aContent) {
|
|
return aContent && IsListElement(*aContent);
|
|
}
|
|
|
|
/**
|
|
* Return true if aContent is an <img>.
|
|
* XXX Should this return true for other elements which is replaced with an
|
|
* image like <object>?
|
|
*/
|
|
[[nodiscard]] static bool IsImageElement(const nsIContent& aContent);
|
|
|
|
/**
|
|
* Return true if aContent is an <a> which has non-empty `href` attribute
|
|
* value.
|
|
*/
|
|
[[nodiscard]] static bool IsHyperlinkElement(const nsIContent& aContent);
|
|
|
|
/**
|
|
* Return true if aContent is an <a> which has non-empty `name` attribute
|
|
* value.
|
|
*/
|
|
[[nodiscard]] static bool IsNamedAnchorElement(const nsIContent& aContent);
|
|
|
|
/**
|
|
* Return true if aContent is a <div type="_moz">.
|
|
*/
|
|
[[nodiscard]] static bool IsMozDivElement(const nsIContent& aContent);
|
|
|
|
/**
|
|
* Return true if aElement is a mailcite element in the mail editor.
|
|
*/
|
|
[[nodiscard]] static bool IsMailCiteElement(const Element& aElement);
|
|
|
|
/**
|
|
* Return true if aElement is a replaced element.
|
|
*/
|
|
[[nodiscard]] static bool IsReplacedElement(const Element& aElement);
|
|
|
|
/**
|
|
* Return true if aContent is a replaced element.
|
|
*/
|
|
[[nodiscard]] static bool IsReplacedElement(const nsIContent& aContent) {
|
|
return aContent.IsElement() && IsReplacedElement(*aContent.AsElement());
|
|
}
|
|
|
|
/**
|
|
* Return true if aElement is a non-void replaced element such as <iframe>,
|
|
* <embed>, <audio>, <video>, <select>, etc.
|
|
*/
|
|
[[nodiscard]] static bool IsNonVoidReplacedElement(const Element& aElement) {
|
|
return IsReplacedElement(aElement) && IsContainerNode(aElement);
|
|
}
|
|
|
|
/**
|
|
* Return true if aContent is an element which can ahve `align` attribute.
|
|
*/
|
|
[[nodiscard]] static bool IsAlignAttrSupported(const nsIContent& aContent);
|
|
|
|
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<nsAtom*>(&aChildNodeName));
|
|
}
|
|
|
|
nsHTMLTag parentTagEnum =
|
|
nsHTMLTags::AtomTagToId(const_cast<nsAtom*>(&aParentNodeName));
|
|
return HTMLEditUtils::CanNodeContain(parentTagEnum, childTagEnum);
|
|
}
|
|
|
|
/**
|
|
* Return a point where can insert a node whose name is aInsertNodeName.
|
|
* Note that if the container of aPointToInsert is not an element, this check
|
|
* whether aInsertNodeName can be inserted into the element. Therefore, the
|
|
* caller may need to split the container when actually inserting a node.
|
|
*/
|
|
[[nodiscard]] static EditorDOMPoint GetPossiblePointToInsert(
|
|
const EditorDOMPoint& aPointToInsert, const nsAtom& aInsertNodeName,
|
|
const Element& aEditingHost) {
|
|
if (MOZ_UNLIKELY(!aPointToInsert.IsInContentNode())) {
|
|
return EditorDOMPoint();
|
|
}
|
|
EditorDOMPoint pointToInsert(aPointToInsert);
|
|
// We shouldn't modify the subtree in a replaced element so that we need to
|
|
// test whether aInsertNodeName is inserted with inclusive ancestors
|
|
// starting from the most distant replaced element ancestor.
|
|
if (Element* const replacedOrVoidElement =
|
|
HTMLEditUtils::GetInclusiveAncestorReplacedOrVoidElement(
|
|
*aPointToInsert.GetContainer()->AsContent(),
|
|
ReplaceOrVoidElementOption::LookForReplacedOrVoidElement)) {
|
|
if (MOZ_UNLIKELY(replacedOrVoidElement == &aEditingHost) ||
|
|
MOZ_UNLIKELY(
|
|
!replacedOrVoidElement->IsInclusiveDescendantOf(&aEditingHost))) {
|
|
return EditorDOMPoint();
|
|
}
|
|
pointToInsert.Set(replacedOrVoidElement);
|
|
}
|
|
if ((pointToInsert.IsInTextNode() &&
|
|
&aInsertNodeName == nsGkAtoms::textTagName) ||
|
|
HTMLEditUtils::CanNodeContain(*pointToInsert.GetContainer(),
|
|
aInsertNodeName)) {
|
|
return pointToInsert;
|
|
}
|
|
if (pointToInsert.IsInTextNode()) {
|
|
Element* const parentElement =
|
|
pointToInsert.GetContainerParentAs<Element>();
|
|
if (NS_WARN_IF(!parentElement)) {
|
|
return EditorDOMPoint();
|
|
}
|
|
if (HTMLEditUtils::CanNodeContain(*parentElement, aInsertNodeName)) {
|
|
// Okay, the insertion point should be fine even though the caller needs
|
|
// to split the `Text`.
|
|
return pointToInsert;
|
|
}
|
|
}
|
|
nsIContent* lastContent = pointToInsert.GetContainer()->AsContent();
|
|
for (Element* const element : lastContent->AncestorsOfType<Element>()) {
|
|
if (HTMLEditUtils::CanNodeContain(*element, aInsertNodeName)) {
|
|
return EditorDOMPoint(lastContent);
|
|
}
|
|
if (MOZ_UNLIKELY(element == &aEditingHost)) {
|
|
return EditorDOMPoint();
|
|
}
|
|
lastContent = element;
|
|
}
|
|
return pointToInsert;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Return a point which can insert a node whose name is aTagName scanning
|
|
* from aPoint to its ancestor points.
|
|
*/
|
|
template <typename EditorDOMPointType>
|
|
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<nsIContent>()
|
|
->template InclusiveAncestorsOfType<Element>()) {
|
|
if (!HTMLEditUtils::IsSimplyEditableNode(*containerElement)) {
|
|
return EditorDOMPoint();
|
|
}
|
|
if (HTMLEditUtils::CanNodeContain(*containerElement, aTagName)) {
|
|
return lastChild ? EditorDOMPoint(lastChild)
|
|
: aPoint.template To<EditorDOMPoint>();
|
|
}
|
|
if (containerElement == aAncestorLimit) {
|
|
return EditorDOMPoint();
|
|
}
|
|
lastChild = containerElement;
|
|
}
|
|
return EditorDOMPoint();
|
|
}
|
|
|
|
/**
|
|
* IsContainerNode() returns true if aContent is a container node.
|
|
*/
|
|
[[nodiscard]] static bool IsContainerNode(const nsIContent& aContent) {
|
|
if (aContent.IsCharacterData()) {
|
|
return false;
|
|
}
|
|
return HTMLEditUtils::IsContainerNode(
|
|
// XXX Why don't we use nsHTMLTags::AtomTagToId? Are there some
|
|
// difference?
|
|
nsHTMLTags::StringTagToId(aContent.NodeName()));
|
|
}
|
|
|
|
/**
|
|
* 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::GetInclusiveAncestorReplacedOrVoidElement(
|
|
aContent,
|
|
ReplaceOrVoidElementOption::LookForReplacedOrVoidElement);
|
|
}
|
|
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
|
|
*/
|
|
[[nodiscard]] static bool IsNonListSingleLineContainer(
|
|
const nsIContent& aContent);
|
|
[[nodiscard]] static bool IsSingleLineContainer(const nsIContent& aContent);
|
|
|
|
/**
|
|
* Return true if aText has only a linefeed and it's preformatted.
|
|
*/
|
|
[[nodiscard]] static bool TextHasOnlyOnePreformattedLinefeed(
|
|
const Text& aText) {
|
|
return aText.TextDataLength() == 1u &&
|
|
aText.DataBuffer().CharAt(0u) == kNewLine &&
|
|
EditorUtils::IsNewLinePreformatted(aText);
|
|
}
|
|
|
|
enum class TreatInvisibleLineBreakAs : bool { Invisible, Visible };
|
|
|
|
/**
|
|
* IsVisibleTextNode() returns true if aText has visible text. If it has
|
|
* only white-spaces and they are collapsed, returns false.
|
|
* If aText has only a preformatted line break which may be surrounded by
|
|
* collapsible white-spaces, retrun true if aTreatInvisibleLineBreakAs is
|
|
* Visible, but false otherwise.
|
|
*/
|
|
[[nodiscard]] static bool IsVisibleTextNode(
|
|
const Text& aText, TreatInvisibleLineBreakAs aTreatInvisibleLineBreakAs);
|
|
|
|
/**
|
|
* 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);
|
|
|
|
/**
|
|
* Return true if aContent is a <br> element and it's followed by a block
|
|
* boundary.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If aContent is followed by a
|
|
* block boundary, this will be set to the block
|
|
* element without add-ref.
|
|
*/
|
|
[[nodiscard]] static bool IsBRElementFollowedByBlockBoundary(
|
|
const nsIContent& aContent, const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr) {
|
|
const auto* const brElement = dom::HTMLBRElement::FromNode(aContent);
|
|
return brElement &&
|
|
IsBRElementFollowedByBlockBoundary(*brElement, aAncestorLimiter,
|
|
aFollowingBlockBoundaryElement);
|
|
}
|
|
|
|
/**
|
|
* Return true if aBRElement is followed by a block boundary.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If aBRElement is followed by a
|
|
* block boundary, this will be set to the block
|
|
* element without add-ref.
|
|
*/
|
|
[[nodiscard]] static bool IsBRElementFollowedByBlockBoundary(
|
|
const dom::HTMLBRElement& aBRElement,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr);
|
|
|
|
/**
|
|
* Return true if aContent is a <br> element and it's followed by the current
|
|
* block boundary.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If aContent is followed by a
|
|
* block boundary, this will be set to the block
|
|
* element without add-ref.
|
|
*/
|
|
[[nodiscard]] static bool IsBRElementFollowedByCurrentBlockBoundary(
|
|
const nsIContent& aContent, const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr) {
|
|
const auto* const brElement = dom::HTMLBRElement::FromNode(aContent);
|
|
return brElement &&
|
|
IsBRElementFollowedByCurrentBlockBoundary(
|
|
*brElement, aAncestorLimiter, aFollowingBlockBoundaryElement);
|
|
}
|
|
|
|
/**
|
|
* Return true if aBRElement is followed by the current block boundary.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If aBRElement is followed by a
|
|
* block boundary, this will be set to the block
|
|
* element without add-ref.
|
|
*/
|
|
[[nodiscard]] static bool IsBRElementFollowedByCurrentBlockBoundary(
|
|
const dom::HTMLBRElement& aBRElement,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr);
|
|
|
|
/**
|
|
* Return true if aContent is a <br> element which is following the current
|
|
* block boundary.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If aContent is followed by a
|
|
* block boundary, this will be set to the block
|
|
* element without add-ref.
|
|
*/
|
|
[[nodiscard]] static bool IsBRElementFollowingCurrentBlockBoundary(
|
|
const nsIContent& aContent, const Element* aAncestorLimiter = nullptr,
|
|
Element** aPrecedingBlockBoundaryElement = nullptr) {
|
|
const auto* const brElement = dom::HTMLBRElement::FromNode(aContent);
|
|
return brElement &&
|
|
IsBRElementFollowingCurrentBlockBoundary(
|
|
*brElement, aAncestorLimiter, aPrecedingBlockBoundaryElement);
|
|
}
|
|
|
|
/**
|
|
* Return true if aBRElement is following the current block boundary.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If aBRElement is followed by a
|
|
* block boundary, this will be set to the block
|
|
* element without add-ref.
|
|
*/
|
|
[[nodiscard]] static bool IsBRElementFollowingCurrentBlockBoundary(
|
|
const dom::HTMLBRElement& aBRElement,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aPrecedingBlockBoundaryElement = nullptr);
|
|
|
|
/**
|
|
* Return true if aContent is a <br> element and it's followed by a block
|
|
* boundary which is not of the current block.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If aContent is followed by a
|
|
* block boundary, this will be set to the block
|
|
* element without add-ref.
|
|
*/
|
|
[[nodiscard]] static bool IsBRElementFollowedByOtherBlockBoundary(
|
|
const nsIContent& aContent, const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr) {
|
|
const auto* const brElement = dom::HTMLBRElement::FromNode(aContent);
|
|
return brElement &&
|
|
IsBRElementFollowedByOtherBlockBoundary(
|
|
*brElement, aAncestorLimiter, aFollowingBlockBoundaryElement);
|
|
}
|
|
|
|
/**
|
|
* Return true if aBRElement is followed by a block boundary which is not of
|
|
* the the current block.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If aBRElement is followed by a
|
|
* block boundary, this will be set to the block
|
|
* element without add-ref.
|
|
*/
|
|
[[nodiscard]] static bool IsBRElementFollowedByOtherBlockBoundary(
|
|
const dom::HTMLBRElement& aBRElement,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr);
|
|
|
|
/**
|
|
* Return true if aContent is a <br> element and it's followed by a line
|
|
* boundary such as a block boundary or a line break.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If aContent is followed by a
|
|
* block boundary, this will be set to the block
|
|
* element without add-ref. If followed by a line
|
|
* break, this is set to nullptr.
|
|
*/
|
|
[[nodiscard]] static bool IsBRElementFollowedByLineBoundary(
|
|
const nsIContent& aContent, const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr) {
|
|
const auto* const brElement = dom::HTMLBRElement::FromNode(aContent);
|
|
return brElement &&
|
|
IsBRElementFollowedByLineBoundary(*brElement, aAncestorLimiter,
|
|
aFollowingBlockBoundaryElement);
|
|
}
|
|
|
|
/**
|
|
* Return true if aBRElement is followed by a line boundary
|
|
* such as a block boundary or a line break.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If aBRElement is followed by a
|
|
* block boundary, this will be set to the block
|
|
* element without add-ref. If followed by a line
|
|
* break, this is set to nullptr.
|
|
*/
|
|
[[nodiscard]] static bool IsBRElementFollowedByLineBoundary(
|
|
const dom::HTMLBRElement& aBRElement,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr);
|
|
|
|
/**
|
|
* Return true if aContent is a <br> element and it follows a line boundary
|
|
* such as a block boundary or a line break.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aPrecedingBlockBoundaryElement
|
|
* [out][optional] If aContent follows a block
|
|
* boundary, this will be set to the block element
|
|
* without add-ref. If follows a line break, this
|
|
* is set to nullptr.
|
|
*/
|
|
[[nodiscard]] static bool IsBRElementFollowingLineBoundary(
|
|
const nsIContent& aContent, const Element* aAncestorLimiter = nullptr,
|
|
Element** aPrecedingBlockBoundaryElement = nullptr) {
|
|
const auto* const brElement = dom::HTMLBRElement::FromNode(aContent);
|
|
return brElement &&
|
|
IsBRElementFollowingLineBoundary(*brElement, aAncestorLimiter,
|
|
aPrecedingBlockBoundaryElement);
|
|
}
|
|
|
|
/**
|
|
* Return true if aBRElement follows a line boundary such as a block boundary
|
|
* or a line break.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aPrecedingBlockBoundaryElement
|
|
* [out][optional] If aBRElement follows a block
|
|
* boundary, this will be set to the block element
|
|
* without add-ref. If follows a line break, this
|
|
* is set to nullptr.
|
|
*/
|
|
[[nodiscard]] static bool IsBRElementFollowingLineBoundary(
|
|
const dom::HTMLBRElement& aBRElement,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aPrecedingBlockBoundaryElement = nullptr);
|
|
|
|
/**
|
|
* Return true if aContent is a <br> element and it follows a line break.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
*/
|
|
[[nodiscard]] static bool IsBRElementFollowingLineBreak(
|
|
const nsIContent& aContent, const Element* aAncestorLimiter = nullptr) {
|
|
const auto* const brElement = dom::HTMLBRElement::FromNode(aContent);
|
|
return brElement &&
|
|
IsBRElementFollowingLineBreak(*brElement, aAncestorLimiter);
|
|
}
|
|
|
|
/**
|
|
* Return true if aBRElement follows a line break.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
*/
|
|
[[nodiscard]] static bool IsBRElementFollowingLineBreak(
|
|
const dom::HTMLBRElement& aBRElement,
|
|
const Element* aAncestorLimiter = nullptr);
|
|
|
|
/**
|
|
* Return true if aContent is a <br> element and does not affect to the
|
|
* layout, i.e., if it's invisible.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If aContent is unnecessary, this
|
|
* will be set to the element whose block boundary
|
|
* follows aContent without add-ref.
|
|
*/
|
|
[[nodiscard]] static bool IsUnnecessaryBRElement(
|
|
const nsIContent& aContent, PaddingForEmptyBlock aPaddingForEmptyBlock,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr) {
|
|
const auto* const brElement = dom::HTMLBRElement::FromNode(aContent);
|
|
return brElement && IsUnnecessaryBRElement(
|
|
*brElement, aPaddingForEmptyBlock, aAncestorLimiter,
|
|
aFollowingBlockBoundaryElement);
|
|
}
|
|
|
|
/**
|
|
* Return true if aBRElement does not affect to the layout. I.e., if it's
|
|
* invisible.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If aBRElement is unnecessary,
|
|
* this will be set to the element whose block
|
|
* boundary follows aBRElement without add-ref.
|
|
*/
|
|
[[nodiscard]] static bool IsUnnecessaryBRElement(
|
|
const dom::HTMLBRElement& aBRElement,
|
|
PaddingForEmptyBlock aPaddingForEmptyBlock,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr);
|
|
|
|
/**
|
|
* Return true if aContent is a <br> element and it actually represents a line
|
|
* break, i.e., if it's visible.
|
|
*/
|
|
[[nodiscard]] static bool IsSignificantBRElement(
|
|
const nsIContent& aContent, PaddingForEmptyBlock aPaddingForEmptyBlock,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr) {
|
|
const auto* const brElement = dom::HTMLBRElement::FromNode(aContent);
|
|
return brElement && !IsUnnecessaryBRElement(
|
|
*brElement, aPaddingForEmptyBlock, aAncestorLimiter,
|
|
aFollowingBlockBoundaryElement);
|
|
}
|
|
|
|
/**
|
|
* Return true if aBRElement actually represents a line break, i.e., if it's
|
|
* visible.
|
|
*/
|
|
[[nodiscard]] static bool IsSignificantBRElement(
|
|
const dom::HTMLBRElement& aBRElement,
|
|
PaddingForEmptyBlock aPaddingForEmptyBlock,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr) {
|
|
return !IsUnnecessaryBRElement(aBRElement, aPaddingForEmptyBlock,
|
|
aAncestorLimiter,
|
|
aFollowingBlockBoundaryElement);
|
|
}
|
|
|
|
enum class SkipWhiteSpaceStyleCheck : bool { No, Yes };
|
|
|
|
/**
|
|
* Return true if the character at aPoint is a preformatted linefeed and
|
|
* followed by a block boundary.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If the linefeed is followed by a
|
|
* block boundary, this will be set to the block
|
|
* element without add-ref.
|
|
*/
|
|
template <typename EditorDOMPointType>
|
|
[[nodiscard]] static bool IsPreformattedLineBreakFollowedByBlockBoundary(
|
|
const EditorDOMPointType& aPoint,
|
|
SkipWhiteSpaceStyleCheck aSkipWhiteSpaceStyleCheck =
|
|
SkipWhiteSpaceStyleCheck::No,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr);
|
|
|
|
/**
|
|
* Return true if the character at aPoint is a preformatted linefeed and
|
|
* followed by the current block boundary.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If the linefeed is followed by a
|
|
* block boundary, this will be set to the block
|
|
* element without add-ref.
|
|
*/
|
|
template <typename EditorDOMPointType>
|
|
[[nodiscard]] static bool
|
|
IsPreformattedLineBreakFollowedByCurrentBlockBoundary(
|
|
const EditorDOMPointType& aPoint,
|
|
SkipWhiteSpaceStyleCheck aSkipWhiteSpaceStyleCheck =
|
|
SkipWhiteSpaceStyleCheck::No,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr);
|
|
|
|
/**
|
|
* Return true if the character at aPoint is a preformatted linefeed and
|
|
* follows the current block boundary.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aPrecedingBlockBoundaryElement
|
|
* [out][optional] If the linefeed is following
|
|
* current block boundary, this will be set to the
|
|
* block element without add-ref.
|
|
*/
|
|
template <typename EditorDOMPointType>
|
|
[[nodiscard]] static bool
|
|
IsPreformattedLineBreakFollowingCurrentBlockBoundary(
|
|
const EditorDOMPointType& aPoint,
|
|
SkipWhiteSpaceStyleCheck aSkipWhiteSpaceStyleCheck =
|
|
SkipWhiteSpaceStyleCheck::No,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aPrecedingBlockBoundaryElement = nullptr);
|
|
|
|
/**
|
|
* Return true if the character at aPoint is a preformatted linefeed and
|
|
* followed by a block boundary which is not of the current block.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If the linefeed is followed by a
|
|
* block boundary, this will be set to the block
|
|
* element without add-ref.
|
|
*/
|
|
template <typename EditorDOMPointType>
|
|
[[nodiscard]] static bool IsPreformattedLineBreakFollowedByOtherBlockBoundary(
|
|
const EditorDOMPointType& aPoint,
|
|
SkipWhiteSpaceStyleCheck aSkipWhiteSpaceStyleCheck =
|
|
SkipWhiteSpaceStyleCheck::No,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr);
|
|
|
|
/**
|
|
* Return true if the character at aPoint is a preformatted linefeed and
|
|
* followed by a line boundary such as a block boundary or a line break.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If the linefeed is followed by a
|
|
* block boundary, this will be set to the block
|
|
* element without add-ref. If the linefeed is
|
|
* followed by a line break, this will be set to
|
|
* nullptr.
|
|
*/
|
|
template <typename EditorDOMPointType>
|
|
[[nodiscard]] static bool IsPreformattedLineBreakFollowedByLineBoundary(
|
|
const EditorDOMPointType& aPoint,
|
|
SkipWhiteSpaceStyleCheck aSkipWhiteSpaceStyleCheck =
|
|
SkipWhiteSpaceStyleCheck::No,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr);
|
|
|
|
/**
|
|
* Return true if the character at aPoint is a preformatted linefeed and
|
|
* follows a line boundary such as a block boundary or a line break.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aPrecedingBlockBoundaryElement
|
|
* [out][optional] If the linefeed follows a block
|
|
* boundary, this will be set to the block element
|
|
* without add-ref. If follows a line break, this
|
|
* is set to nullptr.
|
|
*/
|
|
template <typename EditorDOMPointType>
|
|
[[nodiscard]] static bool IsPreformattedLineBreakFollowingLineBoundary(
|
|
const EditorDOMPointType& aPoint,
|
|
SkipWhiteSpaceStyleCheck aSkipWhiteSpaceStyleCheck =
|
|
SkipWhiteSpaceStyleCheck::No,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aPrecedingBlockBoundaryElement = nullptr);
|
|
|
|
/**
|
|
* Return true if the character at aPoint is a preformatted linefeed and
|
|
* follows a line break.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
*/
|
|
template <typename EditorDOMPointType>
|
|
[[nodiscard]] static bool IsPreformattedLineBreakFollowingLineBreak(
|
|
const EditorDOMPointType& aPoint,
|
|
SkipWhiteSpaceStyleCheck aSkipWhiteSpaceStyleCheck =
|
|
SkipWhiteSpaceStyleCheck::No,
|
|
const Element* aAncestorLimiter = nullptr);
|
|
|
|
/**
|
|
* Return true if the character at aPoint is a preformatted linefeed and
|
|
* does not affect to the layout, i.e., if it's an invisible linefeed.
|
|
*
|
|
* @param aAncestorLimiter [optional] If set, this stops scanning the DOM
|
|
* when it reaches the element boundary. If this
|
|
* is an inline editing host, the result may be
|
|
* changed.
|
|
* @param aFollowingBlockBoundaryElement
|
|
* [out][optional] If the linefeed is unnecessary,
|
|
* this will be set to the element whose block
|
|
* boundary follows the linefeed without add-ref.
|
|
*/
|
|
template <typename EditorDOMPointType>
|
|
[[nodiscard]] static bool IsUnnecessaryPreformattedLineBreak(
|
|
const EditorDOMPointType& aPoint,
|
|
PaddingForEmptyBlock aPaddingForEmptyBlock,
|
|
SkipWhiteSpaceStyleCheck aSkipWhiteSpaceStyleCheck =
|
|
SkipWhiteSpaceStyleCheck::No,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr);
|
|
|
|
/**
|
|
* Return true if the character at aPoint is a preformatted linefeed and it
|
|
* actually represents a line break, i.e., if it's a visible linefeed.
|
|
*/
|
|
template <typename EditorDOMPointType>
|
|
[[nodiscard]] static bool IsSignificantPreformattedLineBreak(
|
|
const EditorDOMPointType& aPoint,
|
|
PaddingForEmptyBlock aPaddingForEmptyBlock,
|
|
SkipWhiteSpaceStyleCheck aSkipWhiteSpaceStyleCheck =
|
|
SkipWhiteSpaceStyleCheck::No,
|
|
const Element* aAncestorLimiter = nullptr,
|
|
Element** aFollowingBlockBoundaryElement = nullptr);
|
|
|
|
/**
|
|
* Return an invisible line break before aPoint if and only if there is.
|
|
* Note that the result may be non-editable and/or non-removable linebreak.
|
|
*/
|
|
template <typename EditorLineBreakType, typename EditorDOMPointType>
|
|
[[nodiscard]] static Maybe<EditorLineBreakType>
|
|
GetPrecedingUnnecessaryLineBreak(const EditorDOMPointType& aPoint,
|
|
const Element* aAncestorLimiter = nullptr);
|
|
|
|
/**
|
|
* Return a following visible thing with ignoring unnecessary line break.
|
|
* You can get the skipped unnecessary line break with
|
|
* WSScanResult::MaybeIgnoredLineBreak().
|
|
*/
|
|
template <typename EditorDOMPointType>
|
|
[[nodiscard]] static WSScanResult
|
|
ScanInclusiveNextThingWithIgnoringUnnecessaryLineBreak(
|
|
const EditorDOMPointType& aPoint,
|
|
PaddingForEmptyBlock aPaddingForEmptyBlock, const Element& aEditingHost,
|
|
const Element* aAncestorLimiter = nullptr);
|
|
|
|
enum class IgnoreInvisibleLineBreak { No, Yes };
|
|
|
|
/**
|
|
* Return true if aPoint is immediately before current block boundary. If
|
|
* aIgnoreInvisibleLineBreak is "Yes", this returns true if aPoint is before
|
|
* invisible line break before a block boundary.
|
|
*/
|
|
template <typename PT, typename CT>
|
|
[[nodiscard]] static bool PointIsImmediatelyBeforeCurrentBlockBoundary(
|
|
const EditorDOMPointBase<PT, CT>& aPoint,
|
|
IgnoreInvisibleLineBreak aIgnoreInvisibleLineBreak);
|
|
|
|
/**
|
|
* Return true if aRange crosses the inclusive ancestor block element at
|
|
* start boundary, in other words, if aRange ends outside of the inclusive
|
|
* ancestor block of the start boundary.
|
|
*/
|
|
template <typename EditorDOMPointType>
|
|
[[nodiscard]] static bool RangeIsAcrossStartBlockBoundary(
|
|
const EditorDOMRangeBase<EditorDOMPointType>& aRange,
|
|
BlockInlineCheck aBlockInlineCheck) {
|
|
MOZ_ASSERT(aRange.IsPositionedAndValid());
|
|
if (MOZ_UNLIKELY(!aRange.StartRef().IsInContentNode())) {
|
|
return false;
|
|
}
|
|
const Element* const startBlockElement =
|
|
HTMLEditUtils::GetInclusiveAncestorElement(
|
|
*aRange.StartRef().template ContainerAs<nsIContent>(),
|
|
ClosestBlockElement,
|
|
UseComputedDisplayStyleIfAuto(aBlockInlineCheck));
|
|
if (MOZ_UNLIKELY(!startBlockElement)) {
|
|
return false;
|
|
}
|
|
return EditorRawDOMPoint::After(*startBlockElement)
|
|
.EqualsOrIsBefore(aRange.EndRef());
|
|
}
|
|
|
|
/**
|
|
* Return true if `display` of inclusive ancestor of aContent is `none`.
|
|
*/
|
|
[[nodiscard]] static bool IsInclusiveAncestorCSSDisplayNone(
|
|
const nsIContent& aContent, const nsIContent* aAncestorLimiter = nullptr);
|
|
|
|
/**
|
|
* Return a point to insert a padding line break if aPoint is following a
|
|
* block boundary and the line containing aPoint requires a following padding
|
|
* line break to make the line visible.
|
|
*/
|
|
template <typename PT, typename CT>
|
|
static EditorDOMPoint LineRequiresPaddingLineBreakToBeVisible(
|
|
const EditorDOMPointBase<PT, CT>& aPoint, const Element& aEditingHost);
|
|
|
|
/**
|
|
* ShouldInsertLinefeedCharacter() returns true if the caller should insert
|
|
* a linefeed character instead of <br> element.
|
|
*/
|
|
static bool ShouldInsertLinefeedCharacter(
|
|
const EditorDOMPoint& aPointToInsert, const Element& aEditingHost);
|
|
|
|
enum class EmptyCheckOption {
|
|
TreatSingleBRElementAsVisible,
|
|
TreatBlockAsVisible,
|
|
TreatListItemAsVisible,
|
|
TreatTableCellAsVisible,
|
|
TreatNonEditableContentAsInvisible,
|
|
TreatCommentAsVisible,
|
|
SafeToAskLayout,
|
|
};
|
|
using EmptyCheckOptions = EnumSet<EmptyCheckOption, uint32_t>;
|
|
|
|
friend std::ostream& operator<<(std::ostream& aStream,
|
|
const EmptyCheckOption& aOption);
|
|
friend std::ostream& operator<<(std::ostream& aStream,
|
|
const EmptyCheckOptions& aOptions);
|
|
|
|
/**
|
|
* Return 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 meeting visible things.
|
|
*/
|
|
static bool IsEmptyNode(nsPresContext* aPresContext, const nsINode& aNode,
|
|
const EmptyCheckOptions& aOptions = {},
|
|
bool* aSeenBR = nullptr);
|
|
|
|
/**
|
|
* Return false if aNode has some visible content nodes, list elements or
|
|
* table elements.
|
|
*
|
|
* @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.
|
|
* Must not contain EmptyCheckOption::SafeToAskLayout.
|
|
* @param aSeenBR [Out] Set to true if this meets an <br> element
|
|
* before meeting visible things.
|
|
*/
|
|
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,
|
|
BlockInlineCheck aBlockInlineCheck) {
|
|
return HTMLEditUtils::IsInlineContent(aContent, aBlockInlineCheck) &&
|
|
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,
|
|
BlockInlineCheck aBlockInlineCheck) {
|
|
return HTMLEditUtils::IsBlockElement(aElement, aBlockInlineCheck) &&
|
|
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::IsListElement(aListElement));
|
|
bool foundListItem = false;
|
|
for (nsIContent* child = aListElement.GetFirstChild(); child;
|
|
child = child->GetNextSibling()) {
|
|
if (HTMLEditUtils::IsListItemElement(*child)) {
|
|
if (foundListItem) {
|
|
return false; // 2 list items found.
|
|
}
|
|
if (!IsEmptyNode(*child, {})) {
|
|
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(), TreatInvisibleLineBreakAs::Invisible)) {
|
|
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::IsListElement(aListElement));
|
|
for (nsIContent* child = aListElement.GetFirstChild(); child;
|
|
child = child->GetNextSibling()) {
|
|
if (HTMLEditUtils::IsListElement(*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 (child->IsText()) [[likely]] {
|
|
// XXX Probably, we should treat the Text which contains only one
|
|
// preformatted line break as invisible. However, even if the `Text`
|
|
// contains any preformatted whites-spaces including line breaks, the
|
|
// list is valid from HTML point of view. So, should we check whether
|
|
// it's empty with assuming that the Text is white-space:normal?
|
|
if (HTMLEditUtils::IsVisibleTextNode(
|
|
*child->AsText(), TreatInvisibleLineBreakAs::Invisible))
|
|
[[unlikely]] {
|
|
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,
|
|
BlockInlineCheck aBlockInlineCheck) {
|
|
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,
|
|
EmptyCheckOption::TreatNonEditableContentAsInvisible},
|
|
aBlockInlineCheck)) {
|
|
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.IsInContentNode() &&
|
|
(maybeStartOfAnchor ? point.IsStartOfContainer()
|
|
: point.IsAtLastContent());
|
|
point = point.ParentPoint()) {
|
|
if (HTMLEditUtils::IsHyperlinkElement(*point.ContainerAs<nsIContent>())) {
|
|
// 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::IsHyperlinkElement(*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);
|
|
}
|
|
|
|
/**
|
|
* 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::GetPreviousLeafContent(
|
|
aPoint, {LeafNodeOption::IgnoreNonEditableNode},
|
|
BlockInlineCheck::Auto, &aEditingHost);
|
|
if (!editableContent) {
|
|
return nullptr; // Not illegal.
|
|
}
|
|
} else {
|
|
editableContent = HTMLEditUtils::GetNextLeafContent(
|
|
aPoint, {LeafNodeOption::IgnoreNonEditableNode},
|
|
BlockInlineCheck::Auto, &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::IsImageElement(*editableContent)) {
|
|
if (aWalkTreeDirection == WalkTreeDirection::Backward) {
|
|
editableContent = HTMLEditUtils::GetPreviousLeafContent(
|
|
*editableContent, {LeafNodeOption::IgnoreNonEditableNode},
|
|
BlockInlineCheck::Auto, &aEditingHost);
|
|
if (NS_WARN_IF(!editableContent)) {
|
|
return nullptr;
|
|
}
|
|
} else {
|
|
editableContent = HTMLEditUtils::GetNextLeafContent(
|
|
*editableContent, {LeafNodeOption::IgnoreNonEditableNode},
|
|
BlockInlineCheck::Auto, &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;
|
|
}
|
|
|
|
enum class LeafNodeOption {
|
|
// Treat a block element as a leaf node.
|
|
TreatChildBlockAsLeafNode,
|
|
// Treat a non-editable node as a leaf node.
|
|
TreatNonEditableNodeAsLeafNode,
|
|
// Ignore non-editable content.
|
|
IgnoreNonEditableNode,
|
|
// Treat a `Comment` node as a significant leaf node.
|
|
TreatCommentAsLeafNode,
|
|
// Ignore empty `Text` node.
|
|
IgnoreEmptyText,
|
|
// Ignore invisible `Text` node such as empty node or all data is collapsed.
|
|
IgnoreInvisibleText,
|
|
// Ignore invisible void elements such as <wbr> and <input type="hidden">.
|
|
IgnoreInvisibleInlineVoidElements,
|
|
// If set, ignore empty inline containers such as <span></span>.
|
|
IgnoreAnyEmptyInlineContainers,
|
|
// If set, ignore empty inline containers which is not visible. E.g.,
|
|
// <span></span> is not ignored but <span style="border:1px solid"></span>
|
|
// and <span style="border:padding 1px"></span> are not ignored.
|
|
// XXX Currently, this does not work well if the inline container has only
|
|
// `::before` and/or `::after` content and the frame is dirty.
|
|
IgnoreInvisibleEmptyInlineContainers,
|
|
};
|
|
using LeafNodeOptions = EnumSet<LeafNodeOption>;
|
|
|
|
friend std::ostream& operator<<(std::ostream& aStream,
|
|
const LeafNodeOption& aOption);
|
|
friend std::ostream& operator<<(std::ostream& aStream,
|
|
const LeafNodeOptions& aOptions);
|
|
|
|
private:
|
|
enum class IgnoreChildren : bool { No, Yes };
|
|
enum class LeafNodeType {
|
|
NonEmptyContainer,
|
|
Leaf,
|
|
Ignore,
|
|
};
|
|
[[nodiscard]] static LeafNodeType GetLeafNodeType(
|
|
const nsIContent& aContent, const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck, IgnoreChildren aIgnoreChildren);
|
|
|
|
public:
|
|
/**
|
|
* GetLastLeafContent() returns rightmost leaf content in aNode. It depends
|
|
* on aOptions whether this which types of nodes are treated as leaf
|
|
* nodes.
|
|
*
|
|
* @param aBlockInlineCheck Can be Unused if aOptions does not contain
|
|
* TreatChildBlockAsLeafNode.
|
|
*/
|
|
[[nodiscard]] static nsIContent* GetLastLeafContent(
|
|
const nsINode& aNode, const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused);
|
|
|
|
/**
|
|
* GetFirstLeafContent() returns leftmost leaf content in aNode. It depends
|
|
* on aOptions whether this scans into a block child or treat block as a
|
|
* leaf.
|
|
*
|
|
* @param aBlockInlineCheck Can be Unused if aOptions does not contain
|
|
* TreatChildBlockAsLeafNode.
|
|
*/
|
|
[[nodiscard]] static nsIContent* GetFirstLeafContent(
|
|
const nsINode& aNode, const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused);
|
|
|
|
private:
|
|
enum class StopAtBlockSibling : bool { No, Yes };
|
|
|
|
static nsIContent* GetNextLeafContentOrNextBlockElementImpl(
|
|
const nsIContent& aStartContent, StopAtBlockSibling aStopAtBlockSibling,
|
|
const LeafNodeOptions& aOptions, BlockInlineCheck aBlockInlineCheck,
|
|
const Element* aAncestorLimiter);
|
|
template <typename PT, typename CT>
|
|
static nsIContent* GetNextLeafContentOrNextBlockElementImpl(
|
|
const EditorDOMPointBase<PT, CT>& aStartPoint,
|
|
StopAtBlockSibling aStopAtBlockSibling, const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter);
|
|
static nsIContent* GetPreviousLeafContentOrPreviousBlockElementImpl(
|
|
const nsIContent& aStartContent, StopAtBlockSibling aStopAtBlockSibling,
|
|
const LeafNodeOptions& aOptions, BlockInlineCheck aBlockInlineCheck,
|
|
const Element* aAncestorLimiter);
|
|
template <typename PT, typename CT>
|
|
static nsIContent* GetPreviousLeafContentOrPreviousBlockElementImpl(
|
|
const EditorDOMPointBase<PT, CT>& aStartPoint,
|
|
StopAtBlockSibling aStopAtBlockSibling, const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter);
|
|
|
|
public:
|
|
/**
|
|
* Return next leaf content of aStartContent inside aAncestorLimiter.
|
|
* This does not stop at a block inclusive ancestor nor a block sibling of an
|
|
* inclusive ancestor different from GetNextLeafContentOrNextBlockElement().
|
|
* However, if you specify LeafNodeOption::TreatChildBlockAsLeafNode, this
|
|
* stops at a child block boundary. So, the behavior becomes complicated so
|
|
* that you need to be careful if you specify that.
|
|
*
|
|
* @param aStartContent The start content to scan next content.
|
|
* @param aOptions See LeafNodeOption.
|
|
* @param aAncestorLimiter Optional, if you set this, it must be an
|
|
* inclusive ancestor of aStartContent.
|
|
*/
|
|
static nsIContent* GetNextLeafContent(
|
|
const nsIContent& aStartContent, const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck,
|
|
const Element* aAncestorLimiter = nullptr) {
|
|
return GetNextLeafContentOrNextBlockElementImpl(
|
|
aStartContent, StopAtBlockSibling::No, aOptions, aBlockInlineCheck,
|
|
aAncestorLimiter);
|
|
}
|
|
|
|
/**
|
|
* Similar to the above method, but take a DOM point to specify scan start
|
|
* point.
|
|
*/
|
|
template <typename PT, typename CT>
|
|
static nsIContent* GetNextLeafContent(
|
|
const EditorDOMPointBase<PT, CT>& aStartPoint,
|
|
const LeafNodeOptions& aOptions, BlockInlineCheck aBlockInlineCheck,
|
|
const Element* aAncestorLimiter = nullptr) {
|
|
return GetNextLeafContentOrNextBlockElementImpl(
|
|
aStartPoint, StopAtBlockSibling::No, aOptions, aBlockInlineCheck,
|
|
aAncestorLimiter);
|
|
}
|
|
|
|
/**
|
|
* Return previous leaf content of aStartContent inside aAncestorLimiter.
|
|
* This does not stop at a block inclusive ancestor nor a block sibling of an
|
|
* inclusive ancestor different from
|
|
* GetPreviousLeafContentOrPreviousBlockElement(). However, if you specify
|
|
* LeafNodeOption::TreatChildBlockAsLeafNode, this stops at a child block
|
|
* boundary. So, the behavior becomes complicated so that you need to be
|
|
* careful if you specify that.
|
|
*
|
|
* @param aStartContent The start content to scan previous content.
|
|
* @param aOptions See LeafNodeOption.
|
|
* @param aAncestorLimiter Optional, if you set this, it must be an
|
|
* inclusive ancestor of aStartContent.
|
|
*/
|
|
static nsIContent* GetPreviousLeafContent(
|
|
const nsIContent& aStartContent, const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck,
|
|
const Element* aAncestorLimiter = nullptr) {
|
|
return GetPreviousLeafContentOrPreviousBlockElementImpl(
|
|
aStartContent, StopAtBlockSibling::No, aOptions, aBlockInlineCheck,
|
|
aAncestorLimiter);
|
|
}
|
|
|
|
/**
|
|
* Similar to the above method, but take a DOM point to specify scan start
|
|
* point.
|
|
*/
|
|
template <typename PT, typename CT>
|
|
static nsIContent* GetPreviousLeafContent(
|
|
const EditorDOMPointBase<PT, CT>& aStartPoint,
|
|
const LeafNodeOptions& aOptions, BlockInlineCheck aBlockInlineCheck,
|
|
const Element* aAncestorLimiter = nullptr) {
|
|
return GetPreviousLeafContentOrPreviousBlockElementImpl(
|
|
aStartPoint, StopAtBlockSibling::No, aOptions, aBlockInlineCheck,
|
|
aAncestorLimiter);
|
|
}
|
|
|
|
/**
|
|
* GetNextLeafContentOrNextBlockElement() returns next leaf content or
|
|
* next block element of aStartContent inside aAncestorLimiter.
|
|
*
|
|
* @param aStartContent The start content to scan next content.
|
|
* @param aOptions See LeafNodeOption.
|
|
* @param aAncestorLimiter Optional, if you set this, it must be an
|
|
* inclusive ancestor of aStartContent.
|
|
*/
|
|
static nsIContent* GetNextLeafContentOrNextBlockElement(
|
|
const nsIContent& aStartContent, const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck,
|
|
const Element* aAncestorLimiter = nullptr) {
|
|
return GetNextLeafContentOrNextBlockElementImpl(
|
|
aStartContent, StopAtBlockSibling::Yes, aOptions, aBlockInlineCheck,
|
|
aAncestorLimiter);
|
|
}
|
|
|
|
/**
|
|
* 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 LeafNodeOptions& aOptions, BlockInlineCheck aBlockInlineCheck,
|
|
const Element* aAncestorLimiter = nullptr) {
|
|
return GetNextLeafContentOrNextBlockElementImpl(
|
|
aStartPoint, StopAtBlockSibling::Yes, aOptions, aBlockInlineCheck,
|
|
aAncestorLimiter);
|
|
}
|
|
|
|
/**
|
|
* GetPreviousLeafContentOrPreviousBlockElement() returns previous leaf
|
|
* content or previous block element of aStartContent inside
|
|
* aAncestorLimiter.
|
|
*
|
|
* @param aStartContent The start content to scan previous content.
|
|
* @param aOptions See LeafNodeOption.
|
|
* @param aAncestorLimiter Optional, if you set this, it must be an
|
|
* inclusive ancestor of aStartContent.
|
|
*/
|
|
static nsIContent* GetPreviousLeafContentOrPreviousBlockElement(
|
|
const nsIContent& aStartContent, const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck,
|
|
const Element* aAncestorLimiter = nullptr) {
|
|
return GetPreviousLeafContentOrPreviousBlockElementImpl(
|
|
aStartContent, StopAtBlockSibling::Yes, aOptions, aBlockInlineCheck,
|
|
aAncestorLimiter);
|
|
}
|
|
|
|
/**
|
|
* 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 LeafNodeOptions& aOptions, BlockInlineCheck aBlockInlineCheck,
|
|
const Element* aAncestorLimiter = nullptr) {
|
|
return GetPreviousLeafContentOrPreviousBlockElementImpl(
|
|
aStartPoint, StopAtBlockSibling::Yes, aOptions, aBlockInlineCheck,
|
|
aAncestorLimiter);
|
|
}
|
|
|
|
private:
|
|
static nsIContent* GetSibling(const nsIContent& aContent,
|
|
WalkTreeDirection aDirection,
|
|
const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck);
|
|
|
|
public:
|
|
/**
|
|
* Return the preceding sibling of aContent with ignoring leaf nodes which are
|
|
* specified by aOptions.
|
|
*
|
|
* @param aOptions If a sibling is a leaf node or does not have meaningful
|
|
* children against aOptions, the sibling is ignored.
|
|
* NOTE: LeafNodeOption::TreatChildBlockAsLeafNode is
|
|
* ignored.
|
|
*/
|
|
static nsIContent* GetPreviousSibling(const nsIContent& aContent,
|
|
const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck) {
|
|
return GetSibling(aContent, WalkTreeDirection::Backward, aOptions,
|
|
aBlockInlineCheck);
|
|
}
|
|
|
|
/**
|
|
* Return the following sibling of aContent with ignoring leaf nodes which are
|
|
* specified by aOptions.
|
|
*
|
|
* @param aOptions If a sibling is a leaf node or does not have meaningful
|
|
* children against aOptions, the sibling is ignored.
|
|
* NOTE: LeafNodeOption::TreatChildBlockAsLeafNode is
|
|
* ignored.
|
|
*/
|
|
static nsIContent* GetNextSibling(const nsIContent& aContent,
|
|
const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck) {
|
|
return GetSibling(aContent, WalkTreeDirection::Forward, aOptions,
|
|
aBlockInlineCheck);
|
|
}
|
|
|
|
private:
|
|
enum class FirstOrLastChild { First, Last };
|
|
static nsIContent* GetFirstOrLastChild(const nsINode& aNode,
|
|
FirstOrLastChild aFirstOrLastChild,
|
|
const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck);
|
|
|
|
public:
|
|
/**
|
|
* Return the last child of aNode with ignoring leaf nodes which are specified
|
|
* by aOptions.
|
|
*
|
|
* @param aOptions If a child is a leaf node or does not have meaningful
|
|
* children against aOptions, the child is ignored.
|
|
* NOTE: LeafNodeOption::TreatChildBlockAsLeafNode is
|
|
* ignored.
|
|
*/
|
|
static nsIContent* GetLastChild(const nsINode& aNode,
|
|
const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck) {
|
|
return GetFirstOrLastChild(aNode, FirstOrLastChild::Last, aOptions,
|
|
aBlockInlineCheck);
|
|
}
|
|
|
|
/**
|
|
* Return the first child of aNode with ignoring leaf nodes which are
|
|
* specified by aOptions.
|
|
*
|
|
* @param aOptions If a child is a leaf node or does not have meaningful
|
|
* children against aOptions, the child is ignored.
|
|
* NOTE: LeafNodeOption::TreatChildBlockAsLeafNode is
|
|
* ignored.
|
|
*/
|
|
static nsIContent* GetFirstChild(const nsINode& aNode,
|
|
const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck) {
|
|
return GetFirstOrLastChild(aNode, FirstOrLastChild::First, aOptions,
|
|
aBlockInlineCheck);
|
|
}
|
|
|
|
/**
|
|
* Return true if aContent is the last child of aNode with ignoring all
|
|
* children which are specified by aOptions.
|
|
*/
|
|
static bool IsLastChild(const nsIContent& aContent,
|
|
const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck) {
|
|
nsINode* const parentNode = aContent.GetParentNode();
|
|
if (MOZ_UNLIKELY(!parentNode)) {
|
|
return false;
|
|
}
|
|
return HTMLEditUtils::GetLastChild(*parentNode, aOptions,
|
|
aBlockInlineCheck) == &aContent;
|
|
}
|
|
|
|
/**
|
|
* Return true if aContent is the first child of aNode with ignoring all
|
|
* children which are specified by aOptions.
|
|
*/
|
|
static bool IsFirstChild(const nsIContent& aContent,
|
|
const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck) {
|
|
nsINode* const parentNode = aContent.GetParentNode();
|
|
if (MOZ_UNLIKELY(!parentNode)) {
|
|
return false;
|
|
}
|
|
return HTMLEditUtils::GetFirstChild(*parentNode, aOptions,
|
|
aBlockInlineCheck) == &aContent;
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
// Return the closest block element.
|
|
ClosestBlockElement,
|
|
// Return the closest container element, either block or inline. I.e., this
|
|
// ignores void elements like <br>, <hr>, etc.
|
|
ClosestContainerElement,
|
|
// Return a child element of the closest block element.
|
|
// NOTE: If the scanning start node is a block element, the methods return
|
|
// nullptr or the ancestor limiter if AllowRootOrAncestorLimiterElement is
|
|
// set.
|
|
// NOTE: If ClosestButtonElement is set, the methods may return a <button>.
|
|
// NOTE: If StopAtClosestButtonElement is set, the methods return a child of
|
|
// <button>.
|
|
MostDistantInlineElementInBlock,
|
|
// Ignore ancestor <hr> elements to check whether a block.
|
|
IgnoreHRElement,
|
|
// Return the closest button element.
|
|
ClosestButtonElement,
|
|
// Stop scanning at <button> element. The methods do not return the
|
|
// <button> element if MostDistantInlineElementInBlock is set.
|
|
StopAtClosestButtonElement,
|
|
// When there is no ancestor which matches with the other flags and reached
|
|
// the ancestor limiter (or an editing host, the editable <body> or the
|
|
// editable root document element if and only if EditableElement is
|
|
// specified), return the ancestor limiter, editable <body> or editable root
|
|
// document element instead.
|
|
ReturnAncestorLimiterIfNoProperAncestor,
|
|
|
|
// Limit to editable elements. If it reaches an non-editable element,
|
|
// return the last ancestor element which matches with the other types.
|
|
EditableElement,
|
|
};
|
|
using AncestorTypes = EnumSet<AncestorType>;
|
|
|
|
friend std::ostream& operator<<(std::ostream& aStream,
|
|
const AncestorType& aType);
|
|
friend std::ostream& operator<<(std::ostream& aStream,
|
|
const AncestorTypes& aTypes);
|
|
|
|
constexpr static AncestorTypes
|
|
ClosestEditableBlockElementOrInlineEditingHost = {
|
|
AncestorType::ClosestBlockElement, AncestorType::EditableElement,
|
|
AncestorType::ReturnAncestorLimiterIfNoProperAncestor};
|
|
constexpr static AncestorTypes ClosestBlockElement = {
|
|
AncestorType::ClosestBlockElement};
|
|
constexpr static AncestorTypes ClosestEditableBlockElement = {
|
|
AncestorType::ClosestBlockElement, AncestorType::EditableElement};
|
|
constexpr static AncestorTypes ClosestBlockElementExceptHRElement = {
|
|
AncestorType::ClosestBlockElement, AncestorType::IgnoreHRElement};
|
|
constexpr static AncestorTypes ClosestEditableBlockElementExceptHRElement = {
|
|
AncestorType::ClosestBlockElement, AncestorType::IgnoreHRElement,
|
|
AncestorType::EditableElement};
|
|
constexpr static AncestorTypes ClosestEditableBlockElementOrButtonElement = {
|
|
AncestorType::ClosestBlockElement, AncestorType::EditableElement,
|
|
AncestorType::ClosestButtonElement};
|
|
// Return the most distant ancestor element in current block or <button>.
|
|
constexpr static AncestorTypes
|
|
MostDistantEditableInlineElementInBlockOrButton = {
|
|
AncestorType::MostDistantInlineElementInBlock,
|
|
AncestorType::StopAtClosestButtonElement};
|
|
// Return the most distant ancestor element in current block or the closest
|
|
// <button> if starting to scan within a <button>. If you want a child in the
|
|
// <button> in the latter case, use
|
|
// MostDistantEditableInlineElementInBlockOrButton.
|
|
constexpr static AncestorTypes
|
|
MostDistantEditableInlineElementInBlockOrClosestButton = {
|
|
AncestorType::MostDistantInlineElementInBlock,
|
|
AncestorType::ClosestButtonElement};
|
|
constexpr static AncestorTypes ClosestContainerElementOrVoidAncestorLimiter =
|
|
{AncestorType::ClosestContainerElement,
|
|
AncestorType::ReturnAncestorLimiterIfNoProperAncestor};
|
|
static Element* GetAncestorElement(const nsIContent& aContent,
|
|
const AncestorTypes& aAncestorTypes,
|
|
BlockInlineCheck aBlockInlineCheck,
|
|
const Element* aAncestorLimiter = nullptr);
|
|
static Element* GetInclusiveAncestorElement(
|
|
const nsIContent& aContent, const AncestorTypes& aAncestorTypes,
|
|
BlockInlineCheck aBlockInlineCheck,
|
|
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 (element->IsHTMLElement(nsGkAtoms::table)) {
|
|
return element;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static Element* GetInclusiveAncestorAnyTableElement(
|
|
const nsIContent& aContent) {
|
|
for (Element* parent : aContent.InclusiveAncestorsOfType<Element>()) {
|
|
if (HTMLEditUtils::IsAnyTableElementExceptColumnElement(*parent)) {
|
|
return parent;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
[[nodiscard]] static Element* GetClosestAncestorAnyListElement(
|
|
const nsIContent& aContent);
|
|
[[nodiscard]] static Element* GetClosestInclusiveAncestorAnyListElement(
|
|
const nsIContent& aContent);
|
|
|
|
/**
|
|
* Return a list item element if aContent or its ancestor in editing host is
|
|
* one. However, this won't cross table related element.
|
|
*/
|
|
static Element* GetClosestInclusiveAncestorListItemElement(
|
|
const nsIContent& aContent, const Element* aAncestorLimit = nullptr) {
|
|
MOZ_ASSERT_IF(aAncestorLimit,
|
|
aContent.IsInclusiveDescendantOf(aAncestorLimit));
|
|
|
|
if (HTMLEditUtils::IsListItemElement(aContent)) {
|
|
return const_cast<Element*>(aContent.AsElement());
|
|
}
|
|
|
|
for (Element* parentElement : aContent.AncestorsOfType<Element>()) {
|
|
if (HTMLEditUtils::IsAnyTableElementExceptColumnElement(*parentElement)) {
|
|
return nullptr;
|
|
}
|
|
if (HTMLEditUtils::IsListItemElement(*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::IsListElement(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::IsListElement(aListElement));
|
|
for (nsIContent* maybeFirstListItem = aListElement.GetFirstChild();
|
|
maybeFirstListItem;
|
|
maybeFirstListItem = maybeFirstListItem->GetNextNode(&aListElement)) {
|
|
if (HTMLEditUtils::IsListItemElement(*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::IsListElement(aListElement));
|
|
for (nsIContent* maybeLastListItem = aListElement.GetLastChild();
|
|
maybeLastListItem;) {
|
|
if (HTMLEditUtils::IsListItemElement(*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* const firstElementChild = aTableRowElement.GetFirstElementChild();
|
|
return HTMLEditUtils::IsTableCellElement(firstElementChild)
|
|
? firstElementChild
|
|
: nullptr;
|
|
}
|
|
static Element* GetLastTableCellElementChild(
|
|
const Element& aTableRowElement) {
|
|
MOZ_ASSERT(aTableRowElement.IsHTMLElement(nsGkAtoms::tr));
|
|
Element* const lastElementChild = aTableRowElement.GetLastElementChild();
|
|
return HTMLEditUtils::IsTableCellElement(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* const previousElementSibling =
|
|
aChildOfTableRow.GetPreviousElementSibling();
|
|
return HTMLEditUtils::IsTableCellElement(previousElementSibling)
|
|
? previousElementSibling
|
|
: nullptr;
|
|
}
|
|
static Element* GetNextTableCellElementSibling(
|
|
const nsIContent& aChildOfTableRow) {
|
|
MOZ_ASSERT(aChildOfTableRow.GetParentNode());
|
|
MOZ_ASSERT(aChildOfTableRow.GetParentNode()->IsHTMLElement(nsGkAtoms::tr));
|
|
Element* const nextElementSibling =
|
|
aChildOfTableRow.GetNextElementSibling();
|
|
return HTMLEditUtils::IsTableCellElement(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, BlockInlineCheck aBlockInlineCheck,
|
|
const Element* aEditingHost = nullptr,
|
|
const nsIContent* aAncestorLimiter = nullptr) {
|
|
aBlockInlineCheck = UseComputedDisplayStyleIfAuto(aBlockInlineCheck);
|
|
if (HTMLEditUtils::IsBlockElement(aContent, aBlockInlineCheck)) {
|
|
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::IsBlockElement(*element, aBlockInlineCheck)) {
|
|
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, BlockInlineCheck aBlockInlineCheck,
|
|
const Element* aEditingHost = nullptr,
|
|
const nsIContent* aAncestorLimiter = nullptr) {
|
|
if (&aEmptyContent == aEditingHost || &aEmptyContent == aAncestorLimiter) {
|
|
return nullptr;
|
|
}
|
|
aBlockInlineCheck = UseComputedDisplayStyleIfAuto(aBlockInlineCheck);
|
|
nsIContent* lastEmptyContent = const_cast<nsIContent*>(&aEmptyContent);
|
|
for (Element* element : aEmptyContent.AncestorsOfType<Element>()) {
|
|
if (element == aEditingHost || element == aAncestorLimiter) {
|
|
break;
|
|
}
|
|
if (!HTMLEditUtils::IsInlineContent(*element, aBlockInlineCheck) ||
|
|
!HTMLEditUtils::IsSimplyEditableNode(*element)) {
|
|
break;
|
|
}
|
|
if (element->GetChildCount() > 1) {
|
|
// FIXME: We should check whether the each other children is empty.
|
|
for (const nsIContent* child = element->GetFirstChild(); child;
|
|
child = child->GetNextSibling()) {
|
|
if (child == lastEmptyContent || child->IsComment()) {
|
|
continue;
|
|
}
|
|
return lastEmptyContent != &aEmptyContent
|
|
? Element::FromNode(lastEmptyContent)
|
|
: nullptr;
|
|
}
|
|
}
|
|
lastEmptyContent = element;
|
|
}
|
|
return lastEmptyContent != &aEmptyContent
|
|
? Element::FromNode(lastEmptyContent)
|
|
: 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 HTMLEditUtils::IsTableCellElement(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);
|
|
}
|
|
|
|
private:
|
|
static uint32_t CountMeaningfulChildren(const nsINode& aNode,
|
|
const LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck) {
|
|
uint32_t count = 0;
|
|
for (nsIContent* child = aNode.GetFirstChild(); child;
|
|
child = child->GetNextSibling()) {
|
|
const LeafNodeType leafNodeType = HTMLEditUtils::GetLeafNodeType(
|
|
*child, aOptions, aBlockInlineCheck, IgnoreChildren::No);
|
|
if (leafNodeType == LeafNodeType::Ignore) {
|
|
continue;
|
|
}
|
|
if (leafNodeType == LeafNodeType::NonEmptyContainer) {
|
|
if (!HTMLEditUtils::GetFirstLeafContent(*child, aOptions,
|
|
aBlockInlineCheck)) {
|
|
continue;
|
|
}
|
|
}
|
|
++count;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
public:
|
|
/**
|
|
* 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 LeafNodeOptions& aOptions,
|
|
BlockInlineCheck aBlockInlineCheck, 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...);
|
|
content = HTMLEditUtils::GetFirstChild(*content, aOptions,
|
|
aBlockInlineCheck)) {
|
|
if (HTMLEditUtils::CountMeaningfulChildren(*content, aOptions,
|
|
aBlockInlineCheck) != 1) {
|
|
return content->AsElement();
|
|
}
|
|
parentElement = content->AsElement();
|
|
}
|
|
return parentElement;
|
|
}
|
|
|
|
/**
|
|
* Get the first line break 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 or a
|
|
* preformatted linefeed.
|
|
*/
|
|
template <typename EditorLineBreakType>
|
|
static Maybe<EditorLineBreakType> GetFirstLineBreak(
|
|
const dom::Element& aElement) {
|
|
for (nsIContent* content = HTMLEditUtils::GetFirstLeafContent(aElement, {});
|
|
content; content = HTMLEditUtils::GetNextLeafContent(
|
|
*content, {LeafNodeOption::IgnoreInvisibleText},
|
|
BlockInlineCheck::Auto, &aElement)) {
|
|
if (auto* brElement = dom::HTMLBRElement::FromNode(*content)) {
|
|
return Some(EditorLineBreakType(*brElement));
|
|
}
|
|
if (auto* textNode = Text::FromNode(*content)) {
|
|
if (EditorUtils::IsNewLinePreformatted(*textNode)) {
|
|
uint32_t offset = textNode->DataBuffer().FindChar(kNewLine);
|
|
if (offset != dom::CharacterDataBuffer::kNotFound) {
|
|
return Some(EditorLineBreakType(*textNode, offset));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return Nothing();
|
|
}
|
|
|
|
/**
|
|
* 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>;
|
|
template <typename PT, typename CT>
|
|
static Maybe<uint32_t> GetPreviousNonCollapsibleCharOffset(
|
|
const EditorDOMPointBase<PT, CT>& aPoint,
|
|
const WalkTextOptions& aWalkTextOptions = {}) {
|
|
static_assert(std::is_same_v<PT, RefPtr<Text>> ||
|
|
std::is_same_v<PT, Text*>);
|
|
MOZ_ASSERT(aPoint.IsSetAndValid());
|
|
return GetPreviousNonCollapsibleCharOffset(
|
|
*aPoint.template ContainerAs<Text>(), aPoint.Offset(),
|
|
aWalkTextOptions);
|
|
}
|
|
static Maybe<uint32_t> GetPreviousNonCollapsibleCharOffset(
|
|
const Text& aTextNode, uint32_t aOffset,
|
|
const WalkTextOptions& aWalkTextOptions = {}) {
|
|
if (MOZ_UNLIKELY(!aOffset)) {
|
|
return Nothing{};
|
|
}
|
|
MOZ_ASSERT(aOffset <= aTextNode.TextDataLength());
|
|
if (EditorUtils::IsWhiteSpacePreformatted(aTextNode)) {
|
|
return Some(aOffset - 1);
|
|
}
|
|
WhitespaceOptions whitespaceOptions{
|
|
WhitespaceOption::FormFeedIsSignificant};
|
|
if (EditorUtils::IsNewLinePreformatted(aTextNode)) {
|
|
whitespaceOptions += WhitespaceOption::NewLineIsSignificant;
|
|
}
|
|
if (aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible)) {
|
|
whitespaceOptions += WhitespaceOption::TreatNBSPAsCollapsible;
|
|
}
|
|
const uint32_t prevVisibleCharOffset =
|
|
aTextNode.DataBuffer().RFindNonWhitespaceChar(whitespaceOptions,
|
|
aOffset - 1);
|
|
return prevVisibleCharOffset != dom::CharacterDataBuffer::kNotFound
|
|
? Some(prevVisibleCharOffset)
|
|
: 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.
|
|
*/
|
|
template <typename PT, typename CT>
|
|
static Maybe<uint32_t> GetInclusiveNextNonCollapsibleCharOffset(
|
|
const EditorDOMPointBase<PT, CT>& aPoint,
|
|
const WalkTextOptions& aWalkTextOptions = {}) {
|
|
static_assert(std::is_same_v<PT, RefPtr<Text>> ||
|
|
std::is_same_v<PT, Text*>);
|
|
MOZ_ASSERT(aPoint.IsSetAndValid());
|
|
return GetInclusiveNextNonCollapsibleCharOffset(
|
|
*aPoint.template ContainerAs<Text>(), aPoint.Offset(),
|
|
aWalkTextOptions);
|
|
}
|
|
static Maybe<uint32_t> GetInclusiveNextNonCollapsibleCharOffset(
|
|
const Text& aTextNode, uint32_t aOffset,
|
|
const WalkTextOptions& aWalkTextOptions = {}) {
|
|
if (MOZ_UNLIKELY(aOffset >= aTextNode.TextDataLength())) {
|
|
return Nothing();
|
|
}
|
|
MOZ_ASSERT(aOffset <= aTextNode.TextDataLength());
|
|
if (EditorUtils::IsWhiteSpacePreformatted(aTextNode)) {
|
|
return Some(aOffset);
|
|
}
|
|
WhitespaceOptions whitespaceOptions{
|
|
WhitespaceOption::FormFeedIsSignificant};
|
|
if (EditorUtils::IsNewLinePreformatted(aTextNode)) {
|
|
whitespaceOptions += WhitespaceOption::NewLineIsSignificant;
|
|
}
|
|
if (aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible)) {
|
|
whitespaceOptions += WhitespaceOption::TreatNBSPAsCollapsible;
|
|
}
|
|
const uint32_t inclusiveNextVisibleCharOffset =
|
|
aTextNode.DataBuffer().FindNonWhitespaceChar(whitespaceOptions,
|
|
aOffset);
|
|
if (inclusiveNextVisibleCharOffset != dom::CharacterDataBuffer::kNotFound) {
|
|
return Some(inclusiveNextVisibleCharOffset);
|
|
}
|
|
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.
|
|
*/
|
|
template <typename PT, typename CT>
|
|
static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith(
|
|
const EditorDOMPointBase<PT, CT>& aPoint,
|
|
const WalkTextOptions& aWalkTextOptions = {}) {
|
|
static_assert(std::is_same_v<PT, RefPtr<Text>> ||
|
|
std::is_same_v<PT, Text*>);
|
|
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.template 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();
|
|
}
|
|
const Text& textNode = *aPoint.template ContainerAs<Text>();
|
|
MOZ_ASSERT(aPoint.Offset() <= textNode.DataBuffer().GetLength());
|
|
const uint32_t previousLineBreakOffset =
|
|
textNode.DataBuffer().RFindChar('\n', aPoint.Offset() - 1u);
|
|
return previousLineBreakOffset != dom::CharacterDataBuffer::kNotFound
|
|
? EditorDOMPointType(&textNode, previousLineBreakOffset)
|
|
: 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();
|
|
}
|
|
const Text& textNode = *aPoint.template ContainerAs<Text>();
|
|
MOZ_ASSERT(aPoint.Offset() <= textNode.DataBuffer().GetLength());
|
|
const uint32_t inclusiveNextVisibleCharOffset =
|
|
textNode.DataBuffer().FindChar('\n', aPoint.Offset());
|
|
return inclusiveNextVisibleCharOffset != dom::CharacterDataBuffer::kNotFound
|
|
? EditorDOMPointType(&textNode, inclusiveNextVisibleCharOffset)
|
|
: EditorDOMPointType();
|
|
}
|
|
|
|
/**
|
|
* Get the first visible char offset in aText. I.e., this returns invisible
|
|
* white-space length at start of aText. If there is no visible char in
|
|
* aText, this returns the text data length.
|
|
* Note that WSRunScanner::GetFirstVisiblePoint() may return different `Text`
|
|
* node point, but this does not scan following `Text` nodes even if aText
|
|
* is completely invisible.
|
|
*/
|
|
[[nodiscard]] static uint32_t GetFirstVisibleCharOffset(const Text& aText);
|
|
|
|
/**
|
|
* Get next offset of the last visible char in aText. I.e., this returns
|
|
* the first offset of invisible trailing white-spaces. If there is no
|
|
* invisible trailing white-spaces in aText, this returns 0.
|
|
* Note that WSRunScanner::GetAfterLastVisiblePoint() may return different
|
|
* `Text` node point, but this does not scan preceding `Text` nodes even if
|
|
* aText is completely invisible.
|
|
*/
|
|
[[nodiscard]] static uint32_t GetOffsetAfterLastVisibleChar(
|
|
const Text& aText);
|
|
|
|
/**
|
|
* Get the number of invisible white-spaces in the white-space sequence. Note
|
|
* that some invisible white-spaces may be after the first visible character.
|
|
* E.g., "SP SP NBSP SP SP NBSP". If this Text follows a block boundary, the
|
|
* first SPs are the leading invisible white-spaces, and the first NBSP is the
|
|
* first visible character. However, following 2 SPs are collapsed to one.
|
|
* Therefore, one of them is counted as an invisible white-space.
|
|
*
|
|
* Note that this assumes that all white-spaces starting from aOffset and
|
|
* ending by aOffset + aLength are collapsible white-spaces including NBSPs.
|
|
*/
|
|
[[nodiscard]] static uint32_t GetInvisibleWhiteSpaceCount(
|
|
const Text& aText, uint32_t aOffset = 0u, uint32_t aLength = UINT32_MAX);
|
|
|
|
/**
|
|
* 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::IsBRElementFollowedByBlockBoundary(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.
|
|
* @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);
|
|
|
|
/**
|
|
* 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);
|
|
|
|
/**
|
|
* 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);
|
|
|
|
/**
|
|
* Return a line break if aPoint is after a line break which is immediately
|
|
* before a block boundary.
|
|
*/
|
|
template <typename EditorLineBreakType, typename EditorDOMPointType>
|
|
static Maybe<EditorLineBreakType>
|
|
GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem(
|
|
const EditorDOMPointType& aPoint, const Element& aEditingHost);
|
|
|
|
/**
|
|
* 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.
|
|
* @param aBlockInlineCheck Whether use computed style or HTML default style
|
|
* when consider block vs. inline.
|
|
* @return Number of found elements.
|
|
*/
|
|
static size_t CollectEmptyInlineContainerDescendants(
|
|
const nsINode& aNode,
|
|
nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
|
|
const EmptyCheckOptions& aOptions, BlockInlineCheck aBlockInlineCheck);
|
|
|
|
/**
|
|
* 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);
|
|
|
|
enum class EditablePointOption {
|
|
// Do not ignore invisible collapsible white-spaces which are next to a
|
|
// block boundary.
|
|
RecognizeInvisibleWhiteSpaces,
|
|
// Stop at Comment node.
|
|
StopAtComment,
|
|
// Stop at List element.
|
|
StopAtListElement,
|
|
// Stop at ListItem element.
|
|
StopAtListItemElement,
|
|
// Stop at Table element.
|
|
StopAtTableElement,
|
|
// Stop at any table element.
|
|
StopAtAnyTableElement,
|
|
};
|
|
using EditablePointOptions = EnumSet<EditablePointOption>;
|
|
|
|
friend std::ostream& operator<<(std::ostream& aStream,
|
|
const EditablePointOption& aOption);
|
|
friend std::ostream& operator<<(std::ostream& aStream,
|
|
const EditablePointOptions& aOptions);
|
|
|
|
private:
|
|
class MOZ_STACK_CLASS AutoEditablePointChecker final {
|
|
public:
|
|
explicit AutoEditablePointChecker(const EditablePointOptions& aOptions)
|
|
: mIgnoreInvisibleText(!aOptions.contains(
|
|
EditablePointOption::RecognizeInvisibleWhiteSpaces)),
|
|
mIgnoreComment(
|
|
!aOptions.contains(EditablePointOption::StopAtComment)),
|
|
mStopAtListElement(
|
|
aOptions.contains(EditablePointOption::StopAtListElement)),
|
|
mStopAtListItemElement(
|
|
aOptions.contains(EditablePointOption::StopAtListItemElement)),
|
|
mStopAtTableElement(
|
|
aOptions.contains(EditablePointOption::StopAtTableElement)),
|
|
mStopAtAnyTableElement(
|
|
aOptions.contains(EditablePointOption::StopAtAnyTableElement)) {}
|
|
|
|
[[nodiscard]] bool IgnoreInvisibleWhiteSpaces() const {
|
|
return mIgnoreInvisibleText;
|
|
}
|
|
|
|
[[nodiscard]] bool NodeShouldBeIgnored(const nsIContent& aContent) const {
|
|
if (mIgnoreInvisibleText && aContent.IsText() &&
|
|
HTMLEditUtils::IsSimplyEditableNode(aContent) &&
|
|
!HTMLEditUtils::IsVisibleTextNode(
|
|
*aContent.AsText(), TreatInvisibleLineBreakAs::Visible)) {
|
|
return true;
|
|
}
|
|
if (mIgnoreComment && aContent.IsComment()) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
[[nodiscard]] bool ShouldStopScanningAt(const nsIContent& aContent) const {
|
|
if (HTMLEditUtils::IsListElement(aContent)) {
|
|
return mStopAtListElement;
|
|
}
|
|
if (HTMLEditUtils::IsListItemElement(aContent)) {
|
|
return mStopAtListItemElement;
|
|
}
|
|
if (HTMLEditUtils::IsAnyTableElementExceptColumnElement(aContent)) {
|
|
return mStopAtAnyTableElement ||
|
|
(mStopAtTableElement &&
|
|
aContent.IsHTMLElement(nsGkAtoms::table));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private:
|
|
const bool mIgnoreInvisibleText;
|
|
const bool mIgnoreComment;
|
|
const bool mStopAtListElement;
|
|
const bool mStopAtListItemElement;
|
|
const bool mStopAtTableElement;
|
|
const bool mStopAtAnyTableElement;
|
|
};
|
|
|
|
public:
|
|
/**
|
|
* Return a point which points deepest editable start point of aContent. This
|
|
* walks the DOM tree in aContent to search meaningful first descendant. If
|
|
* EditablePointOption::IgnoreInvisibleText is specified, this returns first
|
|
* visible char offset if this reaches a visible `Text` first. If there is an
|
|
* empty inline element such as <span>, this returns start of the inline
|
|
* element. If this reaches non-editable element or non-container element
|
|
* like <img>, this returns the position.
|
|
*/
|
|
template <typename EditorDOMPointType>
|
|
[[nodiscard]] static EditorDOMPointType GetDeepestEditableStartPointOf(
|
|
const nsIContent& aContent, const EditablePointOptions& aOptions) {
|
|
if (NS_WARN_IF(!EditorUtils::IsEditableContent(
|
|
aContent, EditorBase::EditorType::HTML))) {
|
|
return EditorDOMPointType();
|
|
}
|
|
const AutoEditablePointChecker checker(aOptions);
|
|
EditorRawDOMPoint 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.
|
|
nsIContent* meaningfulFirstChild = nullptr;
|
|
if (checker.NodeShouldBeIgnored(*firstChild)) {
|
|
// If we ignored a non-empty `Text`, it means that we're next to a block
|
|
// boundary.
|
|
for (nsIContent* nextSibling = firstChild->GetNextSibling();
|
|
nextSibling; nextSibling = nextSibling->GetNextSibling()) {
|
|
if (!checker.NodeShouldBeIgnored(*nextSibling) ||
|
|
checker.ShouldStopScanningAt(*nextSibling)) {
|
|
meaningfulFirstChild = nextSibling;
|
|
break;
|
|
}
|
|
}
|
|
if (!meaningfulFirstChild) {
|
|
break;
|
|
}
|
|
} else {
|
|
meaningfulFirstChild = firstChild;
|
|
}
|
|
if (meaningfulFirstChild->IsText()) {
|
|
if (checker.IgnoreInvisibleWhiteSpaces()) {
|
|
result.Set(meaningfulFirstChild,
|
|
HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
|
|
*meaningfulFirstChild->AsText(), 0u)
|
|
.valueOr(0u));
|
|
} else {
|
|
result.Set(meaningfulFirstChild, 0u);
|
|
}
|
|
break;
|
|
}
|
|
if (checker.ShouldStopScanningAt(*meaningfulFirstChild) ||
|
|
!HTMLEditUtils::IsContainerNode(*meaningfulFirstChild) ||
|
|
!EditorUtils::IsEditableContent(*meaningfulFirstChild,
|
|
EditorBase::EditorType::HTML)) {
|
|
// FIXME: If the node is at middle of invisible white-spaces, we should
|
|
// ignore the node.
|
|
result.Set(meaningfulFirstChild);
|
|
break;
|
|
}
|
|
result.Set(meaningfulFirstChild, 0u);
|
|
}
|
|
return result.To<EditorDOMPointType>();
|
|
}
|
|
|
|
/**
|
|
* Return a point which points deepest editable last point of aContent. This
|
|
* walks the DOM tree in aContent to search meaningful last descendant. If
|
|
* EditablePointOption::IgnoreInvisibleText is specified, this returns next
|
|
* offset of the last visible char if this reaches a visible `Text` first. If
|
|
* there is an empty inline element such as <span>, this returns end of the
|
|
* inline element. If this reaches non-editable element or non-container
|
|
* element like <img>, this returns the position after that.
|
|
*/
|
|
template <typename EditorDOMPointType>
|
|
[[nodiscard]] static EditorDOMPointType GetDeepestEditableEndPointOf(
|
|
const nsIContent& aContent, const EditablePointOptions& aOptions) {
|
|
if (NS_WARN_IF(!EditorUtils::IsEditableContent(
|
|
aContent, EditorBase::EditorType::HTML))) {
|
|
return EditorDOMPointType();
|
|
}
|
|
const AutoEditablePointChecker checker(aOptions);
|
|
auto result = EditorRawDOMPoint::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.
|
|
nsIContent* meaningfulLastChild = nullptr;
|
|
// XXX Should we skip the lastChild if it's an invisible line break?
|
|
if (checker.NodeShouldBeIgnored(*lastChild)) {
|
|
for (nsIContent* nextSibling = lastChild->GetPreviousSibling();
|
|
nextSibling; nextSibling = nextSibling->GetPreviousSibling()) {
|
|
if (!checker.NodeShouldBeIgnored(*nextSibling) ||
|
|
checker.ShouldStopScanningAt(*nextSibling)) {
|
|
meaningfulLastChild = nextSibling;
|
|
break;
|
|
}
|
|
}
|
|
if (!meaningfulLastChild) {
|
|
break;
|
|
}
|
|
} else {
|
|
meaningfulLastChild = lastChild;
|
|
}
|
|
if (meaningfulLastChild->IsText()) {
|
|
if (checker.IgnoreInvisibleWhiteSpaces()) {
|
|
const Maybe<uint32_t> visibleCharOffset =
|
|
HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
|
|
*meaningfulLastChild->AsText(),
|
|
meaningfulLastChild->AsText()->TextDataLength());
|
|
if (visibleCharOffset.isNothing()) {
|
|
result = EditorRawDOMPoint::AtEndOf(*meaningfulLastChild);
|
|
} else {
|
|
result.Set(meaningfulLastChild, visibleCharOffset.value() + 1u);
|
|
}
|
|
} else {
|
|
result = EditorRawDOMPoint::AtEndOf(*meaningfulLastChild);
|
|
}
|
|
break;
|
|
}
|
|
if (checker.ShouldStopScanningAt(*meaningfulLastChild) ||
|
|
!HTMLEditUtils::IsContainerNode(*meaningfulLastChild) ||
|
|
!EditorUtils::IsEditableContent(*meaningfulLastChild,
|
|
EditorBase::EditorType::HTML)) {
|
|
// FIXME: If the node is at middle of invisible white-spaces, we should
|
|
// ignore the node.
|
|
result.SetAfter(meaningfulLastChild);
|
|
break;
|
|
}
|
|
result = EditorRawDOMPoint::AtEndOf(*lastChild);
|
|
}
|
|
return result.To<EditorDOMPointType>();
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
|
|
/**
|
|
* Return true if aColor is completely transparent.
|
|
*/
|
|
[[nodiscard]] static bool IsTransparentCSSColor(const nsAString& aColor);
|
|
|
|
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::IsAnyTableElementExceptColumnElement(aContent)) ||
|
|
(aHowToTreatTableBoundary == TableBoundary::NoCrossTableElement &&
|
|
aContent.IsHTMLElement(nsGkAtoms::table));
|
|
return !cannotCrossBoundary;
|
|
}
|
|
|
|
/**
|
|
* Return true if parent element is a grid or flex container.
|
|
* Note that even if the parent is a grid/flex container, the
|
|
* <display-outside> of aMaybeFlexOrGridItemContent may be "inline" if the
|
|
* parent is also a grid/flex item but has `display:contents`.
|
|
*/
|
|
[[nodiscard]] static bool ParentElementIsGridOrFlexContainer(
|
|
const nsIContent& aMaybeFlexOrGridItemContent);
|
|
};
|
|
|
|
/**
|
|
* 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 AutoClonedRangeArray& 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
|