commit 59dac6a3e4cdf410613f3801eb870ae4fde1530a
parent 7217cfb61c46e0f306d35d4219a08d677b7fb761
Author: Masayuki Nakano <masayuki@d-toybox.com>
Date: Wed, 5 Nov 2025 06:32:05 +0000
Bug 1996758 - Make `WSRunScanner` and `HTMLEditUtils` return `Comment` within optional behavior r=m_kato
They should stop at a `Comment` node or treat `Command` as "visible"
node only when:
- The caller wants to handle only with current or adjacent `Text` node
- The caller wants to list all invisible nodes until the first visible thing
In other words, most users must want to ignore `Comment` nodes.
Therefore, their default behavior should be so but they should treat
`Comment` nodes in an optional mode.
Differential Revision: https://phabricator.services.mozilla.com/D270461
Diffstat:
10 files changed, 887 insertions(+), 306 deletions(-)
diff --git a/editor/libeditor/HTMLEditUtils.cpp b/editor/libeditor/HTMLEditUtils.cpp
@@ -1340,6 +1340,12 @@ bool HTMLEditUtils::IsEmptyNode(nsPresContext* aPresContext,
: !IsVisibleTextNode(*text);
}
+ const bool treatCommentAsVisible =
+ aOptions.contains(EmptyCheckOption::TreatCommentAsVisible);
+ if (aNode.IsComment()) {
+ return !treatCommentAsVisible;
+ }
+
if (!aNode.IsElement()) {
return false;
}
@@ -1412,16 +1418,21 @@ bool HTMLEditUtils::IsEmptyNode(nsPresContext* aPresContext,
return false;
}
+ const bool treatNonEditableContentAsInvisible =
+ aOptions.contains(EmptyCheckOption::TreatNonEditableContentAsInvisible);
bool seenBR = aSeenBR && *aSeenBR;
for (nsIContent* childContent = aNode.GetFirstChild(); childContent;
childContent = childContent->GetNextSibling()) {
- // Is the child editable and non-empty? if so, return false
- if (aOptions.contains(
- EmptyCheckOption::TreatNonEditableContentAsInvisible) &&
- !EditorUtils::IsEditableContent(*childContent, EditorType::HTML)) {
+ if (childContent->IsComment()) {
+ if (treatCommentAsVisible) {
+ return false;
+ }
+ continue;
+ }
+ if (treatNonEditableContentAsInvisible &&
+ !HTMLEditUtils::IsSimplyEditableNode(*childContent)) {
continue;
}
-
if (Text* text = Text::FromNode(childContent)) {
// break out if we find we aren't empty
if (aOptions.contains(EmptyCheckOption::SafeToAskLayout)
@@ -3246,6 +3257,60 @@ std::ostream& operator<<(std::ostream& aStream,
return aStream << "}";
}
+std::ostream& operator<<(std::ostream& aStream,
+ const HTMLEditUtils::EmptyCheckOption& aOption) {
+ constexpr static const char* names[] = {
+ "TreatSingleBRElementAsVisible",
+ "TreatBlockAsVisible",
+ "TreatListItemAsVisible",
+ "TreatTableCellAsVisible",
+ "TreatNonEditableContentAsInvisible",
+ "TreatCommentAsVisible",
+ "SafeToAskLayout",
+ };
+ return aStream << names[static_cast<uint32_t>(aOption)];
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const HTMLEditUtils::EmptyCheckOptions& aOptions) {
+ aStream << "{";
+ bool first = true;
+ for (const auto t : aOptions) {
+ if (!first) {
+ aStream << ", ";
+ }
+ aStream << ToString(t).c_str();
+ first = false;
+ }
+ return aStream << "}";
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const HTMLEditUtils::LeafNodeType& aLeafNodeType) {
+ constexpr static const char* names[] = {
+ "OnlyLeafNode",
+ "LeafNodeOrChildBlock",
+ "LeafNodeOrNonEditableNode",
+ "OnlyEditableLeafNode",
+ "TreatCommentAsLeafNode",
+ };
+ return aStream << names[static_cast<uint32_t>(aLeafNodeType)];
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const HTMLEditUtils::LeafNodeTypes& aLeafNodeTypes) {
+ aStream << "{";
+ bool first = true;
+ for (const auto t : aLeafNodeTypes) {
+ if (!first) {
+ aStream << ", ";
+ }
+ aStream << ToString(t).c_str();
+ first = false;
+ }
+ return aStream << "}";
+}
+
/******************************************************************************
* SelectedTableCellScanner
******************************************************************************/
diff --git a/editor/libeditor/HTMLEditUtils.h b/editor/libeditor/HTMLEditUtils.h
@@ -543,17 +543,14 @@ class HTMLEditUtils final {
/**
* IsContainerNode() returns true if aContent is a container node.
*/
- static bool IsContainerNode(const nsIContent& aContent) {
- nsHTMLTag tagEnum;
- // XXX Should this handle #cdata-section too?
- if (aContent.IsText()) {
- tagEnum = eHTMLTag_text;
- } else {
- // XXX Why don't we use nsHTMLTags::AtomTagToId? Are there some
- // difference?
- tagEnum = nsHTMLTags::StringTagToId(aContent.NodeName());
+ [[nodiscard]] static bool IsContainerNode(const nsIContent& aContent) {
+ if (aContent.IsCharacterData()) {
+ return false;
}
- return HTMLEditUtils::IsContainerNode(tagEnum);
+ return HTMLEditUtils::IsContainerNode(
+ // XXX Why don't we use nsHTMLTags::AtomTagToId? Are there some
+ // difference?
+ nsHTMLTags::StringTagToId(aContent.NodeName()));
}
/**
@@ -764,9 +761,25 @@ class HTMLEditUtils final {
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);
+
/**
- * IsEmptyNode() returns false if aNode has some visible content nodes,
- * list elements or table elements.
+ * 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.
@@ -776,18 +789,21 @@ class HTMLEditUtils final {
* @param aSeenBR [Out] Set to true if this meets an <br> element
* before meeting visible things.
*/
- enum class EmptyCheckOption {
- TreatSingleBRElementAsVisible,
- TreatBlockAsVisible,
- TreatListItemAsVisible,
- TreatTableCellAsVisible,
- TreatNonEditableContentAsInvisible,
- SafeToAskLayout,
- };
- using EmptyCheckOptions = EnumSet<EmptyCheckOption, uint32_t>;
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) {
@@ -1311,9 +1327,16 @@ class HTMLEditUtils final {
LeafNodeOrNonEditableNode,
// Ignore non-editable content at walking the tree.
OnlyEditableLeafNode,
+ // Treat `Comment` nodes are empty leaf nodes.
+ TreatCommentAsLeafNode,
};
using LeafNodeTypes = EnumSet<LeafNodeType>;
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const LeafNodeType& aLeafNodeType);
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const LeafNodeTypes& aLeafNodeTypes);
+
/**
* GetLastLeafContent() returns rightmost leaf content in aNode. It depends
* on aLeafNodeTypes whether this which types of nodes are treated as leaf
@@ -1345,6 +1368,11 @@ class HTMLEditUtils final {
aBlockInlineCheck, aAncestorLimiter);
continue;
}
+ if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) &&
+ content->IsComment()) {
+ content = content->GetPreviousSibling();
+ continue;
+ }
if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) &&
HTMLEditUtils::IsBlockElement(
*content,
@@ -1395,6 +1423,11 @@ class HTMLEditUtils final {
aBlockInlineCheck, aAncestorLimiter);
continue;
}
+ if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) &&
+ content->IsComment()) {
+ content = content->GetNextSibling();
+ continue;
+ }
if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) &&
HTMLEditUtils::IsBlockElement(
*content,
@@ -1435,55 +1468,68 @@ class HTMLEditUtils final {
return nullptr;
}
- nsIContent* nextContent = aStartContent.GetNextSibling();
- if (!nextContent) {
- if (!aStartContent.GetParentElement()) {
- NS_WARNING("Reached orphan node while climbing up the DOM tree");
- return nullptr;
- }
- for (Element* parentElement : aStartContent.AncestorsOfType<Element>()) {
- if (parentElement == aAncestorLimiter ||
- HTMLEditUtils::IsBlockElement(
- *parentElement,
- UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) {
- return nullptr;
- }
- if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
- !parentElement->IsEditable()) {
- return nullptr;
- }
- nextContent = parentElement->GetNextSibling();
- if (nextContent) {
- break;
- }
- if (!parentElement->GetParentElement()) {
+ Element* container = aStartContent.GetParentElement();
+ for (nsIContent* nextContent = aStartContent.GetNextSibling();;) {
+ if (!nextContent) {
+ if (!container) {
NS_WARNING("Reached orphan node while climbing up the DOM tree");
return nullptr;
}
+ for (Element* parentElement :
+ container->InclusiveAncestorsOfType<Element>()) {
+ if (parentElement == aAncestorLimiter ||
+ HTMLEditUtils::IsBlockElement(
+ *parentElement,
+ UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) {
+ return nullptr;
+ }
+ if (aLeafNodeTypes.contains(
+ LeafNodeType::LeafNodeOrNonEditableNode) &&
+ !parentElement->IsEditable()) {
+ return nullptr;
+ }
+ nextContent = parentElement->GetNextSibling();
+ if (nextContent) {
+ container = nextContent->GetParentElement();
+ break;
+ }
+ if (!parentElement->GetParentElement()) {
+ NS_WARNING("Reached orphan node while climbing up the DOM tree");
+ return nullptr;
+ }
+ }
+ MOZ_ASSERT(nextContent);
}
- MOZ_ASSERT(nextContent);
- }
- // We have a next content. If it's a block, return it.
- if (HTMLEditUtils::IsBlockElement(
- *nextContent,
- PreferDisplayOutsideIfUsingDisplay(
- UseComputedDisplayStyleIfAuto(aBlockInlineCheck)))) {
- return nextContent;
- }
- if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
- !nextContent->IsEditable()) {
- return nextContent;
- }
- if (HTMLEditUtils::IsContainerNode(*nextContent)) {
- // Else if it's a container, get deep leftmost child
- if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent(
- *nextContent, aLeafNodeTypes, aBlockInlineCheck)) {
- return child;
+ if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) &&
+ nextContent->IsComment()) {
+ nextContent = nextContent->GetNextSibling();
+ continue;
}
+
+ // We have a next content. If it's a block, return it.
+ if (HTMLEditUtils::IsBlockElement(
+ *nextContent,
+ PreferDisplayOutsideIfUsingDisplay(
+ UseComputedDisplayStyleIfAuto(aBlockInlineCheck)))) {
+ return nextContent;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ !nextContent->IsEditable()) {
+ return nextContent;
+ }
+ if (HTMLEditUtils::IsContainerNode(*nextContent)) {
+ // Else if it's a container, get deep leftmost child
+ if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent(
+ *nextContent, aLeafNodeTypes, aBlockInlineCheck)) {
+ return child;
+ }
+ }
+ // Else return the next content itself.
+ return nextContent;
}
- // Else return the next content itself.
- return nextContent;
+ MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE(
+ "Must return from the preceding for-loop");
}
/**
@@ -1517,43 +1563,52 @@ class HTMLEditUtils final {
aBlockInlineCheck, aAncestorLimiter);
}
- nsCOMPtr<nsIContent> nextContent = aStartPoint.GetChild();
- if (!nextContent) {
- if (aStartPoint.GetContainer() == aAncestorLimiter ||
- HTMLEditUtils::IsBlockElement(
- *aStartPoint.template ContainerAs<nsIContent>(),
- UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) {
- // We are at end of the block.
- return nullptr;
+ for (nsIContent* nextContent = aStartPoint.GetChild();;) {
+ if (!nextContent) {
+ if (aStartPoint.GetContainer() == aAncestorLimiter ||
+ HTMLEditUtils::IsBlockElement(
+ *aStartPoint.template ContainerAs<nsIContent>(),
+ UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) {
+ // We are at end of the block.
+ return nullptr;
+ }
+
+ // We are at end of non-block container
+ return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *aStartPoint.template ContainerAs<nsIContent>(), aLeafNodeTypes,
+ PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck),
+ aAncestorLimiter);
}
- // We are at end of non-block container
- return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
- *aStartPoint.template ContainerAs<nsIContent>(), aLeafNodeTypes,
- PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck),
- aAncestorLimiter);
- }
+ if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) &&
+ nextContent->IsComment()) {
+ nextContent = nextContent->GetNextSibling();
+ continue;
+ }
- // We have a next node. If it's a block, return it.
- if (HTMLEditUtils::IsBlockElement(
- *nextContent,
- UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) {
- return nextContent;
- }
- if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
- !HTMLEditUtils::IsSimplyEditableNode(*nextContent)) {
- return nextContent;
- }
- if (HTMLEditUtils::IsContainerNode(*nextContent)) {
- // else if it's a container, get deep leftmost child
- if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent(
- *nextContent, aLeafNodeTypes,
- PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) {
- return child;
+ // We have a next node. If it's a block, return it.
+ if (HTMLEditUtils::IsBlockElement(
+ *nextContent,
+ UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) {
+ return nextContent;
}
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ !HTMLEditUtils::IsSimplyEditableNode(*nextContent)) {
+ return nextContent;
+ }
+ if (HTMLEditUtils::IsContainerNode(*nextContent)) {
+ // else if it's a container, get deep leftmost child
+ if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent(
+ *nextContent, aLeafNodeTypes,
+ PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) {
+ return child;
+ }
+ }
+ // Else return the node itself
+ return nextContent;
}
- // Else return the node itself
- return nextContent;
+ MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE(
+ "Must return from the preceding for-loop");
}
/**
@@ -1580,56 +1635,69 @@ class HTMLEditUtils final {
return nullptr;
}
- nsIContent* previousContent = aStartContent.GetPreviousSibling();
- if (!previousContent) {
- if (!aStartContent.GetParentElement()) {
- NS_WARNING("Reached orphan node while climbing up the DOM tree");
- return nullptr;
- }
- for (Element* parentElement : aStartContent.AncestorsOfType<Element>()) {
- if (parentElement == aAncestorLimiter ||
- HTMLEditUtils::IsBlockElement(
- *parentElement,
- UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) {
- return nullptr;
- }
- if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
- !parentElement->IsEditable()) {
- return nullptr;
- }
- previousContent = parentElement->GetPreviousSibling();
- if (previousContent) {
- break;
- }
- if (!parentElement->GetParentElement()) {
+ Element* container = aStartContent.GetParentElement();
+ for (nsIContent* previousContent = aStartContent.GetPreviousSibling();;) {
+ if (!previousContent) {
+ if (!container) {
NS_WARNING("Reached orphan node while climbing up the DOM tree");
return nullptr;
}
+ for (Element* parentElement :
+ container->InclusiveAncestorsOfType<Element>()) {
+ if (parentElement == aAncestorLimiter ||
+ HTMLEditUtils::IsBlockElement(
+ *parentElement,
+ UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) {
+ return nullptr;
+ }
+ if (aLeafNodeTypes.contains(
+ LeafNodeType::LeafNodeOrNonEditableNode) &&
+ !parentElement->IsEditable()) {
+ return nullptr;
+ }
+ previousContent = parentElement->GetPreviousSibling();
+ if (previousContent) {
+ container = previousContent->GetParentElement();
+ break;
+ }
+ if (!parentElement->GetParentElement()) {
+ NS_WARNING("Reached orphan node while climbing up the DOM tree");
+ return nullptr;
+ }
+ }
+ MOZ_ASSERT(previousContent);
}
- MOZ_ASSERT(previousContent);
- }
- // We have a next content. If it's a block, return it.
- if (HTMLEditUtils::IsBlockElement(
- *previousContent,
- PreferDisplayOutsideIfUsingDisplay(
- UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck)))) {
- return previousContent;
- }
- if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
- !HTMLEditUtils::IsSimplyEditableNode(*previousContent)) {
- return previousContent;
- }
- if (HTMLEditUtils::IsContainerNode(*previousContent)) {
- // Else if it's a container, get deep rightmost child
- if (nsIContent* child = HTMLEditUtils::GetLastLeafContent(
- *previousContent, aLeafNodeTypes,
- PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) {
- return child;
+ if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) &&
+ previousContent->IsComment()) {
+ previousContent = previousContent->GetPreviousSibling();
+ continue;
}
+
+ // We have a next content. If it's a block, return it.
+ if (HTMLEditUtils::IsBlockElement(
+ *previousContent,
+ PreferDisplayOutsideIfUsingDisplay(
+ UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck)))) {
+ return previousContent;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ !HTMLEditUtils::IsSimplyEditableNode(*previousContent)) {
+ return previousContent;
+ }
+ if (HTMLEditUtils::IsContainerNode(*previousContent)) {
+ // Else if it's a container, get deep rightmost child
+ if (nsIContent* child = HTMLEditUtils::GetLastLeafContent(
+ *previousContent, aLeafNodeTypes,
+ PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) {
+ return child;
+ }
+ }
+ // Else return the next content itself.
+ return previousContent;
}
- // Else return the next content itself.
- return previousContent;
+ MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE(
+ "Must return from the preceding for-loop");
}
/**
@@ -1679,32 +1747,33 @@ class HTMLEditUtils final {
aAncestorLimiter);
}
- nsCOMPtr<nsIContent> previousContent =
- aStartPoint.GetPreviousSiblingOfChild();
- if (NS_WARN_IF(!previousContent)) {
- return nullptr;
- }
-
- // We have a prior node. If it's a block, return it.
- if (HTMLEditUtils::IsBlockElement(
- *previousContent,
- UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) {
- return previousContent;
- }
- if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
- !HTMLEditUtils::IsSimplyEditableNode(*previousContent)) {
- return previousContent;
- }
- if (HTMLEditUtils::IsContainerNode(*previousContent)) {
- // Else if it's a container, get deep rightmost child
- if (nsIContent* child = HTMLEditUtils::GetLastLeafContent(
- *previousContent, aLeafNodeTypes,
- PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) {
- return child;
+ for (nsIContent* previousContent = aStartPoint.GetPreviousSiblingOfChild();
+ previousContent &&
+ (aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) ||
+ !previousContent->IsComment());
+ previousContent = previousContent->GetPreviousSibling()) {
+ // We have a prior node. If it's a block, return it.
+ if (HTMLEditUtils::IsBlockElement(
+ *previousContent,
+ UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) {
+ return previousContent;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ !HTMLEditUtils::IsSimplyEditableNode(*previousContent)) {
+ return previousContent;
+ }
+ if (HTMLEditUtils::IsContainerNode(*previousContent)) {
+ // Else if it's a container, get deep rightmost child
+ if (nsIContent* child = HTMLEditUtils::GetLastLeafContent(
+ *previousContent, aLeafNodeTypes,
+ PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) {
+ return child;
+ }
}
+ // Else return the node itself
+ return previousContent;
}
- // Else return the node itself
- return previousContent;
+ return nullptr;
}
/**
diff --git a/editor/libeditor/HTMLEditorDataTransfer.cpp b/editor/libeditor/HTMLEditorDataTransfer.cpp
@@ -500,6 +500,8 @@ HTMLEditor::HTMLWithContextInserter::GetNewCaretPointAfterInsertingHTML(
// but don't cross tables
nsIContent* containerContent = nullptr;
+ // FIXME: GetChild() might be nullptr, but it's referred as non-null in the
+ // block!
if (!aLastInsertedPoint.GetChild() ||
!aLastInsertedPoint.GetChild()->IsHTMLElement(nsGkAtoms::table)) {
containerContent = HTMLEditUtils::GetLastLeafContent(
@@ -540,9 +542,7 @@ HTMLEditor::HTMLWithContextInserter::GetNewCaretPointAfterInsertingHTML(
// Otherwise, i.e., it's an atomic element, `<table>` element or data node,
// put caret after it.
else {
- pointToPutCaret.Set(containerContent);
- DebugOnly<bool> advanced = pointToPutCaret.AdvanceOffset();
- NS_WARNING_ASSERTION(advanced, "Failed to advance offset from found node");
+ pointToPutCaret.SetAfter(containerContent);
}
// Make sure we don't end up with selection collapsed after an invisible
diff --git a/editor/libeditor/WSRunScanner.cpp b/editor/libeditor/WSRunScanner.cpp
@@ -208,6 +208,9 @@ WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
return WSScanResult::Error();
}
+ MOZ_ASSERT_IF(!ScanOptions().contains(Option::StopAtComment),
+ !Comment::FromNodeOrNull(
+ TextFragmentDataAtStartRef().GetStartReasonContent()));
switch (TextFragmentDataAtStartRef().StartRawReason()) {
case WSType::CollapsibleWhiteSpaces:
case WSType::NonCollapsibleCharacters:
@@ -219,29 +222,6 @@ WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
return WSScanResult(*this, WSScanResult::ScanDirection::Backward,
TextFragmentDataAtStartRef().StartRef(),
TextFragmentDataAtStartRef().StartRawReason());
- case WSType::SpecialContent: {
- const Comment* comment = Comment::FromNode(
- TextFragmentDataAtStartRef().GetStartReasonContent());
- if (!comment) {
- break;
- }
- // If we reached a comment node, we should skip it because it's always
- // invisible.
- while (true) {
- const EditorRawDOMPoint atComment(comment);
- WSRunScanner scanner(ScanOptions(), atComment,
- mTextFragmentDataAtStart.GetAncestorLimiter());
- if (scanner.TextFragmentDataAtStartRef().StartRawReason() ==
- WSType::SpecialContent) {
- if ((comment = Comment::FromNode(scanner.TextFragmentDataAtStartRef()
- .GetStartReasonContent()))) {
- // Reached another comment node, keep scanning...
- continue;
- }
- }
- return scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(atComment);
- }
- }
default:
break;
}
@@ -325,6 +305,9 @@ WSScanResult WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(
return WSScanResult::Error();
}
+ MOZ_ASSERT_IF(!ScanOptions().contains(Option::StopAtComment),
+ !Comment::FromNodeOrNull(
+ TextFragmentDataAtStartRef().GetEndReasonContent()));
switch (TextFragmentDataAtStartRef().EndRawReason()) {
case WSType::CollapsibleWhiteSpaces:
case WSType::NonCollapsibleCharacters:
@@ -337,31 +320,6 @@ WSScanResult WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(
return WSScanResult(*this, WSScanResult::ScanDirection::Forward,
TextFragmentDataAtStartRef().EndRef(),
TextFragmentDataAtStartRef().EndRawReason());
- case WSType::SpecialContent: {
- const Comment* comment =
- Comment::FromNode(TextFragmentDataAtStartRef().GetEndReasonContent());
- if (!comment) {
- break;
- }
- // If we reached a comment node, we should skip it because it's always
- // invisible.
- while (true) {
- const EditorRawDOMPoint afterComment =
- EditorRawDOMPoint::After(*comment);
- WSRunScanner scanner(ScanOptions(), afterComment,
- mTextFragmentDataAtStart.GetAncestorLimiter());
- if (scanner.TextFragmentDataAtStartRef().EndRawReason() ==
- WSType::SpecialContent) {
- if ((comment = Comment::FromNode(scanner.TextFragmentDataAtStartRef()
- .GetEndReasonContent()))) {
- // Reached another comment node, keep scanning...
- continue;
- }
- }
- return scanner.ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(
- afterComment);
- }
- }
default:
break;
}
diff --git a/editor/libeditor/WSRunScanner.h b/editor/libeditor/WSRunScanner.h
@@ -398,6 +398,8 @@ class MOZ_STACK_CLASS WSRunScanner final {
// If set, use the HTML default style to consider whether the found one is a
// block or an inline.
ReferHTMLDefaultStyle,
+ // If set, stop scanning the DOM when it reaches a `Comment` node.
+ StopAtComment,
};
using Options = EnumSet<Option>;
@@ -1086,11 +1088,13 @@ class MOZ_STACK_CLASS WSRunScanner final {
/**
* Return inclusive next point in inclusive next `Text` node from aPoint.
* So, it may be in a collapsed white-space or invisible white-spaces.
+ * NOTE: Option::OnlyEditableNodes is ignored because it's treated as "stop
+ * at non-editable content" in the other places, but this "ignores" them.
*/
template <typename EditorDOMPointType, typename PT, typename CT>
[[nodiscard]] static EditorDOMPointType GetInclusiveNextCharPoint(
const EditorDOMPointBase<PT, CT>& aPoint,
- ReferHTMLDefaultStyle aReferHTMLDefaultStyle,
+ Options aOptions, // NOLINT(performance-unnecessary-value-param)
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
const nsIContent* aFollowingLimiterContent = nullptr);
@@ -1099,18 +1103,19 @@ class MOZ_STACK_CLASS WSRunScanner final {
const EditorDOMPointBase<PT, CT>& aPoint,
IgnoreNonEditableNodes aIgnoreNonEditableNodes) const {
return GetInclusiveNextCharPoint<EditorDOMPointType>(
- aPoint, ShouldReferHTMLDefaultStyle(mOptions),
- aIgnoreNonEditableNodes, GetEndReasonContent());
+ aPoint, mOptions, aIgnoreNonEditableNodes, GetEndReasonContent());
}
/**
* Return previous point in inclusive previous `Text` node from aPoint.
* So, it may be in a collapsed white-space or invisible white-spaces.
+ * NOTE: Option::OnlyEditableNodes is ignored because it's treated as "stop
+ * at non-editable content" in the other places, but this "ignores" them.
*/
template <typename EditorDOMPointType, typename PT, typename CT>
[[nodiscard]] static EditorDOMPointType GetPreviousCharPoint(
const EditorDOMPointBase<PT, CT>& aPoint,
- ReferHTMLDefaultStyle aReferHTMLDefaultStyle,
+ Options aOptions, // NOLINT(performance-unnecessary-value-param)
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
const nsIContent* aPrecedingLimiterContent = nullptr);
@@ -1119,12 +1124,13 @@ class MOZ_STACK_CLASS WSRunScanner final {
const EditorDOMPointBase<PT, CT>& aPoint,
IgnoreNonEditableNodes aIgnoreNonEditableNodes) const {
return GetPreviousCharPoint<EditorDOMPointType>(
- aPoint, ShouldReferHTMLDefaultStyle(mOptions),
- aIgnoreNonEditableNodes, GetStartReasonContent());
+ aPoint, mOptions, aIgnoreNonEditableNodes, GetStartReasonContent());
}
/**
* Return end of current collapsible ASCII white-spaces.
+ * NOTE: Option::OnlyEditableNodes is ignored because it's treated as "stop
+ * at non-editable content" in the other places, but this "ignores" them.
*
* @param aPointAtASCIIWhiteSpace Must be in a sequence of collapsible
* ASCII white-spaces.
@@ -1134,7 +1140,7 @@ class MOZ_STACK_CLASS WSRunScanner final {
[[nodiscard]] static EditorDOMPointType GetEndOfCollapsibleASCIIWhiteSpaces(
const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
nsIEditor::EDirection aDirectionToDelete,
- ReferHTMLDefaultStyle aReferHTMLDefaultStyle,
+ Options aOptions, // NOLINT(performance-unnecessary-value-param)
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
const nsIContent* aFollowingLimiterContent = nullptr);
@@ -1144,13 +1150,14 @@ class MOZ_STACK_CLASS WSRunScanner final {
nsIEditor::EDirection aDirectionToDelete,
IgnoreNonEditableNodes aIgnoreNonEditableNodes) const {
return GetEndOfCollapsibleASCIIWhiteSpaces<EditorDOMPointType>(
- aPointAtASCIIWhiteSpace, aDirectionToDelete,
- ShouldReferHTMLDefaultStyle(mOptions), aIgnoreNonEditableNodes,
- GetEndReasonContent());
+ aPointAtASCIIWhiteSpace, aDirectionToDelete, mOptions,
+ aIgnoreNonEditableNodes, GetEndReasonContent());
}
/**
* Return start of current collapsible ASCII white-spaces.
+ * NOTE: Option::OnlyEditableNodes is ignored because it's treated as "stop
+ * at non-editable content" in the other places, but this "ignores" them.
*
* @param aPointAtASCIIWhiteSpace Must be in a sequence of collapsible
* ASCII white-spaces.
@@ -1161,7 +1168,7 @@ class MOZ_STACK_CLASS WSRunScanner final {
GetFirstASCIIWhiteSpacePointCollapsedTo(
const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
nsIEditor::EDirection aDirectionToDelete,
- ReferHTMLDefaultStyle aReferHTMLDefaultStyle,
+ Options aOptions, // NOLINT(performance-unnecessary-value-param)
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
const nsIContent* aPrecedingLimiterContent = nullptr);
@@ -1171,9 +1178,8 @@ class MOZ_STACK_CLASS WSRunScanner final {
nsIEditor::EDirection aDirectionToDelete,
IgnoreNonEditableNodes aIgnoreNonEditableNodes) const {
return GetFirstASCIIWhiteSpacePointCollapsedTo<EditorDOMPointType>(
- aPointAtASCIIWhiteSpace, aDirectionToDelete,
- ShouldReferHTMLDefaultStyle(mOptions), aIgnoreNonEditableNodes,
- GetStartReasonContent());
+ aPointAtASCIIWhiteSpace, aDirectionToDelete, mOptions,
+ aIgnoreNonEditableNodes, GetStartReasonContent());
}
/**
diff --git a/editor/libeditor/WSRunScannerNestedClasses.cpp b/editor/libeditor/WSRunScannerNestedClasses.cpp
@@ -39,45 +39,44 @@ template WSRunScanner::TextFragmentData::TextFragmentData(
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint,
- const EditorDOMPoint&, ReferHTMLDefaultStyle, IgnoreNonEditableNodes,
- const nsIContent*);
+ const EditorDOMPoint&, Options, IgnoreNonEditableNodes, const nsIContent*);
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint,
- const EditorRawDOMPoint&, ReferHTMLDefaultStyle, IgnoreNonEditableNodes,
+ const EditorRawDOMPoint&, Options, IgnoreNonEditableNodes,
const nsIContent*);
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint,
- const EditorDOMPointInText&, ReferHTMLDefaultStyle, IgnoreNonEditableNodes,
+ const EditorDOMPointInText&, Options, IgnoreNonEditableNodes,
const nsIContent*);
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint,
- const EditorRawDOMPointInText&, ReferHTMLDefaultStyle,
- IgnoreNonEditableNodes, const nsIContent*);
+ const EditorRawDOMPointInText&, Options, IgnoreNonEditableNodes,
+ const nsIContent*);
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
WSRunScanner::TextFragmentData::GetPreviousCharPoint, const EditorDOMPoint&,
- ReferHTMLDefaultStyle, IgnoreNonEditableNodes, const nsIContent*);
+ Options, IgnoreNonEditableNodes, const nsIContent*);
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
WSRunScanner::TextFragmentData::GetPreviousCharPoint,
- const EditorRawDOMPoint&, ReferHTMLDefaultStyle, IgnoreNonEditableNodes,
+ const EditorRawDOMPoint&, Options, IgnoreNonEditableNodes,
const nsIContent*);
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
WSRunScanner::TextFragmentData::GetPreviousCharPoint,
- const EditorDOMPointInText&, ReferHTMLDefaultStyle, IgnoreNonEditableNodes,
+ const EditorDOMPointInText&, Options, IgnoreNonEditableNodes,
const nsIContent*);
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
WSRunScanner::TextFragmentData::GetPreviousCharPoint,
- const EditorRawDOMPointInText&, ReferHTMLDefaultStyle,
- IgnoreNonEditableNodes, const nsIContent*);
+ const EditorRawDOMPointInText&, Options, IgnoreNonEditableNodes,
+ const nsIContent*);
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
WSRunScanner::TextFragmentData::GetEndOfCollapsibleASCIIWhiteSpaces,
- const EditorDOMPointInText&, nsIEditor::EDirection, ReferHTMLDefaultStyle,
+ const EditorDOMPointInText&, nsIEditor::EDirection, Options,
IgnoreNonEditableNodes, const nsIContent*);
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
WSRunScanner::TextFragmentData::GetFirstASCIIWhiteSpacePointCollapsedTo,
- const EditorDOMPointInText&, nsIEditor::EDirection, ReferHTMLDefaultStyle,
+ const EditorDOMPointInText&, nsIEditor::EDirection, Options,
IgnoreNonEditableNodes, const nsIContent*);
// FIXME: I think the scanner should not cross the <button> element boundaries.
@@ -246,10 +245,15 @@ WSRunScanner::TextFragmentData::BoundaryData WSRunScanner::TextFragmentData::
? BlockInlineCheck::UseHTMLDefaultStyle
: BlockInlineCheck::Auto;
// Then, we need to check previous leaf node.
- const auto leafNodeTypes =
- aOptions.contains(Option::OnlyEditableNodes)
- ? LeafNodeTypes{LeafNodeType::LeafNodeOrNonEditableNode}
- : LeafNodeTypes{LeafNodeType::OnlyLeafNode};
+ const auto leafNodeTypes = [&]() -> LeafNodeTypes {
+ auto types = aOptions.contains(Option::OnlyEditableNodes)
+ ? LeafNodeTypes{LeafNodeType::LeafNodeOrNonEditableNode}
+ : LeafNodeTypes{LeafNodeType::OnlyLeafNode};
+ if (aOptions.contains(Option::StopAtComment)) {
+ types += LeafNodeType::TreatCommentAsLeafNode;
+ }
+ return types;
+ }();
nsIContent* previousLeafContentOrBlock =
HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
aPoint, leafNodeTypes, blockInlineCheck, &aAncestorLimiter);
@@ -403,10 +407,15 @@ WSRunScanner::TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
: BlockInlineCheck::Auto;
// Then, we need to check next leaf node.
- const auto leafNodeTypes =
- aOptions.contains(Option::OnlyEditableNodes)
- ? LeafNodeTypes{LeafNodeType::LeafNodeOrNonEditableNode}
- : LeafNodeTypes{LeafNodeType::OnlyLeafNode};
+ const auto leafNodeTypes = [&]() -> LeafNodeTypes {
+ auto types = aOptions.contains(Option::OnlyEditableNodes)
+ ? LeafNodeTypes{LeafNodeType::LeafNodeOrNonEditableNode}
+ : LeafNodeTypes{LeafNodeType::OnlyLeafNode};
+ if (aOptions.contains(Option::StopAtComment)) {
+ types += LeafNodeType::TreatCommentAsLeafNode;
+ }
+ return types;
+ }();
nsIContent* nextLeafContentOrBlock =
HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
aPoint, leafNodeTypes, blockInlineCheck, &aAncestorLimiter);
@@ -849,7 +858,7 @@ WSRunScanner::TextFragmentData::GetReplaceRangeDataAtStartOfDeletionRange(
template <typename EditorDOMPointType, typename PT, typename CT>
EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint(
const EditorDOMPointBase<PT, CT>& aPoint,
- ReferHTMLDefaultStyle aReferHTMLDefaultStyle,
+ Options aOptions, // NOLINT(performance-unnecessary-value-param)
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
const nsIContent* aFollowingLimiterContent /* = nullptr */) {
MOZ_ASSERT(aPoint.IsSetAndValid());
@@ -859,12 +868,20 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint(
}
const BlockInlineCheck blockInlineCheck =
- static_cast<bool>(aReferHTMLDefaultStyle)
+ aOptions.contains(Option::ReferHTMLDefaultStyle)
? BlockInlineCheck::UseHTMLDefaultStyle
: BlockInlineCheck::Auto;
const EditorRawDOMPoint point = [&]() MOZ_NEVER_INLINE_DEBUG {
- nsIContent* const child =
- aPoint.CanContainerHaveChildren() ? aPoint.GetChild() : nullptr;
+ nsIContent* const child = [&]() -> nsIContent* {
+ nsIContent* child =
+ aPoint.CanContainerHaveChildren() ? aPoint.GetChild() : nullptr;
+ // XXX Why don't we skip non-editable nodes here?
+ while (child && child->IsComment() &&
+ !aOptions.contains(Option::StopAtComment)) {
+ child = child->GetNextSibling();
+ }
+ return child;
+ }();
if (!child ||
HTMLEditUtils::IsBlockElement(
*child, UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) ||
@@ -872,7 +889,9 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint(
return aPoint.template To<EditorRawDOMPoint>();
}
if (!child->HasChildNodes()) {
- return EditorRawDOMPoint(child, 0);
+ return child->IsText() || HTMLEditUtils::IsContainerNode(*child)
+ ? EditorRawDOMPoint(child, 0)
+ : EditorRawDOMPoint::After(*child);
}
// FIXME: This may skip aFollowingLimiterContent, so, this utility should
// take a stopper param.
@@ -881,13 +900,15 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint(
// following block boundary.
nsIContent* const leafContent = HTMLEditUtils::GetFirstLeafContent(
*child, {LeafNodeType::LeafNodeOrChildBlock}, blockInlineCheck);
- if (NS_WARN_IF(!leafContent) ||
- HTMLEditUtils::IsBlockElement(
+ if (HTMLEditUtils::IsBlockElement(
*leafContent,
UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) ||
HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*leafContent)) {
return EditorRawDOMPoint();
}
+ if (!leafContent) {
+ return EditorRawDOMPoint(child, 0);
+ }
return EditorRawDOMPoint(leafContent, 0);
}();
if (!point.IsSet()) {
@@ -922,11 +943,16 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint(
return EditorDOMPointType();
}
- const auto leafNodeTypes =
- aIgnoreNonEditableNodes == IgnoreNonEditableNodes::Yes
- ? LeafNodeTypes(LeafNodeType::LeafNodeOrNonEditableNode,
- LeafNodeType::LeafNodeOrChildBlock)
- : LeafNodeTypes(LeafNodeType::LeafNodeOrChildBlock);
+ const auto leafNodeTypes = [&]() -> LeafNodeTypes {
+ auto types = aIgnoreNonEditableNodes == IgnoreNonEditableNodes::Yes
+ ? LeafNodeTypes(LeafNodeType::LeafNodeOrNonEditableNode,
+ LeafNodeType::LeafNodeOrChildBlock)
+ : LeafNodeTypes(LeafNodeType::LeafNodeOrChildBlock);
+ if (aOptions.contains(Option::StopAtComment)) {
+ types += LeafNodeType::TreatCommentAsLeafNode;
+ }
+ return types;
+ }();
for (nsIContent* nextContent =
HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
*point.ContainerAs<nsIContent>(), leafNodeTypes,
@@ -957,7 +983,7 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint(
template <typename EditorDOMPointType, typename PT, typename CT>
EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint(
const EditorDOMPointBase<PT, CT>& aPoint,
- ReferHTMLDefaultStyle aReferHTMLDefaultStyle,
+ Options aOptions, // NOLINT(performance-unnecessary-value-param)
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
const nsIContent* aPrecedingLimiterContent /* = nullptr */) {
MOZ_ASSERT(aPoint.IsSetAndValid());
@@ -967,13 +993,21 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint(
}
const BlockInlineCheck blockInlineCheck =
- static_cast<bool>(aReferHTMLDefaultStyle)
+ aOptions.contains(Option::ReferHTMLDefaultStyle)
? BlockInlineCheck::UseHTMLDefaultStyle
: BlockInlineCheck::Auto;
const EditorRawDOMPoint point = [&]() MOZ_NEVER_INLINE_DEBUG {
- nsIContent* const previousChild = aPoint.CanContainerHaveChildren()
- ? aPoint.GetPreviousSiblingOfChild()
- : nullptr;
+ nsIContent* const previousChild = [&]() -> nsIContent* {
+ nsIContent* previousChild = aPoint.CanContainerHaveChildren()
+ ? aPoint.GetPreviousSiblingOfChild()
+ : nullptr;
+ // XXX Why don't we skip non-editable nodes here?
+ while (previousChild && previousChild->IsComment() &&
+ !aOptions.contains(Option::StopAtComment)) {
+ previousChild = previousChild->GetPreviousSibling();
+ }
+ return previousChild;
+ }();
if (!previousChild ||
HTMLEditUtils::IsBlockElement(
*previousChild,
@@ -982,7 +1016,10 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint(
return aPoint.template To<EditorRawDOMPoint>();
}
if (!previousChild->HasChildren()) {
- return EditorRawDOMPoint::AtEndOf(*previousChild);
+ return previousChild->IsText() ||
+ HTMLEditUtils::IsContainerNode(*previousChild)
+ ? EditorRawDOMPoint::AtEndOf(*previousChild)
+ : EditorRawDOMPoint::After(*previousChild);
}
// FIXME: This may skip aPrecedingLimiterContent, so, this utility should
// take a stopper param.
@@ -991,13 +1028,15 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint(
// following block boundary.
nsIContent* const leafContent = HTMLEditUtils::GetLastLeafContent(
*previousChild, {LeafNodeType::LeafNodeOrChildBlock}, blockInlineCheck);
- if (NS_WARN_IF(!leafContent) ||
- HTMLEditUtils::IsBlockElement(
+ if (HTMLEditUtils::IsBlockElement(
*leafContent,
UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) ||
HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*leafContent)) {
return EditorRawDOMPoint();
}
+ if (!leafContent) {
+ return EditorRawDOMPoint::AtEndOf(*previousChild);
+ }
return EditorRawDOMPoint::AtEndOf(*leafContent);
}();
if (!point.IsSet()) {
@@ -1033,11 +1072,16 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint(
return EditorDOMPointType();
}
- const auto leafNodeTypes =
- aIgnoreNonEditableNodes == IgnoreNonEditableNodes::Yes
- ? LeafNodeTypes(LeafNodeType::LeafNodeOrNonEditableNode,
- LeafNodeType::LeafNodeOrChildBlock)
- : LeafNodeTypes(LeafNodeType::LeafNodeOrChildBlock);
+ const auto leafNodeTypes = [&]() -> LeafNodeTypes {
+ auto types = aIgnoreNonEditableNodes == IgnoreNonEditableNodes::Yes
+ ? LeafNodeTypes(LeafNodeType::LeafNodeOrNonEditableNode,
+ LeafNodeType::LeafNodeOrChildBlock)
+ : LeafNodeTypes(LeafNodeType::LeafNodeOrChildBlock);
+ if (aOptions.contains(Option::StopAtComment)) {
+ types += LeafNodeType::TreatCommentAsLeafNode;
+ }
+ return types;
+ }();
for (
nsIContent* previousContent =
HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
@@ -1074,7 +1118,7 @@ EditorDOMPointType
WSRunScanner::TextFragmentData::GetEndOfCollapsibleASCIIWhiteSpaces(
const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
nsIEditor::EDirection aDirectionToDelete,
- ReferHTMLDefaultStyle aReferHTMLDefaultStyle,
+ Options aOptions, // NOLINT(performance-unnecessary-value-param)
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
const nsIContent* aFollowingLimiterContent /* = nullptr */) {
MOZ_ASSERT(aDirectionToDelete == nsIEditor::eNone ||
@@ -1149,8 +1193,8 @@ WSRunScanner::TextFragmentData::GetEndOfCollapsibleASCIIWhiteSpaces(
for (EditorDOMPointInText atEndOfPreviousTextNode = afterLastWhiteSpace;;) {
const auto atStartOfNextTextNode =
TextFragmentData::GetInclusiveNextCharPoint<EditorDOMPointInText>(
- atEndOfPreviousTextNode, aReferHTMLDefaultStyle,
- aIgnoreNonEditableNodes, aFollowingLimiterContent);
+ atEndOfPreviousTextNode, aOptions, aIgnoreNonEditableNodes,
+ aFollowingLimiterContent);
if (!atStartOfNextTextNode.IsSet()) {
// There is no more text nodes. Return end of the previous text node.
return afterLastWhiteSpace.To<EditorDOMPointType>();
@@ -1192,7 +1236,7 @@ EditorDOMPointType
WSRunScanner::TextFragmentData::GetFirstASCIIWhiteSpacePointCollapsedTo(
const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
nsIEditor::EDirection aDirectionToDelete,
- ReferHTMLDefaultStyle aReferHTMLDefaultStyle,
+ Options aOptions, // NOLINT(performance-unnecessary-value-param)
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
const nsIContent* aPrecedingLimiterContent) {
MOZ_ASSERT(aDirectionToDelete == nsIEditor::eNone ||
@@ -1269,8 +1313,8 @@ WSRunScanner::TextFragmentData::GetFirstASCIIWhiteSpacePointCollapsedTo(
for (EditorDOMPointInText atStartOfPreviousTextNode = atLastWhiteSpace;;) {
const auto atLastCharOfPreviousTextNode =
TextFragmentData::GetPreviousCharPoint<EditorDOMPointInText>(
- atStartOfPreviousTextNode, aReferHTMLDefaultStyle,
- aIgnoreNonEditableNodes, aPrecedingLimiterContent);
+ atStartOfPreviousTextNode, aOptions, aIgnoreNonEditableNodes,
+ aPrecedingLimiterContent);
if (!atLastCharOfPreviousTextNode.IsSet()) {
// There is no more text nodes. Return end of last text node.
return atLastWhiteSpace.To<EditorDOMPointType>();
diff --git a/editor/libeditor/WhiteSpaceVisibilityKeeper.cpp b/editor/libeditor/WhiteSpaceVisibilityKeeper.cpp
@@ -1694,12 +1694,15 @@ nsresult WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesAfter(
AutoTArray<OwningNonNull<nsIContent>, 32> unnecessaryContents;
for (nsIContent* nextContent =
HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
- aPoint, {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock},
+ aPoint,
+ {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock,
+ HTMLEditUtils::LeafNodeType::TreatCommentAsLeafNode},
BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement);
nextContent;
nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
EditorRawDOMPoint::After(*nextContent),
- {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock},
+ {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock,
+ HTMLEditUtils::LeafNodeType::TreatCommentAsLeafNode},
BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement)) {
if (!HTMLEditUtils::IsSimplyEditableNode(*nextContent)) {
// XXX Assume non-editable nodes are visible.
@@ -1775,13 +1778,16 @@ nsresult WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesBefore(
AutoTArray<OwningNonNull<nsIContent>, 32> unnecessaryContents;
for (nsIContent* previousContent =
HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
- aPoint, {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock},
+ aPoint,
+ {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock,
+ HTMLEditUtils::LeafNodeType::TreatCommentAsLeafNode},
BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement);
previousContent;
previousContent =
HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
EditorRawDOMPoint(previousContent),
- {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock},
+ {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock,
+ HTMLEditUtils::LeafNodeType::TreatCommentAsLeafNode},
BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement)) {
if (!HTMLEditUtils::IsSimplyEditableNode(*previousContent)) {
// XXX Assume non-editable nodes are visible.
diff --git a/editor/libeditor/gtest/TestHTMLEditUtils.cpp b/editor/libeditor/gtest/TestHTMLEditUtils.cpp
@@ -6,6 +6,8 @@
#include "gtest/gtest.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/OriginAttributes.h"
+#include "mozilla/dom/CDATASection.h"
+#include "mozilla/dom/Comment.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Text.h"
#include "EditorDOMPoint.h"
@@ -15,6 +17,7 @@
#include "nsIURI.h"
#include "nsNetUtil.h"
#include "nsString.h"
+#include "nsTextNode.h"
namespace mozilla {
@@ -1267,4 +1270,435 @@ TEST(HTMLEditUtilsTest, GetAncestorElement_ClosestContainerElement)
}
}
+TEST(HTMLEditUtilsTest, IsContainerNode)
+{
+ const RefPtr<Document> doc = CreateHTMLDoc();
+ for (const char16_t* tagName :
+ {u"html", u"body", u"div", u"span", u"select", u"option", u"form"}) {
+ const RefPtr<nsAtom> tag = NS_Atomize(tagName);
+ MOZ_RELEASE_ASSERT(tag);
+ const RefPtr<Element> element = doc->CreateHTMLElement(tag);
+ MOZ_RELEASE_ASSERT(element);
+ EXPECT_EQ(true, HTMLEditUtils::IsContainerNode(*element))
+ << "IsContainerNode(<" << NS_ConvertUTF16toUTF8(tagName).get() << ">)";
+ }
+ for (const char16_t* tagName : {u"img", u"input", u"br", u"wbr"}) {
+ const RefPtr<nsAtom> tag = NS_Atomize(tagName);
+ MOZ_RELEASE_ASSERT(tag);
+ const RefPtr<Element> element = doc->CreateHTMLElement(tag);
+ MOZ_RELEASE_ASSERT(element);
+ EXPECT_EQ(false, HTMLEditUtils::IsContainerNode(*element))
+ << "IsContainerNode(<" << NS_ConvertUTF16toUTF8(tagName).get() << ">)";
+ }
+ {
+ const RefPtr<nsTextNode> text = doc->CreateEmptyTextNode();
+ MOZ_RELEASE_ASSERT(text);
+ EXPECT_EQ(false, HTMLEditUtils::IsContainerNode(*text))
+ << "IsContainerNode(Text)";
+ }
+ {
+ const RefPtr<Comment> comment =
+ doc->CreateComment(nsDependentString(u"abc"));
+ MOZ_RELEASE_ASSERT(comment);
+ EXPECT_EQ(false, HTMLEditUtils::IsContainerNode(*comment))
+ << "IsContainerNode(Comment)";
+ }
+}
+
+struct MOZ_STACK_CLASS IsEmptyNodeTest final {
+ const char16_t* mInnerHTML;
+ const char* mTargetSelector;
+ const HTMLEditUtils::EmptyCheckOptions mOptions;
+ const bool mExpectedValue;
+ const bool mExpectedSeenBR;
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const IsEmptyNodeTest& aTest) {
+ return aStream << "Check \"" << aTest.mTargetSelector
+ << "\" with options=" << ToString(aTest.mOptions).c_str()
+ << " in \"" << NS_ConvertUTF16toUTF8(aTest.mInnerHTML).get()
+ << "\"";
+ }
+};
+
+TEST(HTMLEditUtilsTest, IsEmptyNode)
+{
+ using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
+ const RefPtr<Document> doc = CreateHTMLDoc();
+ const RefPtr<nsGenericHTMLElement> body = doc->GetBody();
+ MOZ_RELEASE_ASSERT(body);
+ for (const auto& testData : {
+ IsEmptyNodeTest{u"<div></div>", "div", {}, true, false},
+ IsEmptyNodeTest{u"<div></div>",
+ "div",
+ {EmptyCheckOption::TreatBlockAsVisible},
+ true,
+ false},
+ IsEmptyNodeTest{u"<div><br></div>", "div", {}, true, true},
+ IsEmptyNodeTest{u"<div><br></div>",
+ "div",
+ {EmptyCheckOption::TreatBlockAsVisible},
+ true,
+ true},
+ IsEmptyNodeTest{u"<div><br></div>",
+ "div",
+ {EmptyCheckOption::TreatSingleBRElementAsVisible},
+ false,
+ false},
+ IsEmptyNodeTest{u"<div><!--abc--></div>", "div", {}, true, false},
+ IsEmptyNodeTest{u"<div><!--abc--></div>",
+ "div",
+ {EmptyCheckOption::TreatCommentAsVisible},
+ false,
+ false},
+ IsEmptyNodeTest{u"<ul><li><br></li></ul>", "ul", {}, true, true},
+ IsEmptyNodeTest{u"<ul><li><br></li></ul>",
+ "ul",
+ {EmptyCheckOption::TreatListItemAsVisible},
+ false,
+ false},
+ IsEmptyNodeTest{
+ u"<table><td><br></td></table>", "table", {}, true, true},
+ IsEmptyNodeTest{u"<table><td><br></td></table>",
+ "table",
+ {EmptyCheckOption::TreatTableCellAsVisible},
+ false,
+ false},
+ IsEmptyNodeTest{u"<div>abc</div>", "div", {}, false, false},
+ IsEmptyNodeTest{
+ u"<div><span><br></span></div>", "div", {}, true, true},
+ IsEmptyNodeTest{
+ u"<div><div><br></div></div>", "div", {}, true, true},
+ IsEmptyNodeTest{u"<div><div><br></div></div>",
+ "div",
+ {EmptyCheckOption::TreatBlockAsVisible},
+ false,
+ false},
+ IsEmptyNodeTest{u"<dl><dt><br></dt></dl>", "dl", {}, true, true},
+ IsEmptyNodeTest{u"<dl><dt><br</dt></dl>",
+ "dl",
+ {EmptyCheckOption::TreatListItemAsVisible},
+ false,
+ false},
+ IsEmptyNodeTest{u"<dl><dd><br></dd></dl>", "dl", {}, true, true},
+ IsEmptyNodeTest{u"<dl><dd><br</dd></dl>",
+ "dl",
+ {EmptyCheckOption::TreatListItemAsVisible},
+ false,
+ false},
+ // form controls should be always not empty.
+ IsEmptyNodeTest{u"<input>", "input", {}, false, false},
+ IsEmptyNodeTest{u"<select></select>", "select", {}, false, false},
+ IsEmptyNodeTest{u"<button></button>", "button", {}, false, false},
+ IsEmptyNodeTest{
+ u"<textarea></textarea>", "textarea", {}, false, false},
+ IsEmptyNodeTest{u"<output></output>", "output", {}, false, false},
+ IsEmptyNodeTest{
+ u"<progress></progress>", "progress", {}, false, false},
+ IsEmptyNodeTest{u"<meter></meter>", "meter", {}, false, false},
+ // void elements should be always not empty.
+ IsEmptyNodeTest{u"<br>", "br", {}, false, false},
+ IsEmptyNodeTest{u"<wbr>", "wbr", {}, false, false},
+ IsEmptyNodeTest{u"<img>", "img", {}, false, false},
+ // white-spaces should not be treated as visible in block
+ IsEmptyNodeTest{u"<div> </div>", "div", {}, true, false},
+ IsEmptyNodeTest{u"<span> </span>", "span", {}, true, false},
+ IsEmptyNodeTest{u"a<span> </span>b", "span", {}, false, false},
+ // sublist's list items and table cells should be treated as visible.
+ IsEmptyNodeTest{u"<ul><li><ol><li><br></li></ol></li></ul>",
+ "ul",
+ {},
+ false,
+ false},
+ IsEmptyNodeTest{u"<ul><li><table><td><br></td></table></li></ul>",
+ "ul",
+ {},
+ false,
+ false},
+ IsEmptyNodeTest{
+ u"<table><td><table><td><br></td></table></td></table>",
+ "table",
+ {},
+ false,
+ false},
+ IsEmptyNodeTest{u"<table><td><ul><li><br></li></ul></td></table>",
+ "table",
+ {},
+ false,
+ false},
+ }) {
+ body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML),
+ doc->NodePrincipal(), IgnoreErrors());
+ const Element* const target = body->QuerySelector(
+ nsDependentCString(testData.mTargetSelector), IgnoreErrors());
+ MOZ_RELEASE_ASSERT(target);
+ bool seenBR = false;
+ const bool ret =
+ HTMLEditUtils::IsEmptyNode(*target, testData.mOptions, &seenBR);
+ EXPECT_EQ(ret, testData.mExpectedValue)
+ << "IsEmptyNode(result): " << testData;
+ EXPECT_EQ(seenBR, testData.mExpectedSeenBR)
+ << "IsEmptyNode(seenBR): " << testData;
+ }
+}
+
+struct MOZ_STACK_CLASS GetLeafNodeTest final {
+ const char16_t* mInnerHTML;
+ const char* mContentSelector;
+ const HTMLEditUtils::LeafNodeTypes mTypes;
+ const char* mExpectedTargetSelector;
+ const char* mExpectedTargetContainerSelector = nullptr;
+ const uint32_t mExpectedTargetOffset = 0u;
+
+ nsIContent* GetExpectedTarget(nsINode& aNode) const {
+ if (mExpectedTargetSelector) {
+ return aNode.QuerySelector(nsDependentCString(mExpectedTargetSelector),
+ IgnoreErrors());
+ }
+ if (!mExpectedTargetContainerSelector) {
+ return nullptr;
+ }
+ Element* const container = aNode.QuerySelector(
+ nsDependentCString(mExpectedTargetContainerSelector), IgnoreErrors());
+ MOZ_RELEASE_ASSERT(container);
+ MOZ_RELEASE_ASSERT(!mExpectedTargetOffset ||
+ mExpectedTargetOffset < container->Length());
+ return container->GetChildAt_Deprecated(mExpectedTargetOffset);
+ }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const GetLeafNodeTest& aTest) {
+ return aStream << "Scan from \"" << aTest.mContentSelector
+ << "\" with options=" << ToString(aTest.mTypes).c_str()
+ << " in \"" << NS_ConvertUTF16toUTF8(aTest.mInnerHTML).get()
+ << "\"";
+ }
+};
+
+TEST(HTMLEditUtilsTest, GetLastLeafContent)
+{
+ using LeafNodeType = HTMLEditUtils::LeafNodeType;
+ const RefPtr<Document> doc = CreateHTMLDoc();
+ const RefPtr<nsGenericHTMLElement> body = doc->GetBody();
+ MOZ_RELEASE_ASSERT(body);
+ for (const auto& testData : {
+ GetLeafNodeTest{u"<div></div>", "div", {}, nullptr},
+ GetLeafNodeTest{u"<div><br></div>", "div", {}, "div > br"},
+ GetLeafNodeTest{u"<div>abc<br></div>", "div", {}, "div > br"},
+ GetLeafNodeTest{u"<div>abc</div>", "div", {}, nullptr, "div", 0u},
+ GetLeafNodeTest{
+ u"<div><div><br></div></div>", "div", {}, "div > div > br"},
+ GetLeafNodeTest{u"<div><div><br></div></div>",
+ "div",
+ {LeafNodeType::LeafNodeOrChildBlock},
+ "div > div"},
+ GetLeafNodeTest{u"<div><div><br></div><div><br></div></div>",
+ "div",
+ {},
+ "div > div + div > br"},
+ GetLeafNodeTest{u"<div><div><br></div><div><br></div></div>",
+ "div",
+ {LeafNodeType::LeafNodeOrChildBlock},
+ "div > div + div"},
+ GetLeafNodeTest{u"<div><!--abc--></div>", "div", {}, nullptr},
+ GetLeafNodeTest{u"<div><!--abc--></div>",
+ "div",
+ {LeafNodeType::TreatCommentAsLeafNode},
+ nullptr,
+ "div",
+ 0u},
+ GetLeafNodeTest{u"<div><br><!--abc--></div>", "div", {}, "div > br"},
+ GetLeafNodeTest{u"<div><br><!--abc--></div>",
+ "div",
+ {LeafNodeType::TreatCommentAsLeafNode},
+ nullptr,
+ "div",
+ 1u},
+ GetLeafNodeTest{
+ u"<div><div><br></div><div><br></div><!--abc--></div>",
+ "div",
+ {},
+ "div > div + div > br"},
+ GetLeafNodeTest{
+ u"<div><div><br></div><div><br></div><!--abc--></div>",
+ "div",
+ {LeafNodeType::TreatCommentAsLeafNode},
+ nullptr,
+ "div",
+ 2u},
+ }) {
+ body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML),
+ doc->NodePrincipal(), IgnoreErrors());
+ const Element* const target = body->QuerySelector(
+ nsDependentCString(testData.mContentSelector), IgnoreErrors());
+ MOZ_RELEASE_ASSERT(target);
+ const nsIContent* result = HTMLEditUtils::GetLastLeafContent(
+ *target, testData.mTypes,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ EXPECT_EQ(result, testData.GetExpectedTarget(*body))
+ << "GetLastLeafContent: " << testData
+ << "(Got: " << ToString(RefPtr{result}) << ")";
+ }
+}
+
+TEST(HTMLEditUtilsTest, GetFirstLeafContent)
+{
+ using LeafNodeType = HTMLEditUtils::LeafNodeType;
+ const RefPtr<Document> doc = CreateHTMLDoc();
+ const RefPtr<nsGenericHTMLElement> body = doc->GetBody();
+ MOZ_RELEASE_ASSERT(body);
+ for (const auto& testData : {
+ GetLeafNodeTest{u"<div></div>", "div", {}, nullptr},
+ GetLeafNodeTest{u"<div><br></div>", "div", {}, "div > br"},
+ GetLeafNodeTest{
+ u"<div>abc<br></div>", "div", {}, nullptr, "div", 0u},
+ GetLeafNodeTest{u"<div>abc</div>", "div", {}, nullptr, "div", 0u},
+ GetLeafNodeTest{
+ u"<div><div><br></div></div>", "div", {}, "div > div > br"},
+ GetLeafNodeTest{u"<div><div><br></div></div>",
+ "div",
+ {LeafNodeType::LeafNodeOrChildBlock},
+ "div > div"},
+ GetLeafNodeTest{u"<div><div><br></div><div><br></div></div>",
+ "div",
+ {},
+ "div > div > br"},
+ GetLeafNodeTest{u"<div><div><br></div><div><br></div></div>",
+ "div",
+ {LeafNodeType::LeafNodeOrChildBlock},
+ "div > div"},
+ GetLeafNodeTest{u"<div><!--abc--></div>", "div", {}, nullptr},
+ GetLeafNodeTest{u"<div><!--abc--></div>",
+ "div",
+ {LeafNodeType::TreatCommentAsLeafNode},
+ nullptr,
+ "div",
+ 0u},
+ GetLeafNodeTest{u"<div><!--abc--><br></div>", "div", {}, "div > br"},
+ GetLeafNodeTest{u"<div><!--abc--><br></div>",
+ "div",
+ {LeafNodeType::TreatCommentAsLeafNode},
+ nullptr,
+ "div",
+ 0u},
+ GetLeafNodeTest{
+ u"<div><!--abc--><div><br></div><div><br></div></div>",
+ "div",
+ {},
+ "div > div > br"},
+ GetLeafNodeTest{
+ u"<div><!--abc--><div><br></div><div><br></div></div>",
+ "div",
+ {LeafNodeType::TreatCommentAsLeafNode},
+ nullptr,
+ "div",
+ 0u},
+ }) {
+ body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML),
+ doc->NodePrincipal(), IgnoreErrors());
+ const Element* const target = body->QuerySelector(
+ nsDependentCString(testData.mContentSelector), IgnoreErrors());
+ MOZ_RELEASE_ASSERT(target);
+ const nsIContent* result = HTMLEditUtils::GetFirstLeafContent(
+ *target, testData.mTypes,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ EXPECT_EQ(result, testData.GetExpectedTarget(*body))
+ << "GetFirstLeafContent: " << testData
+ << "(Got: " << ToString(RefPtr{result}) << ")";
+ }
+}
+
+TEST(HTMLEditUtilsTest, GetNextLeafContentOrNextBlockElement_Content)
+{
+ using LeafNodeType = HTMLEditUtils::LeafNodeType;
+ const RefPtr<Document> doc = CreateHTMLDoc();
+ const RefPtr<nsGenericHTMLElement> body = doc->GetBody();
+ MOZ_RELEASE_ASSERT(body);
+ for (const auto& testData : {
+ GetLeafNodeTest{u"<div><br></div><p><br></p>", "div", {}, "p"},
+ GetLeafNodeTest{
+ u"<div><br></div><!--abc--><p><br></p>", "div", {}, "p"},
+ GetLeafNodeTest{u"<div><br></div><!--abc--><p><br></p>",
+ "div",
+ {LeafNodeType::TreatCommentAsLeafNode},
+ nullptr,
+ "body",
+ 1u},
+ GetLeafNodeTest{
+ u"<div><br></div><span><br></span>", "div", {}, "span > br"},
+ GetLeafNodeTest{u"<div><br></div><span><!--abc--><br></span>",
+ "div",
+ {},
+ "span > br"},
+ GetLeafNodeTest{u"<div><br></div><span><!--abc--><br></span>",
+ "div",
+ {LeafNodeType::TreatCommentAsLeafNode},
+ nullptr,
+ "span",
+ 0u},
+ }) {
+ body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML),
+ doc->NodePrincipal(), IgnoreErrors());
+ const Element* const target = body->QuerySelector(
+ nsDependentCString(testData.mContentSelector), IgnoreErrors());
+ MOZ_RELEASE_ASSERT(target);
+ const nsIContent* result =
+ HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *target, testData.mTypes,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ EXPECT_EQ(result, testData.GetExpectedTarget(*body->GetParentNode()))
+ << "GetNextLeafContentOrNextBlockElement: " << testData
+ << "(Got: " << ToString(RefPtr{result}) << ")";
+ }
+}
+
+// TODO: Test GetNextLeafContentOrNextBlockElement() which takes EditorDOMPoint
+
+TEST(HTMLEditUtilsTest, GetPreviousLeafContentOrPreviousBlockElement_Content)
+{
+ using LeafNodeType = HTMLEditUtils::LeafNodeType;
+ const RefPtr<Document> doc = CreateHTMLDoc();
+ const RefPtr<nsGenericHTMLElement> body = doc->GetBody();
+ MOZ_RELEASE_ASSERT(body);
+ for (const auto& testData : {
+ GetLeafNodeTest{u"<p><br></p><div><br></div>", "div", {}, "p"},
+ GetLeafNodeTest{
+ u"<p><br></p><!--abc--><div><br></div>", "div", {}, "p"},
+ GetLeafNodeTest{u"<p><br></p><!--abc--><div><br></div>",
+ "div",
+ {LeafNodeType::TreatCommentAsLeafNode},
+ nullptr,
+ "body",
+ 1u},
+ GetLeafNodeTest{
+ u"<span><br></span><div><br></div>", "div", {}, "span > br"},
+ GetLeafNodeTest{u"<span><br><!--abc--></span><div><br></div>",
+ "div",
+ {},
+ "span > br"},
+ GetLeafNodeTest{u"<span><br><!--abc--></span><div><br></div>",
+ "div",
+ {LeafNodeType::TreatCommentAsLeafNode},
+ nullptr,
+ "span",
+ 1u},
+ }) {
+ body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML),
+ doc->NodePrincipal(), IgnoreErrors());
+ const Element* const target = body->QuerySelector(
+ nsDependentCString(testData.mContentSelector), IgnoreErrors());
+ MOZ_RELEASE_ASSERT(target);
+ const nsIContent* result =
+ HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *target, testData.mTypes,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ EXPECT_EQ(result, testData.GetExpectedTarget(*body->GetParentNode()))
+ << "GetPreviousLeafContentOrPreviousBlockElement: " << testData
+ << "(Got: " << ToString(RefPtr{result}) << ")";
+ }
+}
+
+// TODO: Test GetPreviousLeafContentOrPreviousBlockElement() which takes
+// EditorDOMPoint
+
} // namespace mozilla
diff --git a/testing/web-platform/meta/editing/run/inserthtml.html.ini b/testing/web-platform/meta/editing/run/inserthtml.html.ini
@@ -105,12 +105,3 @@
[[["inserthtml","<!--abc-->"\]\] "<p>{}<span><!--foo--><br><!--bar--></span></p>" compare innerHTML]
expected: FAIL
-
- [[["inserthtml","<!--abc-->"\]\] "<p><span><!--foo--><br><!--bar--></span>{}</p>" compare innerHTML]
- expected: FAIL
-
- [[["inserthtml","<!--abc-->"\]\] "<p>{}<br></p>" compare innerHTML]
- expected: FAIL
-
- [[["inserthtml","<!--abc-->"\]\] "<p><br>{}</p>" compare innerHTML]
- expected: FAIL
diff --git a/testing/web-platform/tests/editing/data/inserthtml.js b/testing/web-platform/tests/editing/data/inserthtml.js
@@ -477,7 +477,7 @@ var browserTests = [
{"stylewithcss":[false,true,"",false,false,""],"inserthtml":[false,false,"",false,false,""]}],
["<p>[foo]</p>",
[["inserthtml","<!--abc-->"]],
- "<p><!--abc-->{}</p>",
+ "<p><!--abc--><br></p>",
[true],
{"inserthtml":[false,false,"",false,false,""]}],
["<p>{}<br></p>",
@@ -487,7 +487,7 @@ var browserTests = [
{"inserthtml":[false,false,"",false,false,""]}],
["<p>{}<br></p>",
[["inserthtml","<!--abc-->"]],
- "<p><!--abc-->{}</p>",
+ "<p><!--abc--><br></p>",
[true],
{"inserthtml":[false,false,"",false,false,""]}],
["<p><!--foo-->{}<span><br></span><!--bar--></p>",
@@ -521,7 +521,8 @@ var browserTests = [
{"inserthtml":[false,false,"",false,false,""]}],
["<p><br>{}</p>",
[["inserthtml","<!--abc-->"]],
- "<p><!--abc--></p>",
+ ["<p><br><!--abc--></p>",
+ "<p><!--abc--><br></p>"],
[true],
{"inserthtml":[false,false,"",false,false,""]}],
["<p><!--foo--><span><br></span>{}<!--bar--></p>",
@@ -535,18 +536,25 @@ var browserTests = [
"<p><!--foo--><span><!--abc-->{}<br></span><!--bar--></p>",
[true],
{"inserthtml":[false,false,"",false,false,""]}],
-// TODO: Fix the insertion not occurring at caret position.
-// Updating the expected value of below two tests as to how other
-// browsers behave these tests will still fail and since the insertion
-// position is incorrect br tag gets removed, after fixing this it shouldn't.
+// Don't insert text after the invisible <br> element. In the normal cases
+// (meaning without the Comment nodes), caret should be put at the <br>.
+// Therefore, inserting text should be done before the <br>. Then, the <br>
+// becomes unnecessary so that it should be removed.
["<p><span><!--foo--><br><!--bar--></span>{}</p>",
[["inserthtml","abc"]],
- "<p><span><!--foo--><br><!--bar--></span>abc</p>",
+ "<p><span><!--foo-->abc<!--bar--></span></p>",
[true],
{"inserthtml":[false,false,"",false,false,""]}],
+// It may be allowed to insert new Comment after the invisible <br> element.
+// Therefore, the first one expects the new position as same as the `Selection`.
+// On the other hand, if the inserting content includes visible things,
+// everything should be inserted before the invisible <br>. Therefore, the
+// new position can be before the <br> as same as handled as inserting visible
+// things.
["<p><span><!--foo--><br><!--bar--></span>{}</p>",
[["inserthtml","<!--abc-->"]],
- "<p><span><!--foo--><br><!--bar--></span><!--abc--></p>",
+ ["<p><span><!--foo--><br><!--bar--></span><!--abc--></p>",
+ "<p><span><!--foo--><!--abc--><br><!--bar--></span></p>"],
[true],
{"inserthtml":[false,false,"",false,false,""]}],