/* -*- 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 "WSRunScanner.h" #include "EditorDOMPoint.h" #include "ErrorList.h" #include "HTMLEditor.h" #include "HTMLEditUtils.h" #include "mozilla/Assertions.h" #include "mozilla/Casting.h" // for AssertedCast #include "nsDebug.h" #include "nsError.h" #include "nsIContent.h" #include "nsIContentInlines.h" #include "nsRange.h" #include "nsTextFragment.h" namespace mozilla { using namespace dom; /****************************************************************************** * mozilla::WSScanResult ******************************************************************************/ void WSScanResult::AssertIfInvalidData(const WSRunScanner& aScanner) const { #ifdef DEBUG MOZ_ASSERT(mReason == WSType::UnexpectedError || mReason == WSType::InUncomposedDoc || mReason == WSType::NonCollapsibleCharacters || mReason == WSType::CollapsibleWhiteSpaces || mReason == WSType::BRElement || mReason == WSType::PreformattedLineBreak || mReason == WSType::SpecialContent || mReason == WSType::CurrentBlockBoundary || mReason == WSType::OtherBlockBoundary || mReason == WSType::InlineEditingHostBoundary); MOZ_ASSERT_IF(mReason == WSType::UnexpectedError, !mContent); MOZ_ASSERT_IF(mReason != WSType::UnexpectedError, mContent); MOZ_ASSERT_IF(mReason == WSType::InUncomposedDoc, !mContent->IsInComposedDoc()); MOZ_ASSERT_IF(mContent && !mContent->IsInComposedDoc(), mReason == WSType::InUncomposedDoc); MOZ_ASSERT_IF(mReason == WSType::NonCollapsibleCharacters || mReason == WSType::CollapsibleWhiteSpaces || mReason == WSType::PreformattedLineBreak, mContent->IsText()); MOZ_ASSERT_IF(mReason == WSType::NonCollapsibleCharacters || mReason == WSType::CollapsibleWhiteSpaces || mReason == WSType::PreformattedLineBreak, mOffset.isSome()); MOZ_ASSERT_IF(mReason == WSType::NonCollapsibleCharacters || mReason == WSType::CollapsibleWhiteSpaces || mReason == WSType::PreformattedLineBreak, mContent->AsText()->TextDataLength() > 0); MOZ_ASSERT_IF(mDirection == ScanDirection::Backward && (mReason == WSType::NonCollapsibleCharacters || mReason == WSType::CollapsibleWhiteSpaces || mReason == WSType::PreformattedLineBreak), *mOffset > 0); MOZ_ASSERT_IF(mDirection == ScanDirection::Forward && (mReason == WSType::NonCollapsibleCharacters || mReason == WSType::CollapsibleWhiteSpaces || mReason == WSType::PreformattedLineBreak), *mOffset < mContent->AsText()->TextDataLength()); MOZ_ASSERT_IF(mReason == WSType::BRElement, mContent->IsHTMLElement(nsGkAtoms::br)); MOZ_ASSERT_IF(mReason == WSType::PreformattedLineBreak, EditorUtils::IsNewLinePreformatted(*mContent)); MOZ_ASSERT_IF(mReason == WSType::SpecialContent, (mContent->IsText() && !mContent->IsEditable()) || (!mContent->IsHTMLElement(nsGkAtoms::br) && !HTMLEditUtils::IsBlockElement( *mContent, aScanner.BlockInlineCheckMode()))); MOZ_ASSERT_IF(mReason == WSType::OtherBlockBoundary, HTMLEditUtils::IsBlockElement(*mContent, aScanner.BlockInlineCheckMode())); MOZ_ASSERT_IF(mReason == WSType::CurrentBlockBoundary, mContent->IsElement()); MOZ_ASSERT_IF(mReason == WSType::CurrentBlockBoundary && aScanner.mScanMode == WSRunScanner::Scan::EditableNodes, mContent->IsEditable()); MOZ_ASSERT_IF(mReason == WSType::CurrentBlockBoundary, HTMLEditUtils::IsBlockElement( *mContent, RespectParentBlockBoundary( aScanner.BlockInlineCheckMode()))); MOZ_ASSERT_IF(mReason == WSType::InlineEditingHostBoundary, mContent->IsElement()); MOZ_ASSERT_IF(mReason == WSType::InlineEditingHostBoundary && aScanner.mScanMode == WSRunScanner::Scan::EditableNodes, mContent->IsEditable()); MOZ_ASSERT_IF(mReason == WSType::InlineEditingHostBoundary, !HTMLEditUtils::IsBlockElement( *mContent, aScanner.BlockInlineCheckMode())); MOZ_ASSERT_IF(mReason == WSType::InlineEditingHostBoundary, !mContent->GetParentElement() || !mContent->GetParentElement()->IsEditable()); #endif // #ifdef DEBUG } /****************************************************************************** * mozilla::WSRunScanner ******************************************************************************/ template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom( const EditorDOMPoint& aPoint) const; template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom( const EditorRawDOMPoint& aPoint) const; template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom( const EditorDOMPointInText& aPoint) const; template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom( const EditorRawDOMPointInText& aPoint) const; template WSScanResult WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( const EditorDOMPoint& aPoint) const; template WSScanResult WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( const EditorRawDOMPoint& aPoint) const; template WSScanResult WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( const EditorDOMPointInText& aPoint) const; template WSScanResult WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( const EditorRawDOMPointInText& aPoint) const; template EditorDOMPoint WSRunScanner::GetAfterLastVisiblePoint( Scan aScanMode, Text& aTextNode, const Element* aAncestorLimiter); template EditorRawDOMPoint WSRunScanner::GetAfterLastVisiblePoint( Scan aScanMode, Text& aTextNode, const Element* aAncestorLimiter); template EditorDOMPoint WSRunScanner::GetFirstVisiblePoint( Scan aScanMode, Text& aTextNode, const Element* aAncestorLimiter); template EditorRawDOMPoint WSRunScanner::GetFirstVisiblePoint( Scan aScanMode, Text& aTextNode, const Element* aAncestorLimiter); template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom( const EditorDOMPointBase& aPoint) const { MOZ_ASSERT(aPoint.IsSet()); MOZ_ASSERT(aPoint.IsInComposedDoc()); if (MOZ_UNLIKELY(!aPoint.IsSet())) { return WSScanResult::Error(); } // We may not be able to check editable state in uncomposed tree as expected. // For example, only some descendants in an editing host is temporarily // removed from the tree, they are not editable unless nested contenteditable // attribute is set to "true". if (MOZ_UNLIKELY(!aPoint.IsInComposedDoc())) { return WSScanResult(*this, WSScanResult::ScanDirection::Backward, *aPoint.template ContainerAs(), WSType::InUncomposedDoc); } if (!TextFragmentDataAtStartRef().IsInitialized()) { return WSScanResult::Error(); } // If the range has visible text and start of the visible text is before // aPoint, return previous character in the text. const VisibleWhiteSpacesData& visibleWhiteSpaces = TextFragmentDataAtStartRef().VisibleWhiteSpacesDataRef(); if (visibleWhiteSpaces.IsInitialized() && visibleWhiteSpaces.StartRef().IsBefore(aPoint)) { // If the visible things are not editable, we shouldn't scan "editable" // things now. Whether keep scanning editable things or not should be // considered by the caller. if (mScanMode == Scan::EditableNodes && aPoint.GetChild() && !HTMLEditUtils::IsSimplyEditableNode((*aPoint.GetChild()))) { return WSScanResult(*this, WSScanResult::ScanDirection::Backward, *aPoint.GetChild(), WSType::SpecialContent); } const auto atPreviousChar = GetPreviousCharPoint(aPoint); // When it's a non-empty text node, return it. if (atPreviousChar.IsSet() && !atPreviousChar.IsContainerEmpty()) { MOZ_ASSERT(!atPreviousChar.IsEndOfContainer()); return WSScanResult(*this, WSScanResult::ScanDirection::Backward, atPreviousChar.template NextPoint(), atPreviousChar.IsCharCollapsibleASCIISpaceOrNBSP() ? WSType::CollapsibleWhiteSpaces : atPreviousChar.IsCharPreformattedNewLine() ? WSType::PreformattedLineBreak : WSType::NonCollapsibleCharacters); } } if (NS_WARN_IF(TextFragmentDataAtStartRef().StartRawReason() == WSType::UnexpectedError)) { return WSScanResult::Error(); } switch (TextFragmentDataAtStartRef().StartRawReason()) { case WSType::CollapsibleWhiteSpaces: case WSType::NonCollapsibleCharacters: case WSType::PreformattedLineBreak: MOZ_ASSERT(TextFragmentDataAtStartRef().StartRef().IsSet()); // XXX: If we find the character at last of a text node and we started // scanning from following text node of it, some callers may work with the // point in the following text node instead of end of the found text node. return WSScanResult(*this, WSScanResult::ScanDirection::Backward, TextFragmentDataAtStartRef().StartRef(), TextFragmentDataAtStartRef().StartRawReason()); default: break; } // Otherwise, return the start of the range. if (TextFragmentDataAtStartRef().GetStartReasonContent() != TextFragmentDataAtStartRef().StartRef().GetContainer()) { if (NS_WARN_IF(!TextFragmentDataAtStartRef().GetStartReasonContent())) { return WSScanResult::Error(); } // In this case, TextFragmentDataAtStartRef().StartRef().Offset() is not // meaningful. return WSScanResult(*this, WSScanResult::ScanDirection::Backward, *TextFragmentDataAtStartRef().GetStartReasonContent(), TextFragmentDataAtStartRef().StartRawReason()); } if (NS_WARN_IF(!TextFragmentDataAtStartRef().StartRef().IsSet())) { return WSScanResult::Error(); } return WSScanResult(*this, WSScanResult::ScanDirection::Backward, TextFragmentDataAtStartRef().StartRef(), TextFragmentDataAtStartRef().StartRawReason()); } template WSScanResult WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( const EditorDOMPointBase& aPoint) const { MOZ_ASSERT(aPoint.IsSet()); MOZ_ASSERT(aPoint.IsInComposedDoc()); if (MOZ_UNLIKELY(!aPoint.IsSet())) { return WSScanResult::Error(); } // We may not be able to check editable state in uncomposed tree as expected. // For example, only some descendants in an editing host is temporarily // removed from the tree, they are not editable unless nested contenteditable // attribute is set to "true". if (MOZ_UNLIKELY(!aPoint.IsInComposedDoc())) { return WSScanResult(*this, WSScanResult::ScanDirection::Forward, *aPoint.template ContainerAs(), WSType::InUncomposedDoc); } if (!TextFragmentDataAtStartRef().IsInitialized()) { return WSScanResult::Error(); } // If the range has visible text and aPoint equals or is before the end of the // visible text, return inclusive next character in the text. const VisibleWhiteSpacesData& visibleWhiteSpaces = TextFragmentDataAtStartRef().VisibleWhiteSpacesDataRef(); if (visibleWhiteSpaces.IsInitialized() && aPoint.EqualsOrIsBefore(visibleWhiteSpaces.EndRef())) { // If the visible things are not editable, we shouldn't scan "editable" // things now. Whether keep scanning editable things or not should be // considered by the caller. if (mScanMode == Scan::EditableNodes && aPoint.GetChild() && !HTMLEditUtils::IsSimplyEditableNode(*aPoint.GetChild())) { return WSScanResult(*this, WSScanResult::ScanDirection::Forward, *aPoint.GetChild(), WSType::SpecialContent); } const auto atNextChar = GetInclusiveNextCharPoint(aPoint); // When it's a non-empty text node, return it. if (atNextChar.IsSet() && !atNextChar.IsContainerEmpty()) { return WSScanResult(*this, WSScanResult::ScanDirection::Forward, atNextChar, !atNextChar.IsEndOfContainer() && atNextChar.IsCharCollapsibleASCIISpaceOrNBSP() ? WSType::CollapsibleWhiteSpaces : !atNextChar.IsEndOfContainer() && atNextChar.IsCharPreformattedNewLine() ? WSType::PreformattedLineBreak : WSType::NonCollapsibleCharacters); } } if (NS_WARN_IF(TextFragmentDataAtStartRef().EndRawReason() == WSType::UnexpectedError)) { return WSScanResult::Error(); } switch (TextFragmentDataAtStartRef().EndRawReason()) { case WSType::CollapsibleWhiteSpaces: case WSType::NonCollapsibleCharacters: case WSType::PreformattedLineBreak: MOZ_ASSERT(TextFragmentDataAtStartRef().StartRef().IsSet()); // XXX: If we find the character at start of a text node and we // started scanning from preceding text node of it, some callers may want // to work with the point at end of the preceding text node instead of // start of the found text node. return WSScanResult(*this, WSScanResult::ScanDirection::Forward, TextFragmentDataAtStartRef().EndRef(), TextFragmentDataAtStartRef().EndRawReason()); default: break; } // Otherwise, return the end of the range. if (TextFragmentDataAtStartRef().GetEndReasonContent() != TextFragmentDataAtStartRef().EndRef().GetContainer()) { if (NS_WARN_IF(!TextFragmentDataAtStartRef().GetEndReasonContent())) { return WSScanResult::Error(); } // In this case, TextFragmentDataAtStartRef().EndRef().Offset() is not // meaningful. return WSScanResult(*this, WSScanResult::ScanDirection::Forward, *TextFragmentDataAtStartRef().GetEndReasonContent(), TextFragmentDataAtStartRef().EndRawReason()); } if (NS_WARN_IF(!TextFragmentDataAtStartRef().EndRef().IsSet())) { return WSScanResult::Error(); } return WSScanResult(*this, WSScanResult::ScanDirection::Forward, TextFragmentDataAtStartRef().EndRef(), TextFragmentDataAtStartRef().EndRawReason()); } // static template EditorDOMPointType WSRunScanner::GetAfterLastVisiblePoint( Scan aScanMode, Text& aTextNode, const Element* aAncestorLimiter /* = nullptr */) { EditorDOMPoint atLastCharOfTextNode( &aTextNode, AssertedCast(std::max( static_cast(aTextNode.Length()) - 1, 0))); if (!atLastCharOfTextNode.IsContainerEmpty() && !atLastCharOfTextNode.IsCharCollapsibleASCIISpace()) { return EditorDOMPointType::AtEndOf(aTextNode); } const TextFragmentData textFragmentData( aScanMode, atLastCharOfTextNode, BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter); if (NS_WARN_IF(!textFragmentData.IsInitialized())) { return EditorDOMPointType(); // TODO: Make here return error with Err. } const EditorDOMRange& invisibleWhiteSpaceRange = textFragmentData.InvisibleTrailingWhiteSpaceRangeRef(); if (!invisibleWhiteSpaceRange.IsPositioned() || invisibleWhiteSpaceRange.Collapsed()) { return EditorDOMPointType::AtEndOf(aTextNode); } return invisibleWhiteSpaceRange.StartRef().To(); } // static template EditorDOMPointType WSRunScanner::GetFirstVisiblePoint( Scan aScanMode, Text& aTextNode, const Element* aAncestorLimiter /* = nullptr */) { EditorDOMPoint atStartOfTextNode(&aTextNode, 0); if (!atStartOfTextNode.IsContainerEmpty() && !atStartOfTextNode.IsCharCollapsibleASCIISpace()) { return atStartOfTextNode.To(); } const TextFragmentData textFragmentData( aScanMode, atStartOfTextNode, BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter); if (NS_WARN_IF(!textFragmentData.IsInitialized())) { return EditorDOMPointType(); // TODO: Make here return error with Err. } const EditorDOMRange& invisibleWhiteSpaceRange = textFragmentData.InvisibleLeadingWhiteSpaceRangeRef(); if (!invisibleWhiteSpaceRange.IsPositioned() || invisibleWhiteSpaceRange.Collapsed()) { return atStartOfTextNode.To(); } return invisibleWhiteSpaceRange.EndRef().To(); } /***************************************************************************** * Implementation for new white-space normalizer *****************************************************************************/ // static EditorDOMRangeInTexts WSRunScanner::ComputeRangeInTextNodesContainingInvisibleWhiteSpaces( const TextFragmentData& aStart, const TextFragmentData& aEnd) { MOZ_ASSERT(aStart.ScanMode() == aEnd.ScanMode()); // Corresponding to handling invisible white-spaces part of // `TextFragmentData::GetReplaceRangeDataAtEndOfDeletionRange()` and // `TextFragmentData::GetReplaceRangeDataAtStartOfDeletionRange()` MOZ_ASSERT(aStart.ScanStartRef().IsSetAndValid()); MOZ_ASSERT(aEnd.ScanStartRef().IsSetAndValid()); MOZ_ASSERT(aStart.ScanStartRef().EqualsOrIsBefore(aEnd.ScanStartRef())); MOZ_ASSERT(aStart.ScanStartRef().IsInTextNode()); MOZ_ASSERT(aEnd.ScanStartRef().IsInTextNode()); // XXX `GetReplaceRangeDataAtEndOfDeletionRange()` and // `GetReplaceRangeDataAtStartOfDeletionRange()` use // `GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt()` and // `GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt()`. // However, they are really odd as mentioned with "XXX" comments // in them. For the new white-space normalizer, we need to treat // invisible white-spaces stricter because the legacy path handles // white-spaces multiple times (e.g., calling `HTMLEditor:: // DeleteNodeIfInvisibleAndEditableTextNode()` later) and that hides // the bug, but in the new path, we should stop doing same things // multiple times for both performance and footprint. Therefore, // even though the result might be different in some edge cases, // we should use clean path for now. Perhaps, we should fix the odd // cases before shipping `beforeinput` event in release channel. const EditorDOMRange& invisibleLeadingWhiteSpaceRange = aStart.InvisibleLeadingWhiteSpaceRangeRef(); const EditorDOMRange& invisibleTrailingWhiteSpaceRange = aEnd.InvisibleTrailingWhiteSpaceRangeRef(); const bool hasInvisibleLeadingWhiteSpaces = invisibleLeadingWhiteSpaceRange.IsPositioned() && !invisibleLeadingWhiteSpaceRange.Collapsed(); const bool hasInvisibleTrailingWhiteSpaces = invisibleLeadingWhiteSpaceRange != invisibleTrailingWhiteSpaceRange && invisibleTrailingWhiteSpaceRange.IsPositioned() && !invisibleTrailingWhiteSpaceRange.Collapsed(); EditorDOMRangeInTexts result(aStart.ScanStartRef().AsInText(), aEnd.ScanStartRef().AsInText()); MOZ_ASSERT(result.IsPositionedAndValid()); if (!hasInvisibleLeadingWhiteSpaces && !hasInvisibleTrailingWhiteSpaces) { return result; } MOZ_ASSERT_IF( hasInvisibleLeadingWhiteSpaces && hasInvisibleTrailingWhiteSpaces, invisibleLeadingWhiteSpaceRange.StartRef().IsBefore( invisibleTrailingWhiteSpaceRange.StartRef())); const EditorDOMPoint& aroundFirstInvisibleWhiteSpace = hasInvisibleLeadingWhiteSpaces ? invisibleLeadingWhiteSpaceRange.StartRef() : invisibleTrailingWhiteSpaceRange.StartRef(); if (aroundFirstInvisibleWhiteSpace.IsBefore(result.StartRef())) { if (aroundFirstInvisibleWhiteSpace.IsInTextNode()) { result.SetStart(aroundFirstInvisibleWhiteSpace.AsInText()); MOZ_ASSERT(result.IsPositionedAndValid()); } else { const auto atFirstInvisibleWhiteSpace = hasInvisibleLeadingWhiteSpaces ? aStart.GetInclusiveNextCharPoint( aroundFirstInvisibleWhiteSpace, ShouldIgnoreNonEditableSiblingsOrDescendants( aStart.ScanMode())) : aEnd.GetInclusiveNextCharPoint( aroundFirstInvisibleWhiteSpace, ShouldIgnoreNonEditableSiblingsOrDescendants( aEnd.ScanMode())); MOZ_ASSERT(atFirstInvisibleWhiteSpace.IsSet()); MOZ_ASSERT( atFirstInvisibleWhiteSpace.EqualsOrIsBefore(result.StartRef())); result.SetStart(atFirstInvisibleWhiteSpace); MOZ_ASSERT(result.IsPositionedAndValid()); } } MOZ_ASSERT_IF( hasInvisibleLeadingWhiteSpaces && hasInvisibleTrailingWhiteSpaces, invisibleLeadingWhiteSpaceRange.EndRef().IsBefore( invisibleTrailingWhiteSpaceRange.EndRef())); const EditorDOMPoint& afterLastInvisibleWhiteSpace = hasInvisibleTrailingWhiteSpaces ? invisibleTrailingWhiteSpaceRange.EndRef() : invisibleLeadingWhiteSpaceRange.EndRef(); if (afterLastInvisibleWhiteSpace.EqualsOrIsBefore(result.EndRef())) { MOZ_ASSERT(result.IsPositionedAndValid()); return result; } if (afterLastInvisibleWhiteSpace.IsInTextNode()) { result.SetEnd(afterLastInvisibleWhiteSpace.AsInText()); MOZ_ASSERT(result.IsPositionedAndValid()); return result; } const auto atLastInvisibleWhiteSpace = hasInvisibleTrailingWhiteSpaces ? aEnd.GetPreviousCharPoint( afterLastInvisibleWhiteSpace, ShouldIgnoreNonEditableSiblingsOrDescendants(aEnd.ScanMode())) : aStart.GetPreviousCharPoint( afterLastInvisibleWhiteSpace, ShouldIgnoreNonEditableSiblingsOrDescendants( aStart.ScanMode())); MOZ_ASSERT(atLastInvisibleWhiteSpace.IsSet()); MOZ_ASSERT(atLastInvisibleWhiteSpace.IsContainerEmpty() || atLastInvisibleWhiteSpace.IsAtLastContent()); MOZ_ASSERT(result.EndRef().EqualsOrIsBefore(atLastInvisibleWhiteSpace)); result.SetEnd(atLastInvisibleWhiteSpace.IsEndOfContainer() ? atLastInvisibleWhiteSpace : atLastInvisibleWhiteSpace.NextPoint()); MOZ_ASSERT(result.IsPositionedAndValid()); return result; } // static Result WSRunScanner::GetRangeInTextNodesToBackspaceFrom( Scan aScanMode, const EditorDOMPoint& aPoint, const Element* aAncestorLimiter /* = nullptr */) { // Corresponding to computing delete range part of // `WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace()` MOZ_ASSERT(aPoint.IsSetAndValid()); const TextFragmentData textFragmentDataAtCaret( aScanMode, aPoint, BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter); if (NS_WARN_IF(!textFragmentDataAtCaret.IsInitialized())) { return Err(NS_ERROR_FAILURE); } auto atPreviousChar = textFragmentDataAtCaret.GetPreviousCharPoint( aPoint, ShouldIgnoreNonEditableSiblingsOrDescendants(aScanMode)); if (!atPreviousChar.IsSet()) { return EditorDOMRangeInTexts(); // There is no content in the block. } // XXX When previous char point is in an empty text node, we do nothing, // but this must look odd from point of user view. We should delete // something before aPoint. if (atPreviousChar.IsEndOfContainer()) { return EditorDOMRangeInTexts(); } // Extend delete range if previous char is a low surrogate following // a high surrogate. EditorDOMPointInText atNextChar = atPreviousChar.NextPoint(); if (!atPreviousChar.IsStartOfContainer()) { if (atPreviousChar.IsCharLowSurrogateFollowingHighSurrogate()) { atPreviousChar = atPreviousChar.PreviousPoint(); } // If caret is in middle of a surrogate pair, delete the surrogate pair // (blink-compat). else if (atPreviousChar.IsCharHighSurrogateFollowedByLowSurrogate()) { atNextChar = atNextChar.NextPoint(); } } // If previous char is an collapsible white-spaces, delete all adjacent // white-spaces which are collapsed together. EditorDOMRangeInTexts rangeToDelete; if (atPreviousChar.IsCharCollapsibleASCIISpace() || atPreviousChar.IsCharPreformattedNewLineCollapsedWithWhiteSpaces()) { const auto startToDelete = textFragmentDataAtCaret .GetFirstASCIIWhiteSpacePointCollapsedTo( atPreviousChar, nsIEditor::ePrevious, ShouldIgnoreNonEditableSiblingsOrDescendants(aScanMode)); if (!startToDelete.IsSet()) { NS_WARNING( "WSRunScanner::GetFirstASCIIWhiteSpacePointCollapsedTo() failed"); return Err(NS_ERROR_FAILURE); } const auto endToDelete = textFragmentDataAtCaret .GetEndOfCollapsibleASCIIWhiteSpaces( atPreviousChar, nsIEditor::ePrevious, ShouldIgnoreNonEditableSiblingsOrDescendants(aScanMode)); if (!endToDelete.IsSet()) { NS_WARNING("WSRunScanner::GetEndOfCollapsibleASCIIWhiteSpaces() failed"); return Err(NS_ERROR_FAILURE); } rangeToDelete = EditorDOMRangeInTexts(startToDelete, endToDelete); } // if previous char is not a collapsible white-space, remove it. else { rangeToDelete = EditorDOMRangeInTexts(atPreviousChar, atNextChar); } // If there is no removable and visible content, we should do nothing. if (rangeToDelete.Collapsed()) { return EditorDOMRangeInTexts(); } // And also delete invisible white-spaces if they become visible. const TextFragmentData textFragmentDataAtStart = rangeToDelete.StartRef() != aPoint ? TextFragmentData(aScanMode, rangeToDelete.StartRef(), BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter) : textFragmentDataAtCaret; const TextFragmentData textFragmentDataAtEnd = rangeToDelete.EndRef() != aPoint ? TextFragmentData(aScanMode, rangeToDelete.EndRef(), BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter) : textFragmentDataAtCaret; if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized()) || NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) { return Err(NS_ERROR_FAILURE); } EditorDOMRangeInTexts extendedRangeToDelete = WSRunScanner::ComputeRangeInTextNodesContainingInvisibleWhiteSpaces( textFragmentDataAtStart, textFragmentDataAtEnd); MOZ_ASSERT(extendedRangeToDelete.IsPositionedAndValid()); return extendedRangeToDelete.IsPositioned() ? extendedRangeToDelete : rangeToDelete; } // static Result WSRunScanner::GetRangeInTextNodesToForwardDeleteFrom( Scan aScanMode, const EditorDOMPoint& aPoint, const Element* aAncestorLimiter /* = nullptr */) { // Corresponding to computing delete range part of // `WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace()` MOZ_ASSERT(aPoint.IsSetAndValid()); const TextFragmentData textFragmentDataAtCaret( aScanMode, aPoint, BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter); if (NS_WARN_IF(!textFragmentDataAtCaret.IsInitialized())) { return Err(NS_ERROR_FAILURE); } auto atCaret = textFragmentDataAtCaret.GetInclusiveNextCharPoint( aPoint, ShouldIgnoreNonEditableSiblingsOrDescendants(aScanMode)); if (!atCaret.IsSet()) { return EditorDOMRangeInTexts(); // There is no content in the block. } // If caret is in middle of a surrogate pair, we should remove next // character (blink-compat). if (!atCaret.IsEndOfContainer() && atCaret.IsCharLowSurrogateFollowingHighSurrogate()) { atCaret = atCaret.NextPoint(); } // XXX When next char point is in an empty text node, we do nothing, // but this must look odd from point of user view. We should delete // something after aPoint. if (atCaret.IsEndOfContainer()) { return EditorDOMRangeInTexts(); } // Extend delete range if previous char is a low surrogate following // a high surrogate. EditorDOMPointInText atNextChar = atCaret.NextPoint(); if (atCaret.IsCharHighSurrogateFollowedByLowSurrogate()) { atNextChar = atNextChar.NextPoint(); } // If next char is a collapsible white-space, delete all adjacent white-spaces // which are collapsed together. EditorDOMRangeInTexts rangeToDelete; if (atCaret.IsCharCollapsibleASCIISpace() || atCaret.IsCharPreformattedNewLineCollapsedWithWhiteSpaces()) { const auto startToDelete = textFragmentDataAtCaret .GetFirstASCIIWhiteSpacePointCollapsedTo( atCaret, nsIEditor::eNext, ShouldIgnoreNonEditableSiblingsOrDescendants(aScanMode)); if (!startToDelete.IsSet()) { NS_WARNING( "WSRunScanner::GetFirstASCIIWhiteSpacePointCollapsedTo() failed"); return Err(NS_ERROR_FAILURE); } const EditorDOMPointInText endToDelete = textFragmentDataAtCaret .GetEndOfCollapsibleASCIIWhiteSpaces( atCaret, nsIEditor::eNext, ShouldIgnoreNonEditableSiblingsOrDescendants(aScanMode)); if (!endToDelete.IsSet()) { NS_WARNING("WSRunScanner::GetEndOfCollapsibleASCIIWhiteSpaces() failed"); return Err(NS_ERROR_FAILURE); } rangeToDelete = EditorDOMRangeInTexts(startToDelete, endToDelete); } // if next char is not a collapsible white-space, remove it. else { rangeToDelete = EditorDOMRangeInTexts(atCaret, atNextChar); } // If there is no removable and visible content, we should do nothing. if (rangeToDelete.Collapsed()) { return EditorDOMRangeInTexts(); } // And also delete invisible white-spaces if they become visible. const TextFragmentData textFragmentDataAtStart = rangeToDelete.StartRef() != aPoint ? TextFragmentData(aScanMode, rangeToDelete.StartRef(), BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter) : textFragmentDataAtCaret; const TextFragmentData textFragmentDataAtEnd = rangeToDelete.EndRef() != aPoint ? TextFragmentData(aScanMode, rangeToDelete.EndRef(), BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter) : textFragmentDataAtCaret; if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized()) || NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) { return Err(NS_ERROR_FAILURE); } EditorDOMRangeInTexts extendedRangeToDelete = WSRunScanner::ComputeRangeInTextNodesContainingInvisibleWhiteSpaces( textFragmentDataAtStart, textFragmentDataAtEnd); MOZ_ASSERT(extendedRangeToDelete.IsPositionedAndValid()); return extendedRangeToDelete.IsPositioned() ? extendedRangeToDelete : rangeToDelete; } // static EditorDOMRange WSRunScanner::GetRangesForDeletingAtomicContent( Scan aScanMode, const nsIContent& aAtomicContent, const Element* aAncestorLimiter /* = nullptr */) { if (aAtomicContent.IsHTMLElement(nsGkAtoms::br)) { // Preceding white-spaces should be preserved, but the following // white-spaces should be invisible around `
` element. const TextFragmentData textFragmentDataAfterBRElement( aScanMode, EditorDOMPoint::After(aAtomicContent), BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter); if (NS_WARN_IF(!textFragmentDataAfterBRElement.IsInitialized())) { return EditorDOMRange(); // TODO: Make here return error with Err. } const EditorDOMRangeInTexts followingInvisibleWhiteSpaces = textFragmentDataAfterBRElement.GetNonCollapsedRangeInTexts( textFragmentDataAfterBRElement .InvisibleLeadingWhiteSpaceRangeRef()); return followingInvisibleWhiteSpaces.IsPositioned() && !followingInvisibleWhiteSpaces.Collapsed() ? EditorDOMRange( EditorDOMPoint(const_cast(&aAtomicContent)), followingInvisibleWhiteSpaces.EndRef()) : EditorDOMRange( EditorDOMPoint(const_cast(&aAtomicContent)), EditorDOMPoint::After(aAtomicContent)); } if (!HTMLEditUtils::IsBlockElement( aAtomicContent, BlockInlineCheck::UseComputedDisplayStyle)) { // Both preceding and following white-spaces around it should be preserved // around inline elements like ``. return EditorDOMRange( EditorDOMPoint(const_cast(&aAtomicContent)), EditorDOMPoint::After(aAtomicContent)); } // Both preceding and following white-spaces can be invisible around a // block element. const TextFragmentData textFragmentDataBeforeAtomicContent( aScanMode, EditorDOMPoint(const_cast(&aAtomicContent)), BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter); if (NS_WARN_IF(!textFragmentDataBeforeAtomicContent.IsInitialized())) { return EditorDOMRange(); // TODO: Make here return error with Err. } const EditorDOMRangeInTexts precedingInvisibleWhiteSpaces = textFragmentDataBeforeAtomicContent.GetNonCollapsedRangeInTexts( textFragmentDataBeforeAtomicContent .InvisibleTrailingWhiteSpaceRangeRef()); const TextFragmentData textFragmentDataAfterAtomicContent( aScanMode, EditorDOMPoint::After(aAtomicContent), BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter); if (NS_WARN_IF(!textFragmentDataAfterAtomicContent.IsInitialized())) { return EditorDOMRange(); // TODO: Make here return error with Err. } const EditorDOMRangeInTexts followingInvisibleWhiteSpaces = textFragmentDataAfterAtomicContent.GetNonCollapsedRangeInTexts( textFragmentDataAfterAtomicContent .InvisibleLeadingWhiteSpaceRangeRef()); if (precedingInvisibleWhiteSpaces.StartRef().IsSet() && followingInvisibleWhiteSpaces.EndRef().IsSet()) { return EditorDOMRange(precedingInvisibleWhiteSpaces.StartRef(), followingInvisibleWhiteSpaces.EndRef()); } if (precedingInvisibleWhiteSpaces.StartRef().IsSet()) { return EditorDOMRange(precedingInvisibleWhiteSpaces.StartRef(), EditorDOMPoint::After(aAtomicContent)); } if (followingInvisibleWhiteSpaces.EndRef().IsSet()) { return EditorDOMRange( EditorDOMPoint(const_cast(&aAtomicContent)), followingInvisibleWhiteSpaces.EndRef()); } return EditorDOMRange( EditorDOMPoint(const_cast(&aAtomicContent)), EditorDOMPoint::After(aAtomicContent)); } // static EditorDOMRange WSRunScanner::GetRangeForDeletingBlockElementBoundaries( Scan aScanMode, const Element& aLeftBlockElement, const Element& aRightBlockElement, const EditorDOMPoint& aPointContainingTheOtherBlock, const Element* aAncestorLimiter /* = nullptr */) { MOZ_ASSERT(&aLeftBlockElement != &aRightBlockElement); MOZ_ASSERT_IF( aPointContainingTheOtherBlock.IsSet(), aPointContainingTheOtherBlock.GetContainer() == &aLeftBlockElement || aPointContainingTheOtherBlock.GetContainer() == &aRightBlockElement); MOZ_ASSERT_IF( aPointContainingTheOtherBlock.GetContainer() == &aLeftBlockElement, aRightBlockElement.IsInclusiveDescendantOf( aPointContainingTheOtherBlock.GetChild())); MOZ_ASSERT_IF( aPointContainingTheOtherBlock.GetContainer() == &aRightBlockElement, aLeftBlockElement.IsInclusiveDescendantOf( aPointContainingTheOtherBlock.GetChild())); MOZ_ASSERT_IF( !aPointContainingTheOtherBlock.IsSet(), !aRightBlockElement.IsInclusiveDescendantOf(&aLeftBlockElement)); MOZ_ASSERT_IF( !aPointContainingTheOtherBlock.IsSet(), !aLeftBlockElement.IsInclusiveDescendantOf(&aRightBlockElement)); MOZ_ASSERT_IF(!aPointContainingTheOtherBlock.IsSet(), EditorRawDOMPoint(const_cast(&aLeftBlockElement)) .IsBefore(EditorRawDOMPoint( const_cast(&aRightBlockElement)))); MOZ_ASSERT_IF(aAncestorLimiter, aLeftBlockElement.IsInclusiveDescendantOf(aAncestorLimiter)); MOZ_ASSERT_IF(aAncestorLimiter, aRightBlockElement.IsInclusiveDescendantOf(aAncestorLimiter)); MOZ_ASSERT_IF(aScanMode == Scan::EditableNodes, const_cast(aLeftBlockElement).GetEditingHost() == const_cast(aRightBlockElement).GetEditingHost()); EditorDOMRange range; // Include trailing invisible white-spaces in aLeftBlockElement. const TextFragmentData textFragmentDataAtEndOfLeftBlockElement( aScanMode, aPointContainingTheOtherBlock.GetContainer() == &aLeftBlockElement ? aPointContainingTheOtherBlock : EditorDOMPoint::AtEndOf(const_cast(aLeftBlockElement)), BlockInlineCheck::UseComputedDisplayOutsideStyle, aAncestorLimiter); if (NS_WARN_IF(!textFragmentDataAtEndOfLeftBlockElement.IsInitialized())) { return EditorDOMRange(); // TODO: Make here return error with Err. } if (textFragmentDataAtEndOfLeftBlockElement.StartsFromInvisibleBRElement()) { // If the left block element ends with an invisible `
` element, // it'll be deleted (and it means there is no invisible trailing // white-spaces). Therefore, the range should start from the invisible // `
` element. range.SetStart(EditorDOMPoint( textFragmentDataAtEndOfLeftBlockElement.StartReasonBRElementPtr())); } else { const EditorDOMRange& trailingWhiteSpaceRange = textFragmentDataAtEndOfLeftBlockElement .InvisibleTrailingWhiteSpaceRangeRef(); if (trailingWhiteSpaceRange.StartRef().IsSet()) { range.SetStart(trailingWhiteSpaceRange.StartRef()); } else { range.SetStart(textFragmentDataAtEndOfLeftBlockElement.ScanStartRef()); } } // Include leading invisible white-spaces in aRightBlockElement. const TextFragmentData textFragmentDataAtStartOfRightBlockElement( aScanMode, aPointContainingTheOtherBlock.GetContainer() == &aRightBlockElement && !aPointContainingTheOtherBlock.IsEndOfContainer() ? aPointContainingTheOtherBlock.NextPoint() : EditorDOMPoint(const_cast(&aRightBlockElement), 0u), BlockInlineCheck::UseComputedDisplayOutsideStyle, aAncestorLimiter); if (NS_WARN_IF(!textFragmentDataAtStartOfRightBlockElement.IsInitialized())) { return EditorDOMRange(); // TODO: Make here return error with Err. } const EditorDOMRange& leadingWhiteSpaceRange = textFragmentDataAtStartOfRightBlockElement .InvisibleLeadingWhiteSpaceRangeRef(); if (leadingWhiteSpaceRange.EndRef().IsSet()) { range.SetEnd(leadingWhiteSpaceRange.EndRef()); } else { range.SetEnd(textFragmentDataAtStartOfRightBlockElement.ScanStartRef()); } return range; } // static EditorDOMRange WSRunScanner::GetRangeContainingInvisibleWhiteSpacesAtRangeBoundaries( Scan aScanMode, const EditorDOMRange& aRange, const Element* aAncestorLimiter /* = nullptr */) { MOZ_ASSERT(aRange.IsPositionedAndValid()); MOZ_ASSERT(aRange.EndRef().IsSetAndValid()); MOZ_ASSERT(aRange.StartRef().IsSetAndValid()); EditorDOMRange result; const TextFragmentData textFragmentDataAtStart( aScanMode, aRange.StartRef(), BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter); if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized())) { return EditorDOMRange(); // TODO: Make here return error with Err. } const EditorDOMRangeInTexts invisibleLeadingWhiteSpacesAtStart = textFragmentDataAtStart.GetNonCollapsedRangeInTexts( textFragmentDataAtStart.InvisibleLeadingWhiteSpaceRangeRef()); if (invisibleLeadingWhiteSpacesAtStart.IsPositioned() && !invisibleLeadingWhiteSpacesAtStart.Collapsed()) { result.SetStart(invisibleLeadingWhiteSpacesAtStart.StartRef()); } else { const EditorDOMRangeInTexts invisibleTrailingWhiteSpacesAtStart = textFragmentDataAtStart.GetNonCollapsedRangeInTexts( textFragmentDataAtStart.InvisibleTrailingWhiteSpaceRangeRef()); if (invisibleTrailingWhiteSpacesAtStart.IsPositioned() && !invisibleTrailingWhiteSpacesAtStart.Collapsed()) { MOZ_ASSERT( invisibleTrailingWhiteSpacesAtStart.StartRef().EqualsOrIsBefore( aRange.StartRef())); result.SetStart(invisibleTrailingWhiteSpacesAtStart.StartRef()); } // If there is no invisible white-space and the line starts with a // text node, shrink the range to start of the text node. else if (!aRange.StartRef().IsInTextNode() && (textFragmentDataAtStart.StartsFromBlockBoundary() || textFragmentDataAtStart.StartsFromInlineEditingHostBoundary()) && textFragmentDataAtStart.EndRef().IsInTextNode()) { result.SetStart(textFragmentDataAtStart.EndRef()); } } if (!result.StartRef().IsSet()) { result.SetStart(aRange.StartRef()); } const TextFragmentData textFragmentDataAtEnd( aScanMode, aRange.EndRef(), BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter); if (NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) { return EditorDOMRange(); // TODO: Make here return error with Err. } const EditorDOMRangeInTexts invisibleLeadingWhiteSpacesAtEnd = textFragmentDataAtEnd.GetNonCollapsedRangeInTexts( textFragmentDataAtEnd.InvisibleTrailingWhiteSpaceRangeRef()); if (invisibleLeadingWhiteSpacesAtEnd.IsPositioned() && !invisibleLeadingWhiteSpacesAtEnd.Collapsed()) { result.SetEnd(invisibleLeadingWhiteSpacesAtEnd.EndRef()); } else { const EditorDOMRangeInTexts invisibleLeadingWhiteSpacesAtEnd = textFragmentDataAtEnd.GetNonCollapsedRangeInTexts( textFragmentDataAtEnd.InvisibleLeadingWhiteSpaceRangeRef()); if (invisibleLeadingWhiteSpacesAtEnd.IsPositioned() && !invisibleLeadingWhiteSpacesAtEnd.Collapsed()) { MOZ_ASSERT(aRange.EndRef().EqualsOrIsBefore( invisibleLeadingWhiteSpacesAtEnd.EndRef())); result.SetEnd(invisibleLeadingWhiteSpacesAtEnd.EndRef()); } // If there is no invisible white-space and the line ends with a text // node, shrink the range to end of the text node. else if (!aRange.EndRef().IsInTextNode() && (textFragmentDataAtEnd.EndsByBlockBoundary() || textFragmentDataAtEnd.EndsByInlineEditingHostBoundary()) && textFragmentDataAtEnd.StartRef().IsInTextNode()) { result.SetEnd(EditorDOMPoint::AtEndOf( *textFragmentDataAtEnd.StartRef().ContainerAs())); } } if (!result.EndRef().IsSet()) { result.SetEnd(aRange.EndRef()); } MOZ_ASSERT(result.IsPositionedAndValid()); return result; } /****************************************************************************** * Utilities for other things. ******************************************************************************/ // static Result WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent( Scan aScanMode, nsRange& aRange, const Element* aAncestorLimiter /* = nullptr */) { MOZ_ASSERT(aRange.IsPositioned()); MOZ_ASSERT(!aRange.IsInAnySelection(), "Changing range in selection may cause running script"); if (NS_WARN_IF(!aRange.GetStartContainer()) || NS_WARN_IF(!aRange.GetEndContainer())) { return Err(NS_ERROR_FAILURE); } if (!aRange.GetStartContainer()->IsContent() || !aRange.GetEndContainer()->IsContent()) { return false; } // If the range crosses a block boundary, we should do nothing for now // because it hits a bug of inserting a padding `
` element after // joining the blocks. if (HTMLEditUtils::GetInclusiveAncestorElement( *aRange.GetStartContainer()->AsContent(), aScanMode == Scan::EditableNodes ? HTMLEditUtils::ClosestEditableBlockElementExceptHRElement : HTMLEditUtils::ClosestBlockElementExceptHRElement, BlockInlineCheck::UseComputedDisplayStyle) != HTMLEditUtils::GetInclusiveAncestorElement( *aRange.GetEndContainer()->AsContent(), aScanMode == Scan::EditableNodes ? HTMLEditUtils::ClosestEditableBlockElementExceptHRElement : HTMLEditUtils::ClosestBlockElementExceptHRElement, BlockInlineCheck::UseComputedDisplayStyle)) { return false; } nsIContent* startContent = nullptr; if (aRange.GetStartContainer() && aRange.GetStartContainer()->IsText() && aRange.GetStartContainer()->AsText()->Length() == aRange.StartOffset()) { // If next content is a visible `
` element, special inline content // (e.g., ``, non-editable text node, etc) or a block level void // element like `
`, the range should start with it. const TextFragmentData textFragmentDataAtStart( aScanMode, EditorRawDOMPoint(aRange.StartRef()), BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter); if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized())) { return Err(NS_ERROR_FAILURE); } if (textFragmentDataAtStart.EndsByVisibleBRElement()) { startContent = textFragmentDataAtStart.EndReasonBRElementPtr(); } else if (textFragmentDataAtStart.EndsBySpecialContent() || (textFragmentDataAtStart.EndsByOtherBlockElement() && !HTMLEditUtils::IsContainerNode( *textFragmentDataAtStart .EndReasonOtherBlockElementPtr()))) { startContent = textFragmentDataAtStart.GetEndReasonContent(); } } nsIContent* endContent = nullptr; if (aRange.GetEndContainer() && aRange.GetEndContainer()->IsText() && !aRange.EndOffset()) { // If previous content is a visible `
` element, special inline content // (e.g., ``, non-editable text node, etc) or a block level void // element like `
`, the range should end after it. const TextFragmentData textFragmentDataAtEnd( aScanMode, EditorRawDOMPoint(aRange.EndRef()), BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter); if (NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) { return Err(NS_ERROR_FAILURE); } if (textFragmentDataAtEnd.StartsFromVisibleBRElement()) { endContent = textFragmentDataAtEnd.StartReasonBRElementPtr(); } else if (textFragmentDataAtEnd.StartsFromSpecialContent() || (textFragmentDataAtEnd.StartsFromOtherBlockElement() && !HTMLEditUtils::IsContainerNode( *textFragmentDataAtEnd .StartReasonOtherBlockElementPtr()))) { endContent = textFragmentDataAtEnd.GetStartReasonContent(); } } if (!startContent && !endContent) { return false; } nsresult rv = aRange.SetStartAndEnd( startContent ? RangeBoundary( startContent->GetParentNode(), startContent->GetPreviousSibling()) // at startContent : aRange.StartRef(), endContent ? RangeBoundary(endContent->GetParentNode(), endContent) // after endContent : aRange.EndRef()); if (NS_FAILED(rv)) { NS_WARNING("nsRange::SetStartAndEnd() failed"); return Err(rv); } return true; } } // namespace mozilla