From 3aef835f6cb12e607154d56d68726767172571e4 Mon Sep 17 00:00:00 2001 From: Masayuki Nakano Date: Tue, 27 Apr 2021 11:17:44 +0000 Subject: [PATCH] Bug 1627175 - part 38: Move `EditorBase::GetNextContent()` and `EditorBase::GetPreviousContent()` to `HTMLEditUtils` r=m_kato Differential Revision: https://phabricator.services.mozilla.com/D113242 --- editor/libeditor/EditorBase.cpp | 238 +------------------ editor/libeditor/EditorBase.h | 81 ------- editor/libeditor/HTMLEditUtils.cpp | 228 ++++++++++++++++++ editor/libeditor/HTMLEditUtils.h | 94 +++++++- editor/libeditor/HTMLEditor.cpp | 33 +-- editor/libeditor/HTMLEditor.h | 6 +- editor/libeditor/HTMLEditorDeleteHandler.cpp | 33 ++- editor/libeditor/HTMLEditorState.cpp | 5 +- 8 files changed, 365 insertions(+), 353 deletions(-) diff --git a/editor/libeditor/EditorBase.cpp b/editor/libeditor/EditorBase.cpp index 01dbbf4e21f4..e157f6670824 100644 --- a/editor/libeditor/EditorBase.cpp +++ b/editor/libeditor/EditorBase.cpp @@ -128,6 +128,7 @@ using namespace widget; using LeafNodeType = HTMLEditUtils::LeafNodeType; using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; +using WalkTreeOption = HTMLEditUtils::WalkTreeOption; /***************************************************************************** * mozilla::EditorBase @@ -2802,231 +2803,6 @@ nsresult EditorBase::DeleteTextWithTransaction(Text& aTextNode, return rv; } -// static -nsIContent* EditorBase::GetPreviousContent( - const nsINode& aNode, const WalkTreeOptions& aOptions, - const Element* aAncestorLimiter /* = nullptr */) { - if (&aNode == aAncestorLimiter || - (aAncestorLimiter && !aNode.IsInclusiveDescendantOf(aAncestorLimiter))) { - return nullptr; - } - return EditorBase::GetAdjacentContent(aNode, WalkTreeDirection::Backward, - aOptions, aAncestorLimiter); -} - -// static -nsIContent* EditorBase::GetPreviousContent( - const EditorRawDOMPoint& aPoint, const WalkTreeOptions& aOptions, - const Element* aAncestorLimiter /* = nullptr */) { - MOZ_ASSERT(aPoint.IsSetAndValid()); - NS_WARNING_ASSERTION( - !aPoint.IsInDataNode() || aPoint.IsInTextNode(), - "GetPreviousContent() doesn't assume that the start point is a " - "data node except text node"); - - // If we are at the beginning of the node, or it is a text node, then just - // look before it. - if (aPoint.IsStartOfContainer() || aPoint.IsInTextNode()) { - if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && - aPoint.IsInContentNode() && - HTMLEditUtils::IsBlockElement(*aPoint.ContainerAsContent())) { - // If we aren't allowed to cross blocks, don't look before this block. - return nullptr; - } - return EditorBase::GetPreviousContent(*aPoint.GetContainer(), aOptions, - aAncestorLimiter); - } - - // else look before the child at 'aOffset' - if (aPoint.GetChild()) { - return EditorBase::GetPreviousContent(*aPoint.GetChild(), aOptions, - aAncestorLimiter); - } - - // unless there isn't one, in which case we are at the end of the node - // and want the deep-right child. - nsIContent* lastLeafContent = HTMLEditUtils::GetLastLeafChild( - *aPoint.GetContainer(), - {aOptions.contains(WalkTreeOption::StopAtBlockBoundary) - ? LeafNodeType::LeafNodeOrChildBlock - : LeafNodeType::OnlyLeafNode}); - if (!lastLeafContent) { - return nullptr; - } - - if ((!aOptions.contains(WalkTreeOption::IgnoreNonEditableNode) || - EditorUtils::IsEditableContent(*lastLeafContent, EditorType::HTML)) && - (!aOptions.contains(WalkTreeOption::IgnoreDataNodeExceptText) || - EditorUtils::IsElementOrText(*lastLeafContent))) { - return lastLeafContent; - } - - // restart the search from the non-editable node we just found - return EditorBase::GetPreviousContent(*lastLeafContent, aOptions, - aAncestorLimiter); -} - -// static -nsIContent* EditorBase::GetNextContent( - const nsINode& aNode, const WalkTreeOptions& aOptions, - const Element* aAncestorLimiter /* = nullptr */) { - if (&aNode == aAncestorLimiter || - (aAncestorLimiter && !aNode.IsInclusiveDescendantOf(aAncestorLimiter))) { - return nullptr; - } - return EditorBase::GetAdjacentContent(aNode, WalkTreeDirection::Forward, - aOptions, aAncestorLimiter); -} - -// static -nsIContent* EditorBase::GetNextContent( - const EditorRawDOMPoint& aPoint, const WalkTreeOptions& aOptions, - const Element* aAncestorLimiter /* = nullptr */) { - MOZ_ASSERT(aPoint.IsSetAndValid()); - NS_WARNING_ASSERTION( - !aPoint.IsInDataNode() || aPoint.IsInTextNode(), - "GetNextContent() doesn't assume that the start point is a " - "data node except text node"); - - EditorRawDOMPoint point(aPoint); - - // if the container is a text node, use its location instead - if (point.IsInTextNode()) { - point.SetAfter(point.GetContainer()); - if (NS_WARN_IF(!point.IsSet())) { - return nullptr; - } - } - - if (point.GetChild()) { - if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && - HTMLEditUtils::IsBlockElement(*point.GetChild())) { - return point.GetChild(); - } - - nsIContent* firstLeafContent = HTMLEditUtils::GetFirstLeafChild( - *point.GetChild(), - {aOptions.contains(WalkTreeOption::StopAtBlockBoundary) - ? LeafNodeType::LeafNodeOrChildBlock - : LeafNodeType::OnlyLeafNode}); - if (!firstLeafContent) { - return point.GetChild(); - } - - // XXX Why do we need to do this check? The leaf node must be a descendant - // of `point.GetChild()`. - if (aAncestorLimiter && - (firstLeafContent == aAncestorLimiter || - !firstLeafContent->IsInclusiveDescendantOf(aAncestorLimiter))) { - return nullptr; - } - - if ((!aOptions.contains(WalkTreeOption::IgnoreNonEditableNode) || - EditorUtils::IsEditableContent(*firstLeafContent, EditorType::HTML)) && - (!aOptions.contains(WalkTreeOption::IgnoreDataNodeExceptText) || - EditorUtils::IsElementOrText(*firstLeafContent))) { - return firstLeafContent; - } - - // restart the search from the non-editable node we just found - return EditorBase::GetNextContent(*firstLeafContent, aOptions, - aAncestorLimiter); - } - - // unless there isn't one, in which case we are at the end of the node - // and want the next one. - if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && - point.IsInContentNode() && - HTMLEditUtils::IsBlockElement(*point.ContainerAsContent())) { - // don't cross out of parent block - return nullptr; - } - - return EditorBase::GetNextContent(*point.GetContainer(), aOptions, - aAncestorLimiter); -} - -// static -nsIContent* EditorBase::GetAdjacentLeafContent( - const nsINode& aNode, WalkTreeDirection aWalkTreeDirection, - const WalkTreeOptions& aOptions, - const Element* aAncestorLimiter /* = nullptr */) { - // called only by GetPriorNode so we don't need to check params. - MOZ_ASSERT(&aNode != aAncestorLimiter); - MOZ_ASSERT_IF(aAncestorLimiter, - aAncestorLimiter->IsInclusiveDescendantOf(aAncestorLimiter)); - - const nsINode* node = &aNode; - for (;;) { - // if aNode has a sibling in the right direction, return - // that sibling's closest child (or itself if it has no children) - nsIContent* sibling = aWalkTreeDirection == WalkTreeDirection::Forward - ? node->GetNextSibling() - : node->GetPreviousSibling(); - if (sibling) { - if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && - HTMLEditUtils::IsBlockElement(*sibling)) { - // don't look inside prevsib, since it is a block - return sibling; - } - const LeafNodeTypes leafNodeTypes = { - aOptions.contains(WalkTreeOption::StopAtBlockBoundary) - ? LeafNodeType::LeafNodeOrChildBlock - : LeafNodeType::OnlyLeafNode}; - nsIContent* leafContent = - aWalkTreeDirection == WalkTreeDirection::Forward - ? HTMLEditUtils::GetFirstLeafChild(*sibling, leafNodeTypes) - : HTMLEditUtils::GetLastLeafChild(*sibling, leafNodeTypes); - return leafContent ? leafContent : sibling; - } - - nsIContent* parent = node->GetParent(); - if (!parent) { - return nullptr; - } - - if (parent == aAncestorLimiter || - (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && - HTMLEditUtils::IsBlockElement(*parent))) { - return nullptr; - } - - node = parent; - } - - MOZ_ASSERT_UNREACHABLE("What part of for(;;) do you not understand?"); - return nullptr; -} - -// static -nsIContent* EditorBase::GetAdjacentContent( - const nsINode& aNode, WalkTreeDirection aWalkTreeDirection, - const WalkTreeOptions& aOptions, - const Element* aAncestorLimiter /* = nullptr */) { - if (&aNode == aAncestorLimiter) { - // Don't allow traversal above the root node! This helps - // prevent us from accidentally editing browser content - // when the editor is in a text widget. - return nullptr; - } - - nsIContent* leafContent = EditorBase::GetAdjacentLeafContent( - aNode, aWalkTreeDirection, aOptions, aAncestorLimiter); - if (!leafContent) { - return nullptr; - } - - if ((!aOptions.contains(WalkTreeOption::IgnoreNonEditableNode) || - EditorUtils::IsEditableContent(*leafContent, EditorType::HTML)) && - (!aOptions.contains(WalkTreeOption::IgnoreDataNodeExceptText) || - EditorUtils::IsElementOrText(*leafContent))) { - return leafContent; - } - - return EditorBase::GetAdjacentContent(*leafContent, aWalkTreeDirection, - aOptions, aAncestorLimiter); -} - bool EditorBase::IsRoot(const nsINode* inNode) const { if (NS_WARN_IF(!inNode)) { return false; @@ -3509,7 +3285,7 @@ EditorBase::CreateTransactionForCollapsedRange( MOZ_ASSERT(IsHTMLEditor()); // We're backspacing from the beginning of a node. Delete the last thing // of previous editable content. - nsIContent* previousEditableContent = EditorBase::GetPreviousContent( + nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousContent( *point.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode}, GetEditorRoot()); if (!previousEditableContent) { @@ -3553,7 +3329,7 @@ EditorBase::CreateTransactionForCollapsedRange( MOZ_ASSERT(IsHTMLEditor()); // We're deleting from the end of a node. Delete the first thing of // next editable content. - nsIContent* nextEditableContent = EditorBase::GetNextContent( + nsIContent* nextEditableContent = HTMLEditUtils::GetNextContent( *point.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode}, GetEditorRoot()); if (!nextEditableContent) { @@ -3616,10 +3392,10 @@ EditorBase::CreateTransactionForCollapsedRange( if (IsHTMLEditor()) { editableContent = aHowToHandleCollapsedRange == HowToHandleCollapsedRange::ExtendBackward - ? EditorBase::GetPreviousContent( + ? HTMLEditUtils::GetPreviousContent( point, {WalkTreeOption::IgnoreNonEditableNode}, GetEditorRoot()) - : EditorBase::GetNextContent( + : HTMLEditUtils::GetNextContent( point, {WalkTreeOption::IgnoreNonEditableNode}, GetEditorRoot()); if (!editableContent) { @@ -3632,10 +3408,10 @@ EditorBase::CreateTransactionForCollapsedRange( editableContent = aHowToHandleCollapsedRange == HowToHandleCollapsedRange::ExtendBackward - ? EditorBase::GetPreviousContent( + ? HTMLEditUtils::GetPreviousContent( *editableContent, {WalkTreeOption::IgnoreNonEditableNode}, GetEditorRoot()) - : EditorBase::GetNextContent( + : HTMLEditUtils::GetNextContent( *editableContent, {WalkTreeOption::IgnoreNonEditableNode}, GetEditorRoot()); } diff --git a/editor/libeditor/EditorBase.h b/editor/libeditor/EditorBase.h index 1a757bc672ac..9dad429acd16 100644 --- a/editor/libeditor/EditorBase.h +++ b/editor/libeditor/EditorBase.h @@ -9,7 +9,6 @@ #include "mozilla/Assertions.h" // for MOZ_ASSERT, etc. #include "mozilla/EditAction.h" // for EditAction and EditSubAction #include "mozilla/EditorDOMPoint.h" // for EditorDOMPoint -#include "mozilla/EnumSet.h" // for EnumSet #include "mozilla/EventForwards.h" // for InputEventTargetRanges #include "mozilla/Maybe.h" // for Maybe #include "mozilla/OwningNonNull.h" // for OwningNonNull @@ -1723,73 +1722,6 @@ class EditorBase : public nsIEditor, MOZ_CAN_RUN_SCRIPT nsresult DoTransactionInternal(nsITransaction* aTransaction); - /** - * Get the next node. - * - * - * On the other hand, the methods taking nsINode behavior must be what - * you want. They start to search the result from next node of the given - * node. - */ - - /** - * Get previous content node of aNode if there is. - * - * @param aNode The node from which we start to walk the DOM tree. - */ - enum class WalkTreeOption { - IgnoreNonEditableNode, // Ignore non-editable nodes and their children. - IgnoreDataNodeExceptText, // Ignore data nodes which are not text node. - StopAtBlockBoundary, // Stop waking the tree at a block boundary. - }; - using WalkTreeOptions = EnumSet; - static nsIContent* GetPreviousContent( - const nsINode& aNode, const WalkTreeOptions& aOptions, - const Element* aAncestorLimiter = nullptr); - - /** - * And another version that takes a point in DOM tree rather than a node. - */ - static nsIContent* GetPreviousContent( - const EditorRawDOMPoint& aPoint, const WalkTreeOptions& aOptions, - const Element* aAncestorLimiter = nullptr); - - /** - * Get next content node of aNode if there is. - * - * @param aNode The node from which we start to walk the DOM tree. - */ - static nsIContent* GetNextContent(const nsINode& aNode, - const WalkTreeOptions& aOptions, - const Element* aAncestorLimiter = nullptr); - - /** - * And another version that takes a point in DOM tree rather than a node. - * - * Note that this may return the child at the offset. E.g., following code - * causes infinite loop. - * - * EditorRawDOMPoint point(aEditableNode); - * while (nsIContent* content = - * GetNextContent(point, {WalkTreeOption::IgnoreNonEditableNode})) { - * // Do something... - * point.Set(content); - * } - * - * Following code must be you expected: - * - * while (nsIContent* content = - * GetNextContent(point, {WalkTreeOption::IgnoreNonEditableNode}) { - * // Do something... - * DebugOnly advanced = point.Advanced(); - * MOZ_ASSERT(advanced); - * point.Set(point.GetChild()); - * } - */ - static nsIContent* GetNextContent(const EditorRawDOMPoint& aPoint, - const WalkTreeOptions& aOptions, - const Element* aAncestorLimiter = nullptr); - /** * Returns true if aNode is our root node. */ @@ -2154,19 +2086,6 @@ class EditorBase : public nsIEditor, */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult ScrollSelectionFocusIntoView(); - /** - * Helper for GetPreviousContent() and GetNextContent(). - */ - enum class WalkTreeDirection { Forward, Backward }; - static nsIContent* GetAdjacentLeafContent( - const nsINode& aNode, WalkTreeDirection aWalkTreeDirection, - const WalkTreeOptions& aOptions, - const Element* aAncestorLimiter = nullptr); - static nsIContent* GetAdjacentContent( - const nsINode& aNode, WalkTreeDirection aWalkTreeDirection, - const WalkTreeOptions& aOptions, - const Element* aAncestorLimiter = nullptr); - virtual nsresult InstallEventListeners(); virtual void CreateEventListeners(); virtual void RemoveEventListeners(); diff --git a/editor/libeditor/HTMLEditUtils.cpp b/editor/libeditor/HTMLEditUtils.cpp index ee2db8834c31..80f55219b064 100644 --- a/editor/libeditor/HTMLEditUtils.cpp +++ b/editor/libeditor/HTMLEditUtils.cpp @@ -38,6 +38,31 @@ namespace mozilla { using namespace dom; using EditorType = EditorBase::EditorType; +template nsIContent* HTMLEditUtils::GetPreviousContent( + const EditorDOMPoint& aPoint, const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter); +template nsIContent* HTMLEditUtils::GetPreviousContent( + const EditorRawDOMPoint& aPoint, const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter); +template nsIContent* HTMLEditUtils::GetPreviousContent( + const EditorDOMPointInText& aPoint, const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter); +template nsIContent* HTMLEditUtils::GetPreviousContent( + const EditorRawDOMPointInText& aPoint, const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter); +template nsIContent* HTMLEditUtils::GetNextContent( + const EditorDOMPoint& aPoint, const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter); +template nsIContent* HTMLEditUtils::GetNextContent( + const EditorRawDOMPoint& aPoint, const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter); +template nsIContent* HTMLEditUtils::GetNextContent( + const EditorDOMPointInText& aPoint, const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter); +template nsIContent* HTMLEditUtils::GetNextContent( + const EditorRawDOMPointInText& aPoint, const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter); + template EditorDOMPoint HTMLEditUtils::GetPreviousEditablePoint( nsIContent& aContent, const Element* aAncestorLimiter, InvisibleWhiteSpaces aInvisibleWhiteSpaces, @@ -865,6 +890,209 @@ bool HTMLEditUtils::IsSingleLineContainer(nsINode& aNode) { aNode.IsAnyOfHTMLElements(nsGkAtoms::li, nsGkAtoms::dt, nsGkAtoms::dd); } +// static +template +nsIContent* HTMLEditUtils::GetPreviousContent( + const EditorDOMPointBase& aPoint, const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter /* = nullptr */) { + MOZ_ASSERT(aPoint.IsSetAndValid()); + NS_WARNING_ASSERTION( + !aPoint.IsInDataNode() || aPoint.IsInTextNode(), + "GetPreviousContent() doesn't assume that the start point is a " + "data node except text node"); + + // If we are at the beginning of the node, or it is a text node, then just + // look before it. + if (aPoint.IsStartOfContainer() || aPoint.IsInTextNode()) { + if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && + aPoint.IsInContentNode() && + HTMLEditUtils::IsBlockElement(*aPoint.ContainerAsContent())) { + // If we aren't allowed to cross blocks, don't look before this block. + return nullptr; + } + return HTMLEditUtils::GetPreviousContent(*aPoint.GetContainer(), aOptions, + aAncestorLimiter); + } + + // else look before the child at 'aOffset' + if (aPoint.GetChild()) { + return HTMLEditUtils::GetPreviousContent(*aPoint.GetChild(), aOptions, + aAncestorLimiter); + } + + // unless there isn't one, in which case we are at the end of the node + // and want the deep-right child. + nsIContent* lastLeafContent = HTMLEditUtils::GetLastLeafChild( + *aPoint.GetContainer(), + {aOptions.contains(WalkTreeOption::StopAtBlockBoundary) + ? LeafNodeType::LeafNodeOrChildBlock + : LeafNodeType::OnlyLeafNode}); + if (!lastLeafContent) { + return nullptr; + } + + if ((!aOptions.contains(WalkTreeOption::IgnoreNonEditableNode) || + EditorUtils::IsEditableContent(*lastLeafContent, EditorType::HTML)) && + (!aOptions.contains(WalkTreeOption::IgnoreDataNodeExceptText) || + EditorUtils::IsElementOrText(*lastLeafContent))) { + return lastLeafContent; + } + + // restart the search from the non-editable node we just found + return HTMLEditUtils::GetPreviousContent(*lastLeafContent, aOptions, + aAncestorLimiter); +} + +// static +template +nsIContent* HTMLEditUtils::GetNextContent( + const EditorDOMPointBase& aPoint, const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter /* = nullptr */) { + MOZ_ASSERT(aPoint.IsSetAndValid()); + NS_WARNING_ASSERTION( + !aPoint.IsInDataNode() || aPoint.IsInTextNode(), + "GetNextContent() doesn't assume that the start point is a " + "data node except text node"); + + EditorRawDOMPoint point(aPoint); + + // if the container is a text node, use its location instead + if (point.IsInTextNode()) { + point.SetAfter(point.GetContainer()); + if (NS_WARN_IF(!point.IsSet())) { + return nullptr; + } + } + + if (point.GetChild()) { + if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && + HTMLEditUtils::IsBlockElement(*point.GetChild())) { + return point.GetChild(); + } + + nsIContent* firstLeafContent = HTMLEditUtils::GetFirstLeafChild( + *point.GetChild(), + {aOptions.contains(WalkTreeOption::StopAtBlockBoundary) + ? LeafNodeType::LeafNodeOrChildBlock + : LeafNodeType::OnlyLeafNode}); + if (!firstLeafContent) { + return point.GetChild(); + } + + // XXX Why do we need to do this check? The leaf node must be a descendant + // of `point.GetChild()`. + if (aAncestorLimiter && + (firstLeafContent == aAncestorLimiter || + !firstLeafContent->IsInclusiveDescendantOf(aAncestorLimiter))) { + return nullptr; + } + + if ((!aOptions.contains(WalkTreeOption::IgnoreNonEditableNode) || + EditorUtils::IsEditableContent(*firstLeafContent, EditorType::HTML)) && + (!aOptions.contains(WalkTreeOption::IgnoreDataNodeExceptText) || + EditorUtils::IsElementOrText(*firstLeafContent))) { + return firstLeafContent; + } + + // restart the search from the non-editable node we just found + return HTMLEditUtils::GetNextContent(*firstLeafContent, aOptions, + aAncestorLimiter); + } + + // unless there isn't one, in which case we are at the end of the node + // and want the next one. + if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && + point.IsInContentNode() && + HTMLEditUtils::IsBlockElement(*point.ContainerAsContent())) { + // don't cross out of parent block + return nullptr; + } + + return HTMLEditUtils::GetNextContent(*point.GetContainer(), aOptions, + aAncestorLimiter); +} + +// static +nsIContent* HTMLEditUtils::GetAdjacentLeafContent( + const nsINode& aNode, WalkTreeDirection aWalkTreeDirection, + const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter /* = nullptr */) { + // called only by GetPriorNode so we don't need to check params. + MOZ_ASSERT(&aNode != aAncestorLimiter); + MOZ_ASSERT_IF(aAncestorLimiter, + aAncestorLimiter->IsInclusiveDescendantOf(aAncestorLimiter)); + + const nsINode* node = &aNode; + for (;;) { + // if aNode has a sibling in the right direction, return + // that sibling's closest child (or itself if it has no children) + nsIContent* sibling = aWalkTreeDirection == WalkTreeDirection::Forward + ? node->GetNextSibling() + : node->GetPreviousSibling(); + if (sibling) { + if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && + HTMLEditUtils::IsBlockElement(*sibling)) { + // don't look inside prevsib, since it is a block + return sibling; + } + const LeafNodeTypes leafNodeTypes = { + aOptions.contains(WalkTreeOption::StopAtBlockBoundary) + ? LeafNodeType::LeafNodeOrChildBlock + : LeafNodeType::OnlyLeafNode}; + nsIContent* leafContent = + aWalkTreeDirection == WalkTreeDirection::Forward + ? HTMLEditUtils::GetFirstLeafChild(*sibling, leafNodeTypes) + : HTMLEditUtils::GetLastLeafChild(*sibling, leafNodeTypes); + return leafContent ? leafContent : sibling; + } + + nsIContent* parent = node->GetParent(); + if (!parent) { + return nullptr; + } + + if (parent == aAncestorLimiter || + (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && + HTMLEditUtils::IsBlockElement(*parent))) { + return nullptr; + } + + node = parent; + } + + MOZ_ASSERT_UNREACHABLE("What part of for(;;) do you not understand?"); + return nullptr; +} + +// static +nsIContent* HTMLEditUtils::GetAdjacentContent( + const nsINode& aNode, WalkTreeDirection aWalkTreeDirection, + const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter /* = nullptr */) { + if (&aNode == aAncestorLimiter) { + // Don't allow traversal above the root node! This helps + // prevent us from accidentally editing browser content + // when the editor is in a text widget. + return nullptr; + } + + nsIContent* leafContent = HTMLEditUtils::GetAdjacentLeafContent( + aNode, aWalkTreeDirection, aOptions, aAncestorLimiter); + if (!leafContent) { + return nullptr; + } + + if ((!aOptions.contains(WalkTreeOption::IgnoreNonEditableNode) || + EditorUtils::IsEditableContent(*leafContent, EditorType::HTML)) && + (!aOptions.contains(WalkTreeOption::IgnoreDataNodeExceptText) || + EditorUtils::IsElementOrText(*leafContent))) { + return leafContent; + } + + return HTMLEditUtils::GetAdjacentContent(*leafContent, aWalkTreeDirection, + aOptions, aAncestorLimiter); +} + // static template EditorDOMPointType HTMLEditUtils::GetPreviousEditablePoint( diff --git a/editor/libeditor/HTMLEditUtils.h b/editor/libeditor/HTMLEditUtils.h index 993a8453fb3b..f30160da363e 100644 --- a/editor/libeditor/HTMLEditUtils.h +++ b/editor/libeditor/HTMLEditUtils.h @@ -370,6 +370,82 @@ class HTMLEditUtils final { aFoundLinkElement); } + /** + * Get adjacent content node of aNode if there is (even if one is in different + * parent element). + * + * @param aNode The node from which we start to walk the DOM + * tree. + * @param aOptions See WalkTreeOption for the detail. + * @param aAncestorLimiter Ancestor limiter element which these methods + * never cross its boundary. This is typically + * the editing host. + */ + enum class WalkTreeOption { + IgnoreNonEditableNode, // Ignore non-editable nodes and their children. + IgnoreDataNodeExceptText, // Ignore data nodes which are not text node. + StopAtBlockBoundary, // Stop waking the tree at a block boundary. + }; + using WalkTreeOptions = EnumSet; + static nsIContent* GetPreviousContent( + const nsINode& aNode, const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter = nullptr) { + if (&aNode == aAncestorLimiter || + (aAncestorLimiter && + !aNode.IsInclusiveDescendantOf(aAncestorLimiter))) { + return nullptr; + } + return HTMLEditUtils::GetAdjacentContent(aNode, WalkTreeDirection::Backward, + aOptions, aAncestorLimiter); + } + static nsIContent* GetNextContent(const nsINode& aNode, + const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter = nullptr) { + if (&aNode == aAncestorLimiter || + (aAncestorLimiter && + !aNode.IsInclusiveDescendantOf(aAncestorLimiter))) { + return nullptr; + } + return HTMLEditUtils::GetAdjacentContent(aNode, WalkTreeDirection::Forward, + aOptions, aAncestorLimiter); + } + + /** + * And another version that takes a point in DOM tree rather than a node. + */ + template + static nsIContent* GetPreviousContent( + const EditorDOMPointBase& aPoint, const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter = nullptr); + + /** + * And another version that takes a point in DOM tree rather than a node. + * + * Note that this may return the child at the offset. E.g., following code + * causes infinite loop. + * + * EditorRawDOMPoint point(aEditableNode); + * while (nsIContent* content = + * GetNextContent(point, {WalkTreeOption::IgnoreNonEditableNode})) { + * // Do something... + * point.Set(content); + * } + * + * Following code must be you expected: + * + * while (nsIContent* content = + * GetNextContent(point, {WalkTreeOption::IgnoreNonEditableNode}) { + * // Do something... + * DebugOnly advanced = point.Advanced(); + * MOZ_ASSERT(advanced); + * point.Set(point.GetChild()); + * } + */ + template + static nsIContent* GetNextContent(const EditorDOMPointBase& aPoint, + const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter = nullptr); + /** * GetLastLeafChild() returns rightmost leaf content in aNode. It depends on * aLeafNodeTypes whether this which types of nodes are treated as leaf nodes. @@ -696,9 +772,8 @@ class HTMLEditUtils final { aRootElement, {LeafNodeType::OnlyLeafNode}); if (leafContent && !EditorUtils::IsEditableContent( *leafContent, EditorBase::EditorType::HTML)) { - leafContent = EditorBase::GetNextContent( - *leafContent, {EditorBase::WalkTreeOption::IgnoreNonEditableNode}, - &aRootElement); + leafContent = HTMLEditUtils::GetNextContent( + *leafContent, {WalkTreeOption::IgnoreNonEditableNode}, &aRootElement); } MOZ_ASSERT(leafContent != &aRootElement); return leafContent; @@ -1068,6 +1143,19 @@ class HTMLEditUtils final { aContent.IsHTMLElement(nsGkAtoms::table)); return !cannotCrossBoundary; } + + /** + * Helper for GetPreviousContent() and GetNextContent(). + */ + enum class WalkTreeDirection { Forward, Backward }; + static nsIContent* GetAdjacentLeafContent( + const nsINode& aNode, WalkTreeDirection aWalkTreeDirection, + const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter = nullptr); + static nsIContent* GetAdjacentContent( + const nsINode& aNode, WalkTreeDirection aWalkTreeDirection, + const WalkTreeOptions& aOptions, + const Element* aAncestorLimiter = nullptr); }; /** diff --git a/editor/libeditor/HTMLEditor.cpp b/editor/libeditor/HTMLEditor.cpp index 5896bddd572b..777dada0a1c2 100644 --- a/editor/libeditor/HTMLEditor.cpp +++ b/editor/libeditor/HTMLEditor.cpp @@ -72,6 +72,7 @@ using namespace widget; using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; using LeafNodeType = HTMLEditUtils::LeafNodeType; using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; +using WalkTreeOption = HTMLEditUtils::WalkTreeOption; const char16_t kNBSP = 160; @@ -5006,12 +5007,12 @@ nsIContent* HTMLEditor::GetPreviousHTMLElementOrTextInternal( return nullptr; } return aNoBlockCrossing - ? EditorBase::GetPreviousContent( + ? HTMLEditUtils::GetPreviousContent( aNode, {WalkTreeOption::IgnoreDataNodeExceptText, WalkTreeOption::StopAtBlockBoundary}, editingHost) - : EditorBase::GetPreviousContent( + : HTMLEditUtils::GetPreviousContent( aNode, {WalkTreeOption::IgnoreDataNodeExceptText}, editingHost); } @@ -5024,12 +5025,12 @@ nsIContent* HTMLEditor::GetPreviousHTMLElementOrTextInternal( return nullptr; } return aNoBlockCrossing - ? EditorBase::GetPreviousContent( + ? HTMLEditUtils::GetPreviousContent( aPoint, {WalkTreeOption::IgnoreDataNodeExceptText, WalkTreeOption::StopAtBlockBoundary}, editingHost) - : EditorBase::GetPreviousContent( + : HTMLEditUtils::GetPreviousContent( aPoint, {WalkTreeOption::IgnoreDataNodeExceptText}, editingHost); } @@ -5041,12 +5042,12 @@ nsIContent* HTMLEditor::GetPreviousEditableHTMLNodeInternal( return nullptr; } return aNoBlockCrossing - ? EditorBase::GetPreviousContent( + ? HTMLEditUtils::GetPreviousContent( aNode, {WalkTreeOption::IgnoreNonEditableNode, WalkTreeOption::StopAtBlockBoundary}, editingHost) - : EditorBase::GetPreviousContent( + : HTMLEditUtils::GetPreviousContent( aNode, {WalkTreeOption::IgnoreNonEditableNode}, editingHost); } @@ -5057,12 +5058,12 @@ nsIContent* HTMLEditor::GetPreviousEditableHTMLNodeInternal( if (NS_WARN_IF(!editingHost)) { return nullptr; } - return aNoBlockCrossing ? EditorBase::GetPreviousContent( + return aNoBlockCrossing ? HTMLEditUtils::GetPreviousContent( aPoint, {WalkTreeOption::IgnoreNonEditableNode, WalkTreeOption::StopAtBlockBoundary}, editingHost) - : EditorBase::GetPreviousContent( + : HTMLEditUtils::GetPreviousContent( aPoint, {WalkTreeOption::IgnoreNonEditableNode}, editingHost); } @@ -5074,12 +5075,12 @@ nsIContent* HTMLEditor::GetNextHTMLElementOrTextInternal( return nullptr; } return aNoBlockCrossing - ? EditorBase::GetNextContent( + ? HTMLEditUtils::GetNextContent( aNode, {WalkTreeOption::IgnoreDataNodeExceptText, WalkTreeOption::StopAtBlockBoundary}, editingHost) - : EditorBase::GetNextContent( + : HTMLEditUtils::GetNextContent( aNode, {WalkTreeOption::IgnoreDataNodeExceptText}, editingHost); } @@ -5092,12 +5093,12 @@ nsIContent* HTMLEditor::GetNextHTMLElementOrTextInternal( return nullptr; } return aNoBlockCrossing - ? EditorBase::GetNextContent( + ? HTMLEditUtils::GetNextContent( aPoint, {WalkTreeOption::IgnoreDataNodeExceptText, WalkTreeOption::StopAtBlockBoundary}, editingHost) - : EditorBase::GetNextContent( + : HTMLEditUtils::GetNextContent( aPoint, {WalkTreeOption::IgnoreDataNodeExceptText}, editingHost); } @@ -5109,12 +5110,12 @@ nsIContent* HTMLEditor::GetNextEditableHTMLNodeInternal( return nullptr; } return aNoBlockCrossing - ? EditorBase::GetNextContent( + ? HTMLEditUtils::GetNextContent( aNode, {WalkTreeOption::IgnoreNonEditableNode, WalkTreeOption::StopAtBlockBoundary}, editingHost) - : EditorBase::GetNextContent( + : HTMLEditUtils::GetNextContent( aNode, {WalkTreeOption::IgnoreNonEditableNode}, editingHost); } @@ -5125,12 +5126,12 @@ nsIContent* HTMLEditor::GetNextEditableHTMLNodeInternal( if (NS_WARN_IF(!editingHost)) { return nullptr; } - return aNoBlockCrossing ? EditorBase::GetNextContent( + return aNoBlockCrossing ? HTMLEditUtils::GetNextContent( aPoint, {WalkTreeOption::IgnoreNonEditableNode, WalkTreeOption::StopAtBlockBoundary}, editingHost) - : EditorBase::GetNextContent( + : HTMLEditUtils::GetNextContent( aPoint, {WalkTreeOption::IgnoreNonEditableNode}, editingHost); } diff --git a/editor/libeditor/HTMLEditor.h b/editor/libeditor/HTMLEditor.h index f012b434723d..ed40d30b703c 100644 --- a/editor/libeditor/HTMLEditor.h +++ b/editor/libeditor/HTMLEditor.h @@ -978,8 +978,8 @@ class HTMLEditor final : public TextEditor, /** * GetPreviousEditableHTMLNode*() methods are similar to - * EditorBase::GetPreviousContent({WalkTreeOption::IgnoreNonEditableNode}) but - * this won't return nodes outside active editing host. + * HTMLEditUtils::GetPreviousContent({WalkTreeOption::IgnoreNonEditableNode}) + * but this won't return nodes outside active editing host. */ nsIContent* GetPreviousEditableHTMLNode(nsINode& aNode) const { return GetPreviousEditableHTMLNodeInternal(aNode, false); @@ -1047,7 +1047,7 @@ class HTMLEditor final : public TextEditor, /** * GetNextEditableHTMLNode*() methods are similar to - * EditorBase::GetNextContent({WalkTreeOption::IgnoreNonEditableNode}) but + * HTMLEditUtils::GetNextContent({WalkTreeOption::IgnoreNonEditableNode}) but * this won't return nodes outside active editing host. * * Note that same as EditorBase::GetTextEditableNode(), methods which take diff --git a/editor/libeditor/HTMLEditorDeleteHandler.cpp b/editor/libeditor/HTMLEditorDeleteHandler.cpp index fd258f6e0a8a..4e4e96dde2cd 100644 --- a/editor/libeditor/HTMLEditorDeleteHandler.cpp +++ b/editor/libeditor/HTMLEditorDeleteHandler.cpp @@ -50,6 +50,7 @@ using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; using InvisibleWhiteSpaces = HTMLEditUtils::InvisibleWhiteSpaces; using StyleDifference = HTMLEditUtils::StyleDifference; using TableBoundary = HTMLEditUtils::TableBoundary; +using WalkTreeOption = HTMLEditUtils::WalkTreeOption; template nsresult HTMLEditor::DeleteTextAndTextNodesWithTransaction( const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint, @@ -3897,9 +3898,9 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteRangesWithTransaction( if (howToHandleCollapsedRange == EditorBase::HowToHandleCollapsedRange::ExtendBackward && caretPoint.IsStartOfContainer()) { - nsIContent* previousEditableContent = EditorBase::GetPreviousContent( - *caretPoint.GetContainer(), - {EditorBase::WalkTreeOption::IgnoreNonEditableNode}, editingHost); + nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousContent( + *caretPoint.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode}, + editingHost); if (!previousEditableContent) { continue; } @@ -3920,9 +3921,9 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteRangesWithTransaction( if (howToHandleCollapsedRange == EditorBase::HowToHandleCollapsedRange::ExtendForward && caretPoint.IsEndOfContainer()) { - nsIContent* nextEditableContent = EditorBase::GetNextContent( - *caretPoint.GetContainer(), - {EditorBase::WalkTreeOption::IgnoreNonEditableNode}, editingHost); + nsIContent* nextEditableContent = HTMLEditUtils::GetNextContent( + *caretPoint.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode}, + editingHost); if (!nextEditableContent) { continue; } @@ -3957,13 +3958,11 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteRangesWithTransaction( nsIContent* editableContent = howToHandleCollapsedRange == EditorBase::HowToHandleCollapsedRange::ExtendBackward - ? EditorBase::GetPreviousContent( - caretPoint, - {EditorBase::WalkTreeOption::IgnoreNonEditableNode}, + ? HTMLEditUtils::GetPreviousContent( + caretPoint, {WalkTreeOption::IgnoreNonEditableNode}, editingHost) - : EditorBase::GetNextContent( - caretPoint, - {EditorBase::WalkTreeOption::IgnoreNonEditableNode}, + : HTMLEditUtils::GetNextContent( + caretPoint, {WalkTreeOption::IgnoreNonEditableNode}, editingHost); if (!editableContent) { continue; @@ -3973,10 +3972,10 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteRangesWithTransaction( editableContent = howToHandleCollapsedRange == EditorBase::HowToHandleCollapsedRange::ExtendBackward - ? EditorBase::GetPreviousContent( + ? HTMLEditUtils::GetPreviousContent( *editableContent, {WalkTreeOption::IgnoreNonEditableNode}, editingHost) - : EditorBase::GetNextContent( + : HTMLEditUtils::GetNextContent( *editableContent, {WalkTreeOption::IgnoreNonEditableNode}, editingHost); } @@ -4428,7 +4427,7 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: atStart.IsEndOfContainer() && range.StartRef().GetChild() && range.StartRef().GetChild()->IsHTMLElement(nsGkAtoms::br) && !aHTMLEditor.IsVisibleBRElement(range.StartRef().GetChild()) - ? EditorBase::GetNextContent( + ? HTMLEditUtils::GetNextContent( *atStart.ContainerAsContent(), {WalkTreeOption::IgnoreDataNodeExceptText, WalkTreeOption::StopAtBlockBoundary}, @@ -5177,7 +5176,7 @@ Result HTMLEditor::AutoDeleteRangesHandler:: EditorDOMPoint afterEmptyBlock( EditorRawDOMPoint::After(mEmptyInclusiveAncestorBlockElement)); MOZ_ASSERT(afterEmptyBlock.IsSet()); - if (nsIContent* nextContentOfEmptyBlock = EditorBase::GetNextContent( + if (nsIContent* nextContentOfEmptyBlock = HTMLEditUtils::GetNextContent( afterEmptyBlock, {}, aHTMLEditor.GetActiveEditingHost())) { EditorDOMPoint pt = aHTMLEditor.GetGoodCaretPointFor( *nextContentOfEmptyBlock, aDirectionAndAmount); @@ -5199,7 +5198,7 @@ Result HTMLEditor::AutoDeleteRangesHandler:: // if there is. Otherwise, to after the empty block. EditorRawDOMPoint atEmptyBlock(mEmptyInclusiveAncestorBlockElement); if (nsIContent* previousContentOfEmptyBlock = - EditorBase::GetPreviousContent( + HTMLEditUtils::GetPreviousContent( atEmptyBlock, {WalkTreeOption::IgnoreNonEditableNode}, aHTMLEditor.GetActiveEditingHost())) { EditorDOMPoint pt = aHTMLEditor.GetGoodCaretPointFor( diff --git a/editor/libeditor/HTMLEditorState.cpp b/editor/libeditor/HTMLEditorState.cpp index 50749837fbc2..b7f718b0c429 100644 --- a/editor/libeditor/HTMLEditorState.cpp +++ b/editor/libeditor/HTMLEditorState.cpp @@ -39,6 +39,7 @@ namespace mozilla { using EditorType = EditorUtils::EditorType; +using WalkTreeOption = HTMLEditUtils::WalkTreeOption; /***************************************************************************** * ListElementSelectionState @@ -247,8 +248,8 @@ AlignStateAtSelection::AlignStateAtSelection(HTMLEditor& aHTMLEditor, else if (atStartOfSelection.IsContainerHTMLElement(nsGkAtoms::html) && atBodyOrDocumentElement.IsSet() && atStartOfSelection.Offset() == atBodyOrDocumentElement.Offset()) { - editTargetContent = EditorBase::GetNextContent( - atStartOfSelection, {EditorBase::WalkTreeOption::IgnoreNonEditableNode}, + editTargetContent = HTMLEditUtils::GetNextContent( + atStartOfSelection, {WalkTreeOption::IgnoreNonEditableNode}, aHTMLEditor.GetActiveEditingHost()); if (NS_WARN_IF(!editTargetContent)) { aRv.Throw(NS_ERROR_FAILURE);