/* -*- 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 "WhiteSpaceVisibilityKeeper.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/SelectionState.h" #include "mozilla/OwningNonNull.h" #include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_* #include "mozilla/InternalMutationEvent.h" #include "mozilla/dom/AncestorIterator.h" #include "nsCRT.h" #include "nsContentUtils.h" #include "nsDebug.h" #include "nsError.h" #include "nsIContent.h" #include "nsIContentInlines.h" #include "nsString.h" namespace mozilla { using namespace dom; using LeafNodeType = HTMLEditUtils::LeafNodeType; using WalkTreeOption = HTMLEditUtils::WalkTreeOption; template nsresult WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aScanStartPoint, const Element& aEditingHost); template nsresult WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt( HTMLEditor& aHTMLEditor, const EditorDOMPointInText& aScanStartPoint, const Element& aEditingHost); Result WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToSplit, const Element& aSplittingBlockElement) { if (NS_WARN_IF(!aPointToSplit.IsInContentNodeAndValidInComposedDoc()) || NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(aSplittingBlockElement)) || NS_WARN_IF(!EditorUtils::IsEditableContent( *aPointToSplit.ContainerAs(), EditorType::HTML))) { return Err(NS_ERROR_FAILURE); } // The container of aPointToSplit may be not splittable, e.g., selection // may be collapsed **in** a `
` element or a comment node. So, look // for splittable point with climbing the tree up. EditorDOMPoint pointToSplit(aPointToSplit); for (nsIContent* content : aPointToSplit.ContainerAs() ->InclusiveAncestorsOfType()) { if (content == &aSplittingBlockElement) { break; } if (HTMLEditUtils::IsSplittableNode(*content)) { break; } pointToSplit.Set(content); } // TODO: Delete this block once we ship the new normalizer. if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) { 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); } } else { // NOTE: Chrome does not normalize white-spaces at splitting `Text` when // inserting a paragraph at least when the surrounding white-spaces being or // end with an NBSP. Result pointToSplitOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt( aHTMLEditor, pointToSplit, {NormalizeOption::StopIfFollowingWhiteSpacesStartsWithNBSP, NormalizeOption::StopIfPrecedingWhiteSpacesEndsWithNBP}); if (MOZ_UNLIKELY(pointToSplitOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt() failed"); return pointToSplitOrError.propagateErr(); } pointToSplit = pointToSplitOrError.unwrap(); } if (NS_WARN_IF(!pointToSplit.IsInContentNode()) || NS_WARN_IF( !pointToSplit.ContainerAs()->IsInclusiveDescendantOf( &aSplittingBlockElement)) || NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(aSplittingBlockElement)) || NS_WARN_IF(!HTMLEditUtils::IsSplittableNode( *pointToSplit.ContainerAs()))) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return pointToSplit; } // static Result WhiteSpaceVisibilityKeeper:: MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement( HTMLEditor& aHTMLEditor, Element& aLeftBlockElement, Element& aRightBlockElement, const EditorDOMPoint& aAtRightBlockChild, const Maybe& aListElementTagName, const HTMLBRElement* aPrecedingInvisibleBRElement, const Element& aEditingHost) { MOZ_ASSERT( EditorUtils::IsDescendantOf(aLeftBlockElement, aRightBlockElement)); MOZ_ASSERT(&aRightBlockElement == aAtRightBlockChild.GetContainer()); OwningNonNull rightBlockElement = aRightBlockElement; EditorDOMPoint afterRightBlockChild = aAtRightBlockChild.NextPoint(); if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) { // 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 `
` element at end of aLeftBlockElement { Result 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); } } { // We can't just track rightBlockElement because it's an Element. AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), &afterRightBlockChild); Result 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(); } } else { MOZ_ASSERT( StaticPrefs::editor_white_space_normalization_blink_compatible()); { AutoTrackDOMPoint trackAfterRightBlockChild(aHTMLEditor.RangeUpdaterRef(), &afterRightBlockChild); // First, delete invisible white-spaces at start of the right block and // normalize the leading visible white-spaces. nsresult rv = WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesAfter( aHTMLEditor, afterRightBlockChild); if (NS_FAILED(rv)) { NS_WARNING( "WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesAfter() " "failed"); return Err(rv); } // Next, delete invisible white-spaces at end of the left block and // normalize the trailing visible white-spaces. rv = WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesBefore( aHTMLEditor, EditorDOMPoint::AtEndOf(aLeftBlockElement)); if (NS_FAILED(rv)) { NS_WARNING( "WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesBefore() " "failed"); return Err(rv); } trackAfterRightBlockChild.FlushAndStopTracking(); if (NS_WARN_IF(afterRightBlockChild.GetContainer() != &aRightBlockElement)) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } // Finally, make sure that we won't create new invisible white-spaces. AutoTrackDOMPoint trackAfterRightBlockChild(aHTMLEditor.RangeUpdaterRef(), &afterRightBlockChild); Result atFirstVisibleThingOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter( aHTMLEditor, afterRightBlockChild, {NormalizeOption::StopIfFollowingWhiteSpacesStartsWithNBSP}); if (MOZ_UNLIKELY(atFirstVisibleThingOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter() failed"); return atFirstVisibleThingOrError.propagateErr(); } Result afterLastVisibleThingOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesBefore( aHTMLEditor, EditorDOMPoint::AtEndOf(aLeftBlockElement), {}); if (MOZ_UNLIKELY(afterLastVisibleThingOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter() failed"); return afterLastVisibleThingOrError.propagateErr(); } } // XXX And afterRightBlockChild.GetContainerAs() always returns // an element pointer so that probably here should not use // accessors of EditorDOMPoint, should use DOM API directly instead. if (afterRightBlockChild.GetContainerAs()) { rightBlockElement = *afterRightBlockChild.ContainerAs(); } else if (NS_WARN_IF( !afterRightBlockChild.GetContainerParentAs())) { return Err(NS_ERROR_UNEXPECTED); } else { rightBlockElement = *afterRightBlockChild.GetContainerParentAs(); } auto atStartOfRightText = [&]() MOZ_NEVER_INLINE_DEBUG -> EditorDOMPoint { if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) { return EditorDOMPoint(); // Don't need to normalize now. } const WSRunScanner scanner( Scan::All, EditorRawDOMPoint(&aRightBlockElement, 0u), BlockInlineCheck::UseComputedDisplayOutsideStyle); for (EditorRawDOMPointInText atFirstChar = scanner.GetInclusiveNextCharPoint( EditorRawDOMPoint(&aRightBlockElement, 0u)); atFirstChar.IsSet(); atFirstChar = scanner.GetInclusiveNextCharPoint( atFirstChar.AfterContainer())) { if (atFirstChar.IsContainerEmpty()) { continue; // Ignore empty text node. } if (atFirstChar.IsCharASCIISpaceOrNBSP() && HTMLEditUtils::IsSimplyEditableNode( *atFirstChar.ContainerAs())) { return atFirstChar.To(); } break; } return EditorDOMPoint(); }(); AutoTrackDOMPoint trackStartOfRightText(aHTMLEditor.RangeUpdaterRef(), &atStartOfRightText); // Do br adjustment. // XXX Why don't we delete the
first? If so, we can skip to track the // MoveNodeResult at last. const RefPtr invisibleBRElementAtEndOfLeftBlockElement = WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound( WSRunScanner::Scan::EditableNodes, EditorDOMPoint::AtEndOf(aLeftBlockElement), BlockInlineCheck::UseComputedDisplayStyle); NS_ASSERTION( aPrecedingInvisibleBRElement == invisibleBRElementAtEndOfLeftBlockElement, "The preceding invisible BR element computation was different"); auto moveContentResult = [&]() MOZ_NEVER_INLINE_DEBUG MOZ_CAN_RUN_SCRIPT -> Result { // 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. return MoveNodeResult::HandledResult( EditorDOMPoint::AtEndOf(aLeftBlockElement)); } AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor); // 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 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); } MoveNodeResult moveResult = MoveNodeResult::IgnoredResult( EditorDOMPoint::AtEndOf(aLeftBlockElement)); AutoTrackDOMMoveNodeResult trackMoveResult(aHTMLEditor.RangeUpdaterRef(), &moveResult); Result moveFirstLineResult = lineMoverToEndOfLeftBlock.Run(aHTMLEditor, aEditingHost); if (MOZ_UNLIKELY(moveFirstLineResult.isErr())) { NS_WARNING("AutoMoveOneLineHandler::Run() failed"); return moveFirstLineResult.propagateErr(); } trackMoveResult.FlushAndStopTracking(); #ifdef DEBUG MOZ_ASSERT(!firstLineHasContent.isErr()); if (firstLineHasContent.inspect()) { NS_ASSERTION(moveFirstLineResult.inspect().Handled(), "Failed to consider whether moving or not something"); } else { NS_ASSERTION(moveFirstLineResult.inspect().Ignored(), "Failed to consider whether moving or not something"); } #endif // #ifdef DEBUG moveResult |= moveFirstLineResult.unwrap(); // Now, all children of rightBlockElement were moved to leftBlockElement. // So, afterRightBlockChild is now invalid. afterRightBlockChild.Clear(); return std::move(moveResult); }(); if (MOZ_UNLIKELY(moveContentResult.isErr())) { return moveContentResult; } MoveNodeResult unwrappedMoveContentResult = moveContentResult.unwrap(); trackStartOfRightText.FlushAndStopTracking(); if (atStartOfRightText.IsInTextNode() && atStartOfRightText.IsSetAndValidInComposedDoc() && atStartOfRightText.IsMiddleOfContainer()) { AutoTrackDOMMoveNodeResult trackMoveContentResult( aHTMLEditor.RangeUpdaterRef(), &unwrappedMoveContentResult); Result startOfRightTextOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAt( aHTMLEditor, atStartOfRightText.AsInText()); if (MOZ_UNLIKELY(startOfRightTextOrError.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAt() failed"); return startOfRightTextOrError.propagateErr(); } } if (!invisibleBRElementAtEndOfLeftBlockElement || !invisibleBRElementAtEndOfLeftBlockElement->IsInComposedDoc()) { return std::move(unwrappedMoveContentResult); } { AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor); AutoTrackDOMMoveNodeResult trackMoveContentResult( aHTMLEditor.RangeUpdaterRef(), &unwrappedMoveContentResult); nsresult rv = aHTMLEditor.DeleteNodeWithTransaction( *invisibleBRElementAtEndOfLeftBlockElement); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed, but ignored"); unwrappedMoveContentResult.IgnoreCaretPointSuggestion(); return Err(rv); } } return std::move(unwrappedMoveContentResult); } // static Result WhiteSpaceVisibilityKeeper:: MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement( HTMLEditor& aHTMLEditor, Element& aLeftBlockElement, Element& aRightBlockElement, const EditorDOMPoint& aAtLeftBlockChild, nsIContent& aLeftContentInBlock, const Maybe& 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()); OwningNonNull originalLeftBlockElement = aLeftBlockElement; OwningNonNull leftBlockElement = aLeftBlockElement; EditorDOMPoint atLeftBlockChild(aAtLeftBlockChild); if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) { // 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 `
` element before aAtLeftBlockChild.GetChild() { Result 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); } } { // We can't just track leftBlockElement because it's an Element, so track // something else. AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), &atLeftBlockChild); Result 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); } } else { MOZ_ASSERT( StaticPrefs::editor_white_space_normalization_blink_compatible()); // First, delete invisible white-spaces before the right block. { AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), &atLeftBlockChild); nsresult rv = WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesBefore( aHTMLEditor, EditorDOMPoint(&aRightBlockElement)); if (NS_FAILED(rv)) { NS_WARNING( "WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesBefore() " "failed"); return Err(rv); } // Next, delete invisible white-spaces at start of the right block. rv = WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesAfter( aHTMLEditor, EditorDOMPoint(&aRightBlockElement, 0u)); if (NS_FAILED(rv)) { NS_WARNING( "WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesAfter() " "failed"); return Err(rv); } tracker.FlushAndStopTracking(); if (NS_WARN_IF( !atLeftBlockChild.IsInContentNodeAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } // Finally, make sure that we won't create new invisible white-spaces. AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), &atLeftBlockChild); Result afterLastVisibleThingOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter( aHTMLEditor, EditorDOMPoint(&aRightBlockElement, 0u), {NormalizeOption::StopIfPrecedingWhiteSpacesEndsWithNBP}); if (MOZ_UNLIKELY(afterLastVisibleThingOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter() failed"); return afterLastVisibleThingOrError.propagateErr(); } Result atFirstVisibleThingOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesBefore( aHTMLEditor, atLeftBlockChild, {}); if (MOZ_UNLIKELY(atFirstVisibleThingOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter() failed"); return atFirstVisibleThingOrError.propagateErr(); } tracker.FlushAndStopTracking(); if (NS_WARN_IF(!atLeftBlockChild.IsInContentNodeAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } // XXX atLeftBlockChild.GetContainerAs() 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); } auto atStartOfRightText = [&]() MOZ_NEVER_INLINE_DEBUG -> EditorDOMPoint { if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) { return EditorDOMPoint(); // Don't need to normalize now. } const WSRunScanner scanner( Scan::All, EditorRawDOMPoint(&aRightBlockElement, 0u), BlockInlineCheck::UseComputedDisplayOutsideStyle); for (EditorRawDOMPointInText atFirstChar = scanner.GetInclusiveNextCharPoint( EditorRawDOMPoint(&aRightBlockElement, 0u)); atFirstChar.IsSet(); atFirstChar = scanner.GetInclusiveNextCharPoint( atFirstChar.AfterContainer())) { if (atFirstChar.IsContainerEmpty()) { continue; // Ignore empty text node. } if (atFirstChar.IsCharASCIISpaceOrNBSP() && HTMLEditUtils::IsSimplyEditableNode( *atFirstChar.ContainerAs())) { return atFirstChar.To(); } break; } return EditorDOMPoint(); }(); AutoTrackDOMPoint trackStartOfRightText(aHTMLEditor.RangeUpdaterRef(), &atStartOfRightText); // Do br adjustment. // XXX Why don't we delete the
first? If so, we can skip to track the // MoveNodeResult at last. const RefPtr invisibleBRElementBeforeLeftBlockElement = WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound( WSRunScanner::Scan::EditableNodes, atLeftBlockChild, BlockInlineCheck::UseComputedDisplayStyle); NS_ASSERTION( aPrecedingInvisibleBRElement == invisibleBRElementBeforeLeftBlockElement, "The preceding invisible BR element computation was different"); auto moveContentResult = [&]() MOZ_NEVER_INLINE_DEBUG MOZ_CAN_RUN_SCRIPT -> Result { // 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 rightBlockHasContent = aHTMLEditor.CanMoveChildren(aRightBlockElement, aLeftBlockElement); #endif // #ifdef DEBUG MoveNodeResult moveResult = MoveNodeResult::IgnoredResult(EditorDOMPoint( atLeftBlockChild.GetContainer(), atLeftBlockChild.Offset())); AutoTrackDOMMoveNodeResult trackMoveResult(aHTMLEditor.RangeUpdaterRef(), &moveResult); // TODO: Stop using HTMLEditor::PreserveWhiteSpaceStyle::No due to no // tests. AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor); Result moveChildrenResult = aHTMLEditor.MoveChildrenWithTransaction( aRightBlockElement, moveResult.NextInsertionPointRef(), HTMLEditor::PreserveWhiteSpaceStyle::No, HTMLEditor::RemoveIfCommentNode::Yes); if (MOZ_UNLIKELY(moveChildrenResult.isErr())) { if (NS_WARN_IF(moveChildrenResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { return moveChildrenResult; } NS_WARNING( "HTMLEditor::MoveChildrenWithTransaction() failed, but ignored"); } else { #ifdef DEBUG MOZ_ASSERT(!rightBlockHasContent.isErr()); if (rightBlockHasContent.inspect()) { NS_ASSERTION(moveChildrenResult.inspect().Handled(), "Failed to consider whether moving or not children"); } else { NS_ASSERTION(moveChildrenResult.inspect().Ignored(), "Failed to consider whether moving or not children"); } #endif // #ifdef DEBUG trackMoveResult.FlushAndStopTracking(); moveResult |= moveChildrenResult.unwrap(); } // atLeftBlockChild was moved to rightListElement. So, it's invalid now. atLeftBlockChild.Clear(); return std::move(moveResult); } // 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 firstLineHasContent = HTMLEditor::AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine( EditorDOMPoint(&aRightBlockElement, 0u), aEditingHost); #endif // #ifdef DEBUG if (&aLeftContentInBlock != &aEditingHost) { Result 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())) { 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(); if (NS_WARN_IF(!pointToMoveFirstLineContent.IsInContentNode())) { return Err(NS_ERROR_FAILURE); } } } MOZ_DIAGNOSTIC_ASSERT(pointToMoveFirstLineContent.IsSetAndValid()); } MoveNodeResult moveResult = MoveNodeResult::IgnoredResult(pointToMoveFirstLineContent); 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); } AutoTrackDOMMoveNodeResult trackMoveResult(aHTMLEditor.RangeUpdaterRef(), &moveResult); AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor); Result moveFirstLineResult = lineMoverToPoint.Run(aHTMLEditor, aEditingHost); if (MOZ_UNLIKELY(moveFirstLineResult.isErr())) { NS_WARNING("AutoMoveOneLineHandler::Run() failed"); return moveFirstLineResult.propagateErr(); } #ifdef DEBUG MOZ_ASSERT(!firstLineHasContent.isErr()); if (firstLineHasContent.inspect()) { NS_ASSERTION(moveFirstLineResult.inspect().Handled(), "Failed to consider whether moving or not something"); } else { NS_ASSERTION(moveFirstLineResult.inspect().Ignored(), "Failed to consider whether moving or not something"); } #endif // #ifdef DEBUG trackMoveResult.FlushAndStopTracking(); moveResult |= moveFirstLineResult.unwrap(); return std::move(moveResult); }(); if (MOZ_UNLIKELY(moveContentResult.isErr())) { return moveContentResult; } MoveNodeResult unwrappedMoveContentResult = moveContentResult.unwrap(); trackStartOfRightText.FlushAndStopTracking(); if (atStartOfRightText.IsInTextNode() && atStartOfRightText.IsSetAndValidInComposedDoc() && atStartOfRightText.IsMiddleOfContainer()) { AutoTrackDOMMoveNodeResult trackMoveContentResult( aHTMLEditor.RangeUpdaterRef(), &unwrappedMoveContentResult); Result startOfRightTextOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAt( aHTMLEditor, atStartOfRightText.AsInText()); if (MOZ_UNLIKELY(startOfRightTextOrError.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAt() failed"); return startOfRightTextOrError.propagateErr(); } } if (!invisibleBRElementBeforeLeftBlockElement || !invisibleBRElementBeforeLeftBlockElement->IsInComposedDoc()) { return std::move(unwrappedMoveContentResult); } { AutoTrackDOMMoveNodeResult trackMoveContentResult( aHTMLEditor.RangeUpdaterRef(), &unwrappedMoveContentResult); AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor); nsresult rv = aHTMLEditor.DeleteNodeWithTransaction( *invisibleBRElementBeforeLeftBlockElement); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed, but ignored"); unwrappedMoveContentResult.IgnoreCaretPointSuggestion(); return Err(rv); } } return std::move(unwrappedMoveContentResult); } // static Result WhiteSpaceVisibilityKeeper:: MergeFirstLineOfRightBlockElementIntoLeftBlockElement( HTMLEditor& aHTMLEditor, Element& aLeftBlockElement, Element& aRightBlockElement, const Maybe& aListElementTagName, const HTMLBRElement* aPrecedingInvisibleBRElement, const Element& aEditingHost) { MOZ_ASSERT( !EditorUtils::IsDescendantOf(aLeftBlockElement, aRightBlockElement)); MOZ_ASSERT( !EditorUtils::IsDescendantOf(aRightBlockElement, aLeftBlockElement)); if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) { // 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 `
` element at end of aLeftBlockElement // Adjust white-space at block boundaries Result 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(); } else { MOZ_ASSERT( StaticPrefs::editor_white_space_normalization_blink_compatible()); // First, delete invisible white-spaces at end of the left block nsresult rv = WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesBefore( aHTMLEditor, EditorDOMPoint::AtEndOf(aLeftBlockElement)); if (NS_FAILED(rv)) { NS_WARNING( "WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesBefore() " "failed"); return Err(rv); } // Next, delete invisible white-spaces at start of the right block and // normalize the leading visible white-spaces. rv = WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesAfter( aHTMLEditor, EditorDOMPoint(&aRightBlockElement, 0u)); if (NS_FAILED(rv)) { NS_WARNING( "WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesAfter() " "failed"); return Err(rv); } // Finally, make sure to that we won't create new invisible white-spaces. Result atFirstVisibleThingOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter( aHTMLEditor, EditorDOMPoint(&aRightBlockElement, 0u), {NormalizeOption::StopIfFollowingWhiteSpacesStartsWithNBSP}); if (MOZ_UNLIKELY(atFirstVisibleThingOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter() failed"); return atFirstVisibleThingOrError.propagateErr(); } Result afterLastVisibleThingOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesBefore( aHTMLEditor, EditorDOMPoint::AtEndOf(aLeftBlockElement), {}); if (MOZ_UNLIKELY(afterLastVisibleThingOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesBefore() failed"); return afterLastVisibleThingOrError.propagateErr(); } } auto atStartOfRightText = [&]() MOZ_NEVER_INLINE_DEBUG -> EditorDOMPoint { if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) { return EditorDOMPoint(); // Don't need to normalize now. } const WSRunScanner scanner( Scan::All, EditorRawDOMPoint(&aRightBlockElement, 0u), BlockInlineCheck::UseComputedDisplayOutsideStyle); for (EditorRawDOMPointInText atFirstChar = scanner.GetInclusiveNextCharPoint( EditorRawDOMPoint(&aRightBlockElement, 0u)); atFirstChar.IsSet(); atFirstChar = scanner.GetInclusiveNextCharPoint( atFirstChar.AfterContainer())) { if (atFirstChar.IsContainerEmpty()) { continue; // Ignore empty text node. } if (atFirstChar.IsCharASCIISpaceOrNBSP() && HTMLEditUtils::IsSimplyEditableNode( *atFirstChar.ContainerAs())) { return atFirstChar.To(); } break; } return EditorDOMPoint(); }(); AutoTrackDOMPoint trackStartOfRightText(aHTMLEditor.RangeUpdaterRef(), &atStartOfRightText); // Do br adjustment. // XXX Why don't we delete the
first? If so, we can skip to track the // MoveNodeResult at last. const RefPtr invisibleBRElementAtEndOfLeftBlockElement = WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound( WSRunScanner::Scan::EditableNodes, EditorDOMPoint::AtEndOf(aLeftBlockElement), BlockInlineCheck::UseComputedDisplayStyle); NS_ASSERTION( aPrecedingInvisibleBRElement == invisibleBRElementAtEndOfLeftBlockElement, "The preceding invisible BR element computation was different"); auto moveContentResult = [&]() MOZ_NEVER_INLINE_DEBUG MOZ_CAN_RUN_SCRIPT -> Result { 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::GetComputedWhiteSpaceStyles(aLeftBlockElement) == EditorUtils::GetComputedWhiteSpaceStyles(aRightBlockElement))) { MoveNodeResult moveResult = MoveNodeResult::IgnoredResult( EditorDOMPoint::AtEndOf(aLeftBlockElement)); AutoTrackDOMMoveNodeResult trackMoveResult(aHTMLEditor.RangeUpdaterRef(), &moveResult); AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor); // 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 convertListTypeResult = aHTMLEditor.ChangeListElementType( // XXX Shouldn't be aLeftBlockElement here? 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(); } } trackMoveResult.FlushAndStopTracking(); moveResult |= MoveNodeResult::HandledResult( EditorDOMPoint::AtEndOf(aLeftBlockElement)); return std::move(moveResult); } #ifdef DEBUG Result firstLineHasContent = HTMLEditor::AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine( EditorDOMPoint(&aRightBlockElement, 0u), aEditingHost); #endif // #ifdef DEBUG MoveNodeResult moveResult = MoveNodeResult::IgnoredResult( EditorDOMPoint::AtEndOf(aLeftBlockElement)); // 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); } AutoTrackDOMMoveNodeResult trackMoveResult(aHTMLEditor.RangeUpdaterRef(), &moveResult); AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor); Result moveFirstLineResult = lineMoverToEndOfLeftBlock.Run(aHTMLEditor, aEditingHost); if (MOZ_UNLIKELY(moveFirstLineResult.isErr())) { NS_WARNING("AutoMoveOneLineHandler::Run() failed"); return moveFirstLineResult.propagateErr(); } #ifdef DEBUG MOZ_ASSERT(!firstLineHasContent.isErr()); if (firstLineHasContent.inspect()) { NS_ASSERTION(moveFirstLineResult.inspect().Handled(), "Failed to consider whether moving or not something"); } else { NS_ASSERTION(moveFirstLineResult.inspect().Ignored(), "Failed to consider whether moving or not something"); } #endif // #ifdef DEBUG trackMoveResult.FlushAndStopTracking(); moveResult |= moveFirstLineResult.unwrap(); return std::move(moveResult); }(); if (MOZ_UNLIKELY(moveContentResult.isErr())) { return moveContentResult; } MoveNodeResult unwrappedMoveContentResult = moveContentResult.unwrap(); trackStartOfRightText.FlushAndStopTracking(); if (atStartOfRightText.IsInTextNode() && atStartOfRightText.IsSetAndValidInComposedDoc() && atStartOfRightText.IsMiddleOfContainer()) { AutoTrackDOMMoveNodeResult trackMoveContentResult( aHTMLEditor.RangeUpdaterRef(), &unwrappedMoveContentResult); Result startOfRightTextOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAt( aHTMLEditor, atStartOfRightText.AsInText()); if (MOZ_UNLIKELY(startOfRightTextOrError.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAt() failed"); return startOfRightTextOrError.propagateErr(); } } if (!invisibleBRElementAtEndOfLeftBlockElement || !invisibleBRElementAtEndOfLeftBlockElement->IsInComposedDoc()) { unwrappedMoveContentResult.ForceToMarkAsHandled(); return std::move(unwrappedMoveContentResult); } { AutoTrackDOMMoveNodeResult trackMoveContentResult( aHTMLEditor.RangeUpdaterRef(), &unwrappedMoveContentResult); AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor); 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"); unwrappedMoveContentResult.IgnoreCaretPointSuggestion(); return Err(rv); } } return std::move(unwrappedMoveContentResult); } // static Result WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAt( HTMLEditor& aHTMLEditor, const EditorDOMPointInText& aPoint) { MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible()); MOZ_ASSERT(aPoint.IsSet()); MOZ_ASSERT(!aPoint.IsEndOfContainer()); if (!aPoint.IsCharCollapsibleASCIISpaceOrNBSP()) { return aPoint.To(); } const HTMLEditor::ReplaceWhiteSpacesData normalizedWhiteSpaces = aHTMLEditor.GetNormalizedStringAt(aPoint).GetMinimizedData( *aPoint.ContainerAs()); if (!normalizedWhiteSpaces.ReplaceLength()) { return aPoint.To(); } const OwningNonNull textNode = *aPoint.ContainerAs(); Result insertTextResultOrError = aHTMLEditor.ReplaceTextWithTransaction(textNode, normalizedWhiteSpaces); if (MOZ_UNLIKELY(insertTextResultOrError.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return insertTextResultOrError.propagateErr(); } return insertTextResultOrError.unwrap().UnwrapCaretPoint(); } // static Result WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesBefore( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint, NormalizeOptions aOptions) { MOZ_ASSERT(aPoint.IsSetAndValid()); MOZ_ASSERT_IF(aPoint.IsInTextNode(), !aPoint.IsMiddleOfContainer()); MOZ_ASSERT( !aOptions.contains(NormalizeOption::HandleOnlyFollowingWhiteSpaces)); MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible()); const RefPtr colsetBlockElement = aPoint.IsInContentNode() ? HTMLEditUtils::GetInclusiveAncestorElement( *aPoint.ContainerAs(), HTMLEditUtils::ClosestEditableBlockElement, BlockInlineCheck::UseComputedDisplayStyle) : nullptr; EditorDOMPoint afterLastVisibleThing(aPoint); AutoTArray, 32> unnecessaryContents; for (nsIContent* previousContent = aPoint.IsInTextNode() && aPoint.IsEndOfContainer() ? aPoint.ContainerAs() : HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( aPoint, {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement); previousContent; previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( EditorRawDOMPoint(previousContent), {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement)) { if (!HTMLEditUtils::IsSimplyEditableNode(*previousContent)) { // XXX Assume non-editable nodes are visible. break; } const RefPtr precedingTextNode = Text::FromNode(previousContent); if (!precedingTextNode && HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*previousContent)) { afterLastVisibleThing.SetAfter(previousContent); break; } if (!precedingTextNode || !precedingTextNode->TextDataLength()) { // If it's an empty inline element like `` or an empty `Text`, // delete it. nsIContent* emptyInlineContent = HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( *previousContent, BlockInlineCheck::UseComputedDisplayStyle); if (!emptyInlineContent) { emptyInlineContent = previousContent; } unnecessaryContents.AppendElement(*emptyInlineContent); continue; } const auto atLastChar = EditorRawDOMPointInText::AtLastContentOf(*precedingTextNode); if (!atLastChar.IsCharCollapsibleASCIISpaceOrNBSP()) { afterLastVisibleThing.SetAfter(precedingTextNode); break; } if (aOptions.contains( NormalizeOption::StopIfPrecedingWhiteSpacesEndsWithNBP) && atLastChar.IsCharNBSP()) { afterLastVisibleThing.SetAfter(precedingTextNode); break; } const HTMLEditor::ReplaceWhiteSpacesData replaceData = aHTMLEditor.GetNormalizedStringAt(atLastChar.AsInText()) .GetMinimizedData(*precedingTextNode); if (!replaceData.ReplaceLength()) { afterLastVisibleThing.SetAfter(precedingTextNode); break; } // If the Text node has only invisible white-spaces, delete the node itself. if (replaceData.ReplaceLength() == precedingTextNode->TextDataLength() && replaceData.mNormalizedString.IsEmpty()) { nsIContent* emptyInlineContent = HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( *precedingTextNode, BlockInlineCheck::UseComputedDisplayStyle); if (!emptyInlineContent) { emptyInlineContent = precedingTextNode; } unnecessaryContents.AppendElement(*emptyInlineContent); continue; } Result replaceWhiteSpacesResultOrError = aHTMLEditor.ReplaceTextWithTransaction(*precedingTextNode, replaceData); if (MOZ_UNLIKELY(replaceWhiteSpacesResultOrError.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return replaceWhiteSpacesResultOrError.propagateErr(); } InsertTextResult replaceWhiteSpacesResult = replaceWhiteSpacesResultOrError.unwrap(); replaceWhiteSpacesResult.IgnoreCaretPointSuggestion(); afterLastVisibleThing = replaceWhiteSpacesResult.EndOfInsertedTextRef(); } AutoTrackDOMPoint trackAfterLastVisibleThing(aHTMLEditor.RangeUpdaterRef(), &afterLastVisibleThing); for (const auto& contentToDelete : unnecessaryContents) { if (MOZ_UNLIKELY(!contentToDelete->IsInComposedDoc())) { continue; } nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(contentToDelete)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } } trackAfterLastVisibleThing.FlushAndStopTracking(); if (NS_WARN_IF( !afterLastVisibleThing.IsInContentNodeAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return std::move(afterLastVisibleThing); } // static Result WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint, NormalizeOptions aOptions) { MOZ_ASSERT(aPoint.IsSetAndValid()); MOZ_ASSERT_IF(aPoint.IsInTextNode(), !aPoint.IsMiddleOfContainer()); MOZ_ASSERT( !aOptions.contains(NormalizeOption::HandleOnlyPrecedingWhiteSpaces)); MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible()); const RefPtr colsetBlockElement = aPoint.IsInContentNode() ? HTMLEditUtils::GetInclusiveAncestorElement( *aPoint.ContainerAs(), HTMLEditUtils::ClosestEditableBlockElement, BlockInlineCheck::UseComputedDisplayStyle) : nullptr; EditorDOMPoint atFirstVisibleThing(aPoint); AutoTArray, 32> unnecessaryContents; for (nsIContent* nextContent = aPoint.IsInTextNode() && aPoint.IsStartOfContainer() ? aPoint.ContainerAs() : HTMLEditUtils::GetNextLeafContentOrNextBlockElement( aPoint, {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement); nextContent; nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( EditorRawDOMPoint::After(*nextContent), {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement)) { if (!HTMLEditUtils::IsSimplyEditableNode(*nextContent)) { // XXX Assume non-editable nodes are visible. break; } const RefPtr followingTextNode = Text::FromNode(nextContent); if (!followingTextNode && HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*nextContent)) { atFirstVisibleThing.Set(nextContent); break; } if (!followingTextNode || !followingTextNode->TextDataLength()) { // If it's an empty inline element like `` or an empty `Text`, // delete it. nsIContent* emptyInlineContent = HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( *nextContent, BlockInlineCheck::UseComputedDisplayStyle); if (!emptyInlineContent) { emptyInlineContent = nextContent; } unnecessaryContents.AppendElement(*emptyInlineContent); continue; } const auto atFirstChar = EditorRawDOMPointInText(followingTextNode, 0u); if (!atFirstChar.IsCharCollapsibleASCIISpaceOrNBSP()) { atFirstVisibleThing.Set(followingTextNode); break; } if (aOptions.contains( NormalizeOption::StopIfPrecedingWhiteSpacesEndsWithNBP) && atFirstChar.IsCharNBSP()) { atFirstVisibleThing.Set(followingTextNode); break; } const HTMLEditor::ReplaceWhiteSpacesData replaceData = aHTMLEditor.GetNormalizedStringAt(atFirstChar.AsInText()) .GetMinimizedData(*followingTextNode); if (!replaceData.ReplaceLength()) { atFirstVisibleThing.Set(followingTextNode); break; } // If the Text node has only invisible white-spaces, delete the node itself. if (replaceData.ReplaceLength() == followingTextNode->TextDataLength() && replaceData.mNormalizedString.IsEmpty()) { nsIContent* emptyInlineContent = HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( *followingTextNode, BlockInlineCheck::UseComputedDisplayStyle); if (!emptyInlineContent) { emptyInlineContent = followingTextNode; } unnecessaryContents.AppendElement(*emptyInlineContent); continue; } Result replaceWhiteSpacesResultOrError = aHTMLEditor.ReplaceTextWithTransaction(*followingTextNode, replaceData); if (MOZ_UNLIKELY(replaceWhiteSpacesResultOrError.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return replaceWhiteSpacesResultOrError.propagateErr(); } replaceWhiteSpacesResultOrError.unwrap().IgnoreCaretPointSuggestion(); atFirstVisibleThing.Set(followingTextNode, 0u); break; } AutoTrackDOMPoint trackAtFirstVisibleThing(aHTMLEditor.RangeUpdaterRef(), &atFirstVisibleThing); for (const auto& contentToDelete : unnecessaryContents) { if (MOZ_UNLIKELY(!contentToDelete->IsInComposedDoc())) { continue; } nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(contentToDelete)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } } trackAtFirstVisibleThing.FlushAndStopTracking(); if (NS_WARN_IF(!atFirstVisibleThing.IsInContentNodeAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return std::move(atFirstVisibleThing); } // static Result WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt( HTMLEditor& aHTMLEditor, const EditorDOMPointInText& aPointToSplit, NormalizeOptions aOptions) { MOZ_ASSERT(aPointToSplit.IsSetAndValid()); MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible()); if (EditorUtils::IsWhiteSpacePreformatted( *aPointToSplit.ContainerAs())) { return aPointToSplit.To(); } const OwningNonNull textNode = *aPointToSplit.ContainerAs(); if (!textNode->TextDataLength()) { // Delete if it's an empty `Text` node and removable. if (!HTMLEditUtils::IsRemovableNode(*textNode)) { // It's logically odd to call this for non-editable `Text`, but it may // happen if surrounding white-space sequence contains empty non-editable // `Text`. In that case, the caller needs to normalize its preceding // `Text` nodes too. return EditorDOMPoint(); } const nsCOMPtr parentNode = textNode->GetParentNode(); const nsCOMPtr nextSibling = textNode->GetNextSibling(); nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(textNode); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } if (NS_WARN_IF(nextSibling && nextSibling->GetParentNode() != parentNode)) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return nextSibling ? EditorDOMPoint(nextSibling) : EditorDOMPoint::AtEndOf(*parentNode); } const HTMLEditor::ReplaceWhiteSpacesData replacePrecedingWhiteSpacesData = aPointToSplit.IsStartOfContainer() || aOptions.contains( NormalizeOption::HandleOnlyFollowingWhiteSpaces) || (aOptions.contains( NormalizeOption::StopIfPrecedingWhiteSpacesEndsWithNBP) && aPointToSplit.IsPreviousCharNBSP()) ? HTMLEditor::ReplaceWhiteSpacesData() : aHTMLEditor.GetPrecedingNormalizedStringToSplitAt(aPointToSplit); const HTMLEditor::ReplaceWhiteSpacesData replaceFollowingWhiteSpaceData = aPointToSplit.IsEndOfContainer() || aOptions.contains( NormalizeOption::HandleOnlyPrecedingWhiteSpaces) || (aOptions.contains( NormalizeOption::StopIfFollowingWhiteSpacesStartsWithNBSP) && aPointToSplit.IsCharNBSP()) ? HTMLEditor::ReplaceWhiteSpacesData() : aHTMLEditor.GetFollowingNormalizedStringToSplitAt(aPointToSplit); const HTMLEditor::ReplaceWhiteSpacesData replaceWhiteSpacesData = (replacePrecedingWhiteSpacesData + replaceFollowingWhiteSpaceData) .GetMinimizedData(*textNode); if (!replaceWhiteSpacesData.ReplaceLength()) { return aPointToSplit.To(); } if (replaceWhiteSpacesData.mNormalizedString.IsEmpty() && replaceWhiteSpacesData.ReplaceLength() == textNode->TextDataLength()) { // If there is only invisible white-spaces, mNormalizedString is empty // string but replace length is same the the `Text` length. In this case, we // should delete the `Text` to avoid empty `Text` to stay in the DOM tree. const nsCOMPtr parentNode = textNode->GetParentNode(); const nsCOMPtr nextSibling = textNode->GetNextSibling(); nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(textNode); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } if (NS_WARN_IF(nextSibling && nextSibling->GetParentNode() != parentNode)) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return nextSibling ? EditorDOMPoint(nextSibling) : EditorDOMPoint::AtEndOf(*parentNode); } Result replaceWhiteSpacesResultOrError = aHTMLEditor.ReplaceTextWithTransaction(textNode, replaceWhiteSpacesData); if (MOZ_UNLIKELY(replaceWhiteSpacesResultOrError.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return replaceWhiteSpacesResultOrError.propagateErr(); } replaceWhiteSpacesResultOrError.unwrap().IgnoreCaretPointSuggestion(); const uint32_t offsetToSplit = aPointToSplit.Offset() - replacePrecedingWhiteSpacesData.ReplaceLength() + replacePrecedingWhiteSpacesData.mNormalizedString.Length(); if (NS_WARN_IF(textNode->TextDataLength() < offsetToSplit)) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return EditorDOMPoint(textNode, offsetToSplit); } // static Result WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToSplit, NormalizeOptions aOptions) { MOZ_ASSERT(aPointToSplit.IsSet()); MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible()); // If the insertion point is not in composed doc, we're probably initializing // an element which will be inserted. In such case, the caller should own the // responsibility for normalizing the white-spaces. if (!aPointToSplit.IsInComposedDoc()) { return aPointToSplit; } EditorDOMPoint pointToSplit(aPointToSplit); { AutoTrackDOMPoint trackPointToSplit(aHTMLEditor.RangeUpdaterRef(), &pointToSplit); Result pointToSplitOrError = WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces(aHTMLEditor, pointToSplit); if (MOZ_UNLIKELY(pointToSplitOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces() failed"); return pointToSplitOrError.propagateErr(); } } if (NS_WARN_IF(!pointToSplit.IsInContentNode())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } if (pointToSplit.IsInTextNode()) { Result pointToSplitOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt( aHTMLEditor, pointToSplit.AsInText(), aOptions); if (MOZ_UNLIKELY(pointToSplitOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt() " "failed"); return pointToSplitOrError.propagateErr(); } pointToSplit = pointToSplitOrError.unwrap().To(); if (NS_WARN_IF(!pointToSplit.IsInContentNode())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // If we normalize white-spaces in middle of the `Text`, we don't need to // touch surrounding `Text` nodes. if (pointToSplit.IsMiddleOfContainer()) { return pointToSplit; } } // Preceding and/or following white-space sequence may be across multiple // `Text` nodes. Then, they may become unexpectedly visible without // normalizing the white-spaces. Therefore, we need to list up all possible // `Text` nodes first. Then, normalize them unless the `Text` is not const RefPtr closestBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *pointToSplit.ContainerAs(), HTMLEditUtils::ClosestBlockElement, BlockInlineCheck::UseComputedDisplayStyle); AutoTArray, 3> precedingTextNodes, followingTextNodes; if (!pointToSplit.IsInTextNode() || pointToSplit.IsStartOfContainer()) { for (nsCOMPtr previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( pointToSplit, {LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, closestBlockElement); previousContent; previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( *previousContent, {LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, closestBlockElement)) { if (auto* const textNode = Text::FromNode(previousContent)) { if (!HTMLEditUtils::IsSimplyEditableNode(*textNode) && textNode->TextDataLength()) { break; } if (aOptions.contains( NormalizeOption::StopIfPrecedingWhiteSpacesEndsWithNBP) && textNode->TextFragment().SafeLastChar() == HTMLEditUtils::kNBSP) { break; } precedingTextNodes.AppendElement(*textNode); if (textNode->TextIsOnlyWhitespace()) { // white-space only `Text` will be removed, so, we need to check // preceding one too. continue; } break; } if (auto* const element = Element::FromNode(previousContent)) { if (HTMLEditUtils::IsBlockElement( *element, BlockInlineCheck::UseComputedDisplayStyle) || HTMLEditUtils::IsNonEditableReplacedContent(*element)) { break; } // Ignore invisible inline elements } } } if (!pointToSplit.IsInTextNode() || pointToSplit.IsEndOfContainer()) { for (nsCOMPtr nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( pointToSplit, {LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, closestBlockElement); nextContent; nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( *nextContent, {LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, closestBlockElement)) { if (auto* const textNode = Text::FromNode(nextContent)) { if (!HTMLEditUtils::IsSimplyEditableNode(*textNode) && textNode->TextDataLength()) { break; } if (aOptions.contains( NormalizeOption::StopIfFollowingWhiteSpacesStartsWithNBSP) && textNode->TextFragment().SafeFirstChar() == HTMLEditUtils::kNBSP) { break; } followingTextNodes.AppendElement(*textNode); if (textNode->TextIsOnlyWhitespace() && EditorUtils::IsWhiteSpacePreformatted(*textNode)) { // white-space only `Text` will be removed, so, we need to check next // one too. continue; } break; } if (auto* const element = Element::FromNode(nextContent)) { if (HTMLEditUtils::IsBlockElement( *element, BlockInlineCheck::UseComputedDisplayStyle) || HTMLEditUtils::IsNonEditableReplacedContent(*element)) { break; } // Ignore invisible inline elements } } } AutoTrackDOMPoint trackPointToSplit(aHTMLEditor.RangeUpdaterRef(), &pointToSplit); for (const auto& textNode : precedingTextNodes) { Result normalizeWhiteSpacesResultOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt( aHTMLEditor, EditorDOMPointInText::AtEndOf(textNode), aOptions); if (MOZ_UNLIKELY(normalizeWhiteSpacesResultOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt() " "failed"); return normalizeWhiteSpacesResultOrError.propagateErr(); } if (normalizeWhiteSpacesResultOrError.inspect().IsInTextNode() && !normalizeWhiteSpacesResultOrError.inspect().IsStartOfContainer()) { // The white-space sequence started from middle of this node, so, we need // to do this for the preceding nodes. break; } } for (const auto& textNode : followingTextNodes) { Result normalizeWhiteSpacesResultOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt( aHTMLEditor, EditorDOMPointInText(textNode, 0u), aOptions); if (MOZ_UNLIKELY(normalizeWhiteSpacesResultOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt() " "failed"); return normalizeWhiteSpacesResultOrError.propagateErr(); } if (normalizeWhiteSpacesResultOrError.inspect().IsInTextNode() && !normalizeWhiteSpacesResultOrError.inspect().IsEndOfContainer()) { // The white-space sequence ended in middle of this node, so, we need // to do this for the following nodes. break; } } trackPointToSplit.FlushAndStopTracking(); if (NS_WARN_IF(!pointToSplit.IsInContentNode())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return std::move(pointToSplit); } Result WhiteSpaceVisibilityKeeper::NormalizeSurroundingWhiteSpacesToJoin( HTMLEditor& aHTMLEditor, const EditorDOMRange& aRangeToDelete) { MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible()); MOZ_ASSERT(!aRangeToDelete.Collapsed()); // Special case if the range for deleting text in same `Text`. In the case, // we need to normalize the white-space sequence which may be joined after // deletion. if (aRangeToDelete.StartRef().IsInTextNode() && aRangeToDelete.InSameContainer()) { const RefPtr textNode = aRangeToDelete.StartRef().ContainerAs(); Result rangeToDeleteOrError = WhiteSpaceVisibilityKeeper:: NormalizeSurroundingWhiteSpacesToDeleteCharacters( aHTMLEditor, *textNode, aRangeToDelete.StartRef().Offset(), aRangeToDelete.EndRef().Offset() - aRangeToDelete.StartRef().Offset()); NS_WARNING_ASSERTION( rangeToDeleteOrError.isOk(), "WhiteSpaceVisibilityKeeper::" "NormalizeSurroundingWhiteSpacesToDeleteCharacters() failed"); return rangeToDeleteOrError; } EditorDOMRange rangeToDelete(aRangeToDelete); // First, delete all invisible white-spaces around the end boundary. // The end boundary may be middle of invisible white-spaces. If so, // NormalizeWhiteSpacesToSplitTextNodeAt() won't work well for this. { AutoTrackDOMRange trackRangeToDelete(aHTMLEditor.RangeUpdaterRef(), &rangeToDelete); const WSScanResult nextThing = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( Scan::All, rangeToDelete.StartRef(), BlockInlineCheck::UseComputedDisplayOutsideStyle); if (nextThing.ReachedLineBoundary()) { nsresult rv = WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesBefore( aHTMLEditor, nextThing.PointAtReachedContent()); if (NS_FAILED(rv)) { NS_WARNING( "WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesBefore() " "failed"); return Err(rv); } } else { Result deleteInvisibleLeadingWhiteSpaceResultOrError = WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces( aHTMLEditor, rangeToDelete.EndRef()); if (MOZ_UNLIKELY(deleteInvisibleLeadingWhiteSpaceResultOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces() " "failed"); return deleteInvisibleLeadingWhiteSpaceResultOrError.propagateErr(); } } trackRangeToDelete.FlushAndStopTracking(); if (NS_WARN_IF(!rangeToDelete.IsPositionedAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } // Then, normalize white-spaces after the end boundary. if (rangeToDelete.EndRef().IsInTextNode() && rangeToDelete.EndRef().IsMiddleOfContainer()) { Result pointToSplitOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt( aHTMLEditor, rangeToDelete.EndRef().AsInText(), {NormalizeOption::HandleOnlyFollowingWhiteSpaces}); if (MOZ_UNLIKELY(pointToSplitOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt(" ") failed"); return pointToSplitOrError.propagateErr(); } EditorDOMPoint pointToSplit = pointToSplitOrError.unwrap(); if (pointToSplit.IsSet() && pointToSplit != rangeToDelete.EndRef()) { MOZ_ASSERT(rangeToDelete.StartRef().EqualsOrIsBefore(pointToSplit)); rangeToDelete.SetEnd(std::move(pointToSplit)); } } else { AutoTrackDOMRange trackRangeToDelete(aHTMLEditor.RangeUpdaterRef(), &rangeToDelete); Result atFirstVisibleThingOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter( aHTMLEditor, rangeToDelete.EndRef(), {}); if (MOZ_UNLIKELY(atFirstVisibleThingOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter() failed"); return atFirstVisibleThingOrError.propagateErr(); } trackRangeToDelete.FlushAndStopTracking(); if (NS_WARN_IF(!rangeToDelete.IsPositionedAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } // If cleaning up the white-spaces around the end boundary made the range // collapsed, the range was in invisible white-spaces. So, in the case, we // don't need to do nothing. if (MOZ_UNLIKELY(rangeToDelete.Collapsed())) { return rangeToDelete; } // Next, delete the invisible white-spaces around the start boundary. { AutoTrackDOMRange trackRangeToDelete(aHTMLEditor.RangeUpdaterRef(), &rangeToDelete); Result deleteInvisibleTrailingWhiteSpaceResultOrError = WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces( aHTMLEditor, rangeToDelete.StartRef()); if (MOZ_UNLIKELY(deleteInvisibleTrailingWhiteSpaceResultOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces() failed"); return deleteInvisibleTrailingWhiteSpaceResultOrError.propagateErr(); } trackRangeToDelete.FlushAndStopTracking(); if (NS_WARN_IF(!rangeToDelete.IsPositionedAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } // Finally, normalize white-spaces before the start boundary only when // the start boundary is middle of a `Text` node. This is compatible with // the other browsers. if (rangeToDelete.StartRef().IsInTextNode() && rangeToDelete.StartRef().IsMiddleOfContainer()) { AutoTrackDOMRange trackRangeToDelete(aHTMLEditor.RangeUpdaterRef(), &rangeToDelete); Result afterLastVisibleThingOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt( aHTMLEditor, rangeToDelete.StartRef().AsInText(), {NormalizeOption::HandleOnlyPrecedingWhiteSpaces}); if (MOZ_UNLIKELY(afterLastVisibleThingOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt() " "failed"); return afterLastVisibleThingOrError.propagateErr(); } trackRangeToDelete.FlushAndStopTracking(); EditorDOMPoint pointToSplit = afterLastVisibleThingOrError.unwrap(); if (pointToSplit.IsSet() && pointToSplit != rangeToDelete.StartRef()) { MOZ_ASSERT(pointToSplit.EqualsOrIsBefore(rangeToDelete.EndRef())); rangeToDelete.SetStart(std::move(pointToSplit)); } } return rangeToDelete; } Result WhiteSpaceVisibilityKeeper::NormalizeSurroundingWhiteSpacesToDeleteCharacters( HTMLEditor& aHTMLEditor, Text& aTextNode, uint32_t aOffset, uint32_t aLength) { MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible()); MOZ_ASSERT(aOffset <= aTextNode.TextDataLength()); MOZ_ASSERT(aOffset + aLength <= aTextNode.TextDataLength()); const HTMLEditor::ReplaceWhiteSpacesData normalizedWhiteSpacesData = aHTMLEditor.GetSurroundingNormalizedStringToDelete(aTextNode, aOffset, aLength); EditorDOMRange rangeToDelete(EditorDOMPoint(&aTextNode, aOffset), EditorDOMPoint(&aTextNode, aOffset + aLength)); if (!normalizedWhiteSpacesData.ReplaceLength()) { return rangeToDelete; } // mNewOffsetAfterReplace is set to aOffset after applying replacing the // range. MOZ_ASSERT(normalizedWhiteSpacesData.mNewOffsetAfterReplace != UINT32_MAX); MOZ_ASSERT(normalizedWhiteSpacesData.mNewOffsetAfterReplace >= normalizedWhiteSpacesData.mReplaceStartOffset); MOZ_ASSERT(normalizedWhiteSpacesData.mNewOffsetAfterReplace <= normalizedWhiteSpacesData.mReplaceEndOffset); #ifdef DEBUG { const HTMLEditor::ReplaceWhiteSpacesData normalizedPrecedingWhiteSpacesData = normalizedWhiteSpacesData.PreviousDataOfNewOffset(aOffset); const HTMLEditor::ReplaceWhiteSpacesData normalizedFollowingWhiteSpacesData = normalizedWhiteSpacesData.NextDataOfNewOffset(aOffset + aLength); MOZ_ASSERT(normalizedPrecedingWhiteSpacesData.ReplaceLength() + aLength + normalizedFollowingWhiteSpacesData.ReplaceLength() == normalizedWhiteSpacesData.ReplaceLength()); MOZ_ASSERT( normalizedPrecedingWhiteSpacesData.mNormalizedString.Length() + normalizedFollowingWhiteSpacesData.mNormalizedString.Length() == normalizedWhiteSpacesData.mNormalizedString.Length()); } #endif const HTMLEditor::ReplaceWhiteSpacesData normalizedPrecedingWhiteSpacesData = normalizedWhiteSpacesData.PreviousDataOfNewOffset(aOffset) .GetMinimizedData(aTextNode); const HTMLEditor::ReplaceWhiteSpacesData normalizedFollowingWhiteSpacesData = normalizedWhiteSpacesData.NextDataOfNewOffset(aOffset + aLength) .GetMinimizedData(aTextNode); if (normalizedFollowingWhiteSpacesData.ReplaceLength()) { AutoTrackDOMRange trackRangeToDelete(aHTMLEditor.RangeUpdaterRef(), &rangeToDelete); Result replaceFollowingWhiteSpacesResultOrError = aHTMLEditor.ReplaceTextWithTransaction( aTextNode, normalizedFollowingWhiteSpacesData); if (MOZ_UNLIKELY(replaceFollowingWhiteSpacesResultOrError.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return replaceFollowingWhiteSpacesResultOrError.propagateErr(); } trackRangeToDelete.FlushAndStopTracking(); if (NS_WARN_IF(!rangeToDelete.IsPositioned())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } if (normalizedPrecedingWhiteSpacesData.ReplaceLength()) { AutoTrackDOMRange trackRangeToDelete(aHTMLEditor.RangeUpdaterRef(), &rangeToDelete); Result replacePrecedingWhiteSpacesResultOrError = aHTMLEditor.ReplaceTextWithTransaction( aTextNode, normalizedPrecedingWhiteSpacesData); if (MOZ_UNLIKELY(replacePrecedingWhiteSpacesResultOrError.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return replacePrecedingWhiteSpacesResultOrError.propagateErr(); } trackRangeToDelete.FlushAndStopTracking(); if (NS_WARN_IF(!rangeToDelete.IsPositioned())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } return std::move(rangeToDelete); } // static Result WhiteSpaceVisibilityKeeper::InsertLineBreak( LineBreakType aLineBreakType, HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToInsert) { if (MOZ_UNLIKELY(NS_WARN_IF(!aPointToInsert.IsSet()))) { return Err(NS_ERROR_INVALID_ARG); } EditorDOMPoint pointToInsert(aPointToInsert); // TODO: Delete this block once we ship the new normalizer. if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) { // MOOSE: for now, we always assume non-PRE formatting. Fix this later. // meanwhile, the pre case is handled in HandleInsertText() in // HTMLEditSubActionHandler.cpp const TextFragmentData textFragmentDataAtInsertionPoint( Scan::EditableNodes, aPointToInsert, BlockInlineCheck::UseComputedDisplayStyle); if (NS_WARN_IF(!textFragmentDataAtInsertionPoint.IsInitialized())) { return Err(NS_ERROR_FAILURE); } EditorDOMRange invisibleLeadingWhiteSpaceRangeOfNewLine = textFragmentDataAtInsertionPoint .GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt(aPointToInsert); EditorDOMRange invisibleTrailingWhiteSpaceRangeOfCurrentLine = textFragmentDataAtInsertionPoint .GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt( aPointToInsert); const Maybe visibleWhiteSpaces = !invisibleLeadingWhiteSpaceRangeOfNewLine.IsPositioned() || !invisibleTrailingWhiteSpaceRangeOfCurrentLine.IsPositioned() ? Some(textFragmentDataAtInsertionPoint.VisibleWhiteSpacesDataRef()) : Nothing(); const PointPosition pointPositionWithVisibleWhiteSpaces = visibleWhiteSpaces.isSome() && visibleWhiteSpaces.ref().IsInitialized() ? visibleWhiteSpaces.ref().ComparePoint(aPointToInsert) : PointPosition::NotInSameDOMTree; EditorDOMPoint atNBSPReplaceableWithSP; if (!invisibleLeadingWhiteSpaceRangeOfNewLine.IsPositioned() && (pointPositionWithVisibleWhiteSpaces == PointPosition::MiddleOfFragment || pointPositionWithVisibleWhiteSpaces == PointPosition::EndOfFragment)) { atNBSPReplaceableWithSP = textFragmentDataAtInsertionPoint .GetPreviousNBSPPointIfNeedToReplaceWithASCIIWhiteSpace( pointToInsert) .To(); } { 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 caretPointOrError = aHTMLEditor.DeleteTextAndTextNodesWithTransaction( invisibleTrailingWhiteSpaceRangeOfCurrentLine.StartRef(), invisibleTrailingWhiteSpaceRangeOfCurrentLine.EndRef(), HTMLEditor::TreatEmptyTextNodes:: KeepIfContainerOfRangeBoundaries); if (MOZ_UNLIKELY(caretPointOrError.isErr())) { NS_WARNING( "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed"); return caretPointOrError.propagateErr(); } trackLeadingWhiteSpaceRange.FlushAndStopTracking(); 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) { const auto atNextCharOfInsertionPoint = textFragmentDataAtInsertionPoint .GetInclusiveNextCharPoint( pointToInsert, IgnoreNonEditableNodes::Yes); if (atNextCharOfInsertionPoint.IsSet() && !atNextCharOfInsertionPoint.IsEndOfContainer() && atNextCharOfInsertionPoint.IsCharCollapsibleASCIISpace()) { const auto atPreviousCharOfNextCharOfInsertionPoint = textFragmentDataAtInsertionPoint .GetPreviousCharPoint( atNextCharOfInsertionPoint, IgnoreNonEditableNodes::Yes); 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 auto endOfCollapsibleASCIIWhiteSpaces = textFragmentDataAtInsertionPoint .GetEndOfCollapsibleASCIIWhiteSpaces( atNextCharOfInsertionPoint, nsIEditor::eNone, // XXX Shouldn't be "No"? Skipping non-editable nodes // may have visible content. IgnoreNonEditableNodes::Yes); 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); } trackLeadingWhiteSpaceRange.FlushAndStopTracking(); // 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 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 `
` 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 replaceTextResult = aHTMLEditor.ReplaceTextWithTransaction( MOZ_KnownLive( *atNBSPReplacedWithASCIIWhiteSpace.ContainerAs()), 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(); } } } } else { // Chrome does not normalize preceding white-spaces at least when it ends // with an NBSP. Result normalizeSurroundingWhiteSpacesResultOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt( aHTMLEditor, aPointToInsert, {NormalizeOption::StopIfPrecedingWhiteSpacesEndsWithNBP}); if (MOZ_UNLIKELY(normalizeSurroundingWhiteSpacesResultOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt() failed"); return normalizeSurroundingWhiteSpacesResultOrError.propagateErr(); } pointToInsert = normalizeSurroundingWhiteSpacesResultOrError.unwrap(); if (NS_WARN_IF(!pointToInsert.IsSet())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } Result insertBRElementResultOrError = aHTMLEditor.InsertLineBreak(WithTransaction::Yes, aLineBreakType, pointToInsert); NS_WARNING_ASSERTION(insertBRElementResultOrError.isOk(), "HTMLEditor::InsertLineBreak(WithTransaction::Yes, " "aLineBreakType, eNone) failed"); return insertBRElementResultOrError; } nsresult WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesAfter( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint) { MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible()); MOZ_ASSERT(aPoint.IsInContentNode()); const RefPtr colsetBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *aPoint.ContainerAs(), HTMLEditUtils::ClosestEditableBlockElement, BlockInlineCheck::UseComputedDisplayStyle); EditorDOMPoint atFirstInvisibleWhiteSpace; AutoTArray, 32> unnecessaryContents; for (nsIContent* nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( aPoint, {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement); nextContent; nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( EditorRawDOMPoint::After(*nextContent), {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement)) { if (!HTMLEditUtils::IsSimplyEditableNode(*nextContent)) { // XXX Assume non-editable nodes are visible. break; } const RefPtr followingTextNode = Text::FromNode(nextContent); if (!followingTextNode && HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*nextContent)) { break; } if (!followingTextNode || !followingTextNode->TextDataLength()) { // If it's an empty inline element like `` or an empty `Text`, // delete it. nsIContent* emptyInlineContent = HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( *nextContent, BlockInlineCheck::UseComputedDisplayStyle); if (!emptyInlineContent) { emptyInlineContent = nextContent; } unnecessaryContents.AppendElement(*emptyInlineContent); continue; } const EditorRawDOMPointInText atFirstChar(followingTextNode, 0u); if (!atFirstChar.IsCharCollapsibleASCIISpace()) { break; } // If the preceding Text is collapsed and invisible, we should delete it // and keep deleting preceding invisible white-spaces. if (!HTMLEditUtils::IsVisibleTextNode(*followingTextNode)) { nsIContent* emptyInlineContent = HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( *followingTextNode, BlockInlineCheck::UseComputedDisplayStyle); if (!emptyInlineContent) { emptyInlineContent = followingTextNode; } unnecessaryContents.AppendElement(*emptyInlineContent); continue; } Result startOfTextOrError = WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces( aHTMLEditor, EditorDOMPoint(followingTextNode, 0u)); if (MOZ_UNLIKELY(startOfTextOrError.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return startOfTextOrError.unwrapErr(); } break; } for (const auto& contentToDelete : unnecessaryContents) { if (MOZ_UNLIKELY(!contentToDelete->IsInComposedDoc())) { continue; } nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(contentToDelete)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return rv; } } return NS_OK; } nsresult WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesBefore( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint) { MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible()); MOZ_ASSERT(aPoint.IsInContentNode()); const RefPtr colsetBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *aPoint.ContainerAs(), HTMLEditUtils::ClosestEditableBlockElement, BlockInlineCheck::UseComputedDisplayStyle); EditorDOMPoint atFirstInvisibleWhiteSpace; AutoTArray, 32> unnecessaryContents; for (nsIContent* previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( aPoint, {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement); previousContent; previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( EditorRawDOMPoint(previousContent), {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement)) { if (!HTMLEditUtils::IsSimplyEditableNode(*previousContent)) { // XXX Assume non-editable nodes are visible. break; } const RefPtr precedingTextNode = Text::FromNode(previousContent); if (!precedingTextNode && HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*previousContent)) { break; } if (!precedingTextNode || !precedingTextNode->TextDataLength()) { // If it's an empty inline element like `` or an empty `Text`, // delete it. nsIContent* emptyInlineContent = HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( *previousContent, BlockInlineCheck::UseComputedDisplayStyle); if (!emptyInlineContent) { emptyInlineContent = previousContent; } unnecessaryContents.AppendElement(*emptyInlineContent); continue; } const auto atLastChar = EditorRawDOMPointInText::AtLastContentOf(*precedingTextNode); if (!atLastChar.IsCharCollapsibleASCIISpace()) { break; } // If the preceding Text is collapsed and invisible, we should delete it // and keep deleting preceding invisible white-spaces. if (!HTMLEditUtils::IsVisibleTextNode(*precedingTextNode)) { nsIContent* emptyInlineContent = HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( *precedingTextNode, BlockInlineCheck::UseComputedDisplayStyle); if (!emptyInlineContent) { emptyInlineContent = precedingTextNode; } unnecessaryContents.AppendElement(*emptyInlineContent); continue; } Result endOfTextOrResult = WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces( aHTMLEditor, EditorDOMPoint::AtEndOf(*precedingTextNode)); if (MOZ_UNLIKELY(endOfTextOrResult.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return endOfTextOrResult.unwrapErr(); } break; } for (const auto& contentToDelete : Reversed(unnecessaryContents)) { if (MOZ_UNLIKELY(!contentToDelete->IsInComposedDoc())) { continue; } nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(contentToDelete)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return rv; } } return NS_OK; } Result WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint) { if (EditorUtils::IsWhiteSpacePreformatted( *aPoint.ContainerAs())) { return EditorDOMPoint(); } if (aPoint.IsInTextNode() && // If there is a previous char and it's not a collapsible ASCII // white-space, the point is not in the leading white-spaces. (!aPoint.IsStartOfContainer() && !aPoint.IsPreviousCharASCIISpace()) && // If it does not points a collapsible ASCII white-space, the point is not // in the trailing white-spaces. (!aPoint.IsEndOfContainer() && !aPoint.IsCharCollapsibleASCIISpace())) { return EditorDOMPoint(); } const Element* const closestBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *aPoint.ContainerAs(), HTMLEditUtils::ClosestBlockElement, BlockInlineCheck::UseComputedDisplayStyle); if (MOZ_UNLIKELY(!closestBlockElement)) { return EditorDOMPoint(); // aPoint is not in a block. } const TextFragmentData textFragmentDataForLeadingWhiteSpaces( Scan::EditableNodes, aPoint.IsStartOfContainer() && aPoint.GetContainer() == closestBlockElement ? aPoint : aPoint.PreviousPointOrParentPoint(), BlockInlineCheck::UseComputedDisplayStyle, closestBlockElement); if (NS_WARN_IF(!textFragmentDataForLeadingWhiteSpaces.IsInitialized())) { return Err(NS_ERROR_FAILURE); } { const EditorDOMRange& leadingWhiteSpaceRange = textFragmentDataForLeadingWhiteSpaces .InvisibleLeadingWhiteSpaceRangeRef(); if (leadingWhiteSpaceRange.IsPositioned() && !leadingWhiteSpaceRange.Collapsed()) { EditorDOMPoint endOfLeadingWhiteSpaces(leadingWhiteSpaceRange.EndRef()); AutoTrackDOMPoint trackEndOfLeadingWhiteSpaces( aHTMLEditor.RangeUpdaterRef(), &endOfLeadingWhiteSpaces); Result caretPointOrError = aHTMLEditor.DeleteTextAndTextNodesWithTransaction( leadingWhiteSpaceRange.StartRef(), leadingWhiteSpaceRange.EndRef(), HTMLEditor::TreatEmptyTextNodes:: KeepIfContainerOfRangeBoundaries); if (MOZ_UNLIKELY(caretPointOrError.isErr())) { NS_WARNING( "HTMLEditor::DeleteTextAndTextNodesWithTransaction(" "TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries) failed"); return caretPointOrError.propagateErr(); } caretPointOrError.unwrap().IgnoreCaretPointSuggestion(); // If the leading white-spaces were split into multiple text node, we need // only the last `Text` node. if (!leadingWhiteSpaceRange.InSameContainer() && leadingWhiteSpaceRange.StartRef().IsInTextNode() && leadingWhiteSpaceRange.StartRef() .ContainerAs() ->IsInComposedDoc() && leadingWhiteSpaceRange.EndRef().IsInTextNode() && leadingWhiteSpaceRange.EndRef() .ContainerAs() ->IsInComposedDoc() && !leadingWhiteSpaceRange.StartRef() .ContainerAs() ->TextDataLength()) { nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive( *leadingWhiteSpaceRange.StartRef().ContainerAs())); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return Err(rv); } } trackEndOfLeadingWhiteSpaces.FlushAndStopTracking(); if (NS_WARN_IF(!endOfLeadingWhiteSpaces.IsSetAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return endOfLeadingWhiteSpaces; } } const TextFragmentData textFragmentData = textFragmentDataForLeadingWhiteSpaces.ScanStartRef() == aPoint ? textFragmentDataForLeadingWhiteSpaces : TextFragmentData(Scan::EditableNodes, aPoint, BlockInlineCheck::UseComputedDisplayStyle, closestBlockElement); const EditorDOMRange& trailingWhiteSpaceRange = textFragmentData.InvisibleTrailingWhiteSpaceRangeRef(); if (trailingWhiteSpaceRange.IsPositioned() && !trailingWhiteSpaceRange.Collapsed()) { EditorDOMPoint startOfTrailingWhiteSpaces( trailingWhiteSpaceRange.StartRef()); AutoTrackDOMPoint trackStartOfTrailingWhiteSpaces( aHTMLEditor.RangeUpdaterRef(), &startOfTrailingWhiteSpaces); Result caretPointOrError = aHTMLEditor.DeleteTextAndTextNodesWithTransaction( trailingWhiteSpaceRange.StartRef(), trailingWhiteSpaceRange.EndRef(), HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries); if (MOZ_UNLIKELY(caretPointOrError.isErr())) { NS_WARNING( "HTMLEditor::DeleteTextAndTextNodesWithTransaction(" "TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries) failed"); return caretPointOrError.propagateErr(); } caretPointOrError.unwrap().IgnoreCaretPointSuggestion(); // If the leading white-spaces were split into multiple text node, we need // only the last `Text` node. if (!trailingWhiteSpaceRange.InSameContainer() && trailingWhiteSpaceRange.StartRef().IsInTextNode() && trailingWhiteSpaceRange.StartRef() .ContainerAs() ->IsInComposedDoc() && trailingWhiteSpaceRange.EndRef().IsInTextNode() && trailingWhiteSpaceRange.EndRef() .ContainerAs() ->IsInComposedDoc() && !trailingWhiteSpaceRange.EndRef() .ContainerAs() ->TextDataLength()) { nsresult rv = aHTMLEditor.DeleteNodeWithTransaction( MOZ_KnownLive(*trailingWhiteSpaceRange.EndRef().ContainerAs())); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return Err(rv); } } trackStartOfTrailingWhiteSpaces.FlushAndStopTracking(); if (NS_WARN_IF(!startOfTrailingWhiteSpaces.IsSetAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return startOfTrailingWhiteSpaces; } const auto atCollapsibleASCIISpace = [&]() MOZ_NEVER_INLINE_DEBUG -> EditorDOMPointInText { const auto point = textFragmentData.GetInclusiveNextCharPoint( textFragmentData.ScanStartRef(), IgnoreNonEditableNodes::Yes); if (point.IsSet() && // XXX Perhaps, we should ignore empty `Text` nodes and keep scanning. !point.IsEndOfContainer() && point.IsCharCollapsibleASCIISpace()) { return point; } const auto prevPoint = textFragmentData.GetPreviousCharPoint( textFragmentData.ScanStartRef(), IgnoreNonEditableNodes::Yes); return prevPoint.IsSet() && // XXX Perhaps, we should ignore empty `Text` nodes and keep // scanning. !prevPoint.IsEndOfContainer() && prevPoint.IsCharCollapsibleASCIISpace() ? prevPoint : EditorDOMPointInText(); }(); if (!atCollapsibleASCIISpace.IsSet()) { return EditorDOMPoint(); } const auto firstCollapsibleASCIISpacePoint = textFragmentData .GetFirstASCIIWhiteSpacePointCollapsedTo( atCollapsibleASCIISpace, nsIEditor::eNone, IgnoreNonEditableNodes::No); const auto endOfCollapsibleASCIISpacePoint = textFragmentData .GetEndOfCollapsibleASCIIWhiteSpaces( atCollapsibleASCIISpace, nsIEditor::eNone, IgnoreNonEditableNodes::No); if (firstCollapsibleASCIISpacePoint.NextPoint() == endOfCollapsibleASCIISpacePoint) { // Only one white-space, so that nothing to do. return EditorDOMPoint(); } // Okay, there are some collapsed white-spaces. We should delete them with // keeping first one. Result deleteTextResultOrError = aHTMLEditor.DeleteTextAndTextNodesWithTransaction( firstCollapsibleASCIISpacePoint.NextPoint(), endOfCollapsibleASCIISpacePoint, HTMLEditor::TreatEmptyTextNodes::Remove); if (MOZ_UNLIKELY(deleteTextResultOrError.isErr())) { NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); return deleteTextResultOrError.propagateErr(); } return deleteTextResultOrError.unwrap().UnwrapCaretPoint(); } // static Result WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString( HTMLEditor& aHTMLEditor, const nsAString& aStringToInsert, const EditorDOMRange& aRangeToBeReplaced, InsertTextTo aInsertTextTo, InsertTextFor aPurpose) { MOZ_ASSERT(aRangeToBeReplaced.StartRef().IsInContentNode()); MOZ_ASSERT_IF(!EditorBase::InsertingTextForExtantComposition(aPurpose), aRangeToBeReplaced.Collapsed()); if (aStringToInsert.IsEmpty()) { MOZ_ASSERT(aRangeToBeReplaced.Collapsed()); return InsertTextResult(); } // TODO: Delete this block once we ship the new normalizer. if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) { // 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 const TextFragmentData textFragmentDataAtStart( Scan::EditableNodes, aRangeToBeReplaced.StartRef(), BlockInlineCheck::UseComputedDisplayStyle); 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(Scan::EditableNodes, aRangeToBeReplaced.EndRef(), BlockInlineCheck::UseComputedDisplayStyle); 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 visibleWhiteSpacesAtStart = !isInvisibleLeadingWhiteSpaceRangeAtStartPositioned ? Some(textFragmentDataAtStart.VisibleWhiteSpacesDataRef()) : Nothing(); const PointPosition pointPositionWithVisibleWhiteSpacesAtStart = visibleWhiteSpacesAtStart.isSome() && visibleWhiteSpacesAtStart.ref().IsInitialized() ? visibleWhiteSpacesAtStart.ref().ComparePoint( aRangeToBeReplaced.StartRef()) : PointPosition::NotInSameDOMTree; const Maybe 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(); } 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 caretPointOrError = aHTMLEditor.DeleteTextAndTextNodesWithTransaction( invisibleTrailingWhiteSpaceRangeAtEnd.StartRef(), invisibleTrailingWhiteSpaceRangeAtEnd.EndRef(), HTMLEditor::TreatEmptyTextNodes:: KeepIfContainerOfRangeBoundaries); if (MOZ_UNLIKELY(caretPointOrError.isErr())) { NS_WARNING( "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed"); return caretPointOrError.propagateErr(); } trackInvisibleTrailingWhiteSpaceRange.FlushAndStopTracking(); trackInvisibleLeadingWhiteSpaceRange.FlushAndStopTracking(); 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 replaceTextResult = aHTMLEditor.ReplaceTextWithTransaction( MOZ_KnownLive( *atNBSPReplacedWithASCIIWhiteSpace.ContainerAs()), 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 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(); trackInvisibleTrailingWhiteSpaceRange.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 replaceTextResult = aHTMLEditor.ReplaceTextWithTransaction( MOZ_KnownLive( *atNBSPReplacedWithASCIIWhiteSpace.ContainerAs()), 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())) { const bool isNewLineCollapsible = !pointToInsert.IsInContentNode() || !EditorUtils::IsNewLinePreformatted( *pointToInsert.ContainerAs()); 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 .GetPreviousCharPoint( pointToInsert, IgnoreNonEditableNodes::Yes); 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() || textFragmentDataAtStart .StartsFromInlineEditingHostBoundary()) && 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 .GetInclusiveNextCharPoint( pointToInsert, IgnoreNonEditableNodes::Yes); 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() || textFragmentDataAtEnd.EndsByInlineEditingHostBoundary()) && 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. AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(), &pointToPutCaret); Result insertTextResult = aHTMLEditor.InsertTextWithTransaction(theString, pointToInsert, aInsertTextTo); 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)); } if (NS_WARN_IF(!aRangeToBeReplaced.StartRef().IsInContentNode())) { return Err(NS_ERROR_FAILURE); // Cannot insert text } EditorDOMPoint pointToInsert = aHTMLEditor.ComputePointToInsertText( aRangeToBeReplaced.StartRef(), aInsertTextTo); MOZ_ASSERT(pointToInsert.IsInContentNode()); const bool isWhiteSpaceCollapsible = !EditorUtils::IsWhiteSpacePreformatted( *aRangeToBeReplaced.StartRef().ContainerAs()); // First, delete invisible leading white-spaces and trailing white-spaces if // they are there around the replacing range boundaries. However, don't do // that if we're updating existing composition string to avoid the composition // transaction is broken by the text change around composition string. if (!EditorBase::InsertingTextForExtantComposition(aPurpose) && isWhiteSpaceCollapsible && pointToInsert.IsInContentNode()) { AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(), &pointToInsert); Result deletePointOfInvisibleWhiteSpacesAtStartOrError = WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces( aHTMLEditor, pointToInsert); if (MOZ_UNLIKELY(deletePointOfInvisibleWhiteSpacesAtStartOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces() failed"); return deletePointOfInvisibleWhiteSpacesAtStartOrError.propagateErr(); } trackPointToInsert.FlushAndStopTracking(); const EditorDOMPoint deletePointOfInvisibleWhiteSpacesAtStart = deletePointOfInvisibleWhiteSpacesAtStartOrError.unwrap(); if (NS_WARN_IF(deletePointOfInvisibleWhiteSpacesAtStart.IsSet() && !pointToInsert.IsSetAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // If we're starting composition, we won't normalizing surrounding // white-spaces until end of the composition. Additionally, at that time, // we need to assume all white-spaces of surrounding white-spaces are // visible because canceling composition may cause previous white-space // invisible temporarily. Therefore, we should normalize surrounding // white-spaces to delete invisible white-spaces contained in the sequence. // E.g., `NBSP SP SP NBSP`, in this case, one of the SP is invisible. if (EditorBase::InsertingTextForStartingComposition(aPurpose) && pointToInsert.IsInTextNode()) { const auto whiteSpaceOffset = [&]() -> Maybe { if (!pointToInsert.IsEndOfContainer() && pointToInsert.IsCharCollapsibleASCIISpaceOrNBSP()) { return Some(pointToInsert.Offset()); } if (!pointToInsert.IsStartOfContainer() && pointToInsert.IsPreviousCharCollapsibleASCIISpaceOrNBSP()) { return Some(pointToInsert.Offset() - 1u); } return Nothing(); }(); if (whiteSpaceOffset.isSome()) { Maybe trackPointToInsert; if (pointToInsert.Offset() != *whiteSpaceOffset) { trackPointToInsert.emplace(aHTMLEditor.RangeUpdaterRef(), &pointToInsert); } Result pointToInsertOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAt( aHTMLEditor, EditorDOMPointInText(pointToInsert.ContainerAs(), *whiteSpaceOffset)); if (MOZ_UNLIKELY(pointToInsertOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAt() failed"); return pointToInsertOrError.propagateErr(); } if (trackPointToInsert.isSome()) { trackPointToInsert.reset(); } else { pointToInsert = pointToInsertOrError.unwrap(); } if (NS_WARN_IF(!pointToInsert.IsInContentNodeAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } } } if (NS_WARN_IF(!pointToInsert.IsInContentNode())) { return Err(NS_ERROR_FAILURE); } const HTMLEditor::NormalizedStringToInsertText insertTextData = [&]() MOZ_NEVER_INLINE_DEBUG { if (!isWhiteSpaceCollapsible) { return HTMLEditor::NormalizedStringToInsertText(aStringToInsert, pointToInsert); } if (pointToInsert.IsInTextNode() && !EditorBase::InsertingTextForComposition(aPurpose)) { // If normalizing the surrounding white-spaces in the `Text`, we // should minimize the replacing range to avoid to unnecessary // replacement. return aHTMLEditor .NormalizeWhiteSpacesToInsertText( pointToInsert, aStringToInsert, HTMLEditor::NormalizeSurroundingWhiteSpaces::Yes) .GetMinimizedData(*pointToInsert.ContainerAs()); } return aHTMLEditor.NormalizeWhiteSpacesToInsertText( pointToInsert, aStringToInsert, // If we're handling composition string, we should not replace // surrounding white-spaces to avoid to make // CompositionTransaction confused. EditorBase::InsertingTextForComposition(aPurpose) ? HTMLEditor::NormalizeSurroundingWhiteSpaces::No : HTMLEditor::NormalizeSurroundingWhiteSpaces::Yes); }(); MOZ_ASSERT_IF(insertTextData.ReplaceLength(), pointToInsert.IsInTextNode()); Result insertOrReplaceTextResultOrError = aHTMLEditor.InsertOrReplaceTextWithTransaction(pointToInsert, insertTextData); if (MOZ_UNLIKELY(insertOrReplaceTextResultOrError.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return insertOrReplaceTextResultOrError; } // If the composition is committed, we should normalize surrounding // white-spaces of the commit string. if (!EditorBase::InsertingTextForCommittingComposition(aPurpose)) { return insertOrReplaceTextResultOrError; } InsertTextResult insertOrReplaceTextResult = insertOrReplaceTextResultOrError.unwrap(); const EditorDOMPointInText endOfCommitString = insertOrReplaceTextResult.EndOfInsertedTextRef().GetAsInText(); if (!endOfCommitString.IsSet() || endOfCommitString.IsContainerEmpty()) { return std::move(insertOrReplaceTextResult); } if (NS_WARN_IF(endOfCommitString.Offset() < insertTextData.mNormalizedString.Length())) { insertOrReplaceTextResult.IgnoreCaretPointSuggestion(); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } const EditorDOMPointInText startOfCommitString( endOfCommitString.ContainerAs(), endOfCommitString.Offset() - insertTextData.mNormalizedString.Length()); MOZ_ASSERT(insertOrReplaceTextResult.EndOfInsertedTextRef() == insertOrReplaceTextResult.CaretPointRef()); EditorDOMPoint pointToPutCaret = insertOrReplaceTextResult.UnwrapCaretPoint(); // First, normalize the trailing white-spaces if there is. Note that its // sequence may start from before the commit string. In such case, the // another call of NormalizeWhiteSpacesAt() won't update the DOM. if (endOfCommitString.IsMiddleOfContainer()) { nsresult rv = WhiteSpaceVisibilityKeeper:: NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces( aHTMLEditor, endOfCommitString.PreviousPoint()); if (NS_FAILED(rv)) { NS_WARNING( "WhiteSpaceVisibilityKeeper::" "NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces() " "failed"); return Err(rv); } if (NS_WARN_IF(!pointToPutCaret.IsSetAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } // Finally, normalize the leading white-spaces if there is and not a part of // the trailing white-spaces. if (!startOfCommitString.IsStartOfContainer()) { nsresult rv = WhiteSpaceVisibilityKeeper:: NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces( aHTMLEditor, startOfCommitString.PreviousPoint()); if (NS_FAILED(rv)) { NS_WARNING( "WhiteSpaceVisibilityKeeper::" "NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces() " "failed"); return Err(rv); } if (NS_WARN_IF(!pointToPutCaret.IsSetAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } EditorDOMPoint endOfCommitStringAfterNormalized = pointToPutCaret; return InsertTextResult(std::move(endOfCommitStringAfterNormalized), CaretPoint(std::move(pointToPutCaret))); } // static nsresult WhiteSpaceVisibilityKeeper:: NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces( HTMLEditor& aHTMLEditor, const EditorDOMPointInText& aPoint) { MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible()); MOZ_ASSERT(aPoint.IsSet()); MOZ_ASSERT(!aPoint.IsEndOfContainer()); if (EditorUtils::IsWhiteSpacePreformatted(*aPoint.ContainerAs())) { return NS_OK; } Text& textNode = *aPoint.ContainerAs(); const bool isNewLinePreformatted = EditorUtils::IsNewLinePreformatted(textNode); const auto IsCollapsibleChar = [&](char16_t aChar) { return aChar == HTMLEditUtils::kNewLine ? !isNewLinePreformatted : nsCRT::IsAsciiSpace(aChar); }; const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) { return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar); }; const auto whiteSpaceOffset = [&]() -> Maybe { if (IsCollapsibleCharOrNBSP(aPoint.Char())) { return Some(aPoint.Offset()); } if (!aPoint.IsAtLastContent() && IsCollapsibleCharOrNBSP(aPoint.NextChar())) { return Some(aPoint.Offset() + 1u); } return Nothing(); }(); if (whiteSpaceOffset.isNothing()) { return NS_OK; } const uint32_t firstOffset = [&]() { for (const uint32_t offset : Reversed(IntegerRange(*whiteSpaceOffset))) { if (!IsCollapsibleCharOrNBSP(textNode.TextFragment().CharAt(offset))) { return offset + 1u; } } return 0u; }(); const uint32_t endOffset = [&]() { for (const uint32_t offset : IntegerRange(*whiteSpaceOffset + 1, textNode.TextDataLength())) { if (!IsCollapsibleCharOrNBSP(textNode.TextFragment().CharAt(offset))) { return offset; } } return textNode.TextDataLength(); }(); nsAutoString normalizedString; const char16_t precedingChar = !firstOffset ? static_cast(0) : textNode.TextFragment().CharAt(firstOffset - 1u); const char16_t followingChar = endOffset == textNode.TextDataLength() ? static_cast(0) : textNode.TextFragment().CharAt(endOffset); HTMLEditor::GenerateWhiteSpaceSequence( normalizedString, endOffset - firstOffset, !firstOffset ? HTMLEditor::CharPointData::InSameTextNode( HTMLEditor::CharPointType::TextEnd) : HTMLEditor::CharPointData::InSameTextNode( precedingChar == HTMLEditUtils::kNewLine ? HTMLEditor::CharPointType::PreformattedLineBreak : HTMLEditor::CharPointType::VisibleChar), endOffset == textNode.TextDataLength() ? HTMLEditor::CharPointData::InSameTextNode( HTMLEditor::CharPointType::TextEnd) : HTMLEditor::CharPointData::InSameTextNode( followingChar == HTMLEditUtils::kNewLine ? HTMLEditor::CharPointType::PreformattedLineBreak : HTMLEditor::CharPointType::VisibleChar)); MOZ_ASSERT(normalizedString.Length() == endOffset - firstOffset); const OwningNonNull text(textNode); Result normalizeWhiteSpaceSequenceResultOrError = aHTMLEditor.ReplaceTextWithTransaction( text, firstOffset, endOffset - firstOffset, normalizedString); if (MOZ_UNLIKELY(normalizeWhiteSpaceSequenceResultOrError.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return normalizeWhiteSpaceSequenceResultOrError.unwrapErr(); } normalizeWhiteSpaceSequenceResultOrError.unwrap() .IgnoreCaretPointSuggestion(); return NS_OK; } // static Result WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint, const Element& aEditingHost) { MOZ_ASSERT(!StaticPrefs::editor_white_space_normalization_blink_compatible()); const TextFragmentData textFragmentDataAtDeletion( Scan::EditableNodes, aPoint, BlockInlineCheck::UseComputedDisplayStyle); if (NS_WARN_IF(!textFragmentDataAtDeletion.IsInitialized())) { return Err(NS_ERROR_FAILURE); } const auto atPreviousCharOfStart = textFragmentDataAtDeletion.GetPreviousCharPoint( aPoint, IgnoreNonEditableNodes::Yes); 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( atPreviousCharOfStart, nsIEditor::ePrevious, // XXX Shouldn't be "No"? Skipping non-editable nodes may have // visible content. IgnoreNonEditableNodes::Yes); auto endToDelete = textFragmentDataAtDeletion .GetEndOfCollapsibleASCIIWhiteSpaces( atPreviousCharOfStart, nsIEditor::ePrevious, // XXX Shouldn't be "No"? Skipping non-editable // nodes may have visible content. IgnoreNonEditableNodes::Yes); EditorDOMPoint pointToPutCaret; { Result 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 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(); auto endToDelete = startToDelete.NextPoint(); EditorDOMPoint pointToPutCaret; { Result 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 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 caretPointOrError = aHTMLEditor.DeleteTextAndTextNodesWithTransaction( atPreviousCharOfStart, atPreviousCharOfStart.NextPoint(), HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries); NS_WARNING_ASSERTION( caretPointOrError.isOk(), "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed"); return caretPointOrError; } // static Result WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint, const Element& aEditingHost) { MOZ_ASSERT(!StaticPrefs::editor_white_space_normalization_blink_compatible()); const TextFragmentData textFragmentDataAtDeletion( Scan::EditableNodes, aPoint, BlockInlineCheck::UseComputedDisplayStyle); if (NS_WARN_IF(!textFragmentDataAtDeletion.IsInitialized())) { return Err(NS_ERROR_FAILURE); } const auto atNextCharOfStart = textFragmentDataAtDeletion .GetInclusiveNextCharPoint( aPoint, IgnoreNonEditableNodes::Yes); 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( atNextCharOfStart, nsIEditor::eNext, IgnoreNonEditableNodes::Yes); auto endToDelete = textFragmentDataAtDeletion .GetEndOfCollapsibleASCIIWhiteSpaces( atNextCharOfStart, nsIEditor::eNext, IgnoreNonEditableNodes::Yes); EditorDOMPoint pointToPutCaret; { Result 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 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(); auto endToDelete = startToDelete.NextPoint(); EditorDOMPoint pointToPutCaret; { Result 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 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 caretPointOrError = aHTMLEditor.DeleteTextAndTextNodesWithTransaction( atNextCharOfStart, atNextCharOfStart.NextPoint(), HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries); NS_WARNING_ASSERTION( caretPointOrError.isOk(), "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed"); return caretPointOrError; } // static Result 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); } EditorDOMPoint pointToPutCaret(aCaretPoint); if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) { AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(), &pointToPutCaret); Result caretPointOrError = WhiteSpaceVisibilityKeeper:: MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange( aHTMLEditor, EditorDOMRange(atContent, atContent.NextPoint()), aEditingHost); if (MOZ_UNLIKELY(caretPointOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::" "MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange() " "failed"); return caretPointOrError; } trackPointToPutCaret.FlushAndStopTracking(); caretPointOrError.unwrap().MoveCaretPointTo( pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); } else { MOZ_ASSERT( StaticPrefs::editor_white_space_normalization_blink_compatible()); // If we're removing a block, it may be surrounded by invisible // white-spaces. We should remove them to avoid to make them accidentally // visible. if (HTMLEditUtils::IsBlockElement( aContentToDelete, BlockInlineCheck::UseComputedDisplayOutsideStyle)) { AutoTrackDOMPoint trackAtContent(aHTMLEditor.RangeUpdaterRef(), &atContent); { AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(), &pointToPutCaret); nsresult rv = WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesBefore( aHTMLEditor, EditorDOMPoint(aContentToDelete.AsElement())); if (NS_FAILED(rv)) { NS_WARNING( "WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesBefore()" " failed"); return Err(rv); } if (NS_WARN_IF(!aContentToDelete.IsInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } rv = WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesAfter( aHTMLEditor, EditorDOMPoint::After(*aContentToDelete.AsElement())); if (NS_FAILED(rv)) { NS_WARNING( "WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesAfter() " "failed"); return Err(rv); } if (NS_WARN_IF(!aContentToDelete.IsInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } if (pointToPutCaret.IsInContentNode()) { // Additionally, we may put caret into the preceding block (this is the // case when caret was in an empty block and type `Backspace`, or when // caret is at end of the preceding block and type `Delete`). In such // case, we need to normalize the white-space of the preceding `Text` of // the deleting empty block for the compatibility with the other // browsers. if (pointToPutCaret.IsBefore(EditorRawDOMPoint(&aContentToDelete))) { WSScanResult nextThingOfCaretPoint = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( Scan::All, pointToPutCaret, BlockInlineCheck::UseComputedDisplayOutsideStyle); if (nextThingOfCaretPoint.ReachedBRElement() || nextThingOfCaretPoint.ReachedPreformattedLineBreak()) { nextThingOfCaretPoint = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( Scan::All, nextThingOfCaretPoint .PointAfterReachedContent(), BlockInlineCheck::UseComputedDisplayOutsideStyle); } if (nextThingOfCaretPoint.ReachedBlockBoundary()) { const EditorDOMPoint atBlockBoundary = nextThingOfCaretPoint.ReachedCurrentBlockBoundary() ? EditorDOMPoint::AtEndOf( *nextThingOfCaretPoint.ElementPtr()) : EditorDOMPoint(nextThingOfCaretPoint.ElementPtr()); Result afterLastVisibleThingOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesBefore( aHTMLEditor, atBlockBoundary, {}); if (MOZ_UNLIKELY(afterLastVisibleThingOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesBefore() " "failed"); return afterLastVisibleThingOrError.propagateErr(); } if (NS_WARN_IF(!aContentToDelete.IsInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } } // Similarly, we may put caret into the following block (this is the // case when caret was in an empty block and type `Delete`, or when // caret is at start of the following block and type `Backspace`). In // such case, we need to normalize the white-space of the following // `Text` of the deleting empty block for the compatibility with the // other browsers. else if (EditorRawDOMPoint::After(aContentToDelete) .EqualsOrIsBefore(pointToPutCaret)) { const WSScanResult previousThingOfCaretPoint = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( Scan::All, pointToPutCaret, BlockInlineCheck::UseComputedDisplayOutsideStyle); if (previousThingOfCaretPoint.ReachedBlockBoundary()) { const EditorDOMPoint atBlockBoundary = previousThingOfCaretPoint.ReachedCurrentBlockBoundary() ? EditorDOMPoint(previousThingOfCaretPoint.ElementPtr(), 0u) : EditorDOMPoint(previousThingOfCaretPoint.ElementPtr()); Result atFirstVisibleThingOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter( aHTMLEditor, atBlockBoundary, {}); if (MOZ_UNLIKELY(atFirstVisibleThingOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter() " "failed"); return atFirstVisibleThingOrError.propagateErr(); } if (NS_WARN_IF(!aContentToDelete.IsInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } } } trackAtContent.FlushAndStopTracking(); if (NS_WARN_IF(!atContent.IsInContentNodeAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } // If we're deleting inline content which is not followed by visible // content, i.e., the preceding text will become the last Text node, we // should normalize the preceding white-spaces for compatibility with the // other browsers. else { const WSScanResult nextThing = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( Scan::All, EditorRawDOMPoint::After(aContentToDelete), BlockInlineCheck::UseComputedDisplayOutsideStyle); if (nextThing.ReachedLineBoundary()) { AutoTrackDOMPoint trackAtContent(aHTMLEditor.RangeUpdaterRef(), &atContent); Result afterLastVisibleThingOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesBefore( aHTMLEditor, atContent, {}); if (MOZ_UNLIKELY(afterLastVisibleThingOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesBefore() " "failed"); return afterLastVisibleThingOrError.propagateErr(); } trackAtContent.FlushAndStopTracking(); if (NS_WARN_IF(!atContent.IsInContentNodeAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } } // Finally, we should normalize the following white-spaces for compatibility // with the other browsers. { AutoTrackDOMPoint trackAtContent(aHTMLEditor.RangeUpdaterRef(), &atContent); Result atFirstVisibleThingOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter( aHTMLEditor, atContent.NextPoint(), {}); if (MOZ_UNLIKELY(atFirstVisibleThingOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesBefore() failed"); return atFirstVisibleThingOrError.propagateErr(); } trackAtContent.FlushAndStopTracking(); if (NS_WARN_IF(!atContent.IsInContentNodeAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } } nsCOMPtr previousEditableSibling = HTMLEditUtils::GetPreviousSibling( aContentToDelete, {WalkTreeOption::IgnoreNonEditableNode}); // Delete the node, and join like nodes if appropriate { AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(), &pointToPutCaret); nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aContentToDelete); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); 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 CaretPoint(std::move(pointToPutCaret)); } nsIContent* nextEditableSibling = HTMLEditUtils::GetNextSibling( *previousEditableSibling, {WalkTreeOption::IgnoreNonEditableNode}); if (aCaretPoint.GetContainer() != nextEditableSibling) { return CaretPoint(std::move(pointToPutCaret)); } if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) { nsresult rv = aHTMLEditor.JoinNearestEditableNodesWithTransaction( *previousEditableSibling, MOZ_KnownLive(*aCaretPoint.ContainerAs()), &pointToPutCaret); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::JoinNearestEditableNodesWithTransaction() failed"); return Err(rv); } if (!pointToPutCaret.IsSet()) { NS_WARNING( "HTMLEditor::JoinNearestEditableNodesWithTransaction() didn't return " "right node position"); return Err(NS_ERROR_FAILURE); } return CaretPoint(std::move(pointToPutCaret)); } Result joinTextNodesResultOrError = aHTMLEditor.JoinTextNodesWithNormalizeWhiteSpaces( MOZ_KnownLive(*previousEditableSibling->AsText()), MOZ_KnownLive(*aCaretPoint.ContainerAs())); if (MOZ_UNLIKELY(joinTextNodesResultOrError.isErr())) { NS_WARNING("HTMLEditor::JoinTextNodesWithNormalizeWhiteSpaces() failed"); return joinTextNodesResultOrError.propagateErr(); } return CaretPoint( joinTextNodesResultOrError.unwrap().AtJoinedPoint()); } // static Result WhiteSpaceVisibilityKeeper:: MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange( HTMLEditor& aHTMLEditor, const EditorDOMRange& aRangeToDelete, const Element& aEditingHost) { MOZ_ASSERT(!StaticPrefs::editor_white_space_normalization_blink_compatible()); 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( Scan::EditableNodes, rangeToDelete.StartRef(), BlockInlineCheck::UseComputedDisplayStyle); if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized())) { return Err(NS_ERROR_FAILURE); } TextFragmentData textFragmentDataAtEnd( Scan::EditableNodes, rangeToDelete.EndRef(), BlockInlineCheck::UseComputedDisplayStyle); 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 // should: // - start with another node if the following text node starts with // white-spaces. // - start from prior point because end of the range may be in collapsible // white-spaces. MOZ_ASSERT_IF(rangeToDelete.EndRef().IsInTextNode() && rangeToDelete.EndRef().IsEndOfContainer(), replaceRangeDataAtEnd.StartRef().EqualsOrIsBefore( rangeToDelete.EndRef()) || 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 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(Scan::EditableNodes, rangeToDelete.StartRef(), BlockInlineCheck::UseComputedDisplayStyle); textFragmentDataAtEnd = TextFragmentData(Scan::EditableNodes, rangeToDelete.EndRef(), BlockInlineCheck::UseComputedDisplayStyle); } } ReplaceRangeData replaceRangeDataAtStart = textFragmentDataAtStart.GetReplaceRangeDataAtStartOfDeletionRange( textFragmentDataAtEnd); if (!replaceRangeDataAtStart.IsSet() || replaceRangeDataAtStart.Collapsed()) { return CaretPoint(std::move(pointToPutCaret)); } if (!replaceRangeDataAtStart.HasReplaceString()) { AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(), &pointToPutCaret); Result 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)); } // static nsresult WhiteSpaceVisibilityKeeper::MakeSureToKeepVisibleWhiteSpacesVisibleAfterSplit( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToSplit) { MOZ_ASSERT(!StaticPrefs::editor_white_space_normalization_blink_compatible()); const TextFragmentData textFragmentDataAtSplitPoint( Scan::EditableNodes, aPointToSplit, BlockInlineCheck::UseComputedDisplayStyle); 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) { auto atNextCharOfStart = textFragmentDataAtSplitPoint .GetInclusiveNextCharPoint( pointToSplit, IgnoreNonEditableNodes::Yes); 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, // XXX Shouldn't be "No"? Skipping non-editable nodes may // have visible content. IgnoreNonEditableNodes::Yes); } const auto endOfCollapsibleASCIIWhiteSpaces = textFragmentDataAtSplitPoint .GetEndOfCollapsibleASCIIWhiteSpaces( atNextCharOfStart, nsIEditor::eNone, // XXX Shouldn't be "No"? Skipping non-editable nodes may // have visible content. IgnoreNonEditableNodes::Yes); 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) { auto atPreviousCharOfStart = textFragmentDataAtSplitPoint.GetPreviousCharPoint( pointToSplit, IgnoreNonEditableNodes::Yes); if (atPreviousCharOfStart.IsSet() && !atPreviousCharOfStart.IsEndOfContainer() && atPreviousCharOfStart.IsCharCollapsibleASCIISpace()) { if (atPreviousCharOfStart.IsStartOfContainer() || atPreviousCharOfStart.IsPreviousCharASCIISpace()) { atPreviousCharOfStart = textFragmentDataAtSplitPoint .GetFirstASCIIWhiteSpacePointCollapsedTo( atPreviousCharOfStart, nsIEditor::eNone, // XXX Shouldn't be "No"? Skipping non-editable nodes may // have visible content. IgnoreNonEditableNodes::Yes); } const auto endOfCollapsibleASCIIWhiteSpaces = textFragmentDataAtSplitPoint .GetEndOfCollapsibleASCIIWhiteSpaces( atPreviousCharOfStart, nsIEditor::eNone, // XXX Shouldn't be "No"? Skipping non-editable nodes may // have visible content. IgnoreNonEditableNodes::Yes); 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; } // 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 caretPointOrError = aHTMLEditor.ReplaceTextWithTransaction( MOZ_KnownLive(*aRangeToReplace.StartRef().ContainerAs()), aRangeToReplace.StartRef().Offset(), aRangeToReplace.InSameContainer() ? aRangeToReplace.EndRef().Offset() - aRangeToReplace.StartRef().Offset() : aRangeToReplace.StartRef().ContainerAs()->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 caretPointOrError = aHTMLEditor.DeleteTextAndTextNodesWithTransaction( EditorDOMPointInText::AtEndOf( *aRangeToReplace.StartRef().ContainerAs()), 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; } // static template nsresult WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt( HTMLEditor& aHTMLEditor, const EditorDOMPointType& aPoint, const Element& aEditingHost) { MOZ_ASSERT(!StaticPrefs::editor_white_space_normalization_blink_compatible()); MOZ_ASSERT(aPoint.IsInContentNode()); MOZ_ASSERT(EditorUtils::IsEditableContent( *aPoint.template ContainerAs(), EditorType::HTML)); const TextFragmentData textFragmentData( Scan::EditableNodes, aPoint, BlockInlineCheck::UseComputedDisplayStyle); 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(); auto atPreviousCharOfEndOfVisibleWhiteSpaces = textFragmentData.GetPreviousCharPoint( atEndOfVisibleWhiteSpaces, IgnoreNonEditableNodes::Yes); 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 auto atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces = textFragmentData.GetPreviousCharPoint( atPreviousCharOfEndOfVisibleWhiteSpaces, IgnoreNonEditableNodes::Yes); 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
element and restore the NBSP to an ASCII white-space. if (maybeNBSPFollowsVisibleContent || isPreviousCharCollapsibleASCIIWhiteSpace) { // First, try to insert
element if NBSP is at end of a block. // XXX We should stop this if there is a visible content. if ((visibleWhiteSpaces.EndsByBlockBoundary() || visibleWhiteSpaces.EndsByInlineEditingHostBoundary()) && aPoint.IsInContentNode()) { bool insertBRElement = HTMLEditUtils::IsBlockElement( *aPoint.template ContainerAs(), BlockInlineCheck::UseComputedDisplayStyle); if (!insertBRElement) { NS_ASSERTION( EditorUtils::IsEditableContent( *aPoint.template ContainerAs(), EditorType::HTML), "Given content is not editable"); NS_ASSERTION(aPoint.template ContainerAs() ->GetAsElementOrParentElement(), "Given content is not an element and an orphan node"); const Element* editableBlockElement = EditorUtils::IsEditableContent( *aPoint.template ContainerAs(), EditorType::HTML) ? HTMLEditUtils::GetInclusiveAncestorElement( *aPoint.template ContainerAs(), HTMLEditUtils::ClosestEditableBlockElement, BlockInlineCheck::UseComputedDisplayStyle) : nullptr; insertBRElement = !!editableBlockElement; } if (insertBRElement) { // We are at a block boundary. Insert a
. Why? Well, first note // that the br will have no visible effect since it is up against a // block boundary. |foo

bar| renders like |foo

bar| and // similarly |

foo

bar| renders like |

foo

bar|. What // this
addition gets us is the ability to convert a trailing // nbsp to a space. Consider: |foo. '|, where ' // represents selection. User types space attempting to put 2 spaces // after the end of their sentence. We used to do this as: // |foo.  | This caused problems with soft wrapping: // the nbsp would wrap to the next line, which looked attrocious. If // you try to do: |foo.  | instead, the trailing // space is invisible because it is against a block boundary. If you // do: // |foo.  | 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
// here, which allows us to do: |foo. 
|, 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. if (NS_WARN_IF(!atEndOfVisibleWhiteSpaces.IsInContentNode())) { return Err(NS_ERROR_FAILURE); } const Maybe lineBreakType = aHTMLEditor.GetPreferredLineBreakType( *atEndOfVisibleWhiteSpaces.ContainerAs(), aEditingHost); if (NS_WARN_IF(lineBreakType.isNothing())) { return Err(NS_ERROR_FAILURE); } Result insertBRElementResultOrError = aHTMLEditor.InsertLineBreak(WithTransaction::Yes, *lineBreakType, atEndOfVisibleWhiteSpaces); if (MOZ_UNLIKELY(insertBRElementResultOrError.isErr())) { NS_WARNING(nsPrintfCString("HTMLEditor::InsertLineBreak(" "WithTransaction::Yes, %s) failed", ToString(*lineBreakType).c_str()) .get()); return insertBRElementResultOrError.propagateErr(); } CreateLineBreakResult insertBRElementResult = insertBRElementResultOrError.unwrap(); MOZ_ASSERT(insertBRElementResult.Handled()); // Ignore caret suggestion because the caller must want to restore // `Selection` due to the purpose of this method. insertBRElementResult.IgnoreCaretPointSuggestion(); atPreviousCharOfEndOfVisibleWhiteSpaces = textFragmentData.GetPreviousCharPoint( atEndOfVisibleWhiteSpaces, IgnoreNonEditableNodes::Yes); if (NS_WARN_IF(!atPreviousCharOfEndOfVisibleWhiteSpaces.IsSet())) { return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; } atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces = textFragmentData.GetPreviousCharPoint( atPreviousCharOfEndOfVisibleWhiteSpaces, IgnoreNonEditableNodes::Yes); isPreviousCharCollapsibleASCIIWhiteSpace = atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces.IsSet() && !atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces .IsEndOfContainer() && atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces .IsCharCollapsibleASCIISpace(); followedByBRElement = true; followedByVisibleContent = followedByPreformattedLineBreak = false; } } // Once insert a
, 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())) { return NS_OK; } // Next, replace the NBSP with an ASCII white-space if it's surrounded // by visible contents (or immediately before a
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 replaceTextResult = aHTMLEditor.ReplaceTextWithTransaction( MOZ_KnownLive(*atPreviousCharOfEndOfVisibleWhiteSpaces .ContainerAs()), 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
// 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())) { return NS_OK; } // Currently, we're at an NBSP following an ASCII space, and we need to // replace them with `"  "` for avoiding collapsing white-spaces. MOZ_ASSERT(!atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces .IsEndOfContainer()); const auto atFirstASCIIWhiteSpace = textFragmentData .GetFirstASCIIWhiteSpacePointCollapsedTo( atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces, nsIEditor::eNone, // XXX Shouldn't be "No"? Skipping non-editable nodes may have // visible content. IgnoreNonEditableNodes::Yes); uint32_t numberOfASCIIWhiteSpacesInStartNode = atFirstASCIIWhiteSpace.ContainerAs() == atPreviousCharOfEndOfVisibleWhiteSpaces.ContainerAs() ? atPreviousCharOfEndOfVisibleWhiteSpaces.Offset() - atFirstASCIIWhiteSpace.Offset() : atFirstASCIIWhiteSpace.ContainerAs()->Length() - atFirstASCIIWhiteSpace.Offset(); // Replace all preceding ASCII white-spaces **and** the NBSP. uint32_t replaceLengthInStartNode = numberOfASCIIWhiteSpacesInStartNode + (atFirstASCIIWhiteSpace.ContainerAs() == atPreviousCharOfEndOfVisibleWhiteSpaces.ContainerAs() ? 1 : 0); Result replaceTextResult = aHTMLEditor.ReplaceTextWithTransaction( MOZ_KnownLive(*atFirstASCIIWhiteSpace.ContainerAs()), 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 caretPointOrError = aHTMLEditor.DeleteTextAndTextNodesWithTransaction( EditorDOMPointInText::AtEndOf( *atFirstASCIIWhiteSpace.ContainerAs()), 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
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 auto atPreviousCharOfEndOfVisibleWhiteSpaces = textFragmentData.GetPreviousCharPoint( atEndOfVisibleWhiteSpaces, IgnoreNonEditableNodes::Yes); 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 auto atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces = textFragmentData.GetPreviousCharPoint( atPreviousCharOfEndOfVisibleWhiteSpaces, IgnoreNonEditableNodes::Yes); // 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, // XXX Shouldn't be "No"? Skipping non-editable nodes may have // visible content. IgnoreNonEditableNodes::Yes); 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 caretPointOrError = aHTMLEditor.DeleteTextAndNormalizeSurroundingWhiteSpaces( startToDelete, endToDelete, HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries, HTMLEditor::DeleteDirection::Forward, aEditingHost); 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; } // static Result WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint) { MOZ_ASSERT(aPoint.IsSet()); const TextFragmentData textFragmentData( Scan::EditableNodes, aPoint, BlockInlineCheck::UseComputedDisplayStyle); 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 leadingWhiteSpacesDeleted = false; if (leadingWhiteSpaceRange.IsPositioned() && !leadingWhiteSpaceRange.Collapsed()) { Result 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 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)); } } // namespace mozilla