summaryrefslogtreecommitdiffstats
path: root/editor/libeditor/WSRunObject.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--editor/libeditor/WSRunObject.cpp4390
1 files changed, 4390 insertions, 0 deletions
diff --git a/editor/libeditor/WSRunObject.cpp b/editor/libeditor/WSRunObject.cpp
new file mode 100644
index 0000000000..3d4554ae73
--- /dev/null
+++ b/editor/libeditor/WSRunObject.cpp
@@ -0,0 +1,4390 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WSRunObject.h"
+
+#include "EditorDOMPoint.h"
+#include "EditorUtils.h"
+#include "ErrorList.h"
+#include "HTMLEditHelpers.h" // for MoveNodeResult, SplitNodeResult
+#include "HTMLEditor.h"
+#include "HTMLEditorNestedClasses.h" // for AutoMoveOneLineHandler
+#include "HTMLEditUtils.h"
+#include "SelectionState.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Casting.h"
+#include "mozilla/mozalloc.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/RangeUtils.h"
+#include "mozilla/StaticPrefs_dom.h" // for StaticPrefs::dom_*
+#include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_*
+#include "mozilla/InternalMutationEvent.h"
+#include "mozilla/dom/AncestorIterator.h"
+
+#include "nsAString.h"
+#include "nsCRT.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIContent.h"
+#include "nsIContentInlines.h"
+#include "nsISupportsImpl.h"
+#include "nsRange.h"
+#include "nsString.h"
+#include "nsTextFragment.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+using LeafNodeType = HTMLEditUtils::LeafNodeType;
+using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
+using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
+
+template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
+ const EditorDOMPoint& aPoint) const;
+template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
+ const EditorRawDOMPoint& aPoint) const;
+template WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom(
+ const EditorDOMPoint& aPoint) const;
+template WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom(
+ const EditorRawDOMPoint& aPoint) const;
+template EditorDOMPoint WSRunScanner::GetAfterLastVisiblePoint(
+ Text& aTextNode, const Element* aAncestorLimiter);
+template EditorRawDOMPoint WSRunScanner::GetAfterLastVisiblePoint(
+ Text& aTextNode, const Element* aAncestorLimiter);
+template EditorDOMPoint WSRunScanner::GetFirstVisiblePoint(
+ Text& aTextNode, const Element* aAncestorLimiter);
+template EditorRawDOMPoint WSRunScanner::GetFirstVisiblePoint(
+ Text& aTextNode, const Element* aAncestorLimiter);
+
+template nsresult WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aScanStartPoint);
+template nsresult WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
+ HTMLEditor& aHTMLEditor, const EditorDOMPointInText& aScanStartPoint);
+
+template WSRunScanner::TextFragmentData::TextFragmentData(
+ const EditorDOMPoint& aPoint, const Element* aEditingHost);
+template WSRunScanner::TextFragmentData::TextFragmentData(
+ const EditorRawDOMPoint& aPoint, const Element* aEditingHost);
+template WSRunScanner::TextFragmentData::TextFragmentData(
+ const EditorDOMPointInText& aPoint, const Element* aEditingHost);
+
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetInclusiveNextEditableCharPoint,
+ const EditorDOMPoint& aPoint);
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetInclusiveNextEditableCharPoint,
+ const EditorRawDOMPoint& aPoint);
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetInclusiveNextEditableCharPoint,
+ const EditorDOMPointInText& aPoint);
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetInclusiveNextEditableCharPoint,
+ const EditorRawDOMPointInText& aPoint);
+
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetPreviousEditableCharPoint,
+ const EditorDOMPoint& aPoint);
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetPreviousEditableCharPoint,
+ const EditorRawDOMPoint& aPoint);
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetPreviousEditableCharPoint,
+ const EditorDOMPointInText& aPoint);
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetPreviousEditableCharPoint,
+ const EditorRawDOMPointInText& aPoint);
+
+Result<EditorDOMPoint, nsresult>
+WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToSplit,
+ const Element& aSplittingBlockElement) {
+ if (NS_WARN_IF(!aPointToSplit.IsInContentNode()) ||
+ NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(aSplittingBlockElement)) ||
+ NS_WARN_IF(!EditorUtils::IsEditableContent(
+ *aPointToSplit.ContainerAs<nsIContent>(), EditorType::HTML))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // The container of aPointToSplit may be not splittable, e.g., selection
+ // may be collapsed **in** a `<br>` element or a comment node. So, look
+ // for splittable point with climbing the tree up.
+ EditorDOMPoint pointToSplit(aPointToSplit);
+ for (nsIContent* content : aPointToSplit.ContainerAs<nsIContent>()
+ ->InclusiveAncestorsOfType<nsIContent>()) {
+ if (content == &aSplittingBlockElement) {
+ break;
+ }
+ if (HTMLEditUtils::IsSplittableNode(*content)) {
+ break;
+ }
+ pointToSplit.Set(content);
+ }
+
+ {
+ AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), &pointToSplit);
+
+ nsresult rv = WhiteSpaceVisibilityKeeper::
+ MakeSureToKeepVisibleWhiteSpacesVisibleAfterSplit(aHTMLEditor,
+ pointToSplit);
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "MakeSureToKeepVisibleWhiteSpacesVisibleAfterSplit() failed");
+ return Err(rv);
+ }
+ }
+
+ if (NS_WARN_IF(!pointToSplit.IsInContentNode()) ||
+ NS_WARN_IF(
+ !pointToSplit.ContainerAs<nsIContent>()->IsInclusiveDescendantOf(
+ &aSplittingBlockElement)) ||
+ NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(aSplittingBlockElement)) ||
+ NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(
+ *pointToSplit.ContainerAs<nsIContent>()))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ return pointToSplit;
+}
+
+// static
+Result<EditActionResult, nsresult> WhiteSpaceVisibilityKeeper::
+ MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement(
+ HTMLEditor& aHTMLEditor, Element& aLeftBlockElement,
+ Element& aRightBlockElement, const EditorDOMPoint& aAtRightBlockChild,
+ const Maybe<nsAtom*>& aListElementTagName,
+ const HTMLBRElement* aPrecedingInvisibleBRElement,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(
+ EditorUtils::IsDescendantOf(aLeftBlockElement, aRightBlockElement));
+ MOZ_ASSERT(&aRightBlockElement == aAtRightBlockChild.GetContainer());
+
+ // NOTE: This method may extend deletion range:
+ // - to delete invisible white-spaces at end of aLeftBlockElement
+ // - to delete invisible white-spaces at start of
+ // afterRightBlockChild.GetChild()
+ // - to delete invisible white-spaces before afterRightBlockChild.GetChild()
+ // - to delete invisible `<br>` element at end of aLeftBlockElement
+
+ {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces(
+ aHTMLEditor, EditorDOMPoint::AtEndOf(aLeftBlockElement));
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces() "
+ "failed");
+ return caretPointOrError.propagateErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ }
+
+ // Check whether aLeftBlockElement is a descendant of aRightBlockElement.
+ if (aHTMLEditor.MayHaveMutationEventListeners()) {
+ EditorDOMPoint leftBlockContainingPointInRightBlockElement;
+ if (aHTMLEditor.MayHaveMutationEventListeners() &&
+ MOZ_UNLIKELY(!EditorUtils::IsDescendantOf(
+ aLeftBlockElement, aRightBlockElement,
+ &leftBlockContainingPointInRightBlockElement))) {
+ NS_WARNING(
+ "Deleting invisible whitespace at end of left block element caused "
+ "moving the left block element outside the right block element");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (MOZ_UNLIKELY(leftBlockContainingPointInRightBlockElement !=
+ aAtRightBlockChild)) {
+ NS_WARNING(
+ "Deleting invisible whitespace at end of left block element caused "
+ "changing the left block element in the right block element");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (MOZ_UNLIKELY(!EditorUtils::IsEditableContent(aRightBlockElement,
+ EditorType::HTML))) {
+ NS_WARNING(
+ "Deleting invisible whitespace at end of left block element caused "
+ "making the right block element non-editable");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (MOZ_UNLIKELY(!EditorUtils::IsEditableContent(aLeftBlockElement,
+ EditorType::HTML))) {
+ NS_WARNING(
+ "Deleting invisible whitespace at end of left block element caused "
+ "making the left block element non-editable");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+
+ OwningNonNull<Element> rightBlockElement = aRightBlockElement;
+ EditorDOMPoint afterRightBlockChild = aAtRightBlockChild.NextPoint();
+ {
+ // We can't just track rightBlockElement because it's an Element.
+ AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(),
+ &afterRightBlockChild);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces(
+ aHTMLEditor, afterRightBlockChild);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces() "
+ "failed");
+ return caretPointOrError.propagateErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+
+ // XXX AutoTrackDOMPoint instance, tracker, hasn't been destroyed here.
+ // Do we really need to do update rightBlockElement here??
+ // XXX And afterRightBlockChild.GetContainerAs<Element>() always returns
+ // an element pointer so that probably here should not use
+ // accessors of EditorDOMPoint, should use DOM API directly instead.
+ if (afterRightBlockChild.GetContainerAs<Element>()) {
+ rightBlockElement = *afterRightBlockChild.ContainerAs<Element>();
+ } else if (NS_WARN_IF(
+ !afterRightBlockChild.GetContainerParentAs<Element>())) {
+ return Err(NS_ERROR_UNEXPECTED);
+ } else {
+ rightBlockElement = *afterRightBlockChild.GetContainerParentAs<Element>();
+ }
+ }
+
+ // Do br adjustment.
+ RefPtr<HTMLBRElement> invisibleBRElementAtEndOfLeftBlockElement =
+ WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound(
+ aHTMLEditor.ComputeEditingHost(),
+ EditorDOMPoint::AtEndOf(aLeftBlockElement));
+ NS_ASSERTION(
+ aPrecedingInvisibleBRElement == invisibleBRElementAtEndOfLeftBlockElement,
+ "The preceding invisible BR element computation was different");
+ auto ret = EditActionResult::IgnoredResult();
+ AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
+ // NOTE: Keep syncing with CanMergeLeftAndRightBlockElements() of
+ // AutoInclusiveAncestorBlockElementsJoiner.
+ if (NS_WARN_IF(aListElementTagName.isSome())) {
+ // Since 2002, here was the following comment:
+ // > The idea here is to take all children in rightListElement that are
+ // > past offset, and pull them into leftlistElement.
+ // However, this has never been performed because we are here only when
+ // neither left list nor right list is a descendant of the other but
+ // in such case, getting a list item in the right list node almost
+ // always failed since a variable for offset of
+ // rightListElement->GetChildAt() was not initialized. So, it might be
+ // a bug, but we should keep this traditional behavior for now. If you
+ // find when we get here, please remove this comment if we don't need to
+ // do it. Otherwise, please move children of the right list node to the
+ // end of the left list node.
+
+ // XXX Although, we do nothing here, but for keeping traditional
+ // behavior, we should mark as handled.
+ ret.MarkAsHandled();
+ } else {
+ // XXX Why do we ignore the result of AutoMoveOneLineHandler::Run()?
+ NS_ASSERTION(rightBlockElement == afterRightBlockChild.GetContainer(),
+ "The relation is not guaranteed but assumed");
+#ifdef DEBUG
+ Result<bool, nsresult> firstLineHasContent =
+ HTMLEditor::AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine(
+ EditorDOMPoint(rightBlockElement, afterRightBlockChild.Offset()),
+ aEditingHost);
+#endif // #ifdef DEBUG
+ HTMLEditor::AutoMoveOneLineHandler lineMoverToEndOfLeftBlock(
+ aLeftBlockElement);
+ nsresult rv = lineMoverToEndOfLeftBlock.Prepare(
+ aHTMLEditor,
+ EditorDOMPoint(rightBlockElement, afterRightBlockChild.Offset()),
+ aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoMoveOneLineHandler::Prepare() failed");
+ return Err(rv);
+ }
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ lineMoverToEndOfLeftBlock.Run(aHTMLEditor, aEditingHost);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("AutoMoveOneLineHandler::Run() failed");
+ return moveNodeResult.propagateErr();
+ }
+
+#ifdef DEBUG
+ MOZ_ASSERT(!firstLineHasContent.isErr());
+ if (firstLineHasContent.inspect()) {
+ NS_ASSERTION(moveNodeResult.inspect().Handled(),
+ "Failed to consider whether moving or not something");
+ } else {
+ NS_ASSERTION(moveNodeResult.inspect().Ignored(),
+ "Failed to consider whether moving or not something");
+ }
+#endif // #ifdef DEBUG
+
+ // We don't need to update selection here because of dontChangeMySelection
+ // above.
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+ ret |= moveNodeResult.unwrap();
+ // Now, all children of rightBlockElement were moved to leftBlockElement.
+ // So, afterRightBlockChild is now invalid.
+ afterRightBlockChild.Clear();
+ }
+
+ if (!invisibleBRElementAtEndOfLeftBlockElement ||
+ !invisibleBRElementAtEndOfLeftBlockElement->IsInComposedDoc()) {
+ return ret;
+ }
+
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
+ *invisibleBRElementAtEndOfLeftBlockElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed, but ignored");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+// static
+Result<EditActionResult, nsresult> WhiteSpaceVisibilityKeeper::
+ MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement(
+ HTMLEditor& aHTMLEditor, Element& aLeftBlockElement,
+ Element& aRightBlockElement, const EditorDOMPoint& aAtLeftBlockChild,
+ nsIContent& aLeftContentInBlock,
+ const Maybe<nsAtom*>& aListElementTagName,
+ const HTMLBRElement* aPrecedingInvisibleBRElement,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(
+ EditorUtils::IsDescendantOf(aRightBlockElement, aLeftBlockElement));
+ MOZ_ASSERT(
+ &aLeftBlockElement == &aLeftContentInBlock ||
+ EditorUtils::IsDescendantOf(aLeftContentInBlock, aLeftBlockElement));
+ MOZ_ASSERT(&aLeftBlockElement == aAtLeftBlockChild.GetContainer());
+
+ // NOTE: This method may extend deletion range:
+ // - to delete invisible white-spaces at start of aRightBlockElement
+ // - to delete invisible white-spaces before aRightBlockElement
+ // - to delete invisible white-spaces at start of aAtLeftBlockChild.GetChild()
+ // - to delete invisible white-spaces before aAtLeftBlockChild.GetChild()
+ // - to delete invisible `<br>` element before aAtLeftBlockChild.GetChild()
+
+ {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces(
+ aHTMLEditor, EditorDOMPoint(&aRightBlockElement, 0));
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces() "
+ "failed");
+ return caretPointOrError.propagateErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ }
+
+ // Check whether aRightBlockElement is a descendant of aLeftBlockElement.
+ if (aHTMLEditor.MayHaveMutationEventListeners()) {
+ EditorDOMPoint rightBlockContainingPointInLeftBlockElement;
+ if (aHTMLEditor.MayHaveMutationEventListeners() &&
+ MOZ_UNLIKELY(!EditorUtils::IsDescendantOf(
+ aRightBlockElement, aLeftBlockElement,
+ &rightBlockContainingPointInLeftBlockElement))) {
+ NS_WARNING(
+ "Deleting invisible whitespace at start of right block element "
+ "caused moving the right block element outside the left block "
+ "element");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (MOZ_UNLIKELY(rightBlockContainingPointInLeftBlockElement !=
+ aAtLeftBlockChild)) {
+ NS_WARNING(
+ "Deleting invisible whitespace at start of right block element "
+ "caused changing the right block element position in the left block "
+ "element");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (MOZ_UNLIKELY(!EditorUtils::IsEditableContent(aLeftBlockElement,
+ EditorType::HTML))) {
+ NS_WARNING(
+ "Deleting invisible whitespace at start of right block element "
+ "caused making the left block element non-editable");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (MOZ_UNLIKELY(!EditorUtils::IsEditableContent(aRightBlockElement,
+ EditorType::HTML))) {
+ NS_WARNING(
+ "Deleting invisible whitespace at start of right block element "
+ "caused making the right block element non-editable");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+
+ OwningNonNull<Element> originalLeftBlockElement = aLeftBlockElement;
+ OwningNonNull<Element> leftBlockElement = aLeftBlockElement;
+ EditorDOMPoint atLeftBlockChild(aAtLeftBlockChild);
+ {
+ // We can't just track leftBlockElement because it's an Element, so track
+ // something else.
+ AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), &atLeftBlockChild);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces(
+ aHTMLEditor, EditorDOMPoint(atLeftBlockChild.GetContainer(),
+ atLeftBlockChild.Offset()));
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces() "
+ "failed");
+ return caretPointOrError.propagateErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ }
+ if (MOZ_UNLIKELY(!atLeftBlockChild.IsSetAndValid())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces() caused "
+ "unexpected DOM tree");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ // XXX atLeftBlockChild.GetContainerAs<Element>() should always return
+ // an element pointer so that probably here should not use
+ // accessors of EditorDOMPoint, should use DOM API directly instead.
+ if (Element* nearestAncestor =
+ atLeftBlockChild.GetContainerOrContainerParentElement()) {
+ leftBlockElement = *nearestAncestor;
+ } else {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ // Do br adjustment.
+ RefPtr<HTMLBRElement> invisibleBRElementBeforeLeftBlockElement =
+ WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound(
+ aHTMLEditor.ComputeEditingHost(), atLeftBlockChild);
+ NS_ASSERTION(
+ aPrecedingInvisibleBRElement == invisibleBRElementBeforeLeftBlockElement,
+ "The preceding invisible BR element computation was different");
+ auto ret = EditActionResult::IgnoredResult();
+ AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
+ // NOTE: Keep syncing with CanMergeLeftAndRightBlockElements() of
+ // AutoInclusiveAncestorBlockElementsJoiner.
+ if (aListElementTagName.isSome()) {
+ // XXX Why do we ignore the error from MoveChildrenWithTransaction()?
+ MOZ_ASSERT(originalLeftBlockElement == atLeftBlockChild.GetContainer(),
+ "This is not guaranteed, but assumed");
+#ifdef DEBUG
+ Result<bool, nsresult> rightBlockHasContent =
+ aHTMLEditor.CanMoveChildren(aRightBlockElement, aLeftBlockElement);
+#endif // #ifdef DEBUG
+ // TODO: Stop using HTMLEditor::PreserveWhiteSpaceStyle::No due to no tests.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ aHTMLEditor.MoveChildrenWithTransaction(
+ aRightBlockElement,
+ EditorDOMPoint(atLeftBlockChild.GetContainer(),
+ atLeftBlockChild.Offset()),
+ HTMLEditor::PreserveWhiteSpaceStyle::No,
+ HTMLEditor::RemoveIfCommentNode::Yes);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ if (NS_WARN_IF(moveNodeResult.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(moveNodeResult.unwrapErr());
+ }
+ NS_WARNING(
+ "HTMLEditor::MoveChildrenWithTransaction() failed, but ignored");
+ } else {
+#ifdef DEBUG
+ MOZ_ASSERT(!rightBlockHasContent.isErr());
+ if (rightBlockHasContent.inspect()) {
+ NS_ASSERTION(moveNodeResult.inspect().Handled(),
+ "Failed to consider whether moving or not children");
+ } else {
+ NS_ASSERTION(moveNodeResult.inspect().Ignored(),
+ "Failed to consider whether moving or not children");
+ }
+#endif // #ifdef DEBUG
+ // We don't need to update selection here because of dontChangeMySelection
+ // above.
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+ ret |= moveNodeResult.unwrap();
+ }
+ // atLeftBlockChild was moved to rightListElement. So, it's invalid now.
+ atLeftBlockChild.Clear();
+ } else {
+ // Left block is a parent of right block, and the parent of the previous
+ // visible content. Right block is a child and contains the contents we
+ // want to move.
+
+ EditorDOMPoint pointToMoveFirstLineContent;
+ if (&aLeftContentInBlock == leftBlockElement) {
+ // We are working with valid HTML, aLeftContentInBlock is a block element,
+ // and is therefore allowed to contain aRightBlockElement. This is the
+ // simple case, we will simply move the content in aRightBlockElement
+ // out of its block.
+ pointToMoveFirstLineContent = atLeftBlockChild;
+ MOZ_ASSERT(pointToMoveFirstLineContent.GetContainer() ==
+ &aLeftBlockElement);
+ } else {
+ if (NS_WARN_IF(!aLeftContentInBlock.IsInComposedDoc())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ // We try to work as well as possible with HTML that's already invalid.
+ // Although "right block" is a block, and a block must not be contained
+ // in inline elements, reality is that broken documents do exist. The
+ // DIRECT parent of "left NODE" might be an inline element. Previous
+ // versions of this code skipped inline parents until the first block
+ // parent was found (and used "left block" as the destination).
+ // However, in some situations this strategy moves the content to an
+ // unexpected position. (see bug 200416) The new idea is to make the
+ // moving content a sibling, next to the previous visible content.
+ pointToMoveFirstLineContent.SetAfter(&aLeftContentInBlock);
+ if (NS_WARN_IF(!pointToMoveFirstLineContent.IsInContentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+
+ MOZ_ASSERT(pointToMoveFirstLineContent.IsSetAndValid());
+
+ // Because we don't want the moving content to receive the style of the
+ // previous content, we split the previous content's style.
+
+#ifdef DEBUG
+ Result<bool, nsresult> firstLineHasContent =
+ HTMLEditor::AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine(
+ EditorDOMPoint(&aRightBlockElement, 0u), aEditingHost);
+#endif // #ifdef DEBUG
+
+ if (&aLeftContentInBlock != &aEditingHost) {
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ aHTMLEditor.SplitAncestorStyledInlineElementsAt(
+ pointToMoveFirstLineContent, EditorInlineStyle::RemoveAllStyles(),
+ HTMLEditor::SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
+ return splitNodeResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
+ nsresult rv = unwrappedSplitNodeResult.SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ if (!unwrappedSplitNodeResult.DidSplit()) {
+ // If nothing was split, we should move the first line content to after
+ // the parent inline elements.
+ for (EditorDOMPoint parentPoint = pointToMoveFirstLineContent;
+ pointToMoveFirstLineContent.IsEndOfContainer() &&
+ pointToMoveFirstLineContent.IsInContentNode();
+ pointToMoveFirstLineContent = EditorDOMPoint::After(
+ *pointToMoveFirstLineContent.ContainerAs<nsIContent>())) {
+ if (pointToMoveFirstLineContent.GetContainer() ==
+ &aLeftBlockElement ||
+ NS_WARN_IF(pointToMoveFirstLineContent.GetContainer() ==
+ &aEditingHost)) {
+ break;
+ }
+ }
+ if (NS_WARN_IF(!pointToMoveFirstLineContent.IsInContentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ } else if (unwrappedSplitNodeResult.Handled()) {
+ // If se split something, we should move the first line contents before
+ // the right elements.
+ if (nsIContent* nextContentAtSplitPoint =
+ unwrappedSplitNodeResult.GetNextContent()) {
+ pointToMoveFirstLineContent.Set(nextContentAtSplitPoint);
+ if (NS_WARN_IF(!pointToMoveFirstLineContent.IsInContentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ } else {
+ pointToMoveFirstLineContent =
+ unwrappedSplitNodeResult.AtSplitPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToMoveFirstLineContent.IsInContentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+ }
+ MOZ_DIAGNOSTIC_ASSERT(pointToMoveFirstLineContent.IsSetAndValid());
+ }
+
+ HTMLEditor::AutoMoveOneLineHandler lineMoverToPoint(
+ pointToMoveFirstLineContent);
+ nsresult rv = lineMoverToPoint.Prepare(
+ aHTMLEditor, EditorDOMPoint(&aRightBlockElement, 0u), aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoMoveOneLineHandler::Prepare() failed");
+ return Err(rv);
+ }
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ lineMoverToPoint.Run(aHTMLEditor, aEditingHost);
+ if (moveNodeResult.isErr()) {
+ NS_WARNING("AutoMoveOneLineHandler::Run() failed");
+ return moveNodeResult.propagateErr();
+ }
+
+#ifdef DEBUG
+ MOZ_ASSERT(!firstLineHasContent.isErr());
+ if (firstLineHasContent.inspect()) {
+ NS_ASSERTION(moveNodeResult.inspect().Handled(),
+ "Failed to consider whether moving or not something");
+ } else {
+ NS_ASSERTION(moveNodeResult.inspect().Ignored(),
+ "Failed to consider whether moving or not something");
+ }
+#endif // #ifdef DEBUG
+
+ // We don't need to update selection here because of dontChangeMySelection
+ // above.
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+ ret |= moveNodeResult.unwrap();
+ }
+
+ if (!invisibleBRElementBeforeLeftBlockElement ||
+ !invisibleBRElementBeforeLeftBlockElement->IsInComposedDoc()) {
+ return ret;
+ }
+
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
+ *invisibleBRElementBeforeLeftBlockElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed, but ignored");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+// static
+Result<EditActionResult, nsresult> WhiteSpaceVisibilityKeeper::
+ MergeFirstLineOfRightBlockElementIntoLeftBlockElement(
+ HTMLEditor& aHTMLEditor, Element& aLeftBlockElement,
+ Element& aRightBlockElement, const Maybe<nsAtom*>& aListElementTagName,
+ const HTMLBRElement* aPrecedingInvisibleBRElement,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(
+ !EditorUtils::IsDescendantOf(aLeftBlockElement, aRightBlockElement));
+ MOZ_ASSERT(
+ !EditorUtils::IsDescendantOf(aRightBlockElement, aLeftBlockElement));
+
+ // NOTE: This method may extend deletion range:
+ // - to delete invisible white-spaces at end of aLeftBlockElement
+ // - to delete invisible white-spaces at start of aRightBlockElement
+ // - to delete invisible `<br>` element at end of aLeftBlockElement
+
+ // Adjust white-space at block boundaries
+ {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::
+ MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange(
+ aHTMLEditor,
+ EditorDOMRange(EditorDOMPoint::AtEndOf(aLeftBlockElement),
+ EditorDOMPoint(&aRightBlockElement, 0)),
+ aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange() "
+ "failed");
+ return caretPointOrError.propagateErr();
+ }
+ // Ignore caret point suggestion because there was
+ // AutoTransactionsConserveSelection.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ }
+ // Do br adjustment.
+ RefPtr<HTMLBRElement> invisibleBRElementAtEndOfLeftBlockElement =
+ WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound(
+ aHTMLEditor.ComputeEditingHost(),
+ EditorDOMPoint::AtEndOf(aLeftBlockElement));
+ NS_ASSERTION(
+ aPrecedingInvisibleBRElement == invisibleBRElementAtEndOfLeftBlockElement,
+ "The preceding invisible BR element computation was different");
+ auto ret = EditActionResult::IgnoredResult();
+ AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
+ if (aListElementTagName.isSome() ||
+ // TODO: We should stop merging entire blocks even if they have same
+ // white-space style because Chrome behave so. However, it's risky to
+ // change our behavior in the major cases so that we should do it in
+ // a bug to manage only the change.
+ (aLeftBlockElement.NodeInfo()->NameAtom() ==
+ aRightBlockElement.NodeInfo()->NameAtom() &&
+ EditorUtils::GetComputedWhiteSpaceStyle(aLeftBlockElement) ==
+ EditorUtils::GetComputedWhiteSpaceStyle(aRightBlockElement))) {
+ // Nodes are same type. merge them.
+ EditorDOMPoint atFirstChildOfRightNode;
+ nsresult rv = aHTMLEditor.JoinNearestEditableNodesWithTransaction(
+ aLeftBlockElement, aRightBlockElement, &atFirstChildOfRightNode);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::JoinNearestEditableNodesWithTransaction()"
+ " failed, but ignored");
+ if (aListElementTagName.isSome() && atFirstChildOfRightNode.IsSet()) {
+ Result<CreateElementResult, nsresult> convertListTypeResult =
+ aHTMLEditor.ChangeListElementType(
+ aRightBlockElement, MOZ_KnownLive(*aListElementTagName.ref()),
+ *nsGkAtoms::li);
+ if (MOZ_UNLIKELY(convertListTypeResult.isErr())) {
+ if (NS_WARN_IF(convertListTypeResult.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING("HTMLEditor::ChangeListElementType() failed, but ignored");
+ } else {
+ // There is AutoTransactionConserveSelection above, therefore, we don't
+ // need to update selection here.
+ convertListTypeResult.inspect().IgnoreCaretPointSuggestion();
+ }
+ }
+ ret.MarkAsHandled();
+ } else {
+#ifdef DEBUG
+ Result<bool, nsresult> firstLineHasContent =
+ HTMLEditor::AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine(
+ EditorDOMPoint(&aRightBlockElement, 0u), aEditingHost);
+#endif // #ifdef DEBUG
+
+ // Nodes are dissimilar types.
+ HTMLEditor::AutoMoveOneLineHandler lineMoverToEndOfLeftBlock(
+ aLeftBlockElement);
+ nsresult rv = lineMoverToEndOfLeftBlock.Prepare(
+ aHTMLEditor, EditorDOMPoint(&aRightBlockElement, 0u), aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoMoveOneLineHandler::Prepare() failed");
+ return Err(rv);
+ }
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ lineMoverToEndOfLeftBlock.Run(aHTMLEditor, aEditingHost);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("AutoMoveOneLineHandler::Run() failed");
+ return moveNodeResult.propagateErr();
+ }
+
+#ifdef DEBUG
+ MOZ_ASSERT(!firstLineHasContent.isErr());
+ if (firstLineHasContent.inspect()) {
+ NS_ASSERTION(moveNodeResult.inspect().Handled(),
+ "Failed to consider whether moving or not something");
+ } else {
+ NS_ASSERTION(moveNodeResult.inspect().Ignored(),
+ "Failed to consider whether moving or not something");
+ }
+#endif // #ifdef DEBUG
+
+ // We don't need to update selection here because of dontChangeMySelection
+ // above.
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+ ret |= moveNodeResult.unwrap();
+ }
+
+ if (!invisibleBRElementAtEndOfLeftBlockElement ||
+ !invisibleBRElementAtEndOfLeftBlockElement->IsInComposedDoc()) {
+ ret.MarkAsHandled();
+ return ret;
+ }
+
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
+ *invisibleBRElementAtEndOfLeftBlockElement);
+ // XXX In other top level if blocks, the result of
+ // DeleteNodeWithTransaction() is ignored. Why does only this result
+ // is respected?
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+// static
+Result<CreateElementResult, nsresult>
+WhiteSpaceVisibilityKeeper::InsertBRElement(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToInsert,
+ const Element& aEditingHost) {
+ if (MOZ_UNLIKELY(NS_WARN_IF(!aPointToInsert.IsSet()))) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ // MOOSE: for now, we always assume non-PRE formatting. Fix this later.
+ // meanwhile, the pre case is handled in HandleInsertText() in
+ // HTMLEditSubActionHandler.cpp
+
+ TextFragmentData textFragmentDataAtInsertionPoint(aPointToInsert,
+ &aEditingHost);
+ if (MOZ_UNLIKELY(
+ NS_WARN_IF(!textFragmentDataAtInsertionPoint.IsInitialized()))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ EditorDOMRange invisibleLeadingWhiteSpaceRangeOfNewLine =
+ textFragmentDataAtInsertionPoint
+ .GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt(aPointToInsert);
+ EditorDOMRange invisibleTrailingWhiteSpaceRangeOfCurrentLine =
+ textFragmentDataAtInsertionPoint
+ .GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt(aPointToInsert);
+ const Maybe<const VisibleWhiteSpacesData> visibleWhiteSpaces =
+ !invisibleLeadingWhiteSpaceRangeOfNewLine.IsPositioned() ||
+ !invisibleTrailingWhiteSpaceRangeOfCurrentLine.IsPositioned()
+ ? Some(textFragmentDataAtInsertionPoint.VisibleWhiteSpacesDataRef())
+ : Nothing();
+ const PointPosition pointPositionWithVisibleWhiteSpaces =
+ visibleWhiteSpaces.isSome() && visibleWhiteSpaces.ref().IsInitialized()
+ ? visibleWhiteSpaces.ref().ComparePoint(aPointToInsert)
+ : PointPosition::NotInSameDOMTree;
+
+ EditorDOMPoint pointToInsert(aPointToInsert);
+ EditorDOMPoint atNBSPReplaceableWithSP;
+ if (!invisibleLeadingWhiteSpaceRangeOfNewLine.IsPositioned() &&
+ (pointPositionWithVisibleWhiteSpaces == PointPosition::MiddleOfFragment ||
+ pointPositionWithVisibleWhiteSpaces == PointPosition::EndOfFragment)) {
+ atNBSPReplaceableWithSP =
+ textFragmentDataAtInsertionPoint
+ .GetPreviousNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
+ pointToInsert)
+ .To<EditorDOMPoint>();
+ }
+
+ {
+ if (invisibleTrailingWhiteSpaceRangeOfCurrentLine.IsPositioned()) {
+ if (!invisibleTrailingWhiteSpaceRangeOfCurrentLine.Collapsed()) {
+ // XXX Why don't we remove all of the invisible white-spaces?
+ MOZ_ASSERT(invisibleTrailingWhiteSpaceRangeOfCurrentLine.StartRef() ==
+ pointToInsert);
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ AutoTrackDOMPoint trackEndOfLineNBSP(aHTMLEditor.RangeUpdaterRef(),
+ &atNBSPReplaceableWithSP);
+ AutoTrackDOMRange trackLeadingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleLeadingWhiteSpaceRangeOfNewLine);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ invisibleTrailingWhiteSpaceRangeOfCurrentLine.StartRef(),
+ invisibleTrailingWhiteSpaceRangeOfCurrentLine.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ // Don't refer the following variables anymore unless tracking the
+ // change.
+ invisibleTrailingWhiteSpaceRangeOfCurrentLine.Clear();
+ }
+ }
+ // If new line will start with visible white-spaces, it needs to be start
+ // with an NBSP.
+ else if (pointPositionWithVisibleWhiteSpaces ==
+ PointPosition::StartOfFragment ||
+ pointPositionWithVisibleWhiteSpaces ==
+ PointPosition::MiddleOfFragment) {
+ auto atNextCharOfInsertionPoint =
+ textFragmentDataAtInsertionPoint
+ .GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(
+ pointToInsert);
+ if (atNextCharOfInsertionPoint.IsSet() &&
+ !atNextCharOfInsertionPoint.IsEndOfContainer() &&
+ atNextCharOfInsertionPoint.IsCharCollapsibleASCIISpace()) {
+ const EditorDOMPointInText atPreviousCharOfNextCharOfInsertionPoint =
+ textFragmentDataAtInsertionPoint.GetPreviousEditableCharPoint(
+ atNextCharOfInsertionPoint);
+ if (!atPreviousCharOfNextCharOfInsertionPoint.IsSet() ||
+ atPreviousCharOfNextCharOfInsertionPoint.IsEndOfContainer() ||
+ !atPreviousCharOfNextCharOfInsertionPoint.IsCharASCIISpace()) {
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ AutoTrackDOMPoint trackEndOfLineNBSP(aHTMLEditor.RangeUpdaterRef(),
+ &atNBSPReplaceableWithSP);
+ AutoTrackDOMRange trackLeadingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleLeadingWhiteSpaceRangeOfNewLine);
+ // We are at start of non-nbsps. Convert to a single nbsp.
+ const EditorDOMPointInText endOfCollapsibleASCIIWhiteSpaces =
+ textFragmentDataAtInsertionPoint
+ .GetEndOfCollapsibleASCIIWhiteSpaces(
+ atNextCharOfInsertionPoint, nsIEditor::eNone);
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes(
+ aHTMLEditor,
+ EditorDOMRangeInTexts(atNextCharOfInsertionPoint,
+ endOfCollapsibleASCIIWhiteSpaces),
+ nsDependentSubstring(&HTMLEditUtils::kNBSP, 1));
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "ReplaceTextAndRemoveEmptyTextNodes() failed");
+ return Err(rv);
+ }
+ // Don't refer the following variables anymore unless tracking the
+ // change.
+ invisibleTrailingWhiteSpaceRangeOfCurrentLine.Clear();
+ }
+ }
+ }
+
+ if (invisibleLeadingWhiteSpaceRangeOfNewLine.IsPositioned()) {
+ if (!invisibleLeadingWhiteSpaceRangeOfNewLine.Collapsed()) {
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ // XXX Why don't we remove all of the invisible white-spaces?
+ MOZ_ASSERT(invisibleLeadingWhiteSpaceRangeOfNewLine.EndRef() ==
+ pointToInsert);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ invisibleLeadingWhiteSpaceRangeOfNewLine.StartRef(),
+ invisibleLeadingWhiteSpaceRangeOfNewLine.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ // Don't refer the following variables anymore unless tracking the
+ // change.
+ atNBSPReplaceableWithSP.Clear();
+ invisibleLeadingWhiteSpaceRangeOfNewLine.Clear();
+ invisibleTrailingWhiteSpaceRangeOfCurrentLine.Clear();
+ }
+ }
+ // If the `<br>` element is put immediately after an NBSP, it should be
+ // replaced with an ASCII white-space.
+ else if (atNBSPReplaceableWithSP.IsInTextNode()) {
+ const EditorDOMPointInText atNBSPReplacedWithASCIIWhiteSpace =
+ atNBSPReplaceableWithSP.AsInText();
+ if (!atNBSPReplacedWithASCIIWhiteSpace.IsEndOfContainer() &&
+ atNBSPReplacedWithASCIIWhiteSpace.IsCharNBSP()) {
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ aHTMLEditor.ReplaceTextWithTransaction(
+ MOZ_KnownLive(
+ *atNBSPReplacedWithASCIIWhiteSpace.ContainerAs<Text>()),
+ atNBSPReplacedWithASCIIWhiteSpace.Offset(), 1, u" "_ns);
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed failed");
+ return replaceTextResult.propagateErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+ // Don't refer the following variables anymore unless tracking the
+ // change.
+ atNBSPReplaceableWithSP.Clear();
+ invisibleLeadingWhiteSpaceRangeOfNewLine.Clear();
+ invisibleTrailingWhiteSpaceRangeOfCurrentLine.Clear();
+ }
+ }
+ }
+
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ aHTMLEditor.InsertBRElement(WithTransaction::Yes, pointToInsert);
+ NS_WARNING_ASSERTION(
+ insertBRElementResult.isOk(),
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes, eNone) failed");
+ return insertBRElementResult;
+}
+
+// static
+Result<InsertTextResult, nsresult> WhiteSpaceVisibilityKeeper::ReplaceText(
+ HTMLEditor& aHTMLEditor, const nsAString& aStringToInsert,
+ const EditorDOMRange& aRangeToBeReplaced, const Element& aEditingHost) {
+ // MOOSE: for now, we always assume non-PRE formatting. Fix this later.
+ // meanwhile, the pre case is handled in HandleInsertText() in
+ // HTMLEditSubActionHandler.cpp
+
+ // MOOSE: for now, just getting the ws logic straight. This implementation
+ // is very slow. Will need to replace edit rules impl with a more efficient
+ // text sink here that does the minimal amount of searching/replacing/copying
+
+ if (aStringToInsert.IsEmpty()) {
+ MOZ_ASSERT(aRangeToBeReplaced.Collapsed());
+ return InsertTextResult();
+ }
+
+ TextFragmentData textFragmentDataAtStart(aRangeToBeReplaced.StartRef(),
+ &aEditingHost);
+ if (MOZ_UNLIKELY(NS_WARN_IF(!textFragmentDataAtStart.IsInitialized()))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ const bool isInsertionPointEqualsOrIsBeforeStartOfText =
+ aRangeToBeReplaced.StartRef().EqualsOrIsBefore(
+ textFragmentDataAtStart.StartRef());
+ TextFragmentData textFragmentDataAtEnd =
+ aRangeToBeReplaced.Collapsed()
+ ? textFragmentDataAtStart
+ : TextFragmentData(aRangeToBeReplaced.EndRef(), &aEditingHost);
+ if (MOZ_UNLIKELY(NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized()))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ const bool isInsertionPointEqualsOrAfterEndOfText =
+ textFragmentDataAtEnd.EndRef().EqualsOrIsBefore(
+ aRangeToBeReplaced.EndRef());
+
+ EditorDOMRange invisibleLeadingWhiteSpaceRangeAtStart =
+ textFragmentDataAtStart
+ .GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt(
+ aRangeToBeReplaced.StartRef());
+ const bool isInvisibleLeadingWhiteSpaceRangeAtStartPositioned =
+ invisibleLeadingWhiteSpaceRangeAtStart.IsPositioned();
+ EditorDOMRange invisibleTrailingWhiteSpaceRangeAtEnd =
+ textFragmentDataAtEnd.GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt(
+ aRangeToBeReplaced.EndRef());
+ const bool isInvisibleTrailingWhiteSpaceRangeAtEndPositioned =
+ invisibleTrailingWhiteSpaceRangeAtEnd.IsPositioned();
+ const Maybe<const VisibleWhiteSpacesData> visibleWhiteSpacesAtStart =
+ !isInvisibleLeadingWhiteSpaceRangeAtStartPositioned
+ ? Some(textFragmentDataAtStart.VisibleWhiteSpacesDataRef())
+ : Nothing();
+ const PointPosition pointPositionWithVisibleWhiteSpacesAtStart =
+ visibleWhiteSpacesAtStart.isSome() &&
+ visibleWhiteSpacesAtStart.ref().IsInitialized()
+ ? visibleWhiteSpacesAtStart.ref().ComparePoint(
+ aRangeToBeReplaced.StartRef())
+ : PointPosition::NotInSameDOMTree;
+ const Maybe<const VisibleWhiteSpacesData> visibleWhiteSpacesAtEnd =
+ !isInvisibleTrailingWhiteSpaceRangeAtEndPositioned
+ ? Some(textFragmentDataAtEnd.VisibleWhiteSpacesDataRef())
+ : Nothing();
+ const PointPosition pointPositionWithVisibleWhiteSpacesAtEnd =
+ visibleWhiteSpacesAtEnd.isSome() &&
+ visibleWhiteSpacesAtEnd.ref().IsInitialized()
+ ? visibleWhiteSpacesAtEnd.ref().ComparePoint(
+ aRangeToBeReplaced.EndRef())
+ : PointPosition::NotInSameDOMTree;
+
+ EditorDOMPoint pointToPutCaret;
+ EditorDOMPoint pointToInsert(aRangeToBeReplaced.StartRef());
+ EditorDOMPoint atNBSPReplaceableWithSP;
+ if (!invisibleTrailingWhiteSpaceRangeAtEnd.IsPositioned() &&
+ (pointPositionWithVisibleWhiteSpacesAtStart ==
+ PointPosition::MiddleOfFragment ||
+ pointPositionWithVisibleWhiteSpacesAtStart ==
+ PointPosition::EndOfFragment)) {
+ atNBSPReplaceableWithSP =
+ textFragmentDataAtStart
+ .GetPreviousNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
+ pointToInsert)
+ .To<EditorDOMPoint>();
+ }
+ nsAutoString theString(aStringToInsert);
+ {
+ if (invisibleTrailingWhiteSpaceRangeAtEnd.IsPositioned()) {
+ if (!invisibleTrailingWhiteSpaceRangeAtEnd.Collapsed()) {
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ AutoTrackDOMPoint trackPrecedingNBSP(aHTMLEditor.RangeUpdaterRef(),
+ &atNBSPReplaceableWithSP);
+ AutoTrackDOMRange trackInvisibleLeadingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleLeadingWhiteSpaceRangeAtStart);
+ AutoTrackDOMRange trackInvisibleTrailingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleTrailingWhiteSpaceRangeAtEnd);
+ // XXX Why don't we remove all of the invisible white-spaces?
+ MOZ_ASSERT(invisibleTrailingWhiteSpaceRangeAtEnd.StartRef() ==
+ pointToInsert);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ invisibleTrailingWhiteSpaceRangeAtEnd.StartRef(),
+ invisibleTrailingWhiteSpaceRangeAtEnd.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ }
+ // Replace an NBSP at inclusive next character of replacing range to an
+ // ASCII white-space if inserting into a visible white-space sequence.
+ // XXX With modifying the inserting string later, this creates a line break
+ // opportunity after the inserting string, but this causes
+ // inconsistent result with inserting order. E.g., type white-space
+ // n times with various order.
+ else if (pointPositionWithVisibleWhiteSpacesAtEnd ==
+ PointPosition::StartOfFragment ||
+ pointPositionWithVisibleWhiteSpacesAtEnd ==
+ PointPosition::MiddleOfFragment) {
+ EditorDOMPointInText atNBSPReplacedWithASCIIWhiteSpace =
+ textFragmentDataAtEnd
+ .GetInclusiveNextNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
+ aRangeToBeReplaced.EndRef());
+ if (atNBSPReplacedWithASCIIWhiteSpace.IsSet()) {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ AutoTrackDOMPoint trackPrecedingNBSP(aHTMLEditor.RangeUpdaterRef(),
+ &atNBSPReplaceableWithSP);
+ AutoTrackDOMRange trackInvisibleLeadingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleLeadingWhiteSpaceRangeAtStart);
+ AutoTrackDOMRange trackInvisibleTrailingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleTrailingWhiteSpaceRangeAtEnd);
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ aHTMLEditor.ReplaceTextWithTransaction(
+ MOZ_KnownLive(
+ *atNBSPReplacedWithASCIIWhiteSpace.ContainerAs<Text>()),
+ atNBSPReplacedWithASCIIWhiteSpace.Offset(), 1, u" "_ns);
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
+ return replaceTextResult.propagateErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+ }
+ }
+
+ if (invisibleLeadingWhiteSpaceRangeAtStart.IsPositioned()) {
+ if (!invisibleLeadingWhiteSpaceRangeAtStart.Collapsed()) {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ AutoTrackDOMRange trackInvisibleTrailingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleTrailingWhiteSpaceRangeAtEnd);
+ // XXX Why don't we remove all of the invisible white-spaces?
+ MOZ_ASSERT(invisibleLeadingWhiteSpaceRangeAtStart.EndRef() ==
+ pointToInsert);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ invisibleLeadingWhiteSpaceRangeAtStart.StartRef(),
+ invisibleLeadingWhiteSpaceRangeAtStart.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ // Don't refer the following variables anymore unless tracking the
+ // change.
+ atNBSPReplaceableWithSP.Clear();
+ invisibleLeadingWhiteSpaceRangeAtStart.Clear();
+ }
+ }
+ // Replace an NBSP at previous character of insertion point to an ASCII
+ // white-space if inserting into a visible white-space sequence.
+ // XXX With modifying the inserting string later, this creates a line break
+ // opportunity before the inserting string, but this causes
+ // inconsistent result with inserting order. E.g., type white-space
+ // n times with various order.
+ else if (atNBSPReplaceableWithSP.IsInTextNode()) {
+ EditorDOMPointInText atNBSPReplacedWithASCIIWhiteSpace =
+ atNBSPReplaceableWithSP.AsInText();
+ if (!atNBSPReplacedWithASCIIWhiteSpace.IsEndOfContainer() &&
+ atNBSPReplacedWithASCIIWhiteSpace.IsCharNBSP()) {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ AutoTrackDOMRange trackInvisibleTrailingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleTrailingWhiteSpaceRangeAtEnd);
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ aHTMLEditor.ReplaceTextWithTransaction(
+ MOZ_KnownLive(
+ *atNBSPReplacedWithASCIIWhiteSpace.ContainerAs<Text>()),
+ atNBSPReplacedWithASCIIWhiteSpace.Offset(), 1, u" "_ns);
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed failed");
+ return replaceTextResult.propagateErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+ // Don't refer the following variables anymore unless tracking the
+ // change.
+ atNBSPReplaceableWithSP.Clear();
+ invisibleLeadingWhiteSpaceRangeAtStart.Clear();
+ }
+ }
+ }
+
+ // If white-space and/or linefeed characters are collapsible, and inserting
+ // string starts and/or ends with a collapsible characters, we need to
+ // replace them with NBSP for making sure the collapsible characters visible.
+ // FYI: There is no case only linefeeds are collapsible. So, we need to
+ // do the things only when white-spaces are collapsible.
+ MOZ_DIAGNOSTIC_ASSERT(!theString.IsEmpty());
+ if (NS_WARN_IF(!pointToInsert.IsInContentNode()) ||
+ !EditorUtils::IsWhiteSpacePreformatted(
+ *pointToInsert.ContainerAs<nsIContent>())) {
+ const bool isNewLineCollapsible =
+ !pointToInsert.IsInContentNode() ||
+ !EditorUtils::IsNewLinePreformatted(
+ *pointToInsert.ContainerAs<nsIContent>());
+ auto IsCollapsibleChar = [&isNewLineCollapsible](char16_t aChar) -> bool {
+ return nsCRT::IsAsciiSpace(aChar) &&
+ (isNewLineCollapsible || aChar != HTMLEditUtils::kNewLine);
+ };
+ if (IsCollapsibleChar(theString[0])) {
+ // If inserting string will follow some invisible leading white-spaces,
+ // the string needs to start with an NBSP.
+ if (isInvisibleLeadingWhiteSpaceRangeAtStartPositioned) {
+ theString.SetCharAt(HTMLEditUtils::kNBSP, 0);
+ }
+ // If inserting around visible white-spaces, check whether the previous
+ // character of insertion point is an NBSP or an ASCII white-space.
+ else if (pointPositionWithVisibleWhiteSpacesAtStart ==
+ PointPosition::MiddleOfFragment ||
+ pointPositionWithVisibleWhiteSpacesAtStart ==
+ PointPosition::EndOfFragment) {
+ const auto atPreviousChar =
+ textFragmentDataAtStart
+ .GetPreviousEditableCharPoint<EditorRawDOMPointInText>(
+ pointToInsert);
+ if (atPreviousChar.IsSet() && !atPreviousChar.IsEndOfContainer() &&
+ atPreviousChar.IsCharASCIISpace()) {
+ theString.SetCharAt(HTMLEditUtils::kNBSP, 0);
+ }
+ }
+ // If the insertion point is (was) before the start of text and it's
+ // immediately after a hard line break, the first ASCII white-space should
+ // be replaced with an NBSP for making it visible.
+ else if (textFragmentDataAtStart.StartsFromHardLineBreak() &&
+ isInsertionPointEqualsOrIsBeforeStartOfText) {
+ theString.SetCharAt(HTMLEditUtils::kNBSP, 0);
+ }
+ }
+
+ // Then the tail. Note that it may be the first character.
+ const uint32_t lastCharIndex = theString.Length() - 1;
+ if (IsCollapsibleChar(theString[lastCharIndex])) {
+ // If inserting string will be followed by some invisible trailing
+ // white-spaces, the string needs to end with an NBSP.
+ if (isInvisibleTrailingWhiteSpaceRangeAtEndPositioned) {
+ theString.SetCharAt(HTMLEditUtils::kNBSP, lastCharIndex);
+ }
+ // If inserting around visible white-spaces, check whether the inclusive
+ // next character of end of replaced range is an NBSP or an ASCII
+ // white-space.
+ if (pointPositionWithVisibleWhiteSpacesAtEnd ==
+ PointPosition::StartOfFragment ||
+ pointPositionWithVisibleWhiteSpacesAtEnd ==
+ PointPosition::MiddleOfFragment) {
+ const auto atNextChar =
+ textFragmentDataAtEnd
+ .GetInclusiveNextEditableCharPoint<EditorRawDOMPointInText>(
+ pointToInsert);
+ if (atNextChar.IsSet() && !atNextChar.IsEndOfContainer() &&
+ atNextChar.IsCharASCIISpace()) {
+ theString.SetCharAt(HTMLEditUtils::kNBSP, lastCharIndex);
+ }
+ }
+ // If the end of replacing range is (was) after the end of text and it's
+ // immediately before block boundary, the last ASCII white-space should
+ // be replaced with an NBSP for making it visible.
+ else if (textFragmentDataAtEnd.EndsByBlockBoundary() &&
+ isInsertionPointEqualsOrAfterEndOfText) {
+ theString.SetCharAt(HTMLEditUtils::kNBSP, lastCharIndex);
+ }
+ }
+
+ // Next, scan string for adjacent ws and convert to nbsp/space combos
+ // MOOSE: don't need to convert tabs here since that is done by
+ // WillInsertText() before we are called. Eventually, all that logic will
+ // be pushed down into here and made more efficient.
+ enum class PreviousChar {
+ NonCollapsibleChar,
+ CollapsibleChar,
+ PreformattedNewLine,
+ };
+ PreviousChar previousChar = PreviousChar::NonCollapsibleChar;
+ for (uint32_t i = 0; i <= lastCharIndex; i++) {
+ if (IsCollapsibleChar(theString[i])) {
+ // If current char is collapsible and 2nd or latter character of
+ // collapsible characters, we need to make the previous character an
+ // NBSP for avoiding current character to be collapsed to it.
+ if (previousChar == PreviousChar::CollapsibleChar) {
+ MOZ_ASSERT(i > 0);
+ theString.SetCharAt(HTMLEditUtils::kNBSP, i - 1);
+ // Keep previousChar as PreviousChar::CollapsibleChar.
+ continue;
+ }
+
+ // If current character is a collapsbile white-space and the previous
+ // character is a preformatted linefeed, we need to replace the current
+ // character with an NBSP for avoiding collapsed with the previous
+ // linefeed.
+ if (previousChar == PreviousChar::PreformattedNewLine) {
+ MOZ_ASSERT(i > 0);
+ theString.SetCharAt(HTMLEditUtils::kNBSP, i);
+ previousChar = PreviousChar::NonCollapsibleChar;
+ continue;
+ }
+
+ previousChar = PreviousChar::CollapsibleChar;
+ continue;
+ }
+
+ if (theString[i] != HTMLEditUtils::kNewLine) {
+ previousChar = PreviousChar::NonCollapsibleChar;
+ continue;
+ }
+
+ // If current character is a preformatted linefeed and the previous
+ // character is collapbile white-space, the previous character will be
+ // collapsed into current linefeed. Therefore, we need to replace the
+ // previous character with an NBSP.
+ MOZ_ASSERT(!isNewLineCollapsible);
+ if (previousChar == PreviousChar::CollapsibleChar) {
+ MOZ_ASSERT(i > 0);
+ theString.SetCharAt(HTMLEditUtils::kNBSP, i - 1);
+ }
+ previousChar = PreviousChar::PreformattedNewLine;
+ }
+ }
+
+ // XXX If the point is not editable, InsertTextWithTransaction() returns
+ // error, but we keep handling it. But I think that it wastes the
+ // runtime cost. So, perhaps, we should return error code which couldn't
+ // modify it and make each caller of this method decide whether it should
+ // keep or stop handling the edit action.
+ if (MOZ_UNLIKELY(!aHTMLEditor.GetDocument())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::ReplaceText() lost proper document");
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ OwningNonNull<Document> document = *aHTMLEditor.GetDocument();
+ Result<InsertTextResult, nsresult> insertTextResult =
+ aHTMLEditor.InsertTextWithTransaction(document, theString, pointToInsert);
+ if (MOZ_UNLIKELY(insertTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed");
+ return insertTextResult.propagateErr();
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ if (insertTextResult.inspect().HasCaretPointSuggestion()) {
+ return insertTextResult;
+ }
+ return InsertTextResult(insertTextResult.unwrap(),
+ std::move(pointToPutCaret));
+}
+
+// static
+Result<CaretPoint, nsresult>
+WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint,
+ const Element& aEditingHost) {
+ TextFragmentData textFragmentDataAtDeletion(aPoint, &aEditingHost);
+ if (NS_WARN_IF(!textFragmentDataAtDeletion.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ const EditorDOMPointInText atPreviousCharOfStart =
+ textFragmentDataAtDeletion.GetPreviousEditableCharPoint(aPoint);
+ if (!atPreviousCharOfStart.IsSet() ||
+ atPreviousCharOfStart.IsEndOfContainer()) {
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ // If the char is a collapsible white-space or a non-collapsible new line
+ // but it can collapse adjacent white-spaces, we need to extend the range
+ // to delete all invisible white-spaces.
+ if (atPreviousCharOfStart.IsCharCollapsibleASCIISpace() ||
+ atPreviousCharOfStart
+ .IsCharPreformattedNewLineCollapsedWithWhiteSpaces()) {
+ auto startToDelete =
+ textFragmentDataAtDeletion
+ .GetFirstASCIIWhiteSpacePointCollapsedTo<EditorDOMPoint>(
+ atPreviousCharOfStart, nsIEditor::ePrevious);
+ auto endToDelete = textFragmentDataAtDeletion
+ .GetEndOfCollapsibleASCIIWhiteSpaces<EditorDOMPoint>(
+ atPreviousCharOfStart, nsIEditor::ePrevious);
+ EditorDOMPoint pointToPutCaret;
+ {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints(
+ aHTMLEditor, &startToDelete, &endToDelete, aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints() "
+ "failed");
+ return caretPointOrError;
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ startToDelete, endToDelete,
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ return CaretPoint(std::move(pointToPutCaret));
+ }
+
+ if (atPreviousCharOfStart.IsCharCollapsibleNBSP()) {
+ auto startToDelete = atPreviousCharOfStart.To<EditorDOMPoint>();
+ auto endToDelete = startToDelete.NextPoint<EditorDOMPoint>();
+ EditorDOMPoint pointToPutCaret;
+ {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints(
+ aHTMLEditor, &startToDelete, &endToDelete, aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints() "
+ "failed");
+ return caretPointOrError;
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ startToDelete, endToDelete,
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ return CaretPoint(std::move(pointToPutCaret));
+ }
+
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ atPreviousCharOfStart, atPreviousCharOfStart.NextPoint(),
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
+ NS_WARNING_ASSERTION(
+ caretPointOrError.isOk(),
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+}
+
+// static
+Result<CaretPoint, nsresult>
+WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint,
+ const Element& aEditingHost) {
+ TextFragmentData textFragmentDataAtDeletion(aPoint, &aEditingHost);
+ if (NS_WARN_IF(!textFragmentDataAtDeletion.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ auto atNextCharOfStart =
+ textFragmentDataAtDeletion
+ .GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(aPoint);
+ if (!atNextCharOfStart.IsSet() || atNextCharOfStart.IsEndOfContainer()) {
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ // If the char is a collapsible white-space or a non-collapsible new line
+ // but it can collapse adjacent white-spaces, we need to extend the range
+ // to delete all invisible white-spaces.
+ if (atNextCharOfStart.IsCharCollapsibleASCIISpace() ||
+ atNextCharOfStart.IsCharPreformattedNewLineCollapsedWithWhiteSpaces()) {
+ auto startToDelete =
+ textFragmentDataAtDeletion
+ .GetFirstASCIIWhiteSpacePointCollapsedTo<EditorDOMPoint>(
+ atNextCharOfStart, nsIEditor::eNext);
+ auto endToDelete = textFragmentDataAtDeletion
+ .GetEndOfCollapsibleASCIIWhiteSpaces<EditorDOMPoint>(
+ atNextCharOfStart, nsIEditor::eNext);
+ EditorDOMPoint pointToPutCaret;
+ {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints(
+ aHTMLEditor, &startToDelete, &endToDelete, aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints() "
+ "failed");
+ return caretPointOrError;
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ startToDelete, endToDelete,
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ return CaretPoint(std::move(pointToPutCaret));
+ }
+
+ if (atNextCharOfStart.IsCharCollapsibleNBSP()) {
+ auto startToDelete = atNextCharOfStart.To<EditorDOMPoint>();
+ auto endToDelete = startToDelete.NextPoint<EditorDOMPoint>();
+ EditorDOMPoint pointToPutCaret;
+ {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints(
+ aHTMLEditor, &startToDelete, &endToDelete, aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints() "
+ "failed");
+ return caretPointOrError;
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ startToDelete, endToDelete,
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ return CaretPoint(std::move(pointToPutCaret));
+ }
+
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ atNextCharOfStart, atNextCharOfStart.NextPoint(),
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
+ NS_WARNING_ASSERTION(
+ caretPointOrError.isOk(),
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+}
+
+// static
+Result<CaretPoint, nsresult>
+WhiteSpaceVisibilityKeeper::DeleteContentNodeAndJoinTextNodesAroundIt(
+ HTMLEditor& aHTMLEditor, nsIContent& aContentToDelete,
+ const EditorDOMPoint& aCaretPoint, const Element& aEditingHost) {
+ EditorDOMPoint atContent(&aContentToDelete);
+ if (!atContent.IsSet()) {
+ NS_WARNING("Deleting content node was an orphan node");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (!HTMLEditUtils::IsRemovableNode(aContentToDelete)) {
+ NS_WARNING("Deleting content node wasn't removable");
+ return Err(NS_ERROR_FAILURE);
+ }
+ Result<CaretPoint, nsresult> caretPointOrError = WhiteSpaceVisibilityKeeper::
+ MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange(
+ aHTMLEditor, EditorDOMRange(atContent, atContent.NextPoint()),
+ aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange() failed");
+ return caretPointOrError;
+ }
+
+ nsCOMPtr<nsIContent> previousEditableSibling =
+ HTMLEditUtils::GetPreviousSibling(
+ aContentToDelete, {WalkTreeOption::IgnoreNonEditableNode});
+ // Delete the node, and join like nodes if appropriate
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aContentToDelete);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ return Err(rv);
+ }
+ // Are they both text nodes? If so, join them!
+ // XXX This may cause odd behavior if there is non-editable nodes
+ // around the atomic content.
+ if (!aCaretPoint.IsInTextNode() || !previousEditableSibling ||
+ !previousEditableSibling->IsText()) {
+ return caretPointOrError;
+ }
+
+ nsIContent* nextEditableSibling = HTMLEditUtils::GetNextSibling(
+ *previousEditableSibling, {WalkTreeOption::IgnoreNonEditableNode});
+ if (aCaretPoint.GetContainer() != nextEditableSibling) {
+ return caretPointOrError;
+ }
+
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+
+ EditorDOMPoint atFirstChildOfRightNode;
+ rv = aHTMLEditor.JoinNearestEditableNodesWithTransaction(
+ *previousEditableSibling, MOZ_KnownLive(*aCaretPoint.ContainerAs<Text>()),
+ &atFirstChildOfRightNode);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::JoinNearestEditableNodesWithTransaction() failed");
+ return Err(rv);
+ }
+ if (!atFirstChildOfRightNode.IsSet()) {
+ NS_WARNING(
+ "HTMLEditor::JoinNearestEditableNodesWithTransaction() didn't return "
+ "right node position");
+ return Err(NS_ERROR_FAILURE);
+ }
+ return CaretPoint(std::move(atFirstChildOfRightNode));
+}
+
+template <typename PT, typename CT>
+WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
+ const EditorDOMPointBase<PT, CT>& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSet());
+
+ if (!TextFragmentDataAtStartRef().IsInitialized()) {
+ return WSScanResult(nullptr, WSType::UnexpectedError);
+ }
+
+ // If the range has visible text and start of the visible text is before
+ // aPoint, return previous character in the text.
+ const VisibleWhiteSpacesData& visibleWhiteSpaces =
+ TextFragmentDataAtStartRef().VisibleWhiteSpacesDataRef();
+ if (visibleWhiteSpaces.IsInitialized() &&
+ visibleWhiteSpaces.StartRef().IsBefore(aPoint)) {
+ // If the visible things are not editable, we shouldn't scan "editable"
+ // things now. Whether keep scanning editable things or not should be
+ // considered by the caller.
+ if (aPoint.GetChild() && !aPoint.GetChild()->IsEditable()) {
+ return WSScanResult(aPoint.GetChild(), WSType::SpecialContent);
+ }
+ const auto atPreviousChar =
+ GetPreviousEditableCharPoint<EditorRawDOMPointInText>(aPoint);
+ // When it's a non-empty text node, return it.
+ if (atPreviousChar.IsSet() && !atPreviousChar.IsContainerEmpty()) {
+ MOZ_ASSERT(!atPreviousChar.IsEndOfContainer());
+ return WSScanResult(atPreviousChar.template NextPoint<EditorDOMPoint>(),
+ atPreviousChar.IsCharCollapsibleASCIISpaceOrNBSP()
+ ? WSType::CollapsibleWhiteSpaces
+ : WSType::NonCollapsibleCharacters);
+ }
+ }
+
+ // Otherwise, return the start of the range.
+ if (TextFragmentDataAtStartRef().GetStartReasonContent() !=
+ TextFragmentDataAtStartRef().StartRef().GetContainer()) {
+ // In this case, TextFragmentDataAtStartRef().StartRef().Offset() is not
+ // meaningful.
+ return WSScanResult(TextFragmentDataAtStartRef().GetStartReasonContent(),
+ TextFragmentDataAtStartRef().StartRawReason());
+ }
+ return WSScanResult(TextFragmentDataAtStartRef().StartRef(),
+ TextFragmentDataAtStartRef().StartRawReason());
+}
+
+template <typename PT, typename CT>
+WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom(
+ const EditorDOMPointBase<PT, CT>& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSet());
+
+ if (!TextFragmentDataAtStartRef().IsInitialized()) {
+ return WSScanResult(nullptr, WSType::UnexpectedError);
+ }
+
+ // If the range has visible text and aPoint equals or is before the end of the
+ // visible text, return inclusive next character in the text.
+ const VisibleWhiteSpacesData& visibleWhiteSpaces =
+ TextFragmentDataAtStartRef().VisibleWhiteSpacesDataRef();
+ if (visibleWhiteSpaces.IsInitialized() &&
+ aPoint.EqualsOrIsBefore(visibleWhiteSpaces.EndRef())) {
+ // If the visible things are not editable, we shouldn't scan "editable"
+ // things now. Whether keep scanning editable things or not should be
+ // considered by the caller.
+ if (aPoint.GetChild() && !aPoint.GetChild()->IsEditable()) {
+ return WSScanResult(aPoint.GetChild(), WSType::SpecialContent);
+ }
+ const auto atNextChar =
+ GetInclusiveNextEditableCharPoint<EditorDOMPoint>(aPoint);
+ // When it's a non-empty text node, return it.
+ if (atNextChar.IsSet() && !atNextChar.IsContainerEmpty()) {
+ return WSScanResult(atNextChar,
+ !atNextChar.IsEndOfContainer() &&
+ atNextChar.IsCharCollapsibleASCIISpaceOrNBSP()
+ ? WSType::CollapsibleWhiteSpaces
+ : WSType::NonCollapsibleCharacters);
+ }
+ }
+
+ // Otherwise, return the end of the range.
+ if (TextFragmentDataAtStartRef().GetEndReasonContent() !=
+ TextFragmentDataAtStartRef().EndRef().GetContainer()) {
+ // In this case, TextFragmentDataAtStartRef().EndRef().Offset() is not
+ // meaningful.
+ return WSScanResult(TextFragmentDataAtStartRef().GetEndReasonContent(),
+ TextFragmentDataAtStartRef().EndRawReason());
+ }
+ return WSScanResult(TextFragmentDataAtStartRef().EndRef(),
+ TextFragmentDataAtStartRef().EndRawReason());
+}
+
+template <typename EditorDOMPointType>
+WSRunScanner::TextFragmentData::TextFragmentData(
+ const EditorDOMPointType& aPoint, const Element* aEditingHost)
+ : mEditingHost(aEditingHost) {
+ if (!aPoint.IsSetAndValid()) {
+ NS_WARNING("aPoint was invalid");
+ return;
+ }
+ if (!aPoint.IsInContentNode()) {
+ NS_WARNING("aPoint was in Document or DocumentFragment");
+ // I.e., we're try to modify outside of root element. We don't need to
+ // support such odd case because web apps cannot append text nodes as
+ // direct child of Document node.
+ return;
+ }
+
+ mScanStartPoint = aPoint.template To<EditorDOMPoint>();
+ NS_ASSERTION(
+ EditorUtils::IsEditableContent(*mScanStartPoint.ContainerAs<nsIContent>(),
+ EditorType::HTML),
+ "Given content is not editable");
+ NS_ASSERTION(
+ mScanStartPoint.ContainerAs<nsIContent>()->GetAsElementOrParentElement(),
+ "Given content is not an element and an orphan node");
+ if (NS_WARN_IF(!EditorUtils::IsEditableContent(
+ *mScanStartPoint.ContainerAs<nsIContent>(), EditorType::HTML))) {
+ return;
+ }
+ const Element* editableBlockElementOrInlineEditingHost =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *mScanStartPoint.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost);
+ if (!editableBlockElementOrInlineEditingHost) {
+ NS_WARNING(
+ "HTMLEditUtils::GetInclusiveAncestorElement(HTMLEditUtils::"
+ "ClosestEditableBlockElementOrInlineEditingHost) couldn't find "
+ "editing host");
+ return;
+ }
+
+ mStart = BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
+ mScanStartPoint, *editableBlockElementOrInlineEditingHost, mEditingHost,
+ &mNBSPData);
+ MOZ_ASSERT_IF(mStart.IsNonCollapsibleCharacters(),
+ !mStart.PointRef().IsPreviousCharPreformattedNewLine());
+ MOZ_ASSERT_IF(mStart.IsPreformattedLineBreak(),
+ mStart.PointRef().IsPreviousCharPreformattedNewLine());
+ mEnd = BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
+ mScanStartPoint, *editableBlockElementOrInlineEditingHost, mEditingHost,
+ &mNBSPData);
+ MOZ_ASSERT_IF(mEnd.IsNonCollapsibleCharacters(),
+ !mEnd.PointRef().IsCharPreformattedNewLine());
+ MOZ_ASSERT_IF(mEnd.IsPreformattedLineBreak(),
+ mEnd.PointRef().IsCharPreformattedNewLine());
+}
+
+// static
+template <typename EditorDOMPointType>
+Maybe<WSRunScanner::TextFragmentData::BoundaryData> WSRunScanner::
+ TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceStartInTextNode(
+ const EditorDOMPointType& aPoint, NoBreakingSpaceData* aNBSPData) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ MOZ_DIAGNOSTIC_ASSERT(aPoint.IsInTextNode());
+
+ const bool isWhiteSpaceCollapsible = !EditorUtils::IsWhiteSpacePreformatted(
+ *aPoint.template ContainerAs<Text>());
+ const bool isNewLineCollapsible =
+ !EditorUtils::IsNewLinePreformatted(*aPoint.template ContainerAs<Text>());
+ const nsTextFragment& textFragment =
+ aPoint.template ContainerAs<Text>()->TextFragment();
+ for (uint32_t i = std::min(aPoint.Offset(), textFragment.GetLength()); i;
+ i--) {
+ WSType wsTypeOfNonCollapsibleChar;
+ switch (textFragment.CharAt(i - 1)) {
+ case HTMLEditUtils::kSpace:
+ case HTMLEditUtils::kCarriageReturn:
+ case HTMLEditUtils::kTab:
+ if (isWhiteSpaceCollapsible) {
+ continue; // collapsible white-space or invisible white-space.
+ }
+ // preformatted white-space.
+ wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
+ break;
+ case HTMLEditUtils::kNewLine:
+ if (isNewLineCollapsible) {
+ continue; // collapsible linefeed.
+ }
+ // preformatted linefeed.
+ wsTypeOfNonCollapsibleChar = WSType::PreformattedLineBreak;
+ break;
+ case HTMLEditUtils::kNBSP:
+ if (isWhiteSpaceCollapsible) {
+ if (aNBSPData) {
+ aNBSPData->NotifyNBSP(
+ EditorDOMPointInText(aPoint.template ContainerAs<Text>(),
+ i - 1),
+ NoBreakingSpaceData::Scanning::Backward);
+ }
+ continue;
+ }
+ // NBSP is never converted from collapsible white-space/linefeed.
+ wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
+ break;
+ default:
+ MOZ_ASSERT(!nsCRT::IsAsciiSpace(textFragment.CharAt(i - 1)));
+ wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
+ break;
+ }
+
+ return Some(BoundaryData(
+ EditorDOMPoint(aPoint.template ContainerAs<Text>(), i),
+ *aPoint.template ContainerAs<Text>(), wsTypeOfNonCollapsibleChar));
+ }
+
+ return Nothing();
+}
+
+// static
+template <typename EditorDOMPointType>
+WSRunScanner::TextFragmentData::BoundaryData WSRunScanner::TextFragmentData::
+ BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
+ const EditorDOMPointType& aPoint,
+ const Element& aEditableBlockParentOrTopmostEditableInlineContent,
+ const Element* aEditingHost, NoBreakingSpaceData* aNBSPData) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ if (aPoint.IsInTextNode() && !aPoint.IsStartOfContainer()) {
+ Maybe<BoundaryData> startInTextNode =
+ BoundaryData::ScanCollapsibleWhiteSpaceStartInTextNode(aPoint,
+ aNBSPData);
+ if (startInTextNode.isSome()) {
+ return startInTextNode.ref();
+ }
+ // The text node does not have visible character, let's keep scanning
+ // preceding nodes.
+ return BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
+ EditorDOMPoint(aPoint.template ContainerAs<Text>(), 0),
+ aEditableBlockParentOrTopmostEditableInlineContent, aEditingHost,
+ aNBSPData);
+ }
+
+ // Then, we need to check previous leaf node.
+ nsIContent* previousLeafContentOrBlock =
+ HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ aPoint, aEditableBlockParentOrTopmostEditableInlineContent,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, aEditingHost);
+ if (!previousLeafContentOrBlock) {
+ // no prior node means we exhausted
+ // aEditableBlockParentOrTopmostEditableInlineContent
+ // mReasonContent can be either a block element or any non-editable
+ // content in this case.
+ return BoundaryData(aPoint,
+ const_cast<Element&>(
+ aEditableBlockParentOrTopmostEditableInlineContent),
+ WSType::CurrentBlockBoundary);
+ }
+
+ if (HTMLEditUtils::IsBlockElement(*previousLeafContentOrBlock)) {
+ return BoundaryData(aPoint, *previousLeafContentOrBlock,
+ WSType::OtherBlockBoundary);
+ }
+
+ if (!previousLeafContentOrBlock->IsText() ||
+ !previousLeafContentOrBlock->IsEditable()) {
+ // it's a break or a special node, like <img>, that is not a block and
+ // not a break but still serves as a terminator to ws runs.
+ return BoundaryData(aPoint, *previousLeafContentOrBlock,
+ previousLeafContentOrBlock->IsHTMLElement(nsGkAtoms::br)
+ ? WSType::BRElement
+ : WSType::SpecialContent);
+ }
+
+ if (!previousLeafContentOrBlock->AsText()->TextLength()) {
+ // If it's an empty text node, keep looking for its previous leaf content.
+ // Note that even if the empty text node is preformatted, we should keep
+ // looking for the previous one.
+ return BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
+ EditorDOMPointInText(previousLeafContentOrBlock->AsText(), 0),
+ aEditableBlockParentOrTopmostEditableInlineContent, aEditingHost,
+ aNBSPData);
+ }
+
+ Maybe<BoundaryData> startInTextNode =
+ BoundaryData::ScanCollapsibleWhiteSpaceStartInTextNode(
+ EditorDOMPointInText::AtEndOf(*previousLeafContentOrBlock->AsText()),
+ aNBSPData);
+ if (startInTextNode.isSome()) {
+ return startInTextNode.ref();
+ }
+
+ // The text node does not have visible character, let's keep scanning
+ // preceding nodes.
+ return BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
+ EditorDOMPointInText(previousLeafContentOrBlock->AsText(), 0),
+ aEditableBlockParentOrTopmostEditableInlineContent, aEditingHost,
+ aNBSPData);
+}
+
+// static
+template <typename EditorDOMPointType>
+Maybe<WSRunScanner::TextFragmentData::BoundaryData> WSRunScanner::
+ TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceEndInTextNode(
+ const EditorDOMPointType& aPoint, NoBreakingSpaceData* aNBSPData) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ MOZ_DIAGNOSTIC_ASSERT(aPoint.IsInTextNode());
+
+ const bool isWhiteSpaceCollapsible = !EditorUtils::IsWhiteSpacePreformatted(
+ *aPoint.template ContainerAs<Text>());
+ const bool isNewLineCollapsible =
+ !EditorUtils::IsNewLinePreformatted(*aPoint.template ContainerAs<Text>());
+ const nsTextFragment& textFragment =
+ aPoint.template ContainerAs<Text>()->TextFragment();
+ for (uint32_t i = aPoint.Offset(); i < textFragment.GetLength(); i++) {
+ WSType wsTypeOfNonCollapsibleChar;
+ switch (textFragment.CharAt(i)) {
+ case HTMLEditUtils::kSpace:
+ case HTMLEditUtils::kCarriageReturn:
+ case HTMLEditUtils::kTab:
+ if (isWhiteSpaceCollapsible) {
+ continue; // collapsible white-space or invisible white-space.
+ }
+ // preformatted white-space.
+ wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
+ break;
+ case HTMLEditUtils::kNewLine:
+ if (isNewLineCollapsible) {
+ continue; // collapsible linefeed.
+ }
+ // preformatted linefeed.
+ wsTypeOfNonCollapsibleChar = WSType::PreformattedLineBreak;
+ break;
+ case HTMLEditUtils::kNBSP:
+ if (isWhiteSpaceCollapsible) {
+ if (aNBSPData) {
+ aNBSPData->NotifyNBSP(
+ EditorDOMPointInText(aPoint.template ContainerAs<Text>(), i),
+ NoBreakingSpaceData::Scanning::Forward);
+ }
+ continue;
+ }
+ // NBSP is never converted from collapsible white-space/linefeed.
+ wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
+ break;
+ default:
+ MOZ_ASSERT(!nsCRT::IsAsciiSpace(textFragment.CharAt(i)));
+ wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
+ break;
+ }
+
+ return Some(BoundaryData(
+ EditorDOMPoint(aPoint.template ContainerAs<Text>(), i),
+ *aPoint.template ContainerAs<Text>(), wsTypeOfNonCollapsibleChar));
+ }
+
+ return Nothing();
+}
+
+// static
+template <typename EditorDOMPointType>
+WSRunScanner::TextFragmentData::BoundaryData
+WSRunScanner::TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
+ const EditorDOMPointType& aPoint,
+ const Element& aEditableBlockParentOrTopmostEditableInlineElement,
+ const Element* aEditingHost, NoBreakingSpaceData* aNBSPData) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ if (aPoint.IsInTextNode() && !aPoint.IsEndOfContainer()) {
+ Maybe<BoundaryData> endInTextNode =
+ BoundaryData::ScanCollapsibleWhiteSpaceEndInTextNode(aPoint, aNBSPData);
+ if (endInTextNode.isSome()) {
+ return endInTextNode.ref();
+ }
+ // The text node does not have visible character, let's keep scanning
+ // following nodes.
+ return BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
+ EditorDOMPointInText::AtEndOf(*aPoint.template ContainerAs<Text>()),
+ aEditableBlockParentOrTopmostEditableInlineElement, aEditingHost,
+ aNBSPData);
+ }
+
+ // Then, we need to check next leaf node.
+ nsIContent* nextLeafContentOrBlock =
+ HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ aPoint, aEditableBlockParentOrTopmostEditableInlineElement,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, aEditingHost);
+ if (!nextLeafContentOrBlock) {
+ // no next node means we exhausted
+ // aEditableBlockParentOrTopmostEditableInlineElement
+ // mReasonContent can be either a block element or any non-editable
+ // content in this case.
+ return BoundaryData(aPoint.template To<EditorDOMPoint>(),
+ const_cast<Element&>(
+ aEditableBlockParentOrTopmostEditableInlineElement),
+ WSType::CurrentBlockBoundary);
+ }
+
+ if (HTMLEditUtils::IsBlockElement(*nextLeafContentOrBlock)) {
+ // we encountered a new block. therefore no more ws.
+ return BoundaryData(aPoint, *nextLeafContentOrBlock,
+ WSType::OtherBlockBoundary);
+ }
+
+ if (!nextLeafContentOrBlock->IsText() ||
+ !nextLeafContentOrBlock->IsEditable()) {
+ // we encountered a break or a special node, like <img>,
+ // that is not a block and not a break but still
+ // serves as a terminator to ws runs.
+ return BoundaryData(aPoint, *nextLeafContentOrBlock,
+ nextLeafContentOrBlock->IsHTMLElement(nsGkAtoms::br)
+ ? WSType::BRElement
+ : WSType::SpecialContent);
+ }
+
+ if (!nextLeafContentOrBlock->AsText()->TextFragment().GetLength()) {
+ // If it's an empty text node, keep looking for its next leaf content.
+ // Note that even if the empty text node is preformatted, we should keep
+ // looking for the next one.
+ return BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
+ EditorDOMPointInText(nextLeafContentOrBlock->AsText(), 0),
+ aEditableBlockParentOrTopmostEditableInlineElement, aEditingHost,
+ aNBSPData);
+ }
+
+ Maybe<BoundaryData> endInTextNode =
+ BoundaryData::ScanCollapsibleWhiteSpaceEndInTextNode(
+ EditorDOMPointInText(nextLeafContentOrBlock->AsText(), 0), aNBSPData);
+ if (endInTextNode.isSome()) {
+ return endInTextNode.ref();
+ }
+
+ // The text node does not have visible character, let's keep scanning
+ // following nodes.
+ return BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
+ EditorDOMPointInText::AtEndOf(*nextLeafContentOrBlock->AsText()),
+ aEditableBlockParentOrTopmostEditableInlineElement, aEditingHost,
+ aNBSPData);
+}
+
+const EditorDOMRange&
+WSRunScanner::TextFragmentData::InvisibleLeadingWhiteSpaceRangeRef() const {
+ if (mLeadingWhiteSpaceRange.isSome()) {
+ return mLeadingWhiteSpaceRange.ref();
+ }
+
+ // If it's start of line, there is no invisible leading white-spaces.
+ if (!StartsFromHardLineBreak()) {
+ mLeadingWhiteSpaceRange.emplace();
+ return mLeadingWhiteSpaceRange.ref();
+ }
+
+ // If there is no NBSP, all of the given range is leading white-spaces.
+ // Note that this result may be collapsed if there is no leading white-spaces.
+ if (!mNBSPData.FoundNBSP()) {
+ MOZ_ASSERT(mStart.PointRef().IsSet() || mEnd.PointRef().IsSet());
+ mLeadingWhiteSpaceRange.emplace(mStart.PointRef(), mEnd.PointRef());
+ return mLeadingWhiteSpaceRange.ref();
+ }
+
+ MOZ_ASSERT(mNBSPData.LastPointRef().IsSetAndValid());
+
+ // Even if the first NBSP is the start, i.e., there is no invisible leading
+ // white-space, return collapsed range.
+ mLeadingWhiteSpaceRange.emplace(mStart.PointRef(), mNBSPData.FirstPointRef());
+ return mLeadingWhiteSpaceRange.ref();
+}
+
+const EditorDOMRange&
+WSRunScanner::TextFragmentData::InvisibleTrailingWhiteSpaceRangeRef() const {
+ if (mTrailingWhiteSpaceRange.isSome()) {
+ return mTrailingWhiteSpaceRange.ref();
+ }
+
+ // If it's not immediately before a block boundary nor an invisible
+ // preformatted linefeed, there is no invisible trailing white-spaces. Note
+ // that collapsible white-spaces before a `<br>` element is visible.
+ if (!EndsByBlockBoundary() && !EndsByInvisiblePreformattedLineBreak()) {
+ mTrailingWhiteSpaceRange.emplace();
+ return mTrailingWhiteSpaceRange.ref();
+ }
+
+ // If there is no NBSP, all of the given range is trailing white-spaces.
+ // Note that this result may be collapsed if there is no trailing white-
+ // spaces.
+ if (!mNBSPData.FoundNBSP()) {
+ MOZ_ASSERT(mStart.PointRef().IsSet() || mEnd.PointRef().IsSet());
+ mTrailingWhiteSpaceRange.emplace(mStart.PointRef(), mEnd.PointRef());
+ return mTrailingWhiteSpaceRange.ref();
+ }
+
+ MOZ_ASSERT(mNBSPData.LastPointRef().IsSetAndValid());
+
+ // If last NBSP is immediately before the end, there is no trailing white-
+ // spaces.
+ if (mEnd.PointRef().IsSet() &&
+ mNBSPData.LastPointRef().GetContainer() ==
+ mEnd.PointRef().GetContainer() &&
+ mNBSPData.LastPointRef().Offset() == mEnd.PointRef().Offset() - 1) {
+ mTrailingWhiteSpaceRange.emplace();
+ return mTrailingWhiteSpaceRange.ref();
+ }
+
+ // Otherwise, the may be some trailing white-spaces.
+ MOZ_ASSERT(!mNBSPData.LastPointRef().IsEndOfContainer());
+ mTrailingWhiteSpaceRange.emplace(mNBSPData.LastPointRef().NextPoint(),
+ mEnd.PointRef());
+ return mTrailingWhiteSpaceRange.ref();
+}
+
+EditorDOMRangeInTexts
+WSRunScanner::TextFragmentData::GetNonCollapsedRangeInTexts(
+ const EditorDOMRange& aRange) const {
+ if (!aRange.IsPositioned()) {
+ return EditorDOMRangeInTexts();
+ }
+ if (aRange.Collapsed()) {
+ // If collapsed, we can do nothing.
+ return EditorDOMRangeInTexts();
+ }
+ if (aRange.IsInTextNodes()) {
+ // Note that this may return a range which don't include any invisible
+ // white-spaces due to empty text nodes.
+ return aRange.GetAsInTexts();
+ }
+
+ const auto firstPoint =
+ aRange.StartRef().IsInTextNode()
+ ? aRange.StartRef().AsInText()
+ : GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(
+ aRange.StartRef());
+ if (!firstPoint.IsSet()) {
+ return EditorDOMRangeInTexts();
+ }
+ EditorDOMPointInText endPoint;
+ if (aRange.EndRef().IsInTextNode()) {
+ endPoint = aRange.EndRef().AsInText();
+ } else {
+ // FYI: GetPreviousEditableCharPoint() returns last character's point
+ // of preceding text node if it's not empty, but we need end of
+ // the text node here.
+ endPoint = GetPreviousEditableCharPoint(aRange.EndRef());
+ if (endPoint.IsSet() && endPoint.IsAtLastContent()) {
+ MOZ_ALWAYS_TRUE(endPoint.AdvanceOffset());
+ }
+ }
+ if (!endPoint.IsSet() || firstPoint == endPoint) {
+ return EditorDOMRangeInTexts();
+ }
+ return EditorDOMRangeInTexts(firstPoint, endPoint);
+}
+
+const WSRunScanner::VisibleWhiteSpacesData&
+WSRunScanner::TextFragmentData::VisibleWhiteSpacesDataRef() const {
+ if (mVisibleWhiteSpacesData.isSome()) {
+ return mVisibleWhiteSpacesData.ref();
+ }
+
+ {
+ // If all things are obviously visible, we can return range for all of the
+ // things quickly.
+ const bool mayHaveInvisibleLeadingSpace =
+ !StartsFromNonCollapsibleCharacters() && !StartsFromSpecialContent();
+ const bool mayHaveInvisibleTrailingWhiteSpace =
+ !EndsByNonCollapsibleCharacters() && !EndsBySpecialContent() &&
+ !EndsByBRElement() && !EndsByInvisiblePreformattedLineBreak();
+
+ if (!mayHaveInvisibleLeadingSpace && !mayHaveInvisibleTrailingWhiteSpace) {
+ VisibleWhiteSpacesData visibleWhiteSpaces;
+ if (mStart.PointRef().IsSet()) {
+ visibleWhiteSpaces.SetStartPoint(mStart.PointRef());
+ }
+ visibleWhiteSpaces.SetStartFrom(mStart.RawReason());
+ if (mEnd.PointRef().IsSet()) {
+ visibleWhiteSpaces.SetEndPoint(mEnd.PointRef());
+ }
+ visibleWhiteSpaces.SetEndBy(mEnd.RawReason());
+ mVisibleWhiteSpacesData.emplace(visibleWhiteSpaces);
+ return mVisibleWhiteSpacesData.ref();
+ }
+ }
+
+ // If all of the range is invisible leading or trailing white-spaces,
+ // there is no visible content.
+ const EditorDOMRange& leadingWhiteSpaceRange =
+ InvisibleLeadingWhiteSpaceRangeRef();
+ const bool maybeHaveLeadingWhiteSpaces =
+ leadingWhiteSpaceRange.StartRef().IsSet() ||
+ leadingWhiteSpaceRange.EndRef().IsSet();
+ if (maybeHaveLeadingWhiteSpaces &&
+ leadingWhiteSpaceRange.StartRef() == mStart.PointRef() &&
+ leadingWhiteSpaceRange.EndRef() == mEnd.PointRef()) {
+ mVisibleWhiteSpacesData.emplace(VisibleWhiteSpacesData());
+ return mVisibleWhiteSpacesData.ref();
+ }
+ const EditorDOMRange& trailingWhiteSpaceRange =
+ InvisibleTrailingWhiteSpaceRangeRef();
+ const bool maybeHaveTrailingWhiteSpaces =
+ trailingWhiteSpaceRange.StartRef().IsSet() ||
+ trailingWhiteSpaceRange.EndRef().IsSet();
+ if (maybeHaveTrailingWhiteSpaces &&
+ trailingWhiteSpaceRange.StartRef() == mStart.PointRef() &&
+ trailingWhiteSpaceRange.EndRef() == mEnd.PointRef()) {
+ mVisibleWhiteSpacesData.emplace(VisibleWhiteSpacesData());
+ return mVisibleWhiteSpacesData.ref();
+ }
+
+ if (!StartsFromHardLineBreak()) {
+ VisibleWhiteSpacesData visibleWhiteSpaces;
+ if (mStart.PointRef().IsSet()) {
+ visibleWhiteSpaces.SetStartPoint(mStart.PointRef());
+ }
+ visibleWhiteSpaces.SetStartFrom(mStart.RawReason());
+ if (!maybeHaveTrailingWhiteSpaces) {
+ visibleWhiteSpaces.SetEndPoint(mEnd.PointRef());
+ visibleWhiteSpaces.SetEndBy(mEnd.RawReason());
+ mVisibleWhiteSpacesData = Some(visibleWhiteSpaces);
+ return mVisibleWhiteSpacesData.ref();
+ }
+ if (trailingWhiteSpaceRange.StartRef().IsSet()) {
+ visibleWhiteSpaces.SetEndPoint(trailingWhiteSpaceRange.StartRef());
+ }
+ visibleWhiteSpaces.SetEndByTrailingWhiteSpaces();
+ mVisibleWhiteSpacesData.emplace(visibleWhiteSpaces);
+ return mVisibleWhiteSpacesData.ref();
+ }
+
+ MOZ_ASSERT(StartsFromHardLineBreak());
+ MOZ_ASSERT(maybeHaveLeadingWhiteSpaces);
+
+ VisibleWhiteSpacesData visibleWhiteSpaces;
+ if (leadingWhiteSpaceRange.EndRef().IsSet()) {
+ visibleWhiteSpaces.SetStartPoint(leadingWhiteSpaceRange.EndRef());
+ }
+ visibleWhiteSpaces.SetStartFromLeadingWhiteSpaces();
+ if (!EndsByBlockBoundary()) {
+ // then no trailing ws. this normal run ends the overall ws run.
+ if (mEnd.PointRef().IsSet()) {
+ visibleWhiteSpaces.SetEndPoint(mEnd.PointRef());
+ }
+ visibleWhiteSpaces.SetEndBy(mEnd.RawReason());
+ mVisibleWhiteSpacesData.emplace(visibleWhiteSpaces);
+ return mVisibleWhiteSpacesData.ref();
+ }
+
+ MOZ_ASSERT(EndsByBlockBoundary());
+
+ if (!maybeHaveTrailingWhiteSpaces) {
+ // normal ws runs right up to adjacent block (nbsp next to block)
+ visibleWhiteSpaces.SetEndPoint(mEnd.PointRef());
+ visibleWhiteSpaces.SetEndBy(mEnd.RawReason());
+ mVisibleWhiteSpacesData.emplace(visibleWhiteSpaces);
+ return mVisibleWhiteSpacesData.ref();
+ }
+
+ if (trailingWhiteSpaceRange.StartRef().IsSet()) {
+ visibleWhiteSpaces.SetEndPoint(trailingWhiteSpaceRange.StartRef());
+ }
+ visibleWhiteSpaces.SetEndByTrailingWhiteSpaces();
+ mVisibleWhiteSpacesData.emplace(visibleWhiteSpaces);
+ return mVisibleWhiteSpacesData.ref();
+}
+
+// static
+Result<CaretPoint, nsresult> WhiteSpaceVisibilityKeeper::
+ MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange(
+ HTMLEditor& aHTMLEditor, const EditorDOMRange& aRangeToDelete,
+ const Element& aEditingHost) {
+ if (NS_WARN_IF(!aRangeToDelete.IsPositionedAndValid()) ||
+ NS_WARN_IF(!aRangeToDelete.IsInContentNodes())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ EditorDOMRange rangeToDelete(aRangeToDelete);
+ bool mayBecomeUnexpectedDOMTree = aHTMLEditor.MayHaveMutationEventListeners(
+ NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED |
+ NS_EVENT_BITS_MUTATION_NODEREMOVED |
+ NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT |
+ NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED);
+
+ TextFragmentData textFragmentDataAtStart(rangeToDelete.StartRef(),
+ &aEditingHost);
+ if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ TextFragmentData textFragmentDataAtEnd(rangeToDelete.EndRef(), &aEditingHost);
+ if (NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ ReplaceRangeData replaceRangeDataAtEnd =
+ textFragmentDataAtEnd.GetReplaceRangeDataAtEndOfDeletionRange(
+ textFragmentDataAtStart);
+ EditorDOMPoint pointToPutCaret;
+ if (replaceRangeDataAtEnd.IsSet() && !replaceRangeDataAtEnd.Collapsed()) {
+ MOZ_ASSERT(rangeToDelete.EndRef().EqualsOrIsBefore(
+ replaceRangeDataAtEnd.EndRef()));
+ // If there is some text after deleting range, replacing range start must
+ // equal or be before end of the deleting range.
+ MOZ_ASSERT_IF(rangeToDelete.EndRef().IsInTextNode() &&
+ !rangeToDelete.EndRef().IsEndOfContainer(),
+ replaceRangeDataAtEnd.StartRef().EqualsOrIsBefore(
+ rangeToDelete.EndRef()));
+ // If the deleting range end is end of a text node, the replacing range
+ // starts with another node if the following text node starts with white-
+ // spaces.
+ MOZ_ASSERT_IF(rangeToDelete.EndRef().IsInTextNode() &&
+ rangeToDelete.EndRef().IsEndOfContainer(),
+ rangeToDelete.EndRef() == replaceRangeDataAtEnd.StartRef() ||
+ replaceRangeDataAtEnd.StartRef().IsStartOfContainer());
+ MOZ_ASSERT(rangeToDelete.StartRef().EqualsOrIsBefore(
+ replaceRangeDataAtEnd.StartRef()));
+ if (!replaceRangeDataAtEnd.HasReplaceString()) {
+ EditorDOMPoint startToDelete(aRangeToDelete.StartRef());
+ EditorDOMPoint endToDelete(replaceRangeDataAtEnd.StartRef());
+ {
+ AutoEditorDOMPointChildInvalidator lockOffsetOfStart(startToDelete);
+ AutoEditorDOMPointChildInvalidator lockOffsetOfEnd(endToDelete);
+ AutoTrackDOMPoint trackStartToDelete(aHTMLEditor.RangeUpdaterRef(),
+ &startToDelete);
+ AutoTrackDOMPoint trackEndToDelete(aHTMLEditor.RangeUpdaterRef(),
+ &endToDelete);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ replaceRangeDataAtEnd.StartRef(),
+ replaceRangeDataAtEnd.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ if (mayBecomeUnexpectedDOMTree &&
+ (NS_WARN_IF(!startToDelete.IsSetAndValid()) ||
+ NS_WARN_IF(!endToDelete.IsSetAndValid()) ||
+ NS_WARN_IF(!startToDelete.EqualsOrIsBefore(endToDelete)))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ MOZ_ASSERT(startToDelete.EqualsOrIsBefore(endToDelete));
+ rangeToDelete.SetStartAndEnd(startToDelete, endToDelete);
+ } else {
+ MOZ_ASSERT(replaceRangeDataAtEnd.RangeRef().IsInTextNodes());
+ EditorDOMPoint startToDelete(aRangeToDelete.StartRef());
+ EditorDOMPoint endToDelete(replaceRangeDataAtEnd.StartRef());
+ {
+ AutoTrackDOMPoint trackStartToDelete(aHTMLEditor.RangeUpdaterRef(),
+ &startToDelete);
+ AutoTrackDOMPoint trackEndToDelete(aHTMLEditor.RangeUpdaterRef(),
+ &endToDelete);
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ // FYI: ReplaceTextAndRemoveEmptyTextNodes() does not have any idea of
+ // new caret position.
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes(
+ aHTMLEditor, replaceRangeDataAtEnd.RangeRef().AsInTexts(),
+ replaceRangeDataAtEnd.ReplaceStringRef());
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "MakeSureToKeepVisibleStateOfWhiteSpacesAtEndOfDeletingRange() "
+ "failed");
+ return Err(rv);
+ }
+ }
+ if (mayBecomeUnexpectedDOMTree &&
+ (NS_WARN_IF(!startToDelete.IsSetAndValid()) ||
+ NS_WARN_IF(!endToDelete.IsSetAndValid()) ||
+ NS_WARN_IF(!startToDelete.EqualsOrIsBefore(endToDelete)))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ MOZ_ASSERT(startToDelete.EqualsOrIsBefore(endToDelete));
+ rangeToDelete.SetStartAndEnd(startToDelete, endToDelete);
+ }
+
+ if (mayBecomeUnexpectedDOMTree) {
+ // If focus is changed by mutation event listeners, we should stop
+ // handling this edit action.
+ if (&aEditingHost != aHTMLEditor.ComputeEditingHost()) {
+ NS_WARNING("Active editing host was changed");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ if (!rangeToDelete.IsInContentNodes()) {
+ NS_WARNING("The modified range was not in content");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ // If the DOM tree might be changed by mutation event listeners, we
+ // should retrieve the latest data for avoiding to delete/replace
+ // unexpected range.
+ textFragmentDataAtStart =
+ TextFragmentData(rangeToDelete.StartRef(), &aEditingHost);
+ textFragmentDataAtEnd =
+ TextFragmentData(rangeToDelete.EndRef(), &aEditingHost);
+ }
+ }
+ ReplaceRangeData replaceRangeDataAtStart =
+ textFragmentDataAtStart.GetReplaceRangeDataAtStartOfDeletionRange(
+ textFragmentDataAtEnd);
+ if (!replaceRangeDataAtStart.IsSet() || replaceRangeDataAtStart.Collapsed()) {
+ return CaretPoint(std::move(pointToPutCaret));
+ }
+ if (!replaceRangeDataAtStart.HasReplaceString()) {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ replaceRangeDataAtStart.StartRef(),
+ replaceRangeDataAtStart.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
+ // XXX Should we validate the range for making this return
+ // NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE in this case?
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ return CaretPoint(std::move(pointToPutCaret));
+ }
+
+ MOZ_ASSERT(replaceRangeDataAtStart.RangeRef().IsInTextNodes());
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ // FYI: ReplaceTextAndRemoveEmptyTextNodes() does not have any idea of
+ // new caret position.
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes(
+ aHTMLEditor, replaceRangeDataAtStart.RangeRef().AsInTexts(),
+ replaceRangeDataAtStart.ReplaceStringRef());
+ // XXX Should we validate the range for making this return
+ // NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE in this case?
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "MakeSureToKeepVisibleStateOfWhiteSpacesAtStartOfDeletingRange() "
+ "failed");
+ return Err(rv);
+ }
+ }
+ return CaretPoint(std::move(pointToPutCaret));
+}
+
+ReplaceRangeData
+WSRunScanner::TextFragmentData::GetReplaceRangeDataAtEndOfDeletionRange(
+ const TextFragmentData& aTextFragmentDataAtStartToDelete) const {
+ const EditorDOMPoint& startToDelete =
+ aTextFragmentDataAtStartToDelete.ScanStartRef();
+ const EditorDOMPoint& endToDelete = mScanStartPoint;
+
+ MOZ_ASSERT(startToDelete.IsSetAndValid());
+ MOZ_ASSERT(endToDelete.IsSetAndValid());
+ MOZ_ASSERT(startToDelete.EqualsOrIsBefore(endToDelete));
+
+ if (EndRef().EqualsOrIsBefore(endToDelete)) {
+ return ReplaceRangeData();
+ }
+
+ // If deleting range is followed by invisible trailing white-spaces, we need
+ // to remove it for making them not visible.
+ const EditorDOMRange invisibleTrailingWhiteSpaceRangeAtEnd =
+ GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt(endToDelete);
+ if (invisibleTrailingWhiteSpaceRangeAtEnd.IsPositioned()) {
+ if (invisibleTrailingWhiteSpaceRangeAtEnd.Collapsed()) {
+ return ReplaceRangeData();
+ }
+ // XXX Why don't we remove all invisible white-spaces?
+ MOZ_ASSERT(invisibleTrailingWhiteSpaceRangeAtEnd.StartRef() == endToDelete);
+ return ReplaceRangeData(invisibleTrailingWhiteSpaceRangeAtEnd, u""_ns);
+ }
+
+ // If end of the deleting range is followed by visible white-spaces which
+ // is not preformatted, we might need to replace the following ASCII
+ // white-spaces with an NBSP.
+ const VisibleWhiteSpacesData& nonPreformattedVisibleWhiteSpacesAtEnd =
+ VisibleWhiteSpacesDataRef();
+ if (!nonPreformattedVisibleWhiteSpacesAtEnd.IsInitialized()) {
+ return ReplaceRangeData();
+ }
+ const PointPosition pointPositionWithNonPreformattedVisibleWhiteSpacesAtEnd =
+ nonPreformattedVisibleWhiteSpacesAtEnd.ComparePoint(endToDelete);
+ if (pointPositionWithNonPreformattedVisibleWhiteSpacesAtEnd !=
+ PointPosition::StartOfFragment &&
+ pointPositionWithNonPreformattedVisibleWhiteSpacesAtEnd !=
+ PointPosition::MiddleOfFragment) {
+ return ReplaceRangeData();
+ }
+ // If start of deleting range follows white-spaces or end of delete
+ // will be start of a line, the following text cannot start with an
+ // ASCII white-space for keeping it visible.
+ if (!aTextFragmentDataAtStartToDelete
+ .FollowingContentMayBecomeFirstVisibleContent(startToDelete)) {
+ return ReplaceRangeData();
+ }
+ auto nextCharOfStartOfEnd =
+ GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(endToDelete);
+ if (!nextCharOfStartOfEnd.IsSet() ||
+ nextCharOfStartOfEnd.IsEndOfContainer() ||
+ !nextCharOfStartOfEnd.IsCharCollapsibleASCIISpace()) {
+ return ReplaceRangeData();
+ }
+ if (nextCharOfStartOfEnd.IsStartOfContainer() ||
+ nextCharOfStartOfEnd.IsPreviousCharCollapsibleASCIISpace()) {
+ nextCharOfStartOfEnd = aTextFragmentDataAtStartToDelete
+ .GetFirstASCIIWhiteSpacePointCollapsedTo(
+ nextCharOfStartOfEnd, nsIEditor::eNone);
+ }
+ const EditorDOMPointInText endOfCollapsibleASCIIWhiteSpaces =
+ aTextFragmentDataAtStartToDelete.GetEndOfCollapsibleASCIIWhiteSpaces(
+ nextCharOfStartOfEnd, nsIEditor::eNone);
+ return ReplaceRangeData(nextCharOfStartOfEnd,
+ endOfCollapsibleASCIIWhiteSpaces,
+ nsDependentSubstring(&HTMLEditUtils::kNBSP, 1));
+}
+
+ReplaceRangeData
+WSRunScanner::TextFragmentData::GetReplaceRangeDataAtStartOfDeletionRange(
+ const TextFragmentData& aTextFragmentDataAtEndToDelete) const {
+ const EditorDOMPoint& startToDelete = mScanStartPoint;
+ const EditorDOMPoint& endToDelete =
+ aTextFragmentDataAtEndToDelete.ScanStartRef();
+
+ MOZ_ASSERT(startToDelete.IsSetAndValid());
+ MOZ_ASSERT(endToDelete.IsSetAndValid());
+ MOZ_ASSERT(startToDelete.EqualsOrIsBefore(endToDelete));
+
+ if (startToDelete.EqualsOrIsBefore(StartRef())) {
+ return ReplaceRangeData();
+ }
+
+ const EditorDOMRange invisibleLeadingWhiteSpaceRangeAtStart =
+ GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt(startToDelete);
+
+ // If deleting range follows invisible leading white-spaces, we need to
+ // remove them for making them not visible.
+ if (invisibleLeadingWhiteSpaceRangeAtStart.IsPositioned()) {
+ if (invisibleLeadingWhiteSpaceRangeAtStart.Collapsed()) {
+ return ReplaceRangeData();
+ }
+
+ // XXX Why don't we remove all leading white-spaces?
+ return ReplaceRangeData(invisibleLeadingWhiteSpaceRangeAtStart, u""_ns);
+ }
+
+ // If start of the deleting range follows visible white-spaces which is not
+ // preformatted, we might need to replace previous ASCII white-spaces with
+ // an NBSP.
+ const VisibleWhiteSpacesData& nonPreformattedVisibleWhiteSpacesAtStart =
+ VisibleWhiteSpacesDataRef();
+ if (!nonPreformattedVisibleWhiteSpacesAtStart.IsInitialized()) {
+ return ReplaceRangeData();
+ }
+ const PointPosition
+ pointPositionWithNonPreformattedVisibleWhiteSpacesAtStart =
+ nonPreformattedVisibleWhiteSpacesAtStart.ComparePoint(startToDelete);
+ if (pointPositionWithNonPreformattedVisibleWhiteSpacesAtStart !=
+ PointPosition::MiddleOfFragment &&
+ pointPositionWithNonPreformattedVisibleWhiteSpacesAtStart !=
+ PointPosition::EndOfFragment) {
+ return ReplaceRangeData();
+ }
+ // If end of the deleting range is (was) followed by white-spaces or
+ // previous character of start of deleting range will be immediately
+ // before a block boundary, the text cannot ends with an ASCII white-space
+ // for keeping it visible.
+ if (!aTextFragmentDataAtEndToDelete.PrecedingContentMayBecomeInvisible(
+ endToDelete)) {
+ return ReplaceRangeData();
+ }
+ EditorDOMPointInText atPreviousCharOfStart =
+ GetPreviousEditableCharPoint(startToDelete);
+ if (!atPreviousCharOfStart.IsSet() ||
+ atPreviousCharOfStart.IsEndOfContainer() ||
+ !atPreviousCharOfStart.IsCharCollapsibleASCIISpace()) {
+ return ReplaceRangeData();
+ }
+ if (atPreviousCharOfStart.IsStartOfContainer() ||
+ atPreviousCharOfStart.IsPreviousCharASCIISpace()) {
+ atPreviousCharOfStart = GetFirstASCIIWhiteSpacePointCollapsedTo(
+ atPreviousCharOfStart, nsIEditor::eNone);
+ }
+ const EditorDOMPointInText endOfCollapsibleASCIIWhiteSpaces =
+ GetEndOfCollapsibleASCIIWhiteSpaces(atPreviousCharOfStart,
+ nsIEditor::eNone);
+ return ReplaceRangeData(atPreviousCharOfStart,
+ endOfCollapsibleASCIIWhiteSpaces,
+ nsDependentSubstring(&HTMLEditUtils::kNBSP, 1));
+}
+
+// static
+nsresult
+WhiteSpaceVisibilityKeeper::MakeSureToKeepVisibleWhiteSpacesVisibleAfterSplit(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToSplit) {
+ TextFragmentData textFragmentDataAtSplitPoint(
+ aPointToSplit, aHTMLEditor.ComputeEditingHost());
+ if (NS_WARN_IF(!textFragmentDataAtSplitPoint.IsInitialized())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // used to prepare white-space sequence to be split across two blocks.
+ // The main issue here is make sure white-spaces around the split point
+ // doesn't end up becoming non-significant leading or trailing ws after
+ // the split.
+ const VisibleWhiteSpacesData& visibleWhiteSpaces =
+ textFragmentDataAtSplitPoint.VisibleWhiteSpacesDataRef();
+ if (!visibleWhiteSpaces.IsInitialized()) {
+ return NS_OK; // No visible white-space sequence.
+ }
+
+ PointPosition pointPositionWithVisibleWhiteSpaces =
+ visibleWhiteSpaces.ComparePoint(aPointToSplit);
+
+ // XXX If we split white-space sequence, the following code modify the DOM
+ // tree twice. This is not reasonable and the latter change may touch
+ // wrong position. We should do this once.
+
+ // If we insert block boundary to start or middle of the white-space sequence,
+ // the character at the insertion point needs to be an NBSP.
+ EditorDOMPoint pointToSplit(aPointToSplit);
+ if (pointPositionWithVisibleWhiteSpaces == PointPosition::StartOfFragment ||
+ pointPositionWithVisibleWhiteSpaces == PointPosition::MiddleOfFragment) {
+ EditorDOMPointInText atNextCharOfStart =
+ textFragmentDataAtSplitPoint.GetInclusiveNextEditableCharPoint(
+ pointToSplit);
+ if (atNextCharOfStart.IsSet() && !atNextCharOfStart.IsEndOfContainer() &&
+ atNextCharOfStart.IsCharCollapsibleASCIISpace()) {
+ // pointToSplit will be referred bellow so that we need to keep
+ // it a valid point.
+ AutoEditorDOMPointChildInvalidator forgetChild(pointToSplit);
+ AutoTrackDOMPoint trackSplitPoint(aHTMLEditor.RangeUpdaterRef(),
+ &pointToSplit);
+ if (atNextCharOfStart.IsStartOfContainer() ||
+ atNextCharOfStart.IsPreviousCharASCIISpace()) {
+ atNextCharOfStart = textFragmentDataAtSplitPoint
+ .GetFirstASCIIWhiteSpacePointCollapsedTo(
+ atNextCharOfStart, nsIEditor::eNone);
+ }
+ const EditorDOMPointInText endOfCollapsibleASCIIWhiteSpaces =
+ textFragmentDataAtSplitPoint.GetEndOfCollapsibleASCIIWhiteSpaces(
+ atNextCharOfStart, nsIEditor::eNone);
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes(
+ aHTMLEditor,
+ EditorDOMRangeInTexts(atNextCharOfStart,
+ endOfCollapsibleASCIIWhiteSpaces),
+ nsDependentSubstring(&HTMLEditUtils::kNBSP, 1));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes() "
+ "failed");
+ return rv;
+ }
+ }
+ }
+
+ // If we insert block boundary to middle of or end of the white-space
+ // sequence, the previous character at the insertion point needs to be an
+ // NBSP.
+ if (pointPositionWithVisibleWhiteSpaces == PointPosition::MiddleOfFragment ||
+ pointPositionWithVisibleWhiteSpaces == PointPosition::EndOfFragment) {
+ EditorDOMPointInText atPreviousCharOfStart =
+ textFragmentDataAtSplitPoint.GetPreviousEditableCharPoint(pointToSplit);
+ if (atPreviousCharOfStart.IsSet() &&
+ !atPreviousCharOfStart.IsEndOfContainer() &&
+ atPreviousCharOfStart.IsCharCollapsibleASCIISpace()) {
+ if (atPreviousCharOfStart.IsStartOfContainer() ||
+ atPreviousCharOfStart.IsPreviousCharASCIISpace()) {
+ atPreviousCharOfStart =
+ textFragmentDataAtSplitPoint
+ .GetFirstASCIIWhiteSpacePointCollapsedTo(atPreviousCharOfStart,
+ nsIEditor::eNone);
+ }
+ const EditorDOMPointInText endOfCollapsibleASCIIWhiteSpaces =
+ textFragmentDataAtSplitPoint.GetEndOfCollapsibleASCIIWhiteSpaces(
+ atPreviousCharOfStart, nsIEditor::eNone);
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes(
+ aHTMLEditor,
+ EditorDOMRangeInTexts(atPreviousCharOfStart,
+ endOfCollapsibleASCIIWhiteSpaces),
+ nsDependentSubstring(&HTMLEditUtils::kNBSP, 1));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes() "
+ "failed");
+ return rv;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+template <typename EditorDOMPointType, typename PT, typename CT>
+EditorDOMPointType
+WSRunScanner::TextFragmentData::GetInclusiveNextEditableCharPoint(
+ const EditorDOMPointBase<PT, CT>& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ if (NS_WARN_IF(!aPoint.IsInContentNode()) ||
+ NS_WARN_IF(!mScanStartPoint.IsInContentNode())) {
+ return EditorDOMPointType();
+ }
+
+ EditorRawDOMPoint point;
+ if (nsIContent* child =
+ aPoint.CanContainerHaveChildren() ? aPoint.GetChild() : nullptr) {
+ nsIContent* leafContent = child->HasChildren()
+ ? HTMLEditUtils::GetFirstLeafContent(
+ *child, {LeafNodeType::OnlyLeafNode})
+ : child;
+ if (NS_WARN_IF(!leafContent)) {
+ return EditorDOMPointType();
+ }
+ point.Set(leafContent, 0);
+ } else {
+ point = aPoint.template To<EditorRawDOMPoint>();
+ }
+
+ // If it points a character in a text node, return it.
+ // XXX For the performance, this does not check whether the container
+ // is outside of our range.
+ if (point.IsInTextNode() && point.GetContainer()->IsEditable() &&
+ !point.IsEndOfContainer()) {
+ return EditorDOMPointType(point.ContainerAs<Text>(), point.Offset());
+ }
+
+ if (point.GetContainer() == GetEndReasonContent()) {
+ return EditorDOMPointType();
+ }
+
+ NS_ASSERTION(
+ EditorUtils::IsEditableContent(*mScanStartPoint.ContainerAs<nsIContent>(),
+ EditorType::HTML),
+ "Given content is not editable");
+ NS_ASSERTION(
+ mScanStartPoint.ContainerAs<nsIContent>()->GetAsElementOrParentElement(),
+ "Given content is not an element and an orphan node");
+ nsIContent* editableBlockElementOrInlineEditingHost =
+ mScanStartPoint.ContainerAs<nsIContent>() &&
+ EditorUtils::IsEditableContent(
+ *mScanStartPoint.ContainerAs<nsIContent>(), EditorType::HTML)
+ ? HTMLEditUtils::GetInclusiveAncestorElement(
+ *mScanStartPoint.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost)
+ : nullptr;
+ if (NS_WARN_IF(!editableBlockElementOrInlineEditingHost)) {
+ // Meaning that the container of `mScanStartPoint` is not editable.
+ editableBlockElementOrInlineEditingHost =
+ mScanStartPoint.ContainerAs<nsIContent>();
+ }
+
+ for (nsIContent* nextContent =
+ HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *point.ContainerAs<nsIContent>(),
+ *editableBlockElementOrInlineEditingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, mEditingHost);
+ nextContent;
+ nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *nextContent, *editableBlockElementOrInlineEditingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, mEditingHost)) {
+ if (!nextContent->IsText() || !nextContent->IsEditable()) {
+ if (nextContent == GetEndReasonContent()) {
+ break; // Reached end of current runs.
+ }
+ continue;
+ }
+ return EditorDOMPointType(nextContent->AsText(), 0);
+ }
+ return EditorDOMPointType();
+}
+
+template <typename EditorDOMPointType, typename PT, typename CT>
+EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousEditableCharPoint(
+ const EditorDOMPointBase<PT, CT>& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ if (NS_WARN_IF(!aPoint.IsInContentNode()) ||
+ NS_WARN_IF(!mScanStartPoint.IsInContentNode())) {
+ return EditorDOMPointType();
+ }
+
+ EditorRawDOMPoint point;
+ if (nsIContent* previousChild = aPoint.CanContainerHaveChildren()
+ ? aPoint.GetPreviousSiblingOfChild()
+ : nullptr) {
+ nsIContent* leafContent =
+ previousChild->HasChildren()
+ ? HTMLEditUtils::GetLastLeafContent(*previousChild,
+ {LeafNodeType::OnlyLeafNode})
+ : previousChild;
+ if (NS_WARN_IF(!leafContent)) {
+ return EditorDOMPointType();
+ }
+ point.SetToEndOf(leafContent);
+ } else {
+ point = aPoint.template To<EditorRawDOMPoint>();
+ }
+
+ // If it points a character in a text node and it's not first character
+ // in it, return its previous point.
+ // XXX For the performance, this does not check whether the container
+ // is outside of our range.
+ if (point.IsInTextNode() && point.GetContainer()->IsEditable() &&
+ !point.IsStartOfContainer()) {
+ return EditorDOMPointType(point.ContainerAs<Text>(), point.Offset() - 1);
+ }
+
+ if (point.GetContainer() == GetStartReasonContent()) {
+ return EditorDOMPointType();
+ }
+
+ NS_ASSERTION(
+ EditorUtils::IsEditableContent(*mScanStartPoint.ContainerAs<nsIContent>(),
+ EditorType::HTML),
+ "Given content is not editable");
+ NS_ASSERTION(
+ mScanStartPoint.ContainerAs<nsIContent>()->GetAsElementOrParentElement(),
+ "Given content is not an element and an orphan node");
+ nsIContent* editableBlockElementOrInlineEditingHost =
+ mScanStartPoint.ContainerAs<nsIContent>() &&
+ EditorUtils::IsEditableContent(
+ *mScanStartPoint.ContainerAs<nsIContent>(), EditorType::HTML)
+ ? HTMLEditUtils::GetInclusiveAncestorElement(
+ *mScanStartPoint.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost)
+ : nullptr;
+ if (NS_WARN_IF(!editableBlockElementOrInlineEditingHost)) {
+ // Meaning that the container of `mScanStartPoint` is not editable.
+ editableBlockElementOrInlineEditingHost =
+ mScanStartPoint.ContainerAs<nsIContent>();
+ }
+
+ for (nsIContent* previousContent =
+ HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *point.ContainerAs<nsIContent>(),
+ *editableBlockElementOrInlineEditingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, mEditingHost);
+ previousContent;
+ previousContent =
+ HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *previousContent, *editableBlockElementOrInlineEditingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, mEditingHost)) {
+ if (!previousContent->IsText() || !previousContent->IsEditable()) {
+ if (previousContent == GetStartReasonContent()) {
+ break; // Reached start of current runs.
+ }
+ continue;
+ }
+ return EditorDOMPointType(previousContent->AsText(),
+ previousContent->AsText()->TextLength()
+ ? previousContent->AsText()->TextLength() - 1
+ : 0);
+ }
+ return EditorDOMPointType();
+}
+
+// static
+template <typename EditorDOMPointType>
+EditorDOMPointType WSRunScanner::GetAfterLastVisiblePoint(
+ Text& aTextNode, const Element* aAncestorLimiter) {
+ EditorDOMPoint atLastCharOfTextNode(
+ &aTextNode, AssertedCast<uint32_t>(std::max<int64_t>(
+ static_cast<int64_t>(aTextNode.Length()) - 1, 0)));
+ if (!atLastCharOfTextNode.IsContainerEmpty() &&
+ !atLastCharOfTextNode.IsCharCollapsibleASCIISpace()) {
+ return EditorDOMPointType::AtEndOf(aTextNode);
+ }
+ TextFragmentData textFragmentData(atLastCharOfTextNode, aAncestorLimiter);
+ if (NS_WARN_IF(!textFragmentData.IsInitialized())) {
+ return EditorDOMPointType(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRange& invisibleWhiteSpaceRange =
+ textFragmentData.InvisibleTrailingWhiteSpaceRangeRef();
+ if (!invisibleWhiteSpaceRange.IsPositioned() ||
+ invisibleWhiteSpaceRange.Collapsed()) {
+ return EditorDOMPointType::AtEndOf(aTextNode);
+ }
+ return invisibleWhiteSpaceRange.StartRef().To<EditorDOMPointType>();
+}
+
+// static
+template <typename EditorDOMPointType>
+EditorDOMPointType WSRunScanner::GetFirstVisiblePoint(
+ Text& aTextNode, const Element* aAncestorLimiter) {
+ EditorDOMPoint atStartOfTextNode(&aTextNode, 0);
+ if (!atStartOfTextNode.IsContainerEmpty() &&
+ atStartOfTextNode.IsCharCollapsibleASCIISpace()) {
+ return atStartOfTextNode.To<EditorDOMPointType>();
+ }
+ TextFragmentData textFragmentData(atStartOfTextNode, aAncestorLimiter);
+ if (NS_WARN_IF(!textFragmentData.IsInitialized())) {
+ return EditorDOMPointType(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRange& invisibleWhiteSpaceRange =
+ textFragmentData.InvisibleLeadingWhiteSpaceRangeRef();
+ if (!invisibleWhiteSpaceRange.IsPositioned() ||
+ invisibleWhiteSpaceRange.Collapsed()) {
+ return atStartOfTextNode.To<EditorDOMPointType>();
+ }
+ return invisibleWhiteSpaceRange.EndRef().To<EditorDOMPointType>();
+}
+
+template <typename EditorDOMPointType>
+EditorDOMPointType
+WSRunScanner::TextFragmentData::GetEndOfCollapsibleASCIIWhiteSpaces(
+ const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
+ nsIEditor::EDirection aDirectionToDelete) const {
+ MOZ_ASSERT(aDirectionToDelete == nsIEditor::eNone ||
+ aDirectionToDelete == nsIEditor::eNext ||
+ aDirectionToDelete == nsIEditor::ePrevious);
+ MOZ_ASSERT(aPointAtASCIIWhiteSpace.IsSet());
+ MOZ_ASSERT(!aPointAtASCIIWhiteSpace.IsEndOfContainer());
+ MOZ_ASSERT_IF(!EditorUtils::IsNewLinePreformatted(
+ *aPointAtASCIIWhiteSpace.ContainerAs<Text>()),
+ aPointAtASCIIWhiteSpace.IsCharCollapsibleASCIISpace());
+ MOZ_ASSERT_IF(EditorUtils::IsNewLinePreformatted(
+ *aPointAtASCIIWhiteSpace.ContainerAs<Text>()),
+ aPointAtASCIIWhiteSpace.IsCharASCIISpace());
+
+ // If we're deleting text forward and the next visible character is first
+ // preformatted new line but white-spaces can be collapsed, we need to
+ // delete its following collapsible white-spaces too.
+ bool hasSeenPreformattedNewLine =
+ aPointAtASCIIWhiteSpace.IsCharPreformattedNewLine();
+ auto NeedToScanFollowingWhiteSpaces =
+ [&hasSeenPreformattedNewLine, &aDirectionToDelete](
+ const EditorDOMPointInText& aAtNextVisibleCharacter) -> bool {
+ MOZ_ASSERT(!aAtNextVisibleCharacter.IsEndOfContainer());
+ return !hasSeenPreformattedNewLine &&
+ aDirectionToDelete == nsIEditor::eNext &&
+ aAtNextVisibleCharacter
+ .IsCharPreformattedNewLineCollapsedWithWhiteSpaces();
+ };
+ auto ScanNextNonCollapsibleChar =
+ [&hasSeenPreformattedNewLine, &NeedToScanFollowingWhiteSpaces](
+ const EditorDOMPointInText& aPoint) -> EditorDOMPointInText {
+ Maybe<uint32_t> nextVisibleCharOffset =
+ HTMLEditUtils::GetNextNonCollapsibleCharOffset(aPoint);
+ if (!nextVisibleCharOffset.isSome()) {
+ return EditorDOMPointInText(); // Keep scanning following text nodes
+ }
+ EditorDOMPointInText atNextVisibleChar(aPoint.ContainerAs<Text>(),
+ nextVisibleCharOffset.value());
+ if (!NeedToScanFollowingWhiteSpaces(atNextVisibleChar)) {
+ return atNextVisibleChar;
+ }
+ hasSeenPreformattedNewLine |= atNextVisibleChar.IsCharPreformattedNewLine();
+ nextVisibleCharOffset =
+ HTMLEditUtils::GetNextNonCollapsibleCharOffset(atNextVisibleChar);
+ if (nextVisibleCharOffset.isSome()) {
+ MOZ_ASSERT(aPoint.ContainerAs<Text>() ==
+ atNextVisibleChar.ContainerAs<Text>());
+ return EditorDOMPointInText(atNextVisibleChar.ContainerAs<Text>(),
+ nextVisibleCharOffset.value());
+ }
+ return EditorDOMPointInText(); // Keep scanning following text nodes
+ };
+
+ // If it's not the last character in the text node, let's scan following
+ // characters in it.
+ if (!aPointAtASCIIWhiteSpace.IsAtLastContent()) {
+ const EditorDOMPointInText atNextVisibleChar(
+ ScanNextNonCollapsibleChar(aPointAtASCIIWhiteSpace));
+ if (atNextVisibleChar.IsSet()) {
+ return atNextVisibleChar.To<EditorDOMPointType>();
+ }
+ }
+
+ // Otherwise, i.e., the text node ends with ASCII white-space, keep scanning
+ // the following text nodes.
+ // XXX Perhaps, we should stop scanning if there is non-editable and visible
+ // content.
+ EditorDOMPointInText afterLastWhiteSpace = EditorDOMPointInText::AtEndOf(
+ *aPointAtASCIIWhiteSpace.ContainerAs<Text>());
+ for (EditorDOMPointInText atEndOfPreviousTextNode = afterLastWhiteSpace;;) {
+ const auto atStartOfNextTextNode =
+ GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(
+ atEndOfPreviousTextNode);
+ if (!atStartOfNextTextNode.IsSet()) {
+ // There is no more text nodes. Return end of the previous text node.
+ return afterLastWhiteSpace.To<EditorDOMPointType>();
+ }
+
+ // We can ignore empty text nodes (even if it's preformatted).
+ if (atStartOfNextTextNode.IsContainerEmpty()) {
+ atEndOfPreviousTextNode = atStartOfNextTextNode;
+ continue;
+ }
+
+ // If next node starts with non-white-space character or next node is
+ // preformatted, return end of previous text node. However, if it
+ // starts with a preformatted linefeed but white-spaces are collapsible,
+ // we need to scan following collapsible white-spaces when we're deleting
+ // text forward.
+ if (!atStartOfNextTextNode.IsCharCollapsibleASCIISpace() &&
+ !NeedToScanFollowingWhiteSpaces(atStartOfNextTextNode)) {
+ return afterLastWhiteSpace.To<EditorDOMPointType>();
+ }
+
+ // Otherwise, scan the text node.
+ const EditorDOMPointInText atNextVisibleChar(
+ ScanNextNonCollapsibleChar(atStartOfNextTextNode));
+ if (atNextVisibleChar.IsSet()) {
+ return atNextVisibleChar.To<EditorDOMPointType>();
+ }
+
+ // The next text nodes ends with white-space too. Try next one.
+ afterLastWhiteSpace = atEndOfPreviousTextNode =
+ EditorDOMPointInText::AtEndOf(
+ *atStartOfNextTextNode.ContainerAs<Text>());
+ }
+}
+
+template <typename EditorDOMPointType>
+EditorDOMPointType
+WSRunScanner::TextFragmentData::GetFirstASCIIWhiteSpacePointCollapsedTo(
+ const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
+ nsIEditor::EDirection aDirectionToDelete) const {
+ MOZ_ASSERT(aDirectionToDelete == nsIEditor::eNone ||
+ aDirectionToDelete == nsIEditor::eNext ||
+ aDirectionToDelete == nsIEditor::ePrevious);
+ MOZ_ASSERT(aPointAtASCIIWhiteSpace.IsSet());
+ MOZ_ASSERT(!aPointAtASCIIWhiteSpace.IsEndOfContainer());
+ MOZ_ASSERT_IF(!EditorUtils::IsNewLinePreformatted(
+ *aPointAtASCIIWhiteSpace.ContainerAs<Text>()),
+ aPointAtASCIIWhiteSpace.IsCharCollapsibleASCIISpace());
+ MOZ_ASSERT_IF(EditorUtils::IsNewLinePreformatted(
+ *aPointAtASCIIWhiteSpace.ContainerAs<Text>()),
+ aPointAtASCIIWhiteSpace.IsCharASCIISpace());
+
+ // If we're deleting text backward and the previous visible character is first
+ // preformatted new line but white-spaces can be collapsed, we need to delete
+ // its preceding collapsible white-spaces too.
+ bool hasSeenPreformattedNewLine =
+ aPointAtASCIIWhiteSpace.IsCharPreformattedNewLine();
+ auto NeedToScanPrecedingWhiteSpaces =
+ [&hasSeenPreformattedNewLine, &aDirectionToDelete](
+ const EditorDOMPointInText& aAtPreviousVisibleCharacter) -> bool {
+ MOZ_ASSERT(!aAtPreviousVisibleCharacter.IsEndOfContainer());
+ return !hasSeenPreformattedNewLine &&
+ aDirectionToDelete == nsIEditor::ePrevious &&
+ aAtPreviousVisibleCharacter
+ .IsCharPreformattedNewLineCollapsedWithWhiteSpaces();
+ };
+ auto ScanPreviousNonCollapsibleChar =
+ [&hasSeenPreformattedNewLine, &NeedToScanPrecedingWhiteSpaces](
+ const EditorDOMPointInText& aPoint) -> EditorDOMPointInText {
+ Maybe<uint32_t> previousVisibleCharOffset =
+ HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(aPoint);
+ if (previousVisibleCharOffset.isNothing()) {
+ return EditorDOMPointInText(); // Keep scanning preceding text nodes
+ }
+ EditorDOMPointInText atPreviousVisibleCharacter(
+ aPoint.ContainerAs<Text>(), previousVisibleCharOffset.value());
+ if (!NeedToScanPrecedingWhiteSpaces(atPreviousVisibleCharacter)) {
+ return atPreviousVisibleCharacter.NextPoint();
+ }
+ hasSeenPreformattedNewLine |=
+ atPreviousVisibleCharacter.IsCharPreformattedNewLine();
+ previousVisibleCharOffset =
+ HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
+ atPreviousVisibleCharacter);
+ if (previousVisibleCharOffset.isSome()) {
+ MOZ_ASSERT(aPoint.ContainerAs<Text>() ==
+ atPreviousVisibleCharacter.ContainerAs<Text>());
+ return EditorDOMPointInText(
+ atPreviousVisibleCharacter.ContainerAs<Text>(),
+ previousVisibleCharOffset.value() + 1);
+ }
+ return EditorDOMPointInText(); // Keep scanning preceding text nodes
+ };
+
+ // If there is some characters before it, scan it in the text node first.
+ if (!aPointAtASCIIWhiteSpace.IsStartOfContainer()) {
+ EditorDOMPointInText atFirstASCIIWhiteSpace(
+ ScanPreviousNonCollapsibleChar(aPointAtASCIIWhiteSpace));
+ if (atFirstASCIIWhiteSpace.IsSet()) {
+ return atFirstASCIIWhiteSpace.To<EditorDOMPointType>();
+ }
+ }
+
+ // Otherwise, i.e., the text node starts with ASCII white-space, keep scanning
+ // the preceding text nodes.
+ // XXX Perhaps, we should stop scanning if there is non-editable and visible
+ // content.
+ EditorDOMPointInText atLastWhiteSpace =
+ EditorDOMPointInText(aPointAtASCIIWhiteSpace.ContainerAs<Text>(), 0u);
+ for (EditorDOMPointInText atStartOfPreviousTextNode = atLastWhiteSpace;;) {
+ const EditorDOMPointInText atLastCharOfPreviousTextNode =
+ GetPreviousEditableCharPoint(atStartOfPreviousTextNode);
+ if (!atLastCharOfPreviousTextNode.IsSet()) {
+ // There is no more text nodes. Return end of last text node.
+ return atLastWhiteSpace.To<EditorDOMPointType>();
+ }
+
+ // We can ignore empty text nodes (even if it's preformatted).
+ if (atLastCharOfPreviousTextNode.IsContainerEmpty()) {
+ atStartOfPreviousTextNode = atLastCharOfPreviousTextNode;
+ continue;
+ }
+
+ // If next node ends with non-white-space character or next node is
+ // preformatted, return start of previous text node.
+ if (!atLastCharOfPreviousTextNode.IsCharCollapsibleASCIISpace() &&
+ !NeedToScanPrecedingWhiteSpaces(atLastCharOfPreviousTextNode)) {
+ return atLastWhiteSpace.To<EditorDOMPointType>();
+ }
+
+ // Otherwise, scan the text node.
+ const EditorDOMPointInText atFirstASCIIWhiteSpace(
+ ScanPreviousNonCollapsibleChar(atLastCharOfPreviousTextNode));
+ if (atFirstASCIIWhiteSpace.IsSet()) {
+ return atFirstASCIIWhiteSpace.To<EditorDOMPointType>();
+ }
+
+ // The next text nodes starts with white-space too. Try next one.
+ atLastWhiteSpace = atStartOfPreviousTextNode = EditorDOMPointInText(
+ atLastCharOfPreviousTextNode.ContainerAs<Text>(), 0u);
+ }
+}
+
+// static
+nsresult WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes(
+ HTMLEditor& aHTMLEditor, const EditorDOMRangeInTexts& aRangeToReplace,
+ const nsAString& aReplaceString) {
+ MOZ_ASSERT(aRangeToReplace.IsPositioned());
+ MOZ_ASSERT(aRangeToReplace.StartRef().IsSetAndValid());
+ MOZ_ASSERT(aRangeToReplace.EndRef().IsSetAndValid());
+ MOZ_ASSERT(aRangeToReplace.StartRef().IsBefore(aRangeToReplace.EndRef()));
+
+ {
+ Result<InsertTextResult, nsresult> caretPointOrError =
+ aHTMLEditor.ReplaceTextWithTransaction(
+ MOZ_KnownLive(*aRangeToReplace.StartRef().ContainerAs<Text>()),
+ aRangeToReplace.StartRef().Offset(),
+ aRangeToReplace.InSameContainer()
+ ? aRangeToReplace.EndRef().Offset() -
+ aRangeToReplace.StartRef().Offset()
+ : aRangeToReplace.StartRef().ContainerAs<Text>()->TextLength() -
+ aRangeToReplace.StartRef().Offset(),
+ aReplaceString);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
+ return caretPointOrError.unwrapErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ }
+
+ if (aRangeToReplace.InSameContainer()) {
+ return NS_OK;
+ }
+
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ EditorDOMPointInText::AtEndOf(
+ *aRangeToReplace.StartRef().ContainerAs<Text>()),
+ aRangeToReplace.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.unwrapErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+char16_t WSRunScanner::GetCharAt(Text* aTextNode, uint32_t aOffset) const {
+ // return 0 if we can't get a char, for whatever reason
+ if (NS_WARN_IF(!aTextNode) ||
+ NS_WARN_IF(aOffset >= aTextNode->TextDataLength())) {
+ return 0;
+ }
+ return aTextNode->TextFragment().CharAt(aOffset);
+}
+
+// static
+template <typename EditorDOMPointType>
+nsresult WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
+ HTMLEditor& aHTMLEditor, const EditorDOMPointType& aPoint) {
+ MOZ_ASSERT(aPoint.IsInContentNode());
+ MOZ_ASSERT(EditorUtils::IsEditableContent(
+ *aPoint.template ContainerAs<nsIContent>(), EditorType::HTML));
+ Element* editingHost = aHTMLEditor.ComputeEditingHost();
+ TextFragmentData textFragmentData(aPoint, editingHost);
+ if (NS_WARN_IF(!textFragmentData.IsInitialized())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // this routine examines a run of ws and tries to get rid of some unneeded
+ // nbsp's, replacing them with regular ascii space if possible. Keeping
+ // things simple for now and just trying to fix up the trailing ws in the run.
+ if (!textFragmentData.FoundNoBreakingWhiteSpaces()) {
+ // nothing to do!
+ return NS_OK;
+ }
+ const VisibleWhiteSpacesData& visibleWhiteSpaces =
+ textFragmentData.VisibleWhiteSpacesDataRef();
+ if (!visibleWhiteSpaces.IsInitialized()) {
+ return NS_OK;
+ }
+
+ // Remove this block if we ship Blink-compat white-space normalization.
+ if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) {
+ // now check that what is to the left of it is compatible with replacing
+ // nbsp with space
+ const EditorDOMPoint& atEndOfVisibleWhiteSpaces =
+ visibleWhiteSpaces.EndRef();
+ EditorDOMPointInText atPreviousCharOfEndOfVisibleWhiteSpaces =
+ textFragmentData.GetPreviousEditableCharPoint(
+ atEndOfVisibleWhiteSpaces);
+ if (!atPreviousCharOfEndOfVisibleWhiteSpaces.IsSet() ||
+ atPreviousCharOfEndOfVisibleWhiteSpaces.IsEndOfContainer() ||
+ // If the NBSP is never replaced from an ASCII white-space, we cannot
+ // replace it with an ASCII white-space.
+ !atPreviousCharOfEndOfVisibleWhiteSpaces.IsCharCollapsibleNBSP()) {
+ return NS_OK;
+ }
+
+ // now check that what is to the left of it is compatible with replacing
+ // nbsp with space
+ EditorDOMPointInText atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces =
+ textFragmentData.GetPreviousEditableCharPoint(
+ atPreviousCharOfEndOfVisibleWhiteSpaces);
+ bool isPreviousCharCollapsibleASCIIWhiteSpace =
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces.IsSet() &&
+ !atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces
+ .IsEndOfContainer() &&
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces
+ .IsCharCollapsibleASCIISpace();
+ const bool maybeNBSPFollowsVisibleContent =
+ (atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces.IsSet() &&
+ !isPreviousCharCollapsibleASCIIWhiteSpace) ||
+ (!atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces.IsSet() &&
+ (visibleWhiteSpaces.StartsFromNonCollapsibleCharacters() ||
+ visibleWhiteSpaces.StartsFromSpecialContent()));
+ bool followedByVisibleContent =
+ visibleWhiteSpaces.EndsByNonCollapsibleCharacters() ||
+ visibleWhiteSpaces.EndsBySpecialContent();
+ bool followedByBRElement = visibleWhiteSpaces.EndsByBRElement();
+ bool followedByPreformattedLineBreak =
+ visibleWhiteSpaces.EndsByPreformattedLineBreak();
+
+ // If the NBSP follows a visible content or a collapsible ASCII white-space,
+ // i.e., unless NBSP is first character and start of a block, we may need to
+ // insert <br> element and restore the NBSP to an ASCII white-space.
+ if (maybeNBSPFollowsVisibleContent ||
+ isPreviousCharCollapsibleASCIIWhiteSpace) {
+ // First, try to insert <br> element if NBSP is at end of a block.
+ // XXX We should stop this if there is a visible content.
+ if (visibleWhiteSpaces.EndsByBlockBoundary() &&
+ aPoint.IsInContentNode()) {
+ bool insertBRElement = HTMLEditUtils::IsBlockElement(
+ *aPoint.template ContainerAs<nsIContent>());
+ if (!insertBRElement) {
+ NS_ASSERTION(
+ EditorUtils::IsEditableContent(
+ *aPoint.template ContainerAs<nsIContent>(), EditorType::HTML),
+ "Given content is not editable");
+ NS_ASSERTION(aPoint.template ContainerAs<nsIContent>()
+ ->GetAsElementOrParentElement(),
+ "Given content is not an element and an orphan node");
+ const Element* editableBlockElement =
+ EditorUtils::IsEditableContent(
+ *aPoint.template ContainerAs<nsIContent>(), EditorType::HTML)
+ ? HTMLEditUtils::GetInclusiveAncestorElement(
+ *aPoint.template ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElement)
+ : nullptr;
+ insertBRElement = !!editableBlockElement;
+ }
+ if (insertBRElement) {
+ // We are at a block boundary. Insert a <br>. Why? Well, first note
+ // that the br will have no visible effect since it is up against a
+ // block boundary. |foo<br><p>bar| renders like |foo<p>bar| and
+ // similarly |<p>foo<br></p>bar| renders like |<p>foo</p>bar|. What
+ // this <br> addition gets us is the ability to convert a trailing
+ // nbsp to a space. Consider: |<body>foo. '</body>|, where '
+ // represents selection. User types space attempting to put 2 spaces
+ // after the end of their sentence. We used to do this as:
+ // |<body>foo. &nbsp</body>| This caused problems with soft wrapping:
+ // the nbsp would wrap to the next line, which looked attrocious. If
+ // you try to do: |<body>foo.&nbsp </body>| instead, the trailing
+ // space is invisible because it is against a block boundary. If you
+ // do:
+ // |<body>foo.&nbsp&nbsp</body>| then you get an even uglier soft
+ // wrapping problem, where foo is on one line until you type the final
+ // space, and then "foo " jumps down to the next line. Ugh. The
+ // best way I can find out of this is to throw in a harmless <br>
+ // here, which allows us to do: |<body>foo.&nbsp <br></body>|, which
+ // doesn't cause foo to jump lines, doesn't cause spaces to show up at
+ // the beginning of soft wrapped lines, and lets the user see 2 spaces
+ // when they type 2 spaces.
+
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ aHTMLEditor.InsertBRElement(WithTransaction::Yes,
+ atEndOfVisibleWhiteSpaces);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode());
+ // Ignore caret suggestion because the caller must want to restore
+ // `Selection` due to the purpose of this method.
+ insertBRElementResult.unwrap().IgnoreCaretPointSuggestion();
+
+ atPreviousCharOfEndOfVisibleWhiteSpaces =
+ textFragmentData.GetPreviousEditableCharPoint(
+ atEndOfVisibleWhiteSpaces);
+ if (NS_WARN_IF(!atPreviousCharOfEndOfVisibleWhiteSpaces.IsSet())) {
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces =
+ textFragmentData.GetPreviousEditableCharPoint(
+ atPreviousCharOfEndOfVisibleWhiteSpaces);
+ isPreviousCharCollapsibleASCIIWhiteSpace =
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces.IsSet() &&
+ !atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces
+ .IsEndOfContainer() &&
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces
+ .IsCharCollapsibleASCIISpace();
+ followedByBRElement = true;
+ followedByVisibleContent = followedByPreformattedLineBreak = false;
+ }
+ }
+
+ // Once insert a <br>, the remaining work is only for normalizing
+ // white-space sequence in white-space collapsible text node.
+ // So, if the the text node's white-spaces are preformatted, we need
+ // to do nothing anymore.
+ if (EditorUtils::IsWhiteSpacePreformatted(
+ *atPreviousCharOfEndOfVisibleWhiteSpaces.ContainerAs<Text>())) {
+ return NS_OK;
+ }
+
+ // Next, replace the NBSP with an ASCII white-space if it's surrounded
+ // by visible contents (or immediately before a <br> element).
+ // However, if it follows or is followed by a preformatted linefeed,
+ // we shouldn't do this because an ASCII white-space will be collapsed
+ // **into** the linefeed.
+ if (maybeNBSPFollowsVisibleContent &&
+ (followedByVisibleContent || followedByBRElement) &&
+ !visibleWhiteSpaces.StartsFromPreformattedLineBreak()) {
+ MOZ_ASSERT(!followedByPreformattedLineBreak);
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ aHTMLEditor.ReplaceTextWithTransaction(
+ MOZ_KnownLive(*atPreviousCharOfEndOfVisibleWhiteSpaces
+ .ContainerAs<Text>()),
+ atPreviousCharOfEndOfVisibleWhiteSpaces.Offset(), 1, u" "_ns);
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
+ return replaceTextResult.propagateErr();
+ }
+ // Ignore caret suggestion because the caller must want to restore
+ // `Selection` due to the purpose of this method.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+ return NS_OK;
+ }
+ }
+ // If the text node is not preformatted, and the NBSP is followed by a <br>
+ // element and following (maybe multiple) collapsible ASCII white-spaces,
+ // remove the NBSP, but inserts a NBSP before the spaces. This makes a line
+ // break opportunity to wrap the line.
+ // XXX This is different behavior from Blink. Blink generates pairs of
+ // an NBSP and an ASCII white-space, but put NBSP at the end of the
+ // sequence. We should follow the behavior for web-compat.
+ if (maybeNBSPFollowsVisibleContent ||
+ !isPreviousCharCollapsibleASCIIWhiteSpace ||
+ !(followedByVisibleContent || followedByBRElement) ||
+ EditorUtils::IsWhiteSpacePreformatted(
+ *atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces
+ .ContainerAs<Text>())) {
+ return NS_OK;
+ }
+
+ // Currently, we're at an NBSP following an ASCII space, and we need to
+ // replace them with `"&nbsp; "` for avoiding collapsing white-spaces.
+ MOZ_ASSERT(!atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces
+ .IsEndOfContainer());
+ const EditorDOMPointInText atFirstASCIIWhiteSpace =
+ textFragmentData.GetFirstASCIIWhiteSpacePointCollapsedTo(
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces,
+ nsIEditor::eNone);
+ uint32_t numberOfASCIIWhiteSpacesInStartNode =
+ atFirstASCIIWhiteSpace.ContainerAs<Text>() ==
+ atPreviousCharOfEndOfVisibleWhiteSpaces.ContainerAs<Text>()
+ ? atPreviousCharOfEndOfVisibleWhiteSpaces.Offset() -
+ atFirstASCIIWhiteSpace.Offset()
+ : atFirstASCIIWhiteSpace.ContainerAs<Text>()->Length() -
+ atFirstASCIIWhiteSpace.Offset();
+ // Replace all preceding ASCII white-spaces **and** the NBSP.
+ uint32_t replaceLengthInStartNode =
+ numberOfASCIIWhiteSpacesInStartNode +
+ (atFirstASCIIWhiteSpace.ContainerAs<Text>() ==
+ atPreviousCharOfEndOfVisibleWhiteSpaces.ContainerAs<Text>()
+ ? 1
+ : 0);
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ aHTMLEditor.ReplaceTextWithTransaction(
+ MOZ_KnownLive(*atFirstASCIIWhiteSpace.ContainerAs<Text>()),
+ atFirstASCIIWhiteSpace.Offset(), replaceLengthInStartNode,
+ textFragmentData.StartsFromPreformattedLineBreak() &&
+ textFragmentData.EndsByPreformattedLineBreak()
+ ? u"\x00A0\x00A0"_ns
+ : (textFragmentData.EndsByPreformattedLineBreak()
+ ? u" \x00A0"_ns
+ : u"\x00A0 "_ns));
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
+ return replaceTextResult.propagateErr();
+ }
+ // Ignore caret suggestion because the caller must want to restore
+ // `Selection` due to the purpose of this method.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+
+ if (atFirstASCIIWhiteSpace.GetContainer() ==
+ atPreviousCharOfEndOfVisibleWhiteSpaces.GetContainer()) {
+ return NS_OK;
+ }
+
+ // We need to remove the following unnecessary ASCII white-spaces and
+ // NBSP at atPreviousCharOfEndOfVisibleWhiteSpaces because we collapsed them
+ // into the start node.
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ EditorDOMPointInText::AtEndOf(
+ *atFirstASCIIWhiteSpace.ContainerAs<Text>()),
+ atPreviousCharOfEndOfVisibleWhiteSpaces.NextPoint(),
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ // Ignore caret suggestion because the caller must want to restore
+ // `Selection` due to the purpose of this method. }
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ return NS_OK;
+ }
+
+ // XXX This is called when top-level edit sub-action handling ends for
+ // 3 points at most. However, this is not compatible with Blink.
+ // Blink touches white-space sequence which includes new character
+ // or following white-space sequence of new <br> element or, if and
+ // only if deleting range is followed by white-space sequence (i.e.,
+ // not touched previous white-space sequence of deleting range).
+ // This should be done when we change to make each edit action
+ // handler directly normalize white-space sequence rather than
+ // OnEndHandlingTopLevelEditSucAction().
+
+ // First, check if the last character is an NBSP. Otherwise, we don't need
+ // to do nothing here.
+ const EditorDOMPoint& atEndOfVisibleWhiteSpaces = visibleWhiteSpaces.EndRef();
+ const EditorDOMPointInText atPreviousCharOfEndOfVisibleWhiteSpaces =
+ textFragmentData.GetPreviousEditableCharPoint(atEndOfVisibleWhiteSpaces);
+ if (!atPreviousCharOfEndOfVisibleWhiteSpaces.IsSet() ||
+ atPreviousCharOfEndOfVisibleWhiteSpaces.IsEndOfContainer() ||
+ !atPreviousCharOfEndOfVisibleWhiteSpaces.IsCharCollapsibleNBSP() ||
+ // If the next character of the NBSP is a preformatted linefeed, we
+ // shouldn't replace it with an ASCII white-space for avoiding collapsed
+ // into the linefeed.
+ visibleWhiteSpaces.EndsByPreformattedLineBreak()) {
+ return NS_OK;
+ }
+
+ // Next, consider the range to collapse ASCII white-spaces before there.
+ EditorDOMPointInText startToDelete, endToDelete;
+
+ const EditorDOMPointInText
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces =
+ textFragmentData.GetPreviousEditableCharPoint(
+ atPreviousCharOfEndOfVisibleWhiteSpaces);
+ // If there are some preceding ASCII white-spaces, we need to treat them
+ // as one white-space. I.e., we need to collapse them.
+ if (atPreviousCharOfEndOfVisibleWhiteSpaces.IsCharNBSP() &&
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces.IsSet() &&
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces
+ .IsCharCollapsibleASCIISpace()) {
+ startToDelete = textFragmentData.GetFirstASCIIWhiteSpacePointCollapsedTo(
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces,
+ nsIEditor::eNone);
+ endToDelete = atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces;
+ }
+ // Otherwise, we don't need to remove any white-spaces, but we may need
+ // to normalize the white-space sequence containing the previous NBSP.
+ else {
+ startToDelete = endToDelete =
+ atPreviousCharOfEndOfVisibleWhiteSpaces.NextPoint();
+ }
+
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndNormalizeSurroundingWhiteSpaces(
+ startToDelete, endToDelete,
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries,
+ HTMLEditor::DeleteDirection::Forward);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpace() failed");
+ return caretPointOrError.unwrapErr();
+ }
+ // Ignore caret suggestion because the caller must want to restore
+ // `Selection` due to the purpose of this method.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+EditorDOMPointInText WSRunScanner::TextFragmentData::
+ GetPreviousNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
+ const EditorDOMPoint& aPointToInsert) const {
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+ MOZ_ASSERT(VisibleWhiteSpacesDataRef().IsInitialized());
+ NS_ASSERTION(VisibleWhiteSpacesDataRef().ComparePoint(aPointToInsert) ==
+ PointPosition::MiddleOfFragment ||
+ VisibleWhiteSpacesDataRef().ComparePoint(aPointToInsert) ==
+ PointPosition::EndOfFragment,
+ "Previous char of aPoint should be in the visible white-spaces");
+
+ // Try to change an NBSP to a space, if possible, just to prevent NBSP
+ // proliferation. This routine is called when we are about to make this
+ // point in the ws abut an inserted break or text, so we don't have to worry
+ // about what is after it. What is after it now will end up after the
+ // inserted object.
+ const EditorDOMPointInText atPreviousChar =
+ GetPreviousEditableCharPoint(aPointToInsert);
+ if (!atPreviousChar.IsSet() || atPreviousChar.IsEndOfContainer() ||
+ !atPreviousChar.IsCharNBSP() ||
+ EditorUtils::IsWhiteSpacePreformatted(
+ *atPreviousChar.ContainerAs<Text>())) {
+ return EditorDOMPointInText();
+ }
+
+ const EditorDOMPointInText atPreviousCharOfPreviousChar =
+ GetPreviousEditableCharPoint(atPreviousChar);
+ if (atPreviousCharOfPreviousChar.IsSet()) {
+ // If the previous char is in different text node and it's preformatted,
+ // we shouldn't touch it.
+ if (atPreviousChar.ContainerAs<Text>() !=
+ atPreviousCharOfPreviousChar.ContainerAs<Text>() &&
+ EditorUtils::IsWhiteSpacePreformatted(
+ *atPreviousCharOfPreviousChar.ContainerAs<Text>())) {
+ return EditorDOMPointInText();
+ }
+ // If the previous char of the NBSP at previous position of aPointToInsert
+ // is an ASCII white-space, we don't need to replace it with same character.
+ if (!atPreviousCharOfPreviousChar.IsEndOfContainer() &&
+ atPreviousCharOfPreviousChar.IsCharASCIISpace()) {
+ return EditorDOMPointInText();
+ }
+ return atPreviousChar;
+ }
+
+ // If previous content of the NBSP is block boundary, we cannot replace the
+ // NBSP with an ASCII white-space to keep it rendered.
+ const VisibleWhiteSpacesData& visibleWhiteSpaces =
+ VisibleWhiteSpacesDataRef();
+ if (!visibleWhiteSpaces.StartsFromNonCollapsibleCharacters() &&
+ !visibleWhiteSpaces.StartsFromSpecialContent()) {
+ return EditorDOMPointInText();
+ }
+ return atPreviousChar;
+}
+
+EditorDOMPointInText WSRunScanner::TextFragmentData::
+ GetInclusiveNextNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
+ const EditorDOMPoint& aPointToInsert) const {
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+ MOZ_ASSERT(VisibleWhiteSpacesDataRef().IsInitialized());
+ NS_ASSERTION(VisibleWhiteSpacesDataRef().ComparePoint(aPointToInsert) ==
+ PointPosition::StartOfFragment ||
+ VisibleWhiteSpacesDataRef().ComparePoint(aPointToInsert) ==
+ PointPosition::MiddleOfFragment,
+ "Inclusive next char of aPointToInsert should be in the visible "
+ "white-spaces");
+
+ // Try to change an nbsp to a space, if possible, just to prevent nbsp
+ // proliferation This routine is called when we are about to make this point
+ // in the ws abut an inserted text, so we don't have to worry about what is
+ // before it. What is before it now will end up before the inserted text.
+ auto atNextChar =
+ GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(aPointToInsert);
+ if (!atNextChar.IsSet() || NS_WARN_IF(atNextChar.IsEndOfContainer()) ||
+ !atNextChar.IsCharNBSP() ||
+ EditorUtils::IsWhiteSpacePreformatted(*atNextChar.ContainerAs<Text>())) {
+ return EditorDOMPointInText();
+ }
+
+ const auto atNextCharOfNextCharOfNBSP =
+ GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(
+ atNextChar.NextPoint<EditorRawDOMPointInText>());
+ if (atNextCharOfNextCharOfNBSP.IsSet()) {
+ // If the next char is in different text node and it's preformatted,
+ // we shouldn't touch it.
+ if (atNextChar.ContainerAs<Text>() !=
+ atNextCharOfNextCharOfNBSP.ContainerAs<Text>() &&
+ EditorUtils::IsWhiteSpacePreformatted(
+ *atNextCharOfNextCharOfNBSP.ContainerAs<Text>())) {
+ return EditorDOMPointInText();
+ }
+ // If following character of an NBSP is an ASCII white-space, we don't
+ // need to replace it with same character.
+ if (!atNextCharOfNextCharOfNBSP.IsEndOfContainer() &&
+ atNextCharOfNextCharOfNBSP.IsCharASCIISpace()) {
+ return EditorDOMPointInText();
+ }
+ return atNextChar;
+ }
+
+ // If the NBSP is last character in the hard line, we don't need to
+ // replace it because it's required to render multiple white-spaces.
+ const VisibleWhiteSpacesData& visibleWhiteSpaces =
+ VisibleWhiteSpacesDataRef();
+ if (!visibleWhiteSpaces.EndsByNonCollapsibleCharacters() &&
+ !visibleWhiteSpaces.EndsBySpecialContent() &&
+ !visibleWhiteSpaces.EndsByBRElement()) {
+ return EditorDOMPointInText();
+ }
+
+ return atNextChar;
+}
+
+// static
+Result<CaretPoint, nsresult>
+WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint) {
+ MOZ_ASSERT(aPoint.IsSet());
+ Element* editingHost = aHTMLEditor.ComputeEditingHost();
+ TextFragmentData textFragmentData(aPoint, editingHost);
+ if (NS_WARN_IF(!textFragmentData.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ const EditorDOMRange& leadingWhiteSpaceRange =
+ textFragmentData.InvisibleLeadingWhiteSpaceRangeRef();
+ // XXX Getting trailing white-space range now must be wrong because
+ // mutation event listener may invalidate it.
+ const EditorDOMRange& trailingWhiteSpaceRange =
+ textFragmentData.InvisibleTrailingWhiteSpaceRangeRef();
+ EditorDOMPoint pointToPutCaret;
+ DebugOnly<bool> leadingWhiteSpacesDeleted = false;
+ if (leadingWhiteSpaceRange.IsPositioned() &&
+ !leadingWhiteSpaceRange.Collapsed()) {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ leadingWhiteSpaceRange.StartRef(), leadingWhiteSpaceRange.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ leadingWhiteSpacesDeleted = true;
+ }
+ if (trailingWhiteSpaceRange.IsPositioned() &&
+ !trailingWhiteSpaceRange.Collapsed() &&
+ leadingWhiteSpaceRange != trailingWhiteSpaceRange) {
+ NS_ASSERTION(!leadingWhiteSpacesDeleted,
+ "We're trying to remove trailing white-spaces with maybe "
+ "outdated range");
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ trailingWhiteSpaceRange.StartRef(),
+ trailingWhiteSpaceRange.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ return CaretPoint(std::move(pointToPutCaret));
+}
+
+/*****************************************************************************
+ * Implementation for new white-space normalizer
+ *****************************************************************************/
+
+// static
+EditorDOMRangeInTexts
+WSRunScanner::ComputeRangeInTextNodesContainingInvisibleWhiteSpaces(
+ const TextFragmentData& aStart, const TextFragmentData& aEnd) {
+ // Corresponding to handling invisible white-spaces part of
+ // `TextFragmentData::GetReplaceRangeDataAtEndOfDeletionRange()` and
+ // `TextFragmentData::GetReplaceRangeDataAtStartOfDeletionRange()`
+
+ MOZ_ASSERT(aStart.ScanStartRef().IsSetAndValid());
+ MOZ_ASSERT(aEnd.ScanStartRef().IsSetAndValid());
+ MOZ_ASSERT(aStart.ScanStartRef().EqualsOrIsBefore(aEnd.ScanStartRef()));
+ MOZ_ASSERT(aStart.ScanStartRef().IsInTextNode());
+ MOZ_ASSERT(aEnd.ScanStartRef().IsInTextNode());
+
+ // XXX `GetReplaceRangeDataAtEndOfDeletionRange()` and
+ // `GetReplaceRangeDataAtStartOfDeletionRange()` use
+ // `GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt()` and
+ // `GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt()`.
+ // However, they are really odd as mentioned with "XXX" comments
+ // in them. For the new white-space normalizer, we need to treat
+ // invisible white-spaces stricter because the legacy path handles
+ // white-spaces multiple times (e.g., calling `HTMLEditor::
+ // DeleteNodeIfInvisibleAndEditableTextNode()` later) and that hides
+ // the bug, but in the new path, we should stop doing same things
+ // multiple times for both performance and footprint. Therefore,
+ // even though the result might be different in some edge cases,
+ // we should use clean path for now. Perhaps, we should fix the odd
+ // cases before shipping `beforeinput` event in release channel.
+
+ const EditorDOMRange& invisibleLeadingWhiteSpaceRange =
+ aStart.InvisibleLeadingWhiteSpaceRangeRef();
+ const EditorDOMRange& invisibleTrailingWhiteSpaceRange =
+ aEnd.InvisibleTrailingWhiteSpaceRangeRef();
+ const bool hasInvisibleLeadingWhiteSpaces =
+ invisibleLeadingWhiteSpaceRange.IsPositioned() &&
+ !invisibleLeadingWhiteSpaceRange.Collapsed();
+ const bool hasInvisibleTrailingWhiteSpaces =
+ invisibleLeadingWhiteSpaceRange != invisibleTrailingWhiteSpaceRange &&
+ invisibleTrailingWhiteSpaceRange.IsPositioned() &&
+ !invisibleTrailingWhiteSpaceRange.Collapsed();
+
+ EditorDOMRangeInTexts result(aStart.ScanStartRef().AsInText(),
+ aEnd.ScanStartRef().AsInText());
+ MOZ_ASSERT(result.IsPositionedAndValid());
+ if (!hasInvisibleLeadingWhiteSpaces && !hasInvisibleTrailingWhiteSpaces) {
+ return result;
+ }
+
+ MOZ_ASSERT_IF(
+ hasInvisibleLeadingWhiteSpaces && hasInvisibleTrailingWhiteSpaces,
+ invisibleLeadingWhiteSpaceRange.StartRef().IsBefore(
+ invisibleTrailingWhiteSpaceRange.StartRef()));
+ const EditorDOMPoint& aroundFirstInvisibleWhiteSpace =
+ hasInvisibleLeadingWhiteSpaces
+ ? invisibleLeadingWhiteSpaceRange.StartRef()
+ : invisibleTrailingWhiteSpaceRange.StartRef();
+ if (aroundFirstInvisibleWhiteSpace.IsBefore(result.StartRef())) {
+ if (aroundFirstInvisibleWhiteSpace.IsInTextNode()) {
+ result.SetStart(aroundFirstInvisibleWhiteSpace.AsInText());
+ MOZ_ASSERT(result.IsPositionedAndValid());
+ } else {
+ const auto atFirstInvisibleWhiteSpace =
+ hasInvisibleLeadingWhiteSpaces
+ ? aStart.GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(
+ aroundFirstInvisibleWhiteSpace)
+ : aEnd.GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(
+ aroundFirstInvisibleWhiteSpace);
+ MOZ_ASSERT(atFirstInvisibleWhiteSpace.IsSet());
+ MOZ_ASSERT(
+ atFirstInvisibleWhiteSpace.EqualsOrIsBefore(result.StartRef()));
+ result.SetStart(atFirstInvisibleWhiteSpace);
+ MOZ_ASSERT(result.IsPositionedAndValid());
+ }
+ }
+ MOZ_ASSERT_IF(
+ hasInvisibleLeadingWhiteSpaces && hasInvisibleTrailingWhiteSpaces,
+ invisibleLeadingWhiteSpaceRange.EndRef().IsBefore(
+ invisibleTrailingWhiteSpaceRange.EndRef()));
+ const EditorDOMPoint& afterLastInvisibleWhiteSpace =
+ hasInvisibleTrailingWhiteSpaces
+ ? invisibleTrailingWhiteSpaceRange.EndRef()
+ : invisibleLeadingWhiteSpaceRange.EndRef();
+ if (afterLastInvisibleWhiteSpace.EqualsOrIsBefore(result.EndRef())) {
+ MOZ_ASSERT(result.IsPositionedAndValid());
+ return result;
+ }
+ if (afterLastInvisibleWhiteSpace.IsInTextNode()) {
+ result.SetEnd(afterLastInvisibleWhiteSpace.AsInText());
+ MOZ_ASSERT(result.IsPositionedAndValid());
+ return result;
+ }
+ const auto atLastInvisibleWhiteSpace =
+ hasInvisibleTrailingWhiteSpaces
+ ? aEnd.GetPreviousEditableCharPoint(afterLastInvisibleWhiteSpace)
+ : aStart.GetPreviousEditableCharPoint(afterLastInvisibleWhiteSpace);
+ MOZ_ASSERT(atLastInvisibleWhiteSpace.IsSet());
+ MOZ_ASSERT(atLastInvisibleWhiteSpace.IsContainerEmpty() ||
+ atLastInvisibleWhiteSpace.IsAtLastContent());
+ MOZ_ASSERT(result.EndRef().EqualsOrIsBefore(atLastInvisibleWhiteSpace));
+ result.SetEnd(atLastInvisibleWhiteSpace.IsEndOfContainer()
+ ? atLastInvisibleWhiteSpace
+ : atLastInvisibleWhiteSpace.NextPoint());
+ MOZ_ASSERT(result.IsPositionedAndValid());
+ return result;
+}
+
+// static
+Result<EditorDOMRangeInTexts, nsresult>
+WSRunScanner::GetRangeInTextNodesToBackspaceFrom(const EditorDOMPoint& aPoint,
+ const Element& aEditingHost) {
+ // Corresponding to computing delete range part of
+ // `WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace()`
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ TextFragmentData textFragmentDataAtCaret(aPoint, &aEditingHost);
+ if (NS_WARN_IF(!textFragmentDataAtCaret.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ EditorDOMPointInText atPreviousChar =
+ textFragmentDataAtCaret.GetPreviousEditableCharPoint(aPoint);
+ if (!atPreviousChar.IsSet()) {
+ return EditorDOMRangeInTexts(); // There is no content in the block.
+ }
+
+ // XXX When previous char point is in an empty text node, we do nothing,
+ // but this must look odd from point of user view. We should delete
+ // something before aPoint.
+ if (atPreviousChar.IsEndOfContainer()) {
+ return EditorDOMRangeInTexts();
+ }
+
+ // Extend delete range if previous char is a low surrogate following
+ // a high surrogate.
+ EditorDOMPointInText atNextChar = atPreviousChar.NextPoint();
+ if (!atPreviousChar.IsStartOfContainer()) {
+ if (atPreviousChar.IsCharLowSurrogateFollowingHighSurrogate()) {
+ atPreviousChar = atPreviousChar.PreviousPoint();
+ }
+ // If caret is in middle of a surrogate pair, delete the surrogate pair
+ // (blink-compat).
+ else if (atPreviousChar.IsCharHighSurrogateFollowedByLowSurrogate()) {
+ atNextChar = atNextChar.NextPoint();
+ }
+ }
+
+ // If previous char is an collapsible white-spaces, delete all adjcent
+ // white-spaces which are collapsed together.
+ EditorDOMRangeInTexts rangeToDelete;
+ if (atPreviousChar.IsCharCollapsibleASCIISpace() ||
+ atPreviousChar.IsCharPreformattedNewLineCollapsedWithWhiteSpaces()) {
+ const EditorDOMPointInText startToDelete =
+ textFragmentDataAtCaret.GetFirstASCIIWhiteSpacePointCollapsedTo(
+ atPreviousChar, nsIEditor::ePrevious);
+ if (!startToDelete.IsSet()) {
+ NS_WARNING(
+ "WSRunScanner::GetFirstASCIIWhiteSpacePointCollapsedTo() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ const EditorDOMPointInText endToDelete =
+ textFragmentDataAtCaret.GetEndOfCollapsibleASCIIWhiteSpaces(
+ atPreviousChar, nsIEditor::ePrevious);
+ if (!endToDelete.IsSet()) {
+ NS_WARNING("WSRunScanner::GetEndOfCollapsibleASCIIWhiteSpaces() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ rangeToDelete = EditorDOMRangeInTexts(startToDelete, endToDelete);
+ }
+ // if previous char is not a collapsible white-space, remove it.
+ else {
+ rangeToDelete = EditorDOMRangeInTexts(atPreviousChar, atNextChar);
+ }
+
+ // If there is no removable and visible content, we should do nothing.
+ if (rangeToDelete.Collapsed()) {
+ return EditorDOMRangeInTexts();
+ }
+
+ // And also delete invisible white-spaces if they become visible.
+ TextFragmentData textFragmentDataAtStart =
+ rangeToDelete.StartRef() != aPoint
+ ? TextFragmentData(rangeToDelete.StartRef(), &aEditingHost)
+ : textFragmentDataAtCaret;
+ TextFragmentData textFragmentDataAtEnd =
+ rangeToDelete.EndRef() != aPoint
+ ? TextFragmentData(rangeToDelete.EndRef(), &aEditingHost)
+ : textFragmentDataAtCaret;
+ if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized()) ||
+ NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ EditorDOMRangeInTexts extendedRangeToDelete =
+ WSRunScanner::ComputeRangeInTextNodesContainingInvisibleWhiteSpaces(
+ textFragmentDataAtStart, textFragmentDataAtEnd);
+ MOZ_ASSERT(extendedRangeToDelete.IsPositionedAndValid());
+ return extendedRangeToDelete.IsPositioned() ? extendedRangeToDelete
+ : rangeToDelete;
+}
+
+// static
+Result<EditorDOMRangeInTexts, nsresult>
+WSRunScanner::GetRangeInTextNodesToForwardDeleteFrom(
+ const EditorDOMPoint& aPoint, const Element& aEditingHost) {
+ // Corresponding to computing delete range part of
+ // `WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace()`
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ TextFragmentData textFragmentDataAtCaret(aPoint, &aEditingHost);
+ if (NS_WARN_IF(!textFragmentDataAtCaret.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ auto atCaret =
+ textFragmentDataAtCaret
+ .GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(aPoint);
+ if (!atCaret.IsSet()) {
+ return EditorDOMRangeInTexts(); // There is no content in the block.
+ }
+ // If caret is in middle of a surrogate pair, we should remove next
+ // character (blink-compat).
+ if (!atCaret.IsEndOfContainer() &&
+ atCaret.IsCharLowSurrogateFollowingHighSurrogate()) {
+ atCaret = atCaret.NextPoint();
+ }
+
+ // XXX When next char point is in an empty text node, we do nothing,
+ // but this must look odd from point of user view. We should delete
+ // something after aPoint.
+ if (atCaret.IsEndOfContainer()) {
+ return EditorDOMRangeInTexts();
+ }
+
+ // Extend delete range if previous char is a low surrogate following
+ // a high surrogate.
+ EditorDOMPointInText atNextChar = atCaret.NextPoint();
+ if (atCaret.IsCharHighSurrogateFollowedByLowSurrogate()) {
+ atNextChar = atNextChar.NextPoint();
+ }
+
+ // If next char is a collapsible white-space, delete all adjcent white-spaces
+ // which are collapsed together.
+ EditorDOMRangeInTexts rangeToDelete;
+ if (atCaret.IsCharCollapsibleASCIISpace() ||
+ atCaret.IsCharPreformattedNewLineCollapsedWithWhiteSpaces()) {
+ const EditorDOMPointInText startToDelete =
+ textFragmentDataAtCaret.GetFirstASCIIWhiteSpacePointCollapsedTo(
+ atCaret, nsIEditor::eNext);
+ if (!startToDelete.IsSet()) {
+ NS_WARNING(
+ "WSRunScanner::GetFirstASCIIWhiteSpacePointCollapsedTo() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ const EditorDOMPointInText endToDelete =
+ textFragmentDataAtCaret.GetEndOfCollapsibleASCIIWhiteSpaces(
+ atCaret, nsIEditor::eNext);
+ if (!endToDelete.IsSet()) {
+ NS_WARNING("WSRunScanner::GetEndOfCollapsibleASCIIWhiteSpaces() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ rangeToDelete = EditorDOMRangeInTexts(startToDelete, endToDelete);
+ }
+ // if next char is not a collapsible white-space, remove it.
+ else {
+ rangeToDelete = EditorDOMRangeInTexts(atCaret, atNextChar);
+ }
+
+ // If there is no removable and visible content, we should do nothing.
+ if (rangeToDelete.Collapsed()) {
+ return EditorDOMRangeInTexts();
+ }
+
+ // And also delete invisible white-spaces if they become visible.
+ TextFragmentData textFragmentDataAtStart =
+ rangeToDelete.StartRef() != aPoint
+ ? TextFragmentData(rangeToDelete.StartRef(), &aEditingHost)
+ : textFragmentDataAtCaret;
+ TextFragmentData textFragmentDataAtEnd =
+ rangeToDelete.EndRef() != aPoint
+ ? TextFragmentData(rangeToDelete.EndRef(), &aEditingHost)
+ : textFragmentDataAtCaret;
+ if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized()) ||
+ NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ EditorDOMRangeInTexts extendedRangeToDelete =
+ WSRunScanner::ComputeRangeInTextNodesContainingInvisibleWhiteSpaces(
+ textFragmentDataAtStart, textFragmentDataAtEnd);
+ MOZ_ASSERT(extendedRangeToDelete.IsPositionedAndValid());
+ return extendedRangeToDelete.IsPositioned() ? extendedRangeToDelete
+ : rangeToDelete;
+}
+
+// static
+EditorDOMRange WSRunScanner::GetRangesForDeletingAtomicContent(
+ Element* aEditingHost, const nsIContent& aAtomicContent) {
+ if (aAtomicContent.IsHTMLElement(nsGkAtoms::br)) {
+ // Preceding white-spaces should be preserved, but the following
+ // white-spaces should be invisible around `<br>` element.
+ TextFragmentData textFragmentDataAfterBRElement(
+ EditorDOMPoint::After(aAtomicContent), aEditingHost);
+ if (NS_WARN_IF(!textFragmentDataAfterBRElement.IsInitialized())) {
+ return EditorDOMRange(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRangeInTexts followingInvisibleWhiteSpaces =
+ textFragmentDataAfterBRElement.GetNonCollapsedRangeInTexts(
+ textFragmentDataAfterBRElement
+ .InvisibleLeadingWhiteSpaceRangeRef());
+ return followingInvisibleWhiteSpaces.IsPositioned() &&
+ !followingInvisibleWhiteSpaces.Collapsed()
+ ? EditorDOMRange(
+ EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
+ followingInvisibleWhiteSpaces.EndRef())
+ : EditorDOMRange(
+ EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
+ EditorDOMPoint::After(aAtomicContent));
+ }
+
+ if (!HTMLEditUtils::IsBlockElement(aAtomicContent)) {
+ // Both preceding and following white-spaces around it should be preserved
+ // around inline elements like `<img>`.
+ return EditorDOMRange(
+ EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
+ EditorDOMPoint::After(aAtomicContent));
+ }
+
+ // Both preceding and following white-spaces can be invisible around a
+ // block element.
+ TextFragmentData textFragmentDataBeforeAtomicContent(
+ EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)), aEditingHost);
+ if (NS_WARN_IF(!textFragmentDataBeforeAtomicContent.IsInitialized())) {
+ return EditorDOMRange(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRangeInTexts precedingInvisibleWhiteSpaces =
+ textFragmentDataBeforeAtomicContent.GetNonCollapsedRangeInTexts(
+ textFragmentDataBeforeAtomicContent
+ .InvisibleTrailingWhiteSpaceRangeRef());
+ TextFragmentData textFragmentDataAfterAtomicContent(
+ EditorDOMPoint::After(aAtomicContent), aEditingHost);
+ if (NS_WARN_IF(!textFragmentDataAfterAtomicContent.IsInitialized())) {
+ return EditorDOMRange(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRangeInTexts followingInvisibleWhiteSpaces =
+ textFragmentDataAfterAtomicContent.GetNonCollapsedRangeInTexts(
+ textFragmentDataAfterAtomicContent
+ .InvisibleLeadingWhiteSpaceRangeRef());
+ if (precedingInvisibleWhiteSpaces.StartRef().IsSet() &&
+ followingInvisibleWhiteSpaces.EndRef().IsSet()) {
+ return EditorDOMRange(precedingInvisibleWhiteSpaces.StartRef(),
+ followingInvisibleWhiteSpaces.EndRef());
+ }
+ if (precedingInvisibleWhiteSpaces.StartRef().IsSet()) {
+ return EditorDOMRange(precedingInvisibleWhiteSpaces.StartRef(),
+ EditorDOMPoint::After(aAtomicContent));
+ }
+ if (followingInvisibleWhiteSpaces.EndRef().IsSet()) {
+ return EditorDOMRange(
+ EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
+ followingInvisibleWhiteSpaces.EndRef());
+ }
+ return EditorDOMRange(
+ EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
+ EditorDOMPoint::After(aAtomicContent));
+}
+
+// static
+EditorDOMRange WSRunScanner::GetRangeForDeletingBlockElementBoundaries(
+ const HTMLEditor& aHTMLEditor, const Element& aLeftBlockElement,
+ const Element& aRightBlockElement,
+ const EditorDOMPoint& aPointContainingTheOtherBlock) {
+ MOZ_ASSERT(&aLeftBlockElement != &aRightBlockElement);
+ MOZ_ASSERT_IF(
+ aPointContainingTheOtherBlock.IsSet(),
+ aPointContainingTheOtherBlock.GetContainer() == &aLeftBlockElement ||
+ aPointContainingTheOtherBlock.GetContainer() == &aRightBlockElement);
+ MOZ_ASSERT_IF(
+ aPointContainingTheOtherBlock.GetContainer() == &aLeftBlockElement,
+ aRightBlockElement.IsInclusiveDescendantOf(
+ aPointContainingTheOtherBlock.GetChild()));
+ MOZ_ASSERT_IF(
+ aPointContainingTheOtherBlock.GetContainer() == &aRightBlockElement,
+ aLeftBlockElement.IsInclusiveDescendantOf(
+ aPointContainingTheOtherBlock.GetChild()));
+ MOZ_ASSERT_IF(
+ !aPointContainingTheOtherBlock.IsSet(),
+ !aRightBlockElement.IsInclusiveDescendantOf(&aLeftBlockElement));
+ MOZ_ASSERT_IF(
+ !aPointContainingTheOtherBlock.IsSet(),
+ !aLeftBlockElement.IsInclusiveDescendantOf(&aRightBlockElement));
+ MOZ_ASSERT_IF(!aPointContainingTheOtherBlock.IsSet(),
+ EditorRawDOMPoint(const_cast<Element*>(&aLeftBlockElement))
+ .IsBefore(EditorRawDOMPoint(
+ const_cast<Element*>(&aRightBlockElement))));
+
+ const Element* editingHost = aHTMLEditor.ComputeEditingHost();
+
+ EditorDOMRange range;
+ // Include trailing invisible white-spaces in aLeftBlockElement.
+ TextFragmentData textFragmentDataAtEndOfLeftBlockElement(
+ aPointContainingTheOtherBlock.GetContainer() == &aLeftBlockElement
+ ? aPointContainingTheOtherBlock
+ : EditorDOMPoint::AtEndOf(const_cast<Element&>(aLeftBlockElement)),
+ editingHost);
+ if (NS_WARN_IF(!textFragmentDataAtEndOfLeftBlockElement.IsInitialized())) {
+ return EditorDOMRange(); // TODO: Make here return error with Err.
+ }
+ if (textFragmentDataAtEndOfLeftBlockElement.StartsFromInvisibleBRElement()) {
+ // If the left block element ends with an invisible `<br>` element,
+ // it'll be deleted (and it means there is no invisible trailing
+ // white-spaces). Therefore, the range should start from the invisible
+ // `<br>` element.
+ range.SetStart(EditorDOMPoint(
+ textFragmentDataAtEndOfLeftBlockElement.StartReasonBRElementPtr()));
+ } else {
+ const EditorDOMRange& trailingWhiteSpaceRange =
+ textFragmentDataAtEndOfLeftBlockElement
+ .InvisibleTrailingWhiteSpaceRangeRef();
+ if (trailingWhiteSpaceRange.StartRef().IsSet()) {
+ range.SetStart(trailingWhiteSpaceRange.StartRef());
+ } else {
+ range.SetStart(textFragmentDataAtEndOfLeftBlockElement.ScanStartRef());
+ }
+ }
+ // Include leading invisible white-spaces in aRightBlockElement.
+ TextFragmentData textFragmentDataAtStartOfRightBlockElement(
+ aPointContainingTheOtherBlock.GetContainer() == &aRightBlockElement &&
+ !aPointContainingTheOtherBlock.IsEndOfContainer()
+ ? aPointContainingTheOtherBlock.NextPoint()
+ : EditorDOMPoint(const_cast<Element*>(&aRightBlockElement), 0u),
+ editingHost);
+ if (NS_WARN_IF(!textFragmentDataAtStartOfRightBlockElement.IsInitialized())) {
+ return EditorDOMRange(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRange& leadingWhiteSpaceRange =
+ textFragmentDataAtStartOfRightBlockElement
+ .InvisibleLeadingWhiteSpaceRangeRef();
+ if (leadingWhiteSpaceRange.EndRef().IsSet()) {
+ range.SetEnd(leadingWhiteSpaceRange.EndRef());
+ } else {
+ range.SetEnd(textFragmentDataAtStartOfRightBlockElement.ScanStartRef());
+ }
+ return range;
+}
+
+// static
+EditorDOMRange
+WSRunScanner::GetRangeContainingInvisibleWhiteSpacesAtRangeBoundaries(
+ Element* aEditingHost, const EditorDOMRange& aRange) {
+ MOZ_ASSERT(aRange.IsPositionedAndValid());
+ MOZ_ASSERT(aRange.EndRef().IsSetAndValid());
+ MOZ_ASSERT(aRange.StartRef().IsSetAndValid());
+
+ EditorDOMRange result;
+ TextFragmentData textFragmentDataAtStart(aRange.StartRef(), aEditingHost);
+ if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized())) {
+ return EditorDOMRange(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRangeInTexts invisibleLeadingWhiteSpacesAtStart =
+ textFragmentDataAtStart.GetNonCollapsedRangeInTexts(
+ textFragmentDataAtStart.InvisibleLeadingWhiteSpaceRangeRef());
+ if (invisibleLeadingWhiteSpacesAtStart.IsPositioned() &&
+ !invisibleLeadingWhiteSpacesAtStart.Collapsed()) {
+ result.SetStart(invisibleLeadingWhiteSpacesAtStart.StartRef());
+ } else {
+ const EditorDOMRangeInTexts invisibleTrailingWhiteSpacesAtStart =
+ textFragmentDataAtStart.GetNonCollapsedRangeInTexts(
+ textFragmentDataAtStart.InvisibleTrailingWhiteSpaceRangeRef());
+ if (invisibleTrailingWhiteSpacesAtStart.IsPositioned() &&
+ !invisibleTrailingWhiteSpacesAtStart.Collapsed()) {
+ MOZ_ASSERT(
+ invisibleTrailingWhiteSpacesAtStart.StartRef().EqualsOrIsBefore(
+ aRange.StartRef()));
+ result.SetStart(invisibleTrailingWhiteSpacesAtStart.StartRef());
+ }
+ // If there is no invisible white-space and the line starts with a
+ // text node, shrink the range to start of the text node.
+ else if (!aRange.StartRef().IsInTextNode() &&
+ textFragmentDataAtStart.StartsFromBlockBoundary() &&
+ textFragmentDataAtStart.EndRef().IsInTextNode()) {
+ result.SetStart(textFragmentDataAtStart.EndRef());
+ }
+ }
+ if (!result.StartRef().IsSet()) {
+ result.SetStart(aRange.StartRef());
+ }
+
+ TextFragmentData textFragmentDataAtEnd(aRange.EndRef(), aEditingHost);
+ if (NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) {
+ return EditorDOMRange(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRangeInTexts invisibleLeadingWhiteSpacesAtEnd =
+ textFragmentDataAtEnd.GetNonCollapsedRangeInTexts(
+ textFragmentDataAtEnd.InvisibleTrailingWhiteSpaceRangeRef());
+ if (invisibleLeadingWhiteSpacesAtEnd.IsPositioned() &&
+ !invisibleLeadingWhiteSpacesAtEnd.Collapsed()) {
+ result.SetEnd(invisibleLeadingWhiteSpacesAtEnd.EndRef());
+ } else {
+ const EditorDOMRangeInTexts invisibleLeadingWhiteSpacesAtEnd =
+ textFragmentDataAtEnd.GetNonCollapsedRangeInTexts(
+ textFragmentDataAtEnd.InvisibleLeadingWhiteSpaceRangeRef());
+ if (invisibleLeadingWhiteSpacesAtEnd.IsPositioned() &&
+ !invisibleLeadingWhiteSpacesAtEnd.Collapsed()) {
+ MOZ_ASSERT(aRange.EndRef().EqualsOrIsBefore(
+ invisibleLeadingWhiteSpacesAtEnd.EndRef()));
+ result.SetEnd(invisibleLeadingWhiteSpacesAtEnd.EndRef());
+ }
+ // If there is no invisible white-space and the line ends with a text
+ // node, shrink the range to end of the text node.
+ else if (!aRange.EndRef().IsInTextNode() &&
+ textFragmentDataAtEnd.EndsByBlockBoundary() &&
+ textFragmentDataAtEnd.StartRef().IsInTextNode()) {
+ result.SetEnd(EditorDOMPoint::AtEndOf(
+ *textFragmentDataAtEnd.StartRef().ContainerAs<Text>()));
+ }
+ }
+ if (!result.EndRef().IsSet()) {
+ result.SetEnd(aRange.EndRef());
+ }
+ MOZ_ASSERT(result.IsPositionedAndValid());
+ return result;
+}
+
+/******************************************************************************
+ * Utilities for other things.
+ ******************************************************************************/
+
+// static
+Result<bool, nsresult>
+WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
+ const HTMLEditor& aHTMLEditor, nsRange& aRange,
+ const Element* aEditingHost) {
+ MOZ_ASSERT(aRange.IsPositioned());
+ MOZ_ASSERT(!aRange.IsInAnySelection(),
+ "Changing range in selection may cause running script");
+
+ if (NS_WARN_IF(!aRange.GetStartContainer()) ||
+ NS_WARN_IF(!aRange.GetEndContainer())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (!aRange.GetStartContainer()->IsContent() ||
+ !aRange.GetEndContainer()->IsContent()) {
+ return false;
+ }
+
+ // If the range crosses a block boundary, we should do nothing for now
+ // because it hits a bug of inserting a padding `<br>` element after
+ // joining the blocks.
+ if (HTMLEditUtils::GetInclusiveAncestorElement(
+ *aRange.GetStartContainer()->AsContent(),
+ HTMLEditUtils::ClosestEditableBlockElementExceptHRElement) !=
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *aRange.GetEndContainer()->AsContent(),
+ HTMLEditUtils::ClosestEditableBlockElementExceptHRElement)) {
+ return false;
+ }
+
+ nsIContent* startContent = nullptr;
+ if (aRange.GetStartContainer() && aRange.GetStartContainer()->IsText() &&
+ aRange.GetStartContainer()->AsText()->Length() == aRange.StartOffset()) {
+ // If next content is a visible `<br>` element, special inline content
+ // (e.g., `<img>`, non-editable text node, etc) or a block level void
+ // element like `<hr>`, the range should start with it.
+ TextFragmentData textFragmentDataAtStart(
+ EditorRawDOMPoint(aRange.StartRef()), aEditingHost);
+ if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (textFragmentDataAtStart.EndsByVisibleBRElement()) {
+ startContent = textFragmentDataAtStart.EndReasonBRElementPtr();
+ } else if (textFragmentDataAtStart.EndsBySpecialContent() ||
+ (textFragmentDataAtStart.EndsByOtherBlockElement() &&
+ !HTMLEditUtils::IsContainerNode(
+ *textFragmentDataAtStart
+ .EndReasonOtherBlockElementPtr()))) {
+ startContent = textFragmentDataAtStart.GetEndReasonContent();
+ }
+ }
+
+ nsIContent* endContent = nullptr;
+ if (aRange.GetEndContainer() && aRange.GetEndContainer()->IsText() &&
+ !aRange.EndOffset()) {
+ // If previous content is a visible `<br>` element, special inline content
+ // (e.g., `<img>`, non-editable text node, etc) or a block level void
+ // element like `<hr>`, the range should end after it.
+ TextFragmentData textFragmentDataAtEnd(EditorRawDOMPoint(aRange.EndRef()),
+ aEditingHost);
+ if (NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (textFragmentDataAtEnd.StartsFromVisibleBRElement()) {
+ endContent = textFragmentDataAtEnd.StartReasonBRElementPtr();
+ } else if (textFragmentDataAtEnd.StartsFromSpecialContent() ||
+ (textFragmentDataAtEnd.StartsFromOtherBlockElement() &&
+ !HTMLEditUtils::IsContainerNode(
+ *textFragmentDataAtEnd
+ .StartReasonOtherBlockElementPtr()))) {
+ endContent = textFragmentDataAtEnd.GetStartReasonContent();
+ }
+ }
+
+ if (!startContent && !endContent) {
+ return false;
+ }
+
+ nsresult rv = aRange.SetStartAndEnd(
+ startContent ? RangeBoundary(
+ startContent->GetParentNode(),
+ startContent->GetPreviousSibling()) // at startContent
+ : aRange.StartRef(),
+ endContent ? RangeBoundary(endContent->GetParentNode(),
+ endContent) // after endContent
+ : aRange.EndRef());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsRange::SetStartAndEnd() failed");
+ return Err(rv);
+ }
+ return true;
+}
+
+} // namespace mozilla