diff options
Diffstat (limited to 'editor')
-rw-r--r-- | editor/libeditor/EditAction.h | 1 | ||||
-rw-r--r-- | editor/libeditor/EditorBase.cpp | 73 | ||||
-rw-r--r-- | editor/libeditor/HTMLEditSubActionHandler.cpp | 119 | ||||
-rw-r--r-- | editor/libeditor/HTMLEditUtils.cpp | 24 | ||||
-rw-r--r-- | editor/libeditor/HTMLEditUtils.h | 7 | ||||
-rw-r--r-- | editor/libeditor/HTMLEditor.cpp | 105 | ||||
-rw-r--r-- | editor/libeditor/HTMLEditor.h | 5 | ||||
-rw-r--r-- | editor/libeditor/HTMLEditorDataTransfer.cpp | 6 | ||||
-rw-r--r-- | editor/libeditor/HTMLEditorDeleteHandler.cpp | 1104 | ||||
-rw-r--r-- | editor/libeditor/HTMLStyleEditor.cpp | 6 | ||||
-rw-r--r-- | editor/libeditor/WSRunObject.cpp | 181 | ||||
-rw-r--r-- | editor/libeditor/WSRunObject.h | 237 |
12 files changed, 1445 insertions, 423 deletions
diff --git a/editor/libeditor/EditAction.h b/editor/libeditor/EditAction.h index f74d1c6949..6133d82f4a 100644 --- a/editor/libeditor/EditAction.h +++ b/editor/libeditor/EditAction.h @@ -642,6 +642,7 @@ inline EditorInputType ToInputType(EditAction aEditAction) { inline bool MayEditActionDeleteAroundCollapsedSelection( const EditAction aEditAction) { switch (aEditAction) { + case EditAction::eCut: case EditAction::eDeleteSelection: case EditAction::eDeleteBackward: case EditAction::eDeleteForward: diff --git a/editor/libeditor/EditorBase.cpp b/editor/libeditor/EditorBase.cpp index cd1439e4c0..d63086e78e 100644 --- a/editor/libeditor/EditorBase.cpp +++ b/editor/libeditor/EditorBase.cpp @@ -66,17 +66,18 @@ #include "mozilla/TextInputListener.h" // for TextInputListener #include "mozilla/TextServicesDocument.h" // for TextServicesDocument #include "mozilla/TextEvents.h" -#include "mozilla/TransactionManager.h" // for TransactionManager -#include "mozilla/dom/AbstractRange.h" // for AbstractRange -#include "mozilla/dom/Attr.h" // for Attr -#include "mozilla/dom/BrowsingContext.h" // for BrowsingContext -#include "mozilla/dom/CharacterData.h" // for CharacterData -#include "mozilla/dom/DataTransfer.h" // for DataTransfer -#include "mozilla/dom/Document.h" // for Document -#include "mozilla/dom/DocumentInlines.h" // for GetObservingPresShell -#include "mozilla/dom/DragEvent.h" // for DragEvent -#include "mozilla/dom/Element.h" // for Element, nsINode::AsElement -#include "mozilla/dom/EventTarget.h" // for EventTarget +#include "mozilla/TransactionManager.h" // for TransactionManager +#include "mozilla/dom/AbstractRange.h" // for AbstractRange +#include "mozilla/dom/Attr.h" // for Attr +#include "mozilla/dom/BorrowedAttrInfo.h" // for BorrowedAttrInfo +#include "mozilla/dom/BrowsingContext.h" // for BrowsingContext +#include "mozilla/dom/CharacterData.h" // for CharacterData +#include "mozilla/dom/DataTransfer.h" // for DataTransfer +#include "mozilla/dom/Document.h" // for Document +#include "mozilla/dom/DocumentInlines.h" // for GetObservingPresShell +#include "mozilla/dom/DragEvent.h" // for DragEvent +#include "mozilla/dom/Element.h" // for Element, nsINode::AsElement +#include "mozilla/dom/EventTarget.h" // for EventTarget #include "mozilla/dom/HTMLBodyElement.h" #include "mozilla/dom/HTMLBRElement.h" #include "mozilla/dom/Selection.h" // for Selection, etc. @@ -2940,33 +2941,52 @@ void EditorBase::CloneAttributesWithTransaction(Element& aDestElement, bool isDestElementInBody = rootElement->Contains(destElement); // Clear existing attributes - RefPtr<nsDOMAttributeMap> destAttributes = destElement->Attributes(); - while (RefPtr<Attr> attr = destAttributes->Item(0)) { + AutoTArray<OwningNonNull<nsAtom>, 16> destElementAttributes; + if (const uint32_t attrCount = destElement->GetAttrCount()) { + destElementAttributes.SetCapacity(attrCount); + for (const uint32_t i : IntegerRange(attrCount)) { + if (const nsAttrName* attrName = destElement->GetUnsafeAttrNameAt(i)) { + MOZ_ASSERT(attrName->LocalName()); + destElementAttributes.AppendElement(*attrName->LocalName()); + } + } + } + for (const OwningNonNull<nsAtom>& attr : destElementAttributes) { if (isDestElementInBody) { - DebugOnly<nsresult> rvIgnored = RemoveAttributeWithTransaction( - destElement, MOZ_KnownLive(*attr->NodeInfo()->NameAtom())); + DebugOnly<nsresult> rvIgnored = + RemoveAttributeWithTransaction(destElement, MOZ_KnownLive(*attr)); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "EditorBase::RemoveAttributeWithTransaction() failed, but ignored"); } else { - DebugOnly<nsresult> rvIgnored = destElement->UnsetAttr( - kNameSpaceID_None, attr->NodeInfo()->NameAtom(), true); + DebugOnly<nsresult> rvIgnored = + destElement->UnsetAttr(kNameSpaceID_None, attr, true); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "Element::UnsetAttr() failed, but ignored"); } } // Set just the attributes that the source element has - RefPtr<nsDOMAttributeMap> sourceAttributes = sourceElement->Attributes(); - uint32_t sourceCount = sourceAttributes->Length(); - for (uint32_t i = 0; i < sourceCount; i++) { - RefPtr<Attr> attr = sourceAttributes->Item(i); - nsAutoString value; - attr->GetValue(value); + AutoTArray<std::pair<OwningNonNull<nsAtom>, nsString>, 16> + sourceElementAttributes; + if (const uint32_t attrCount = sourceElement->GetAttrCount()) { + sourceElementAttributes.SetCapacity(attrCount); + for (const uint32_t i : IntegerRange(attrCount)) { + const BorrowedAttrInfo attrInfo = sourceElement->GetAttrInfoAt(i); + if (const nsAttrName* attrName = attrInfo.mName) { + MOZ_ASSERT(attrName->LocalName()); + MOZ_ASSERT(attrInfo.mValue); + nsString value; + attrInfo.mValue->ToString(value); + sourceElementAttributes.AppendElement(std::make_pair( + OwningNonNull<nsAtom>(*attrName->LocalName()), std::move(value))); + } + } + } + for (const auto& attr : sourceElementAttributes) { if (isDestElementInBody) { DebugOnly<nsresult> rvIgnored = SetAttributeOrEquivalent( - destElement, MOZ_KnownLive(attr->NodeInfo()->NameAtom()), value, - false); + destElement, MOZ_KnownLive(attr.first), attr.second, false); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "EditorBase::SetAttributeOrEquivalent() failed, but ignored"); @@ -2974,8 +2994,7 @@ void EditorBase::CloneAttributesWithTransaction(Element& aDestElement, // The element is not inserted in the document yet, we don't want to put // a transaction on the UndoStack DebugOnly<nsresult> rvIgnored = SetAttributeOrEquivalent( - destElement, MOZ_KnownLive(attr->NodeInfo()->NameAtom()), value, - true); + destElement, MOZ_KnownLive(attr.first), attr.second, true); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "EditorBase::SetAttributeOrEquivalent() failed, but ignored"); diff --git a/editor/libeditor/HTMLEditSubActionHandler.cpp b/editor/libeditor/HTMLEditSubActionHandler.cpp index 28e0ad1595..50c35c04ed 100644 --- a/editor/libeditor/HTMLEditSubActionHandler.cpp +++ b/editor/libeditor/HTMLEditSubActionHandler.cpp @@ -1604,7 +1604,7 @@ nsresult HTMLEditor::InsertLineBreakAsSubAction() { NS_WARNING("Inserted <br> was unexpectedly removed"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } - WSScanResult backwardScanFromBeforeBRElementResult = + const WSScanResult backwardScanFromBeforeBRElementResult = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( editingHost, EditorDOMPoint(unwrappedInsertBRElementResult.GetNewNode()), @@ -1615,8 +1615,8 @@ nsresult HTMLEditor::InsertLineBreakAsSubAction() { return Err(NS_ERROR_FAILURE); } - WSScanResult forwardScanFromAfterBRElementResult = - WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( + const WSScanResult forwardScanFromAfterBRElementResult = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( editingHost, pointToPutCaret, BlockInlineCheck::UseComputedDisplayStyle); if (MOZ_UNLIKELY(forwardScanFromAfterBRElementResult.Failed())) { @@ -1624,9 +1624,18 @@ nsresult HTMLEditor::InsertLineBreakAsSubAction() { return Err(NS_ERROR_FAILURE); } const bool brElementIsAfterBlock = - backwardScanFromBeforeBRElementResult.ReachedBlockBoundary(); + backwardScanFromBeforeBRElementResult.ReachedBlockBoundary() || + // FIXME: This is wrong considering because the inline editing host may + // be surrounded by visible inline content. However, WSRunScanner is + // not aware of block boundary around it and stopping this change causes + // starting to fail some WPT. Therefore, we need to keep doing this for + // now. + backwardScanFromBeforeBRElementResult + .ReachedInlineEditingHostBoundary(); const bool brElementIsBeforeBlock = - forwardScanFromAfterBRElementResult.ReachedBlockBoundary(); + forwardScanFromAfterBRElementResult.ReachedBlockBoundary() || + // FIXME: See above comment. + forwardScanFromAfterBRElementResult.ReachedInlineEditingHostBoundary(); const bool isEmptyEditingHost = HTMLEditUtils::IsEmptyNode( *editingHost, {EmptyCheckOption::TreatNonEditableContentAsInvisible}); if (brElementIsBeforeBlock && @@ -1647,13 +1656,13 @@ nsresult HTMLEditor::InsertLineBreakAsSubAction() { unwrappedInvisibleAdditionalBRElement.IgnoreCaretPointSuggestion(); } else if (forwardScanFromAfterBRElementResult .InVisibleOrCollapsibleCharacters()) { - pointToPutCaret = - forwardScanFromAfterBRElementResult.Point<EditorDOMPoint>(); + pointToPutCaret = forwardScanFromAfterBRElementResult + .PointAtReachedContent<EditorDOMPoint>(); } else if (forwardScanFromAfterBRElementResult.ReachedSpecialContent()) { // Next inserting text should be inserted into styled inline elements if // they have first visible thing in the new line. - pointToPutCaret = - forwardScanFromAfterBRElementResult.PointAtContent<EditorDOMPoint>(); + pointToPutCaret = forwardScanFromAfterBRElementResult + .PointAtReachedContent<EditorDOMPoint>(); } nsresult rv = CollapseSelectionTo(pointToPutCaret); @@ -2254,21 +2263,32 @@ Result<CreateElementResult, nsresult> HTMLEditor::HandleInsertBRElement( aEditingHost, {EmptyCheckOption::TreatNonEditableContentAsInvisible}); WSRunScanner wsRunScanner(&aEditingHost, aPointToBreak, BlockInlineCheck::UseComputedDisplayStyle); - WSScanResult backwardScanResult = + const WSScanResult backwardScanResult = wsRunScanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(aPointToBreak); if (MOZ_UNLIKELY(backwardScanResult.Failed())) { NS_WARNING( "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom() failed"); return Err(NS_ERROR_FAILURE); } - const bool brElementIsAfterBlock = backwardScanResult.ReachedBlockBoundary(); - WSScanResult forwardScanResult = - wsRunScanner.ScanNextVisibleNodeOrBlockBoundaryFrom(aPointToBreak); + const bool brElementIsAfterBlock = + backwardScanResult.ReachedBlockBoundary() || + // FIXME: This is wrong considering because the inline editing host may + // be surrounded by visible inline content. However, WSRunScanner is + // not aware of block boundary around it and stopping this change causes + // starting to fail some WPT. Therefore, we need to keep doing this for + // now. + backwardScanResult.ReachedInlineEditingHostBoundary(); + const WSScanResult forwardScanResult = + wsRunScanner.ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( + aPointToBreak); if (MOZ_UNLIKELY(forwardScanResult.Failed())) { NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed"); return Err(NS_ERROR_FAILURE); } - const bool brElementIsBeforeBlock = forwardScanResult.ReachedBlockBoundary(); + const bool brElementIsBeforeBlock = + forwardScanResult.ReachedBlockBoundary() || + // FIXME: See above comment + forwardScanResult.ReachedInlineEditingHostBoundary(); // First, insert a <br> element. RefPtr<Element> brElement; @@ -2383,8 +2403,8 @@ Result<CreateElementResult, nsresult> HTMLEditor::HandleInsertBRElement( std::move(pointToPutCaret)); } - WSScanResult forwardScanFromAfterBRElementResult = - WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( + const WSScanResult forwardScanFromAfterBRElementResult = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( &aEditingHost, afterBRElement, BlockInlineCheck::UseComputedDisplayStyle); if (MOZ_UNLIKELY(forwardScanFromAfterBRElementResult.Failed())) { @@ -2422,7 +2442,14 @@ Result<CreateElementResult, nsresult> HTMLEditor::HandleInsertBRElement( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "MoveNodeResult::SuggestCaretPointTo() failed, but ignored"); } - } else if (forwardScanFromAfterBRElementResult.ReachedBlockBoundary() && + } else if ((forwardScanFromAfterBRElementResult.ReachedBlockBoundary() || + // FIXME: This is wrong considering because the inline editing + // host may be surrounded by visible inline content. However, + // WSRunScanner is not aware of block boundary around it and + // stopping this change causes starting to fail some WPT. + // Therefore, we need to keep doing this for now. + forwardScanFromAfterBRElementResult + .ReachedInlineEditingHostBoundary()) && !brElementIsAfterBlock) { Result<CreateElementResult, nsresult> invisibleAdditionalBRElementResult = InsertAdditionalInvisibleLineBreak(); @@ -2530,7 +2557,8 @@ Result<EditorDOMPoint, nsresult> HTMLEditor::HandleInsertLinefeed( WSRunScanner wsScannerAtCaret(&aEditingHost, pointToPutCaret, BlockInlineCheck::UseComputedDisplayStyle); if (wsScannerAtCaret.StartsFromPreformattedLineBreak() && - wsScannerAtCaret.EndsByBlockBoundary() && + (wsScannerAtCaret.EndsByBlockBoundary() || + wsScannerAtCaret.EndsByInlineEditingHostBoundary()) && HTMLEditUtils::CanNodeContain(*wsScannerAtCaret.GetEndReasonContent(), *nsGkAtoms::br)) { AutoTrackDOMPoint trackingInsertedPosition(RangeUpdaterRef(), @@ -2612,8 +2640,8 @@ HTMLEditor::HandleInsertParagraphInMailCiteElement( // mailquote (in either inline or block case). The latter can confuse a // user if they click there and start typing, because being in the // mailquote may affect wrapping behavior, or font color, etc. - WSScanResult forwardScanFromPointToSplitResult = - WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( + const WSScanResult forwardScanFromPointToSplitResult = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( &aEditingHost, pointToSplit, BlockInlineCheck::UseHTMLDefaultStyle); if (forwardScanFromPointToSplitResult.Failed()) { return Err(NS_ERROR_FAILURE); @@ -2624,8 +2652,8 @@ HTMLEditor::HandleInsertParagraphInMailCiteElement( forwardScanFromPointToSplitResult.BRElementPtr() != &aMailCiteElement && aMailCiteElement.Contains( forwardScanFromPointToSplitResult.BRElementPtr())) { - pointToSplit = - forwardScanFromPointToSplitResult.PointAfterContent<EditorDOMPoint>(); + pointToSplit = forwardScanFromPointToSplitResult + .PointAfterReachedContent<EditorDOMPoint>(); } if (NS_WARN_IF(!pointToSplit.IsInContentNode())) { @@ -2726,7 +2754,7 @@ HTMLEditor::HandleInsertParagraphInMailCiteElement( // XXX Cannot we replace this complicated check with just a call of // HTMLEditUtils::IsVisibleBRElement with // resultOfInsertingBRElement.inspect()? - WSScanResult backwardScanFromPointToCreateNewBRElementResult = + const WSScanResult backwardScanFromPointToCreateNewBRElementResult = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( &aEditingHost, pointToCreateNewBRElement, BlockInlineCheck::UseComputedDisplayStyle); @@ -2743,8 +2771,8 @@ HTMLEditor::HandleInsertParagraphInMailCiteElement( .ReachedSpecialContent()) { return NS_SUCCESS_DOM_NO_OPERATION; } - WSScanResult forwardScanFromPointAfterNewBRElementResult = - WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( + const WSScanResult forwardScanFromPointAfterNewBRElementResult = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( &aEditingHost, EditorRawDOMPoint::After(pointToCreateNewBRElement), BlockInlineCheck::UseComputedDisplayStyle); @@ -3343,12 +3371,14 @@ HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( BlockInlineCheck::UseComputedDisplayStyle); // If the point is not start of a hard line, we don't need to put a `<br>` // element here. - if (!wsRunScanner.StartsFromHardLineBreak()) { + if (!wsRunScanner.StartsFromHardLineBreak() && + !wsRunScanner.StartsFromInlineEditingHostBoundary()) { return CaretPoint(EditorDOMPoint()); } // If the point is not end of a hard line or the hard line does not end with // block boundary, we don't need to put a `<br>` element here. - if (!wsRunScanner.EndsByBlockBoundary()) { + if (!wsRunScanner.EndsByBlockBoundary() && + !wsRunScanner.EndsByInlineEditingHostBoundary()) { return CaretPoint(EditorDOMPoint()); } @@ -7654,7 +7684,7 @@ HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction( // element even if selected in a blocked phrase element or // non-HTMLelement. BlockInlineCheck::UseHTMLDefaultStyle); - WSScanResult scanResultAtEnd = + const WSScanResult scanResultAtEnd = wsScannerAtEnd.ScanPreviousVisibleNodeOrBlockBoundaryFrom(endPoint); if (scanResultAtEnd.Failed()) { NS_WARNING( @@ -7673,7 +7703,8 @@ HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction( newRange.SetEnd(EditorRawDOMPoint::After(*child)); } // else block is empty - we can leave selection alone here, i think. - } else if (wsScannerAtEnd.StartsFromCurrentBlockBoundary()) { + } else if (wsScannerAtEnd.StartsFromCurrentBlockBoundary() || + wsScannerAtEnd.StartsFromInlineEditingHostBoundary()) { // endpoint is just after start of this block if (nsIContent* child = HTMLEditUtils::GetPreviousContent( endPoint, {WalkTreeOption::IgnoreNonEditableNode}, @@ -7692,8 +7723,9 @@ HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction( // selection past that, it would visibly change meaning of users selection. WSRunScanner wsScannerAtStart(&aEditingHost, startPoint, BlockInlineCheck::UseHTMLDefaultStyle); - WSScanResult scanResultAtStart = - wsScannerAtStart.ScanNextVisibleNodeOrBlockBoundaryFrom(startPoint); + const WSScanResult scanResultAtStart = + wsScannerAtStart.ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( + startPoint); if (scanResultAtStart.Failed()) { NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed"); return Err(NS_ERROR_FAILURE); @@ -7710,7 +7742,8 @@ HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction( newRange.SetStart(EditorRawDOMPoint(child)); } // else block is empty - we can leave selection alone here, i think. - } else if (wsScannerAtStart.EndsByCurrentBlockBoundary()) { + } else if (wsScannerAtStart.EndsByCurrentBlockBoundary() || + wsScannerAtStart.EndsByInlineEditingHostBoundary()) { // startpoint is just before end of this block if (nsIContent* child = HTMLEditUtils::GetNextContent( startPoint, {WalkTreeOption::IgnoreNonEditableNode}, @@ -8944,8 +8977,8 @@ HTMLEditor::HandleInsertParagraphInListItemElement( // If the right list item element is not empty, we need to consider where to // put caret in it. If it has non-container inline elements, <br> or <hr>, at // the element is proper position. - WSScanResult forwardScanFromStartOfListItemResult = - WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( + const WSScanResult forwardScanFromStartOfListItemResult = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( &aEditingHost, EditorRawDOMPoint(&rightListItemElement, 0u), BlockInlineCheck::UseComputedDisplayStyle); if (MOZ_UNLIKELY(forwardScanFromStartOfListItemResult.Failed())) { @@ -8955,8 +8988,8 @@ HTMLEditor::HandleInsertParagraphInListItemElement( if (forwardScanFromStartOfListItemResult.ReachedSpecialContent() || forwardScanFromStartOfListItemResult.ReachedBRElement() || forwardScanFromStartOfListItemResult.ReachedHRElement()) { - auto atFoundElement = - forwardScanFromStartOfListItemResult.PointAtContent<EditorDOMPoint>(); + auto atFoundElement = forwardScanFromStartOfListItemResult + .PointAtReachedContent<EditorDOMPoint>(); if (NS_WARN_IF(!atFoundElement.IsSetAndValid())) { return Err(NS_ERROR_FAILURE); } @@ -8966,7 +8999,13 @@ HTMLEditor::HandleInsertParagraphInListItemElement( // If we reached a block boundary (end of the list item or a child block), // let's put deepest start of the list item or the child block. - if (forwardScanFromStartOfListItemResult.ReachedBlockBoundary()) { + if (forwardScanFromStartOfListItemResult.ReachedBlockBoundary() || + // FIXME: This is wrong considering because the inline editing host may + // be surrounded by visible inline content. However, WSRunScanner is + // not aware of block boundary around it and stopping this change causes + // starting to fail some WPT. Therefore, we need to keep doing this for + // now. + forwardScanFromStartOfListItemResult.ReachedInlineEditingHostBoundary()) { return InsertParagraphResult( &rightListItemElement, HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>( @@ -8978,9 +9017,9 @@ HTMLEditor::HandleInsertParagraphInListItemElement( // Otherwise, return the point at first visible thing. // XXX This may be not meaningful position if it reached block element // in aListItemElement. - return InsertParagraphResult( - &rightListItemElement, - forwardScanFromStartOfListItemResult.Point<EditorDOMPoint>()); + return InsertParagraphResult(&rightListItemElement, + forwardScanFromStartOfListItemResult + .PointAtReachedContent<EditorDOMPoint>()); } Result<CreateElementResult, nsresult> diff --git a/editor/libeditor/HTMLEditUtils.cpp b/editor/libeditor/HTMLEditUtils.cpp index c2cbbe6157..cf0f4fd04a 100644 --- a/editor/libeditor/HTMLEditUtils.cpp +++ b/editor/libeditor/HTMLEditUtils.cpp @@ -2203,8 +2203,8 @@ nsIContent* HTMLEditUtils::GetContentToPreserveInlineStyles( return aPoint.template ContainerAs<nsIContent>(); } for (auto point = aPoint.template To<EditorRawDOMPoint>(); point.IsSet();) { - WSScanResult nextVisibleThing = - WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( + const WSScanResult nextVisibleThing = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( &aEditingHost, point, BlockInlineCheck::UseComputedDisplayOutsideStyle); if (nextVisibleThing.InVisibleOrCollapsibleCharacters()) { @@ -2215,8 +2215,8 @@ nsIContent* HTMLEditUtils::GetContentToPreserveInlineStyles( // view of users. if (nextVisibleThing.ReachedSpecialContent() && nextVisibleThing.IsContentEditable() && - nextVisibleThing.GetContent()->IsElement() && - !nextVisibleThing.GetContent()->HasChildNodes() && + nextVisibleThing.ContentIsElement() && + !nextVisibleThing.ElementPtr()->HasChildNodes() && HTMLEditUtils::IsContainerNode(*nextVisibleThing.ElementPtr())) { point.SetAfter(nextVisibleThing.ElementPtr()); continue; @@ -2260,13 +2260,12 @@ EditorDOMPointType HTMLEditUtils::GetBetterInsertionPointFor( // If the insertion position is after the last visible item in a line, // i.e., the insertion position is just before a visible line break <br>, // we want to skip to the position just after the line break (see bug 68767). - WSScanResult forwardScanFromPointToInsertResult = - wsScannerForPointToInsert.ScanNextVisibleNodeOrBlockBoundaryFrom( + const WSScanResult forwardScanFromPointToInsertResult = + wsScannerForPointToInsert.ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( pointToInsert); // So, if the next visible node isn't a <br> element, we can insert the block // level element to the point. - if (!forwardScanFromPointToInsertResult.GetContent() || - !forwardScanFromPointToInsertResult.ReachedBRElement()) { + if (!forwardScanFromPointToInsertResult.ReachedBRElement()) { return pointToInsert; } @@ -2274,7 +2273,7 @@ EditorDOMPointType HTMLEditUtils::GetBetterInsertionPointFor( // positioned at the beginning of a block, in that case skipping the <br> // would not insert the <br> at the caret position, but after the current // empty line. - WSScanResult backwardScanFromPointToInsertResult = + const WSScanResult backwardScanFromPointToInsertResult = wsScannerForPointToInsert.ScanPreviousVisibleNodeOrBlockBoundaryFrom( pointToInsert); // So, if there is no previous visible node, @@ -2282,14 +2281,15 @@ EditorDOMPointType HTMLEditUtils::GetBetterInsertionPointFor( // or, if the previous visible node is different block, // we need to skip the following <br>. So, otherwise, we can insert the // block at the insertion point. - if (!backwardScanFromPointToInsertResult.GetContent() || + if (NS_WARN_IF(backwardScanFromPointToInsertResult.Failed()) || + backwardScanFromPointToInsertResult.ReachedInlineEditingHostBoundary() || backwardScanFromPointToInsertResult.ReachedBRElement() || backwardScanFromPointToInsertResult.ReachedCurrentBlockBoundary()) { return pointToInsert; } return forwardScanFromPointToInsertResult - .template PointAfterContent<EditorDOMPointType>(); + .template PointAfterReachedContent<EditorDOMPointType>(); } // static @@ -2310,7 +2310,7 @@ EditorDOMPointType HTMLEditUtils::GetBetterCaretPositionToInsertText( if (aPoint.IsEndOfContainer()) { WSRunScanner scanner(&aEditingHost, aPoint, BlockInlineCheck::UseComputedDisplayStyle); - WSScanResult previousThing = + const WSScanResult previousThing = scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(aPoint); if (previousThing.InVisibleOrCollapsibleCharacters()) { return EditorDOMPointType::AtEndOf(*previousThing.TextPtr()); diff --git a/editor/libeditor/HTMLEditUtils.h b/editor/libeditor/HTMLEditUtils.h index 115a329247..01e41c147d 100644 --- a/editor/libeditor/HTMLEditUtils.h +++ b/editor/libeditor/HTMLEditUtils.h @@ -1853,14 +1853,15 @@ class HTMLEditUtils final { continue; } return lastEmptyContent != &aEmptyContent - ? lastEmptyContent->AsElement() + ? Element::FromNode(lastEmptyContent) : nullptr; } } lastEmptyContent = element; } - return lastEmptyContent != &aEmptyContent ? lastEmptyContent->AsElement() - : nullptr; + return lastEmptyContent != &aEmptyContent + ? Element::FromNode(lastEmptyContent) + : nullptr; } /** diff --git a/editor/libeditor/HTMLEditor.cpp b/editor/libeditor/HTMLEditor.cpp index fc88c79477..11d021e70b 100644 --- a/editor/libeditor/HTMLEditor.cpp +++ b/editor/libeditor/HTMLEditor.cpp @@ -46,6 +46,7 @@ #include "mozilla/css/Loader.h" #include "mozilla/dom/AncestorIterator.h" #include "mozilla/dom/Attr.h" +#include "mozilla/dom/BorrowedAttrInfo.h" #include "mozilla/dom/DocumentFragment.h" #include "mozilla/dom/DocumentInlines.h" #include "mozilla/dom/Element.h" @@ -63,7 +64,6 @@ #include "nsContentUtils.h" #include "nsCRT.h" #include "nsDebug.h" -#include "nsDOMAttributeMap.h" #include "nsElementTable.h" #include "nsFocusManager.h" #include "nsGenericHTMLElement.h" @@ -215,27 +215,23 @@ HTMLEditor::AppendNewElementWithBRToInsertingElement( } HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributes = - [](HTMLEditor&, const Element&, const Element&, const Attr&, nsString&) { - return true; - }; + [](HTMLEditor&, const Element&, const Element&, int32_t, const nsAtom&, + nsString&) { return true; }; HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributesExceptId = - [](HTMLEditor&, const Element&, const Element&, const Attr& aAttr, - nsString&) { - return aAttr.NodeInfo()->NamespaceID() != kNameSpaceID_None || - aAttr.NodeInfo()->NameAtom() != nsGkAtoms::id; + [](HTMLEditor&, const Element&, const Element&, int32_t aNamespaceID, + const nsAtom& aAttrName, nsString&) { + return aNamespaceID != kNameSpaceID_None || &aAttrName != nsGkAtoms::id; }; HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributesExceptDir = - [](HTMLEditor&, const Element&, const Element&, const Attr& aAttr, - nsString&) { - return aAttr.NodeInfo()->NamespaceID() != kNameSpaceID_None || - aAttr.NodeInfo()->NameAtom() != nsGkAtoms::dir; + [](HTMLEditor&, const Element&, const Element&, int32_t aNamespaceID, + const nsAtom& aAttrName, nsString&) { + return aNamespaceID != kNameSpaceID_None || &aAttrName != nsGkAtoms::dir; }; HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributesExceptIdAndDir = - [](HTMLEditor&, const Element&, const Element&, const Attr& aAttr, - nsString&) { - return !(aAttr.NodeInfo()->NamespaceID() == kNameSpaceID_None && - (aAttr.NodeInfo()->NameAtom() == nsGkAtoms::id || - aAttr.NodeInfo()->NameAtom() == nsGkAtoms::dir)); + [](HTMLEditor&, const Element&, const Element&, int32_t aNamespaceID, + const nsAtom& aAttrName, nsString&) { + return !(aNamespaceID == kNameSpaceID_None && + (&aAttrName == nsGkAtoms::id || &aAttrName == nsGkAtoms::dir)); }; HTMLEditor::HTMLEditor(const Document& aDocument) @@ -1150,15 +1146,15 @@ nsresult HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode( if (Text* text = leafContent->GetAsText()) { // If there is editable and visible text node, move caret at first of // the visible character. - WSScanResult scanResultInTextNode = - WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( + const WSScanResult scanResultInTextNode = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( editingHost, EditorRawDOMPoint(text, 0), BlockInlineCheck::UseComputedDisplayStyle); if ((scanResultInTextNode.InVisibleOrCollapsibleCharacters() || scanResultInTextNode.ReachedPreformattedLineBreak()) && scanResultInTextNode.TextPtr() == text) { nsresult rv = CollapseSelectionTo( - scanResultInTextNode.Point<EditorRawDOMPoint>()); + scanResultInTextNode.PointAtReachedContent<EditorRawDOMPoint>()); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; @@ -3603,29 +3599,35 @@ Result<CreateElementResult, nsresult> HTMLEditor::CreateAndInsertElement( nsresult HTMLEditor::CopyAttributes(WithTransaction aWithTransaction, Element& aDestElement, Element& aSrcElement, const AttributeFilter& aFilterFunc) { - RefPtr<nsDOMAttributeMap> srcAttributes = aSrcElement.Attributes(); - if (!srcAttributes->Length()) { + if (!aSrcElement.GetAttrCount()) { return NS_OK; } - AutoTArray<OwningNonNull<Attr>, 16> srcAttrs; - srcAttrs.SetCapacity(srcAttributes->Length()); - for (uint32_t i = 0; i < srcAttributes->Length(); i++) { - RefPtr<Attr> attr = srcAttributes->Item(i); - if (!attr) { - break; + struct MOZ_STACK_CLASS AttrCache { + int32_t mNamespaceID; + OwningNonNull<nsAtom> mName; + nsString mValue; + }; + AutoTArray<AttrCache, 16> srcAttrs; + srcAttrs.SetCapacity(aSrcElement.GetAttrCount()); + for (const uint32_t i : IntegerRange(aSrcElement.GetAttrCount())) { + const BorrowedAttrInfo attrInfo = aSrcElement.GetAttrInfoAt(i); + if (const nsAttrName* attrName = attrInfo.mName) { + MOZ_ASSERT(attrName->LocalName()); + MOZ_ASSERT(attrInfo.mValue); + nsString attrValue; + attrInfo.mValue->ToString(attrValue); + srcAttrs.AppendElement(AttrCache{attrInfo.mName->NamespaceID(), + *attrName->LocalName(), attrValue}); } - srcAttrs.AppendElement(std::move(attr)); } if (aWithTransaction == WithTransaction::No) { - for (const OwningNonNull<Attr>& attr : srcAttrs) { - nsString value; - attr->GetValue(value); - if (!aFilterFunc(*this, aSrcElement, aDestElement, attr, value)) { + for (auto& attr : srcAttrs) { + if (!aFilterFunc(*this, aSrcElement, aDestElement, attr.mNamespaceID, + attr.mName, attr.mValue)) { continue; } - DebugOnly<nsresult> rvIgnored = - aDestElement.SetAttr(attr->NodeInfo()->NamespaceID(), - attr->NodeInfo()->NameAtom(), value, false); + DebugOnly<nsresult> rvIgnored = aDestElement.SetAttr( + attr.mNamespaceID, attr.mName, attr.mValue, false); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "Element::SetAttr() failed, but ignored"); } @@ -3787,31 +3789,24 @@ nsresult HTMLEditor::InsertLinkAroundSelectionAsAction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); // Set all attributes found on the supplied anchor element - RefPtr<nsDOMAttributeMap> attributeMap = anchor->Attributes(); - if (NS_WARN_IF(!attributeMap)) { - return NS_ERROR_FAILURE; - } - // TODO: We should stop using this loop for adding attributes to newly created // `<a href="...">` elements. Then, we can avoid to increate the ref- // counter of attribute names since we can use nsStaticAtom if we don't // need to support unknown attributes. AutoTArray<EditorInlineStyleAndValue, 32> stylesToSet; - stylesToSet.SetCapacity(attributeMap->Length()); - nsString value; - for (uint32_t i : IntegerRange(attributeMap->Length())) { - RefPtr<Attr> attribute = attributeMap->Item(i); - if (!attribute) { - continue; + if (const uint32_t attrCount = anchor->GetAttrCount()) { + stylesToSet.SetCapacity(attrCount); + for (const uint32_t i : IntegerRange(attrCount)) { + const BorrowedAttrInfo attrInfo = anchor->GetAttrInfoAt(i); + if (const nsAttrName* attrName = attrInfo.mName) { + RefPtr<nsAtom> attributeName = attrName->LocalName(); + MOZ_ASSERT(attrInfo.mValue); + nsString attrValue; + attrInfo.mValue->ToString(attrValue); + stylesToSet.AppendElement(EditorInlineStyleAndValue( + *nsGkAtoms::a, std::move(attributeName), std::move(attrValue))); + } } - - RefPtr<nsAtom> attributeName = attribute->NodeInfo()->NameAtom(); - - MOZ_ASSERT(value.IsEmpty()); - attribute->GetValue(value); - - stylesToSet.AppendElement(EditorInlineStyleAndValue( - *nsGkAtoms::a, std::move(attributeName), std::move(value))); } rv = SetInlinePropertiesAsSubAction(stylesToSet); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), diff --git a/editor/libeditor/HTMLEditor.h b/editor/libeditor/HTMLEditor.h index 2a31ead29d..e56ae0fe33 100644 --- a/editor/libeditor/HTMLEditor.h +++ b/editor/libeditor/HTMLEditor.h @@ -1333,7 +1333,8 @@ class HTMLEditor final : public EditorBase, * @param aHTMLEditor The HTML editor. * @param aSrcElement The element which have the attribute. * @param aDestElement The element which will have the attribute. - * @param aAttr [in] The attribute which will be copied. + * @param aNamespaceID [in] The namespace ID of aAttrName. + * @param aAttrName [in] The attribute name which will be copied. * @param aValue [in/out] The attribute value which will be copied. * Once updated, the new value is used. * @return true if the attribute should be copied, otherwise, @@ -1341,7 +1342,7 @@ class HTMLEditor final : public EditorBase, */ using AttributeFilter = std::function<bool( HTMLEditor& aHTMLEditor, Element& aSrcElement, Element& aDestElement, - const dom::Attr& aAttr, nsString& aValue)>; + int32_t aNamespaceID, const nsAtom& aAttrName, nsString& aValue)>; static AttributeFilter CopyAllAttributes; static AttributeFilter CopyAllAttributesExceptId; static AttributeFilter CopyAllAttributesExceptDir; diff --git a/editor/libeditor/HTMLEditorDataTransfer.cpp b/editor/libeditor/HTMLEditorDataTransfer.cpp index 72aeb7eee8..f7b785722a 100644 --- a/editor/libeditor/HTMLEditorDataTransfer.cpp +++ b/editor/libeditor/HTMLEditorDataTransfer.cpp @@ -505,12 +505,12 @@ HTMLEditor::HTMLWithContextInserter::GetNewCaretPointAfterInsertingHTML( editingHost, EditorDOMPoint(wsRunScannerAtCaret.GetStartReasonContent()), BlockInlineCheck::UseComputedDisplayStyle); - WSScanResult backwardScanFromPointToCaretResult = + const WSScanResult backwardScanFromPointToCaretResult = wsRunScannerAtStartReason.ScanPreviousVisibleNodeOrBlockBoundaryFrom( pointToPutCaret); if (backwardScanFromPointToCaretResult.InVisibleOrCollapsibleCharacters()) { - pointToPutCaret = - backwardScanFromPointToCaretResult.Point<EditorDOMPoint>(); + pointToPutCaret = backwardScanFromPointToCaretResult + .PointAfterReachedContent<EditorDOMPoint>(); } else if (backwardScanFromPointToCaretResult.ReachedSpecialContent()) { // XXX In my understanding, this is odd. The end reason may not be // same as the reached special content because the equality is diff --git a/editor/libeditor/HTMLEditorDeleteHandler.cpp b/editor/libeditor/HTMLEditorDeleteHandler.cpp index 39bb95151e..8c58979001 100644 --- a/editor/libeditor/HTMLEditorDeleteHandler.cpp +++ b/editor/libeditor/HTMLEditorDeleteHandler.cpp @@ -353,6 +353,28 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { const EditorDOMRangeType& aRangeToDelete) const; /** + * Extend the start boundary of aRangeToDelete to contain ancestor inline + * elements which will be empty once the content in aRangeToDelete is removed + * from the tree. + * + * NOTE: This is designed for deleting inline elements which become empty if + * aRangeToDelete which crosses a block boundary of right block child. + * Therefore, you may need to improve this method if you want to use this in + * the other cases. + * + * @param aRangeToDelete [in/out] The range to delete. This start + * boundary may be modified. + * @param aEditingHost The editing host. + * @return true if aRangeToDelete is modified. + * false if aRangeToDelete is not modified. + * error if aRangeToDelete gets unexpected + * situation. + */ + static Result<bool, nsresult> + ExtendRangeToContainAncestorInlineElementsAtStart( + nsRange& aRangeToDelete, const Element& aEditingHost); + + /** * A helper method for ExtendOrShrinkRangeToDelete(). This returns shrunken * range if aRangeToDelete selects all over list elements which have some list * item elements to avoid to delete all list items from the list element. @@ -529,13 +551,15 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { * @param aCurrentBlockElement The current block element. * @param aCaretPoint The caret point (i.e., selection start * or end). + * @param aEditingHost The editing host. * @return true if can continue to handle the * deletion. */ bool PrepareToDeleteAtCurrentBlockBoundary( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, - Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint); + Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint, + const Element& aEditingHost); /** * PrepareToDeleteAtOtherBlockBoundary() considers left content and right @@ -567,11 +591,13 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { * @param aHTMLEditor The HTML editor. * @param aRangeToDelete The range to delete. Must not be * collapsed. + * @param aEditingHost The editing host. * @return true if can continue to handle the * deletion. */ bool PrepareToDeleteNonCollapsedRange(const HTMLEditor& aHTMLEditor, - const nsRange& aRangeToDelete); + const nsRange& aRangeToDelete, + const Element& aEditingHost); /** * Run() executes the joining. @@ -609,17 +635,20 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { "HandleDeleteAtOtherBlockBoundary() failed"); return result; } - case Mode::DeleteBRElement: { - Result<EditActionResult, nsresult> result = - DeleteBRElement(aHTMLEditor, aDirectionAndAmount, aEditingHost); + case Mode::DeleteBRElement: + case Mode::DeletePrecedingBRElementOfBlock: + case Mode::DeletePrecedingPreformattedLineBreak: { + Result<EditActionResult, nsresult> result = HandleDeleteLineBreak( + aHTMLEditor, aDirectionAndAmount, aCaretPoint, aEditingHost); NS_WARNING_ASSERTION( result.isOk(), - "AutoBlockElementsJoiner::DeleteBRElement() failed"); + "AutoBlockElementsJoiner::HandleDeleteLineBreak() failed"); return result; } case Mode::JoinBlocksInSameParent: case Mode::DeleteContentInRange: case Mode::DeleteNonCollapsedRange: + case Mode::DeletePrecedingLinesAndContentInRange: MOZ_ASSERT_UNREACHABLE( "This mode should be handled in the other Run()"); return Err(NS_ERROR_UNEXPECTED); @@ -654,16 +683,21 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { "ComputeRangeToDeleteAtOtherBlockBoundary() failed"); return rv; } - case Mode::DeleteBRElement: { - nsresult rv = ComputeRangeToDeleteBRElement(aRangeToDelete); + case Mode::DeleteBRElement: + case Mode::DeletePrecedingBRElementOfBlock: + case Mode::DeletePrecedingPreformattedLineBreak: { + nsresult rv = ComputeRangeToDeleteLineBreak( + aHTMLEditor, aRangeToDelete, aEditingHost, + ComputeRangeFor::GetTargetRanges); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoBlockElementsJoiner::" - "ComputeRangeToDeleteBRElement() failed"); + "ComputeRangeToDeleteLineBreak() failed"); return rv; } case Mode::JoinBlocksInSameParent: case Mode::DeleteContentInRange: case Mode::DeleteNonCollapsedRange: + case Mode::DeletePrecedingLinesAndContentInRange: MOZ_ASSERT_UNREACHABLE( "This mode should be handled in the other " "ComputeRangesToDelete()"); @@ -696,6 +730,8 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { case Mode::JoinCurrentBlock: case Mode::JoinOtherBlock: case Mode::DeleteBRElement: + case Mode::DeletePrecedingBRElementOfBlock: + case Mode::DeletePrecedingPreformattedLineBreak: MOZ_ASSERT_UNREACHABLE( "This mode should be handled in the other Run()"); return Err(NS_ERROR_UNEXPECTED); @@ -717,7 +753,8 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { "AutoBlockElementsJoiner::DeleteContentInRange() failed"); return result; } - case Mode::DeleteNonCollapsedRange: { + case Mode::DeleteNonCollapsedRange: + case Mode::DeletePrecedingLinesAndContentInRange: { Result<EditActionResult, nsresult> result = HandleDeleteNonCollapsedRange( aHTMLEditor, aDirectionAndAmount, aStripWrappers, @@ -744,6 +781,8 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { case Mode::JoinCurrentBlock: case Mode::JoinOtherBlock: case Mode::DeleteBRElement: + case Mode::DeletePrecedingBRElementOfBlock: + case Mode::DeletePrecedingPreformattedLineBreak: MOZ_ASSERT_UNREACHABLE( "This mode should be handled in the other " "ComputeRangesToDelete()"); @@ -765,7 +804,8 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { "ComputeRangesToDeleteContentInRanges() failed"); return rv; } - case Mode::DeleteNonCollapsedRange: { + case Mode::DeleteNonCollapsedRange: + case Mode::DeletePrecedingLinesAndContentInRange: { nsresult rv = ComputeRangeToDeleteNonCollapsedRange( aHTMLEditor, aDirectionAndAmount, aRangeToDelete, aSelectionWasCollapsed, aEditingHost); @@ -825,10 +865,14 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { nsIEditor::EDirection aDirectionAndAmount, nsRange& aRangeToDelete, const Element& aEditingHost) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> - DeleteBRElement(HTMLEditor& aHTMLEditor, - nsIEditor::EDirection aDirectionAndAmount, - const Element& aEditingHost); - nsresult ComputeRangeToDeleteBRElement(nsRange& aRangeToDelete) const; + HandleDeleteLineBreak(HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aCaretPoint, + const Element& aEditingHost); + enum class ComputeRangeFor : bool { GetTargetRanges, ToDeleteTheRange }; + nsresult ComputeRangeToDeleteLineBreak( + const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete, + const Element& aEditingHost, ComputeRangeFor aComputeRangeFor) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> DeleteContentInRange(HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, @@ -913,6 +957,27 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult DeleteTextAtStartAndEndOfRange(HTMLEditor& aHTMLEditor, nsRange& aRange); + /** + * Return a block element which is an inclusive ancestor of the container of + * aPoint if aPoint is start of ancestor blocks. For example, if `<div + * id=div1>abc<div id=div2><div id=div3>[]def</div></div></div>`, return + * #div2. + */ + template <typename EditorDOMPointType> + static Result<Element*, nsresult> + GetMostDistantBlockAncestorIfPointIsStartAtBlock( + const EditorDOMPointType& aPoint, const Element& aEditingHost, + const Element* aAncestorLimiter = nullptr); + + /** + * Extend aRangeToDelete to contain new empty inline ancestors and contain + * an invisible <br> element before right child block which causes an empty + * line but the range starts after it. + */ + void ExtendRangeToDeleteNonCollapsedRange( + const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete, + const Element& aEditingHost, ComputeRangeFor aComputeRangeFor) const; + class MOZ_STACK_CLASS AutoInclusiveAncestorBlockElementsJoiner final { public: AutoInclusiveAncestorBlockElementsJoiner() = delete; @@ -1030,8 +1095,17 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { JoinOtherBlock, JoinBlocksInSameParent, DeleteBRElement, + // The instance will handle only the <br> element immediately before a + // block. + DeletePrecedingBRElementOfBlock, + // The instance will handle only the preceding preformatted line break + // before a block. + DeletePrecedingPreformattedLineBreak, DeleteContentInRange, DeleteNonCollapsedRange, + // The instance will handle preceding lines of the right block and content + // in the range in the right block. + DeletePrecedingLinesAndContentInRange, }; AutoDeleteRangesHandler* mDeleteRangesHandler; const AutoDeleteRangesHandler& mDeleteRangesHandlerConst; @@ -1043,6 +1117,7 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { // removed at deletion. AutoTArray<OwningNonNull<nsIContent>, 8> mSkippedInvisibleContents; RefPtr<dom::HTMLBRElement> mBRElement; + EditorDOMPointInText mPreformattedLineBreak; Mode mMode = Mode::NotInitialized; }; // HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner @@ -1371,10 +1446,10 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete( WSRunScanner wsRunScannerAtCaret( editingHost, caretPoint, BlockInlineCheck::UseComputedDisplayOutsideStyle); - WSScanResult scanFromCaretPointResult = + const WSScanResult scanFromCaretPointResult = aDirectionAndAmount == nsIEditor::eNext - ? wsRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom( - caretPoint) + ? wsRunScannerAtCaret + .ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(caretPoint) : wsRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom( caretPoint); if (scanFromCaretPointResult.Failed()) { @@ -1383,26 +1458,23 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete( "failed"); return NS_ERROR_FAILURE; } - if (!scanFromCaretPointResult.GetContent()) { - return NS_SUCCESS_DOM_NO_OPERATION; - } + MOZ_ASSERT(scanFromCaretPointResult.GetContent()); if (scanFromCaretPointResult.ReachedBRElement()) { if (scanFromCaretPointResult.BRElementPtr() == wsRunScannerAtCaret.GetEditingHost()) { return NS_OK; } - if (!EditorUtils::IsEditableContent( - *scanFromCaretPointResult.BRElementPtr(), EditorType::HTML)) { + if (!scanFromCaretPointResult.IsContentEditable()) { return NS_SUCCESS_DOM_NO_OPERATION; } - if (HTMLEditUtils::IsInvisibleBRElement( - *scanFromCaretPointResult.BRElementPtr())) { + if (scanFromCaretPointResult.ReachedInvisibleBRElement()) { EditorDOMPoint newCaretPosition = aDirectionAndAmount == nsIEditor::eNext - ? EditorDOMPoint::After( - *scanFromCaretPointResult.BRElementPtr()) - : EditorDOMPoint(scanFromCaretPointResult.BRElementPtr()); + ? scanFromCaretPointResult + .PointAfterReachedContent<EditorDOMPoint>() + : scanFromCaretPointResult + .PointAtReachedContent<EditorDOMPoint>(); if (NS_WARN_IF(!newCaretPosition.IsSet())) { return NS_ERROR_FAILURE; } @@ -1450,7 +1522,8 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete( // Otherwise, extend the range to contain the invisible `<br>` // element. - if (EditorRawDOMPoint(scanFromCaretPointResult.BRElementPtr()) + if (scanFromCaretPointResult + .PointAtReachedContent<EditorRawDOMPoint>() .IsBefore( aRangesToDelete .GetFirstRangeStartPoint<EditorRawDOMPoint>())) { @@ -1463,12 +1536,13 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete( return rv; } if (aRangesToDelete.GetFirstRangeEndPoint<EditorRawDOMPoint>() - .IsBefore(EditorRawDOMPoint::After( - *scanFromCaretPointResult.BRElementPtr()))) { + .IsBefore( + scanFromCaretPointResult + .PointAfterReachedContent<EditorRawDOMPoint>())) { nsresult rv = aRangesToDelete.FirstRangeRef()->SetStartAndEnd( aRangesToDelete.FirstRangeRef()->StartRef(), - EditorRawDOMPoint::After( - *scanFromCaretPointResult.BRElementPtr()) + scanFromCaretPointResult + .PointAfterReachedContent<EditorRawDOMPoint>() .ToRawRangeBoundary()); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::SetStartAndEnd() failed"); @@ -1670,10 +1744,11 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::Run( WSRunScanner wsRunScannerAtCaret( &aEditingHost, caretPoint.ref(), BlockInlineCheck::UseComputedDisplayOutsideStyle); - WSScanResult scanFromCaretPointResult = + const WSScanResult scanFromCaretPointResult = aDirectionAndAmount == nsIEditor::eNext - ? wsRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom( - caretPoint.ref()) + ? wsRunScannerAtCaret + .ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( + caretPoint.ref()) : wsRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom( caretPoint.ref()); if (MOZ_UNLIKELY(scanFromCaretPointResult.Failed())) { @@ -1682,20 +1757,17 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::Run( "failed"); return Err(NS_ERROR_FAILURE); } - if (!scanFromCaretPointResult.GetContent()) { - return EditActionResult::CanceledResult(); - } + MOZ_ASSERT(scanFromCaretPointResult.GetContent()); + // Short circuit for invisible breaks. delete them and recurse. if (scanFromCaretPointResult.ReachedBRElement()) { if (scanFromCaretPointResult.BRElementPtr() == &aEditingHost) { return EditActionResult::HandledResult(); } - if (!EditorUtils::IsEditableContent( - *scanFromCaretPointResult.BRElementPtr(), EditorType::HTML)) { + if (!scanFromCaretPointResult.IsContentEditable()) { return EditActionResult::CanceledResult(); } - if (HTMLEditUtils::IsInvisibleBRElement( - *scanFromCaretPointResult.BRElementPtr())) { + if (scanFromCaretPointResult.ReachedInvisibleBRElement()) { // TODO: We should extend the range to delete again before/after // the caret point and use `HandleDeleteNonCollapsedRanges()` // instead after we would create delete range computation @@ -1728,10 +1800,10 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::Run( WSRunScanner wsRunScannerAtCaret( &aEditingHost, caretPoint.ref(), BlockInlineCheck::UseComputedDisplayOutsideStyle); - WSScanResult scanFromCaretPointResult = + const WSScanResult scanFromCaretPointResult = aDirectionAndAmount == nsIEditor::eNext ? wsRunScannerAtCaret - .ScanNextVisibleNodeOrBlockBoundaryFrom( + .ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( caretPoint.ref()) : wsRunScannerAtCaret .ScanPreviousVisibleNodeOrBlockBoundaryFrom( @@ -1742,7 +1814,7 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::Run( "VisibleNodeOrBlockBoundaryFrom() failed"); return Err(NS_ERROR_FAILURE); } - if (MOZ_UNLIKELY( + if (NS_WARN_IF( scanFromCaretPointResult.ReachedInvisibleBRElement())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } @@ -1786,8 +1858,11 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges( if (aScanFromCaretPointResult.InCollapsibleWhiteSpaces() || aScanFromCaretPointResult.InNonCollapsibleCharacters() || aScanFromCaretPointResult.ReachedPreformattedLineBreak()) { + // This means that if aDirectionAndAmount == nsIEditor::eNext, collapse + // selection at the found character. Otherwise, collapse selection after + // the found character. nsresult rv = aRangesToDelete.Collapse( - aScanFromCaretPointResult.Point<EditorRawDOMPoint>()); + aScanFromCaretPointResult.Point_Deprecated<EditorRawDOMPoint>()); if (MOZ_UNLIKELY(NS_FAILED(rv))) { NS_WARNING("AutoRangeArray::Collapse() failed"); return NS_ERROR_FAILURE; @@ -1826,7 +1901,7 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges( } if (aScanFromCaretPointResult.ReachedOtherBlockElement()) { - if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) { + if (NS_WARN_IF(!aScanFromCaretPointResult.ContentIsElement())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty()); @@ -1854,10 +1929,9 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges( return handled ? NS_OK : NS_SUCCESS_DOM_NO_OPERATION; } - if (aScanFromCaretPointResult.ReachedCurrentBlockBoundary()) { - if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) { - return NS_ERROR_FAILURE; - } + if (aScanFromCaretPointResult.ReachedCurrentBlockBoundary() || + aScanFromCaretPointResult.ReachedInlineEditingHostBoundary()) { + MOZ_ASSERT(aScanFromCaretPointResult.ContentIsElement()); MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty()); bool handled = false; for (const OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) { @@ -1865,7 +1939,7 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges( if (!joiner.PrepareToDeleteAtCurrentBlockBoundary( aHTMLEditor, aDirectionAndAmount, *aScanFromCaretPointResult.ElementPtr(), - aWSRunScannerAtCaret.ScanStartRef())) { + aWSRunScannerAtCaret.ScanStartRef(), aEditingHost)) { continue; } handled = true; @@ -1904,8 +1978,11 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges( if (aScanFromCaretPointResult.InCollapsibleWhiteSpaces() || aScanFromCaretPointResult.InNonCollapsibleCharacters() || aScanFromCaretPointResult.ReachedPreformattedLineBreak()) { + // This means that if aDirectionAndAmount == nsIEditor::eNext, collapse + // selection at the found character. Otherwise, collapse selection after + // the found character. nsresult rv = aRangesToDelete.Collapse( - aScanFromCaretPointResult.Point<EditorRawDOMPoint>()); + aScanFromCaretPointResult.Point_Deprecated<EditorRawDOMPoint>()); if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::Collapse() failed"); return Err(NS_ERROR_FAILURE); @@ -1959,13 +2036,16 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges( } if (aScanFromCaretPointResult.InNonCollapsibleCharacters()) { - if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsText())) { + if (NS_WARN_IF(!aScanFromCaretPointResult.ContentIsText())) { return Err(NS_ERROR_FAILURE); } Result<CaretPoint, nsresult> caretPointOrError = HandleDeleteCollapsedSelectionAtVisibleChar( aHTMLEditor, aDirectionAndAmount, aRangesToDelete, - aScanFromCaretPointResult.Point<EditorDOMPoint>(), aEditingHost); + // This means that if aDirectionAndAmount == nsIEditor::eNext, + // at the found character. Otherwise, after the found character. + aScanFromCaretPointResult.Point_Deprecated<EditorDOMPoint>(), + aEditingHost); if (MOZ_UNLIKELY(caretPointOrError.isErr())) { NS_WARNING( "AutoDeleteRangesHandler::" @@ -2019,7 +2099,7 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges( } if (aScanFromCaretPointResult.ReachedOtherBlockElement()) { - if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) { + if (NS_WARN_IF(!aScanFromCaretPointResult.ContentIsElement())) { return Err(NS_ERROR_FAILURE); } MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty()); @@ -2049,10 +2129,9 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges( : std::move(ret); } - if (aScanFromCaretPointResult.ReachedCurrentBlockBoundary()) { - if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) { - return Err(NS_ERROR_FAILURE); - } + if (aScanFromCaretPointResult.ReachedCurrentBlockBoundary() || + aScanFromCaretPointResult.ReachedInlineEditingHostBoundary()) { + MOZ_ASSERT(aScanFromCaretPointResult.ContentIsElement()); MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty()); bool allRangesNotHandled = true; auto ret = EditActionResult::IgnoredResult(); @@ -2061,7 +2140,7 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges( if (!joiner.PrepareToDeleteAtCurrentBlockBoundary( aHTMLEditor, aDirectionAndAmount, *aScanFromCaretPointResult.ElementPtr(), - aWSRunScannerAtCaret.ScanStartRef())) { + aWSRunScannerAtCaret.ScanStartRef(), aEditingHost)) { continue; } allRangesNotHandled = false; @@ -2484,6 +2563,109 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAtomicContent( return CaretPoint(std::move(pointToPutCaret)); } +// static +Result<bool, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + ExtendRangeToContainAncestorInlineElementsAtStart( + nsRange& aRangeToDelete, const Element& aEditingHost) { + MOZ_ASSERT(aRangeToDelete.IsPositioned()); + MOZ_ASSERT(aRangeToDelete.GetCommonAncestorContainer(IgnoreErrors())); + MOZ_ASSERT(aRangeToDelete.GetCommonAncestorContainer(IgnoreErrors()) + ->IsInclusiveDescendantOf(&aEditingHost)); + + EditorRawDOMPoint startPoint(aRangeToDelete.StartRef()); + if (startPoint.IsInTextNode()) { + if (!startPoint.IsStartOfContainer()) { + // FIXME: If before the point has only collapsible white-spaces and the + // text node follows a block boundary, we should treat the range start + // from start of the text node. + return true; + } + startPoint.Set(startPoint.ContainerAs<Text>()); + if (NS_WARN_IF(!startPoint.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + if (startPoint.GetContainer() == &aEditingHost) { + return false; + } + } else if (startPoint.IsInDataNode()) { + startPoint.Set(startPoint.ContainerAs<nsIContent>()); + if (NS_WARN_IF(!startPoint.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + if (startPoint.GetContainer() == &aEditingHost) { + return false; + } + } else if (startPoint.GetContainer() == &aEditingHost) { + return false; + } + + // FYI: This method is designed for deleting inline elements which become + // empty if aRangeToDelete which crosses a block boundary of right block + // child. Therefore, you may need to improve this method if you want to use + // this in the other cases. + + nsINode* const commonAncestor = + nsContentUtils::GetClosestCommonInclusiveAncestor( + startPoint.GetContainer(), aRangeToDelete.GetEndContainer()); + if (NS_WARN_IF(!commonAncestor)) { + return Err(NS_ERROR_FAILURE); + } + MOZ_ASSERT(commonAncestor->IsInclusiveDescendantOf(&aEditingHost)); + + EditorRawDOMPoint newStartPoint(startPoint); + while (newStartPoint.GetContainer() != &aEditingHost && + newStartPoint.GetContainer() != commonAncestor) { + if (NS_WARN_IF(!newStartPoint.IsInContentNode())) { + return Err(NS_ERROR_FAILURE); + } + if (!HTMLEditUtils::IsInlineContent( + *newStartPoint.ContainerAs<nsIContent>(), + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { + break; + } + // The container is inline, check whether the point is first visible point + // or not to consider whether climbing up the tree. + bool foundVisiblePrevSibling = false; + for (nsIContent* content = newStartPoint.GetPreviousSiblingOfChild(); + content; content = content->GetPreviousSibling()) { + if (Text* text = Text::FromNode(content)) { + if (HTMLEditUtils::IsVisibleTextNode(*text)) { + foundVisiblePrevSibling = true; + break; + } + // The text node is invisible. + } else if (content->IsComment()) { + // Ignore the comment node. + } else if (!HTMLEditUtils::IsInlineContent( + *content, + BlockInlineCheck::UseComputedDisplayOutsideStyle) || + !HTMLEditUtils::IsEmptyNode( + *content, + {EmptyCheckOption::TreatSingleBRElementAsVisible})) { + foundVisiblePrevSibling = true; + break; + } + } + if (foundVisiblePrevSibling) { + break; + } + // the point can be treated as start of the parent inline now. + newStartPoint.Set(newStartPoint.ContainerAs<nsIContent>()); + if (NS_WARN_IF(!newStartPoint.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + } + if (newStartPoint == startPoint) { + return false; // Don't need to modify the range + } + IgnoredErrorResult error; + aRangeToDelete.SetStart(newStartPoint.ToRawRangeBoundary(), error); + if (MOZ_UNLIKELY(error.Failed())) { + return Err(NS_ERROR_FAILURE); + } + return true; +} + bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: PrepareToDeleteAtOtherBlockBoundary( const HTMLEditor& aHTMLEditor, @@ -2519,12 +2701,12 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: // Next to a block. See if we are between the block and a `<br>`. // If so, we really want to delete the `<br>`. Else join content at // selection to the block. - WSScanResult scanFromCaretResult = + const WSScanResult scanFromCaretResult = aDirectionAndAmount == nsIEditor::eNext ? aWSRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom( aCaretPoint) - : aWSRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom( - aCaretPoint); + : aWSRunScannerAtCaret + .ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(aCaretPoint); // If we found a `<br>` element, we need to delete it instead of joining the // contents. if (scanFromCaretResult.ReachedBRElement()) { @@ -2537,58 +2719,159 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: } nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: - ComputeRangeToDeleteBRElement(nsRange& aRangeToDelete) const { - MOZ_ASSERT(mBRElement); - // XXX Why don't we scan invisible leading white-spaces which follows the - // `<br>` element? + ComputeRangeToDeleteLineBreak(const HTMLEditor& aHTMLEditor, + nsRange& aRangeToDelete, + const Element& aEditingHost, + ComputeRangeFor aComputeRangeFor) const { + // FIXME: Scan invisible leading white-spaces after the <br>. + MOZ_ASSERT_IF(mMode == Mode::DeleteBRElement, mBRElement); + MOZ_ASSERT_IF(mMode == Mode::DeletePrecedingBRElementOfBlock, mBRElement); + MOZ_ASSERT_IF(mMode == Mode::DeletePrecedingPreformattedLineBreak, + mPreformattedLineBreak.IsSetAndValid()); + MOZ_ASSERT_IF(mMode == Mode::DeletePrecedingPreformattedLineBreak, + mPreformattedLineBreak.IsCharPreformattedNewLine()); + MOZ_ASSERT_IF(aComputeRangeFor == ComputeRangeFor::GetTargetRanges, + aRangeToDelete.IsPositioned()); + + // If we're computing for beforeinput.getTargetRanges() and the inputType + // is not a simple deletion like replacing selected content with new + // content, the range should end at the original end boundary of the given + // range. + const bool preserveEndBoundary = + (mMode == Mode::DeletePrecedingBRElementOfBlock || + mMode == Mode::DeletePrecedingPreformattedLineBreak) && + aComputeRangeFor == ComputeRangeFor::GetTargetRanges && + !MayEditActionDeleteAroundCollapsedSelection(aHTMLEditor.GetEditAction()); + + if (mMode != Mode::DeletePrecedingPreformattedLineBreak) { + Element* const mostDistantInlineAncestor = + HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( + *mBRElement, BlockInlineCheck::UseComputedDisplayOutsideStyle, + &aEditingHost); + if (preserveEndBoundary) { + // FIXME: If the range ends at end of an inline element, we may need to + // extend the range. + IgnoredErrorResult error; + aRangeToDelete.SetStart(EditorRawDOMPoint(mostDistantInlineAncestor + ? mostDistantInlineAncestor + : mBRElement) + .ToRawRangeBoundary(), + error); + NS_WARNING_ASSERTION(!error.Failed(), "nsRange::SetStart() failed"); + MOZ_ASSERT_IF(!error.Failed(), !aRangeToDelete.Collapsed()); + return error.StealNSResult(); + } + IgnoredErrorResult error; + aRangeToDelete.SelectNode( + mostDistantInlineAncestor ? *mostDistantInlineAncestor : *mBRElement, + error); + NS_WARNING_ASSERTION(!error.Failed(), "nsRange::SelectNode() failed"); + return error.StealNSResult(); + } + + Element* const mostDistantInlineAncestor = + mPreformattedLineBreak.ContainerAs<Text>()->TextDataLength() == 1 + ? HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( + *mPreformattedLineBreak.ContainerAs<Text>(), + BlockInlineCheck::UseComputedDisplayOutsideStyle, &aEditingHost) + : nullptr; + + if (!mostDistantInlineAncestor) { + if (preserveEndBoundary) { + // FIXME: If the range ends at end of an inline element, we may need to + // extend the range. + IgnoredErrorResult error; + aRangeToDelete.SetStart(mPreformattedLineBreak.ToRawRangeBoundary(), + error); + MOZ_ASSERT_IF(!error.Failed(), !aRangeToDelete.Collapsed()); + NS_WARNING_ASSERTION(!error.Failed(), "nsRange::SetStart() failed"); + return error.StealNSResult(); + } + nsresult rv = aRangeToDelete.SetStartAndEnd( + mPreformattedLineBreak.ToRawRangeBoundary(), + mPreformattedLineBreak.NextPoint().ToRawRangeBoundary()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::SetStartAndEnd() failed"); + return rv; + } + + if (preserveEndBoundary) { + // FIXME: If the range ends at end of an inline element, we may need to + // extend the range. + IgnoredErrorResult error; + aRangeToDelete.SetStart( + EditorRawDOMPoint(mostDistantInlineAncestor).ToRawRangeBoundary(), + error); + MOZ_ASSERT_IF(!error.Failed(), !aRangeToDelete.Collapsed()); + NS_WARNING_ASSERTION(!error.Failed(), "nsRange::SetStart() failed"); + return error.StealNSResult(); + } + IgnoredErrorResult error; - aRangeToDelete.SelectNode(*mBRElement, error); + aRangeToDelete.SelectNode(*mostDistantInlineAncestor, error); NS_WARNING_ASSERTION(!error.Failed(), "nsRange::SelectNode() failed"); return error.StealNSResult(); } -Result<EditActionResult, nsresult> -HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::DeleteBRElement( - HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, - const Element& aEditingHost) { +Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + AutoBlockElementsJoiner::HandleDeleteLineBreak( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aCaretPoint, const Element& aEditingHost) { MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); - MOZ_ASSERT(mBRElement); + MOZ_ASSERT(mBRElement || mPreformattedLineBreak.IsSet()); // If we're deleting selection (not replacing with new content), we should // put caret to end of preceding text node if there is. Then, users can type // text in it like the other browsers. EditorDOMPoint pointToPutCaret = [&]() { + // but when we're deleting a preceding line break of current block, we + // should keep the caret position in the current block. + if (mMode == Mode::DeletePrecedingBRElementOfBlock || + mMode == Mode::DeletePrecedingPreformattedLineBreak) { + return aCaretPoint; + } if (!MayEditActionDeleteAroundCollapsedSelection( aHTMLEditor.GetEditAction())) { return EditorDOMPoint(); } WSRunScanner scanner(&aEditingHost, EditorRawDOMPoint(mBRElement), BlockInlineCheck::UseComputedDisplayOutsideStyle); - WSScanResult maybePreviousText = + const WSScanResult maybePreviousText = scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom( EditorRawDOMPoint(mBRElement)); if (maybePreviousText.IsContentEditable() && maybePreviousText.InVisibleOrCollapsibleCharacters() && !HTMLEditor::GetLinkElement(maybePreviousText.TextPtr())) { - return maybePreviousText.Point<EditorDOMPoint>(); + return maybePreviousText.PointAfterReachedContent<EditorDOMPoint>(); } - WSScanResult maybeNextText = scanner.ScanNextVisibleNodeOrBlockBoundaryFrom( - EditorRawDOMPoint::After(*mBRElement)); + const WSScanResult maybeNextText = + scanner.ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( + EditorRawDOMPoint::After(*mBRElement)); if (maybeNextText.IsContentEditable() && maybeNextText.InVisibleOrCollapsibleCharacters()) { - return maybeNextText.Point<EditorDOMPoint>(); + return maybeNextText.PointAtReachedContent<EditorDOMPoint>(); } return EditorDOMPoint(); }(); - // If we found a `<br>` element, we should delete it instead of joining the - // contents. + RefPtr<nsRange> rangeToDelete = + nsRange::Create(const_cast<Element*>(&aEditingHost)); + MOZ_ASSERT(rangeToDelete); nsresult rv = - aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(*mBRElement)); + ComputeRangeToDeleteLineBreak(aHTMLEditor, *rangeToDelete, aEditingHost, + ComputeRangeFor::ToDeleteTheRange); if (NS_FAILED(rv)) { - NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + NS_WARNING( + "AutoBlockElementsJoiner::ComputeRangeToDeleteLineBreak() failed"); return Err(rv); } + Result<EditActionResult, nsresult> result = HandleDeleteNonCollapsedRange( + aHTMLEditor, aDirectionAndAmount, nsIEditor::eNoStrip, *rangeToDelete, + SelectionWasCollapsed::Yes, aEditingHost); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING( + "AutoBlockElementsJoiner::HandleDeleteNonCollapsedRange() failed"); + return result; + } if (mLeftContent && mRightContent && HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mLeftContent) != @@ -2597,7 +2880,7 @@ HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::DeleteBRElement( } // Put selection at edge of block and we are done. - if (NS_WARN_IF(!mLeafContentInOtherBlock)) { + if (NS_WARN_IF(mMode == Mode::DeleteBRElement && !mLeafContentInOtherBlock)) { // XXX This must be odd case. The other block can be empty. return Err(NS_ERROR_FAILURE); } @@ -2607,7 +2890,7 @@ HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::DeleteBRElement( if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } - if (NS_SUCCEEDED(rv)) { + if (mMode == Mode::DeleteBRElement && NS_SUCCEEDED(rv)) { // If we prefer to use style in the previous line, we should forget // previous styles since the caret position has all styles which we want // to use with new content. @@ -2622,7 +2905,9 @@ HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::DeleteBRElement( ->ClearLinkAndItsSpecifiedStyle(); } } else { - NS_WARNING("EditorBase::CollapseSelectionTo() failed, but ignored"); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed, but ignored"); } return EditActionResult::HandledResult(); } @@ -2937,7 +3222,8 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: PrepareToDeleteAtCurrentBlockBoundary( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, - Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint) { + Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint, + const Element& aEditingHost) { MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); // At edge of our block. Look beside it and see if we can join to an @@ -2956,20 +3242,15 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: return false; } - Element* editingHost = aHTMLEditor.ComputeEditingHost(); - if (NS_WARN_IF(!editingHost)) { - return false; - } - auto ScanJoinTarget = [&]() -> nsIContent* { nsIContent* targetContent = aDirectionAndAmount == nsIEditor::ePrevious ? HTMLEditUtils::GetPreviousContent( aCurrentBlockElement, {WalkTreeOption::IgnoreNonEditableNode}, - BlockInlineCheck::Unused, editingHost) + BlockInlineCheck::Unused, &aEditingHost) : HTMLEditUtils::GetNextContent( aCurrentBlockElement, {WalkTreeOption::IgnoreNonEditableNode}, - BlockInlineCheck::Unused, editingHost); + BlockInlineCheck::Unused, &aEditingHost); // If found content is an invisible text node, let's scan visible things. auto IsIgnorableDataNode = [](nsIContent* aContent) { return aContent && HTMLEditUtils::IsRemovableNode(*aContent) && @@ -2987,22 +3268,22 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: ? HTMLEditUtils::GetPreviousContent( *targetContent, {WalkTreeOption::StopAtBlockBoundary}, BlockInlineCheck::UseComputedDisplayOutsideStyle, - editingHost) + &aEditingHost) : HTMLEditUtils::GetNextContent( *targetContent, {WalkTreeOption::StopAtBlockBoundary}, BlockInlineCheck::UseComputedDisplayOutsideStyle, - editingHost); + &aEditingHost); adjacentContent; adjacentContent = aDirectionAndAmount == nsIEditor::ePrevious ? HTMLEditUtils::GetPreviousContent( *adjacentContent, {WalkTreeOption::StopAtBlockBoundary}, BlockInlineCheck::UseComputedDisplayOutsideStyle, - editingHost) + &aEditingHost) : HTMLEditUtils::GetNextContent( *adjacentContent, {WalkTreeOption::StopAtBlockBoundary}, BlockInlineCheck::UseComputedDisplayOutsideStyle, - editingHost)) { + &aEditingHost)) { // If non-editable element is found, we should not skip it to avoid // joining too far nodes. if (!HTMLEditUtils::IsSimplyEditableNode(*adjacentContent)) { @@ -3036,6 +3317,77 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: }; if (aDirectionAndAmount == nsIEditor::ePrevious) { + const WSScanResult prevVisibleThing = [&]() { + // When Backspace at start of a block, we need to delete only a preceding + // <br> element if there is. + const Result<Element*, nsresult> + inclusiveAncestorOfRightChildBlockOrError = AutoBlockElementsJoiner:: + GetMostDistantBlockAncestorIfPointIsStartAtBlock(aCaretPoint, + aEditingHost); + if (NS_WARN_IF(inclusiveAncestorOfRightChildBlockOrError.isErr()) || + !inclusiveAncestorOfRightChildBlockOrError.inspect()) { + return WSScanResult::Error(); + } + const WSScanResult prevVisibleThingBeforeCurrentBlock = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + &aEditingHost, + EditorRawDOMPoint( + inclusiveAncestorOfRightChildBlockOrError.inspect()), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (!prevVisibleThingBeforeCurrentBlock.ReachedBRElement() && + !prevVisibleThingBeforeCurrentBlock.ReachedPreformattedLineBreak()) { + return WSScanResult::Error(); + } + // There is a preceding line break, but it may be invisible. Then, users + // want to delete its preceding content not only the line break. + // Therefore, let's check whether the line break follows another line + // break or a block boundary. In these cases, the line break causes an + // empty line which users may want to delete. + const auto atPrecedingLineBreak = + prevVisibleThingBeforeCurrentBlock + .PointAtReachedContent<EditorRawDOMPoint>(); + MOZ_ASSERT(atPrecedingLineBreak.IsSet()); + const WSScanResult prevVisibleThingBeforeLineBreak = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + &aEditingHost, atPrecedingLineBreak, + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (prevVisibleThingBeforeLineBreak.ReachedBRElement() || + prevVisibleThingBeforeLineBreak.ReachedPreformattedLineBreak() || + prevVisibleThingBeforeLineBreak.ReachedCurrentBlockBoundary()) { + // Target the latter line break for things simpler. It's easier to + // compute the target range. + MOZ_ASSERT_IF( + prevVisibleThingBeforeCurrentBlock.ReachedPreformattedLineBreak() && + prevVisibleThingBeforeLineBreak.ReachedPreformattedLineBreak(), + prevVisibleThingBeforeCurrentBlock + .PointAtReachedContent<EditorRawDOMPoint>() != + prevVisibleThingBeforeLineBreak + .PointAtReachedContent<EditorRawDOMPoint>()); + return prevVisibleThingBeforeCurrentBlock; + } + return WSScanResult::Error(); + }(); + + // If previous visible thing is a <br>, we should just delete it without + // unwrapping the first line of the right child block. Note that the <br> + // is always treated as invisible by HTMLEditUtils because it's immediately + // preceding <br> of the block boundary. However, deleting it is fine + // because the above checks whether it causes empty line or not. + if (prevVisibleThing.ReachedBRElement()) { + mMode = Mode::DeletePrecedingBRElementOfBlock; + mBRElement = prevVisibleThing.BRElementPtr(); + return true; + } + + // Same for a preformatted line break. + if (prevVisibleThing.ReachedPreformattedLineBreak()) { + mMode = Mode::DeletePrecedingPreformattedLineBreak; + mPreformattedLineBreak = + prevVisibleThing.PointAtReachedContent<EditorRawDOMPoint>() + .AsInText(); + return true; + } + mLeftContent = ScanJoinTarget(); mRightContent = aCaretPoint.GetContainerAs<nsIContent>(); } else { @@ -3283,7 +3635,8 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteNonCollapsedRanges( continue; } AutoBlockElementsJoiner joiner(*this); - if (!joiner.PrepareToDeleteNonCollapsedRange(aHTMLEditor, range)) { + if (!joiner.PrepareToDeleteNonCollapsedRange(aHTMLEditor, range, + aEditingHost)) { return NS_ERROR_FAILURE; } nsresult rv = @@ -3473,7 +3826,8 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteNonCollapsedRanges( continue; } AutoBlockElementsJoiner joiner(*this); - if (!joiner.PrepareToDeleteNonCollapsedRange(aHTMLEditor, range)) { + if (!joiner.PrepareToDeleteNonCollapsedRange(aHTMLEditor, range, + aEditingHost)) { return Err(NS_ERROR_FAILURE); } Result<EditActionResult, nsresult> result = @@ -3490,7 +3844,8 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteNonCollapsedRanges( bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: PrepareToDeleteNonCollapsedRange(const HTMLEditor& aHTMLEditor, - const nsRange& aRangeToDelete) { + const nsRange& aRangeToDelete, + const Element& aEditingHost) { MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); MOZ_ASSERT(!aRangeToDelete.Collapsed()); @@ -3528,6 +3883,125 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: return true; } + // If the range starts immediately after a line end and ends in a + // child right block, we should not unwrap the right block unless the + // right block will have no nodes. + if (mRightContent->IsInclusiveDescendantOf(mLeftContent)) { + // FYI: Chrome does not remove the right child block even if there will be + // only single <br> or a comment node in it. Therefore, we should use this + // rough check. + const WSScanResult nextVisibleThingOfEndBoundary = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + &aEditingHost, EditorRawDOMPoint(aRangeToDelete.EndRef()), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (!nextVisibleThingOfEndBoundary.ReachedCurrentBlockBoundary()) { + MOZ_ASSERT(mLeftContent->IsElement()); + Result<Element*, nsresult> mostDistantBlockOrError = + AutoBlockElementsJoiner:: + GetMostDistantBlockAncestorIfPointIsStartAtBlock( + EditorRawDOMPoint(mRightContent, 0), aEditingHost, + mLeftContent->AsElement()); + MOZ_ASSERT(mostDistantBlockOrError.isOk()); + if (MOZ_LIKELY(mostDistantBlockOrError.inspect())) { + const WSScanResult prevVisibleThingOfStartBoundary = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + &aEditingHost, EditorRawDOMPoint(aRangeToDelete.StartRef()), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (prevVisibleThingOfStartBoundary.ReachedBRElement()) { + // If the range start after a <br> followed by the block boundary, + // we want to delete the <br> or following <br> element unless it's + // not a part of empty line like `<div>abc<br>{<div>]def`. + const WSScanResult nextVisibleThingOfBR = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + &aEditingHost, + EditorRawDOMPoint::After( + *prevVisibleThingOfStartBoundary.GetContent()), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + MOZ_ASSERT(!nextVisibleThingOfBR.ReachedCurrentBlockBoundary()); + if (!nextVisibleThingOfBR.ReachedOtherBlockElement() || + nextVisibleThingOfBR.GetContent() != + mostDistantBlockOrError.inspect()) { + // The range selects a non-empty line or a child block at least. + mMode = Mode::DeletePrecedingLinesAndContentInRange; + return true; + } + const WSScanResult prevVisibleThingOfBR = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + &aEditingHost, + EditorRawDOMPoint( + prevVisibleThingOfStartBoundary.GetContent()), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (prevVisibleThingOfBR.ReachedBRElement() || + prevVisibleThingOfBR.ReachedPreformattedLineBreak() || + prevVisibleThingOfBR.ReachedBlockBoundary()) { + // The preceding <br> causes an empty line. + mMode = Mode::DeletePrecedingLinesAndContentInRange; + return true; + } + } else if (prevVisibleThingOfStartBoundary + .ReachedPreformattedLineBreak()) { + const WSScanResult nextVisibleThingOfLineBreak = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + &aEditingHost, + prevVisibleThingOfStartBoundary + .PointAfterReachedContent<EditorRawDOMPoint>(), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + MOZ_ASSERT( + !nextVisibleThingOfLineBreak.ReachedCurrentBlockBoundary()); + if (!nextVisibleThingOfLineBreak.ReachedOtherBlockElement() || + nextVisibleThingOfLineBreak.GetContent() != + mostDistantBlockOrError.inspect()) { + // The range selects a non-empty line or a child block at least. + mMode = Mode::DeletePrecedingLinesAndContentInRange; + return true; + } + const WSScanResult prevVisibleThingOfLineBreak = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + &aEditingHost, + prevVisibleThingOfStartBoundary + .PointAtReachedContent<EditorRawDOMPoint>(), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (prevVisibleThingOfLineBreak.ReachedBRElement() || + prevVisibleThingOfLineBreak.ReachedPreformattedLineBreak() || + prevVisibleThingOfLineBreak.ReachedBlockBoundary()) { + // The preceding line break causes an empty line. + mMode = Mode::DeletePrecedingLinesAndContentInRange; + return true; + } + } else if (prevVisibleThingOfStartBoundary + .ReachedCurrentBlockBoundary()) { + MOZ_ASSERT(prevVisibleThingOfStartBoundary.ElementPtr() == + mLeftContent); + const WSScanResult firstVisibleThingInBlock = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + &aEditingHost, + EditorRawDOMPoint( + prevVisibleThingOfStartBoundary.ElementPtr(), 0), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (!firstVisibleThingInBlock.ReachedOtherBlockElement() || + firstVisibleThingInBlock.ElementPtr() != + mostDistantBlockOrError.inspect()) { + mMode = Mode::DeletePrecedingLinesAndContentInRange; + return true; + } + } else if (prevVisibleThingOfStartBoundary.ReachedOtherBlockElement()) { + const WSScanResult firstVisibleThingAfterBlock = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + &aEditingHost, + EditorRawDOMPoint::After( + *prevVisibleThingOfStartBoundary.ElementPtr()), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (!firstVisibleThingAfterBlock.ReachedOtherBlockElement() || + firstVisibleThingAfterBlock.ElementPtr() != + mostDistantBlockOrError.inspect()) { + mMode = Mode::DeletePrecedingLinesAndContentInRange; + return true; + } + } + } + } + } + mMode = Mode::DeleteNonCollapsedRange; return true; } @@ -3744,12 +4218,12 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: aHTMLEditor.GetEditAction())) { WSRunScanner scanner(&aEditingHost, startOfRightContent, BlockInlineCheck::UseComputedDisplayOutsideStyle); - WSScanResult maybePreviousText = + const WSScanResult maybePreviousText = scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(startOfRightContent); if (maybePreviousText.IsContentEditable() && maybePreviousText.InVisibleOrCollapsibleCharacters()) { nsresult rv = aHTMLEditor.CollapseSelectionTo( - maybePreviousText.Point<EditorRawDOMPoint>()); + maybePreviousText.PointAfterReachedContent<EditorRawDOMPoint>()); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::CollapseSelectionTo() failed"); return Err(rv); @@ -3851,6 +4325,16 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents, AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed) const { + switch (mMode) { + case Mode::DeletePrecedingLinesAndContentInRange: + case Mode::DeleteBRElement: + case Mode::DeletePrecedingBRElementOfBlock: + case Mode::DeletePrecedingPreformattedLineBreak: + return false; + default: + break; + } + // If original selection was collapsed, we need always to join the nodes. // XXX Why? if (aSelectionWasCollapsed == @@ -3890,41 +4374,103 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: DeleteTextAtStartAndEndOfRange(HTMLEditor& aHTMLEditor, nsRange& aRange) { EditorDOMPoint rangeStart(aRange.StartRef()); EditorDOMPoint rangeEnd(aRange.EndRef()); - if (rangeStart.IsInTextNode() && !rangeStart.IsEndOfContainer()) { - // Delete to last character + if (MOZ_UNLIKELY(aRange.Collapsed())) { + return NS_OK; + } + + EditorDOMPoint pointToPutCaret; + // If the range is in a text node, delete middle of the text or the text node + // itself. + if (rangeStart.IsInTextNode() && + rangeStart.ContainerAs<Text>() == rangeEnd.GetContainer()) { OwningNonNull<Text> textNode = *rangeStart.ContainerAs<Text>(); - Result<CaretPoint, nsresult> caretPointOrError = - aHTMLEditor.DeleteTextWithTransaction( - textNode, rangeStart.Offset(), - rangeStart.GetContainer()->Length() - rangeStart.Offset()); - if (MOZ_UNLIKELY(caretPointOrError.isErr())) { - NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); - return caretPointOrError.unwrapErr(); + if (rangeStart.IsStartOfContainer() && rangeEnd.IsEndOfContainer()) { + EditorDOMPoint pointToPutCaret(textNode); + AutoTrackDOMPoint trackTextNodePoint(aHTMLEditor.RangeUpdaterRef(), + &pointToPutCaret); + nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(textNode); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + } else { + MOZ_ASSERT(rangeEnd.Offset() - rangeStart.Offset() > 0); + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteTextWithTransaction( + textNode, rangeStart.Offset(), + rangeEnd.Offset() - rangeStart.Offset()); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); + return caretPointOrError.unwrapErr(); + } + caretPointOrError.unwrap().MoveCaretPointTo( + pointToPutCaret, aHTMLEditor, + {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); } - nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo( - aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion, - SuggestCaret::OnlyIfTransactionsAllowedToDoIt, - SuggestCaret::AndIgnoreTrivialError}); - if (NS_FAILED(rv)) { - NS_WARNING("CaretPoint::SuggestCaretPointTo() failed"); - return rv; + } else { + // If the range starts in a text node and ends in a different node, delete + // the text after the start boundary. + if (rangeStart.IsInTextNode() && !rangeStart.IsEndOfContainer()) { + OwningNonNull<Text> textNode = *rangeStart.ContainerAs<Text>(); + if (rangeStart.IsStartOfContainer()) { + pointToPutCaret.Set(textNode); + AutoTrackDOMPoint trackTextNodePoint(aHTMLEditor.RangeUpdaterRef(), + &pointToPutCaret); + nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(textNode); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + } else { + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteTextWithTransaction( + textNode, rangeStart.Offset(), + rangeStart.GetContainer()->Length() - rangeStart.Offset()); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); + return caretPointOrError.unwrapErr(); + } + caretPointOrError.unwrap().MoveCaretPointTo( + pointToPutCaret, aHTMLEditor, + {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); + } } - NS_WARNING_ASSERTION( - rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, - "CaretPoint::SuggestCaretPointTo() failed, but ignored"); - } - if (rangeEnd.IsInTextNode() && !rangeEnd.IsStartOfContainer()) { - // Delete to first character - OwningNonNull<Text> textNode = *rangeEnd.ContainerAs<Text>(); - Result<CaretPoint, nsresult> caretPointOrError = - aHTMLEditor.DeleteTextWithTransaction(textNode, 0, rangeEnd.Offset()); - if (MOZ_UNLIKELY(caretPointOrError.isErr())) { - NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); - return caretPointOrError.unwrapErr(); + + // If the range ends in a text node and starts from a different node, delete + // the text before the end boundary. + if (rangeEnd.IsInTextNode() && !rangeEnd.IsStartOfContainer()) { + OwningNonNull<Text> textNode = *rangeEnd.ContainerAs<Text>(); + if (rangeEnd.IsEndOfContainer()) { + pointToPutCaret.Set(textNode); + AutoTrackDOMPoint trackTextNodePoint(aHTMLEditor.RangeUpdaterRef(), + &pointToPutCaret); + nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(textNode); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + } else { + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteTextWithTransaction(textNode, 0, + rangeEnd.Offset()); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); + return caretPointOrError.unwrapErr(); + } + caretPointOrError.unwrap().MoveCaretPointTo( + pointToPutCaret, aHTMLEditor, + {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); + } } - nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo( - aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion, - SuggestCaret::OnlyIfTransactionsAllowedToDoIt, + } + + if (pointToPutCaret.IsSet()) { + CaretPoint caretPoint(std::move(pointToPutCaret)); + nsresult rv = caretPoint.SuggestCaretPointTo( + aHTMLEditor, {SuggestCaret::OnlyIfTransactionsAllowedToDoIt, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CaretPoint::SuggestCaretPointTo() failed"); @@ -3937,6 +4483,201 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: return NS_OK; } +// static +template <typename EditorDOMPointType> +Result<Element*, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + AutoBlockElementsJoiner::GetMostDistantBlockAncestorIfPointIsStartAtBlock( + const EditorDOMPointType& aPoint, const Element& aEditingHost, + const Element* aAncestorLimiter /* = nullptr */) { + MOZ_ASSERT(aPoint.IsSetAndValid()); + MOZ_ASSERT(aPoint.IsInComposedDoc()); + + if (!aAncestorLimiter) { + aAncestorLimiter = &aEditingHost; + } + + const auto ReachedCurrentBlockBoundaryWhichWeCanCross = + [&aEditingHost, aAncestorLimiter](const WSScanResult& aScanResult) { + // When the scan result is "reached current block boundary", it may not + // be so literally. + return aScanResult.ReachedCurrentBlockBoundary() && + HTMLEditUtils::IsRemovableFromParentNode( + *aScanResult.ElementPtr()) && + aScanResult.ElementPtr() != &aEditingHost && + aScanResult.ElementPtr() != aAncestorLimiter && + // Don't cross <body>, <head> and <html> + !aScanResult.ElementPtr()->IsAnyOfHTMLElements( + nsGkAtoms::body, nsGkAtoms::head, nsGkAtoms::html) && + // Don't cross table elements + !HTMLEditUtils::IsAnyTableElement(aScanResult.ElementPtr()); + }; + + const WSScanResult prevVisibleThing = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + aAncestorLimiter, aPoint, + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (!ReachedCurrentBlockBoundaryWhichWeCanCross(prevVisibleThing)) { + return nullptr; + } + MOZ_ASSERT(HTMLEditUtils::IsBlockElement( + *prevVisibleThing.ElementPtr(), + BlockInlineCheck::UseComputedDisplayOutsideStyle)); + for (Element* ancestorBlock = prevVisibleThing.ElementPtr(); ancestorBlock;) { + const WSScanResult prevVisibleThing = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + aAncestorLimiter, EditorRawDOMPoint(ancestorBlock), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (!ReachedCurrentBlockBoundaryWhichWeCanCross(prevVisibleThing)) { + return ancestorBlock; + } + MOZ_ASSERT(HTMLEditUtils::IsBlockElement( + *prevVisibleThing.ElementPtr(), + BlockInlineCheck::UseComputedDisplayOutsideStyle)); + ancestorBlock = prevVisibleThing.ElementPtr(); + } + return Err(NS_ERROR_FAILURE); +} + +void HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + ExtendRangeToDeleteNonCollapsedRange( + const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete, + const Element& aEditingHost, ComputeRangeFor aComputeRangeFor) const { + MOZ_ASSERT_IF(aComputeRangeFor == ComputeRangeFor::GetTargetRanges, + aRangeToDelete.IsPositioned()); + MOZ_ASSERT(!aRangeToDelete.Collapsed()); + MOZ_ASSERT(mLeftContent); + MOZ_ASSERT(mLeftContent->IsElement()); + MOZ_ASSERT(aRangeToDelete.GetStartContainer()->IsInclusiveDescendantOf( + mLeftContent)); + MOZ_ASSERT(mRightContent); + MOZ_ASSERT(mRightContent->IsElement()); + MOZ_ASSERT( + aRangeToDelete.GetEndContainer()->IsInclusiveDescendantOf(mRightContent)); + + const DebugOnly<Result<bool, nsresult>> extendRangeResult = + AutoDeleteRangesHandler:: + ExtendRangeToContainAncestorInlineElementsAtStart(aRangeToDelete, + aEditingHost); + NS_WARNING_ASSERTION(extendRangeResult.value.isOk(), + "AutoDeleteRangesHandler::" + "ExtendRangeToContainAncestorInlineElementsAtStart() " + "failed, but ignored"); + if (mMode != Mode::DeletePrecedingLinesAndContentInRange) { + return; + } + + // If we're computing for beforeinput.getTargetRanges() and the inputType + // is not a simple deletion like replacing selected content with new + // content, the range should end at the original end boundary of the given + // range even if we're deleting only preceding lines of the right child + // block. + const bool preserveEndBoundary = + aComputeRangeFor == ComputeRangeFor::GetTargetRanges && + !MayEditActionDeleteAroundCollapsedSelection(aHTMLEditor.GetEditAction()); + // We need to delete only the preceding lines of the right block. Therefore, + // we need to shrink the range to ends before the right block if the range + // does not contain any meaningful content in the right block. + const Result<Element*, nsresult> inclusiveAncestorCurrentBlockOrError = + AutoBlockElementsJoiner::GetMostDistantBlockAncestorIfPointIsStartAtBlock( + EditorRawDOMPoint(aRangeToDelete.EndRef()), aEditingHost, + mLeftContent->AsElement()); + MOZ_ASSERT(inclusiveAncestorCurrentBlockOrError.isOk()); + MOZ_ASSERT_IF(inclusiveAncestorCurrentBlockOrError.inspect(), + mRightContent->IsInclusiveDescendantOf( + inclusiveAncestorCurrentBlockOrError.inspect())); + if (MOZ_UNLIKELY(!inclusiveAncestorCurrentBlockOrError.isOk() || + !inclusiveAncestorCurrentBlockOrError.inspect())) { + return; + } + + const WSScanResult prevVisibleThingOfStartBoundary = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + &aEditingHost, EditorRawDOMPoint(aRangeToDelete.StartRef()), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + // If the range starts after an invisible <br> of empty line immediately + // before the most distant inclusive ancestor of the right block like + // `<br><br>{<div>]abc`, we should delete the last empty line because + // users won't see any reaction of the builtin editor in this case. + if (prevVisibleThingOfStartBoundary.ReachedBRElement() || + prevVisibleThingOfStartBoundary.ReachedPreformattedLineBreak()) { + const WSScanResult prevVisibleThingOfPreviousLineBreak = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + &aEditingHost, + prevVisibleThingOfStartBoundary + .PointAtReachedContent<EditorRawDOMPoint>(), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + const WSScanResult nextVisibleThingOfPreviousBR = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + &aEditingHost, + prevVisibleThingOfStartBoundary + .PointAfterReachedContent<EditorRawDOMPoint>(), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if ((prevVisibleThingOfPreviousLineBreak.ReachedBRElement() || + prevVisibleThingOfPreviousLineBreak.ReachedPreformattedLineBreak()) && + nextVisibleThingOfPreviousBR.ReachedOtherBlockElement() && + nextVisibleThingOfPreviousBR.ElementPtr() == + inclusiveAncestorCurrentBlockOrError.inspect()) { + aRangeToDelete.SetStart(prevVisibleThingOfStartBoundary + .PointAtReachedContent<EditorRawDOMPoint>() + .ToRawRangeBoundary(), + IgnoreErrors()); + } + } + + if (preserveEndBoundary) { + return; + } + + if (aComputeRangeFor == ComputeRangeFor::GetTargetRanges) { + // When we set the end boundary to around the right block, the new end + // boundary should not after inline ancestors of the line break which won't + // be deleted. + const WSScanResult lastVisibleThingBeforeRightChildBlock = + [&]() -> WSScanResult { + EditorRawDOMPoint scanStartPoint(aRangeToDelete.StartRef()); + WSScanResult lastScanResult = WSScanResult::Error(); + while (true) { + WSScanResult scanResult = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + mLeftContent->AsElement(), scanStartPoint, + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (scanResult.ReachedBlockBoundary() || + scanResult.ReachedInlineEditingHostBoundary()) { + return lastScanResult; + } + scanStartPoint = + scanResult.PointAfterReachedContent<EditorRawDOMPoint>(); + lastScanResult = scanResult; + } + }(); + if (lastVisibleThingBeforeRightChildBlock.GetContent()) { + const nsIContent* commonAncestor = nsIContent::FromNode( + nsContentUtils::GetClosestCommonInclusiveAncestor( + aRangeToDelete.StartRef().Container(), + lastVisibleThingBeforeRightChildBlock.GetContent())); + MOZ_ASSERT(commonAncestor); + if (commonAncestor && + !mRightContent->IsInclusiveDescendantOf(commonAncestor)) { + IgnoredErrorResult error; + aRangeToDelete.SetEnd( + EditorRawDOMPoint::AtEndOf(*commonAncestor).ToRawRangeBoundary(), + error); + NS_WARNING_ASSERTION(!error.Failed(), + "nsRange::SetEnd() failed, but ignored"); + return; + } + } + } + + IgnoredErrorResult error; + aRangeToDelete.SetEnd( + EditorRawDOMPoint(inclusiveAncestorCurrentBlockOrError.inspect()) + .ToRawRangeBoundary(), + error); + NS_WARNING_ASSERTION(!error.Failed(), + "nsRange::SetEnd() failed, but ignored"); +} + nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: ComputeRangeToDeleteNonCollapsedRange( const HTMLEditor& aHTMLEditor, @@ -3954,6 +4695,10 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: MOZ_ASSERT( aRangeToDelete.GetEndContainer()->IsInclusiveDescendantOf(mRightContent)); + ExtendRangeToDeleteNonCollapsedRange(aHTMLEditor, aRangeToDelete, + aEditingHost, + ComputeRangeFor::GetTargetRanges); + Result<bool, nsresult> result = ComputeRangeToDeleteNodesEntirelyInRangeButKeepTableStructure( aHTMLEditor, aRangeToDelete, aSelectionWasCollapsed); @@ -4003,14 +4748,20 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); MOZ_ASSERT(!aRangeToDelete.Collapsed()); MOZ_ASSERT(mDeleteRangesHandler); - MOZ_ASSERT(mLeftContent); - MOZ_ASSERT(mLeftContent->IsElement()); - MOZ_ASSERT(aRangeToDelete.GetStartContainer()->IsInclusiveDescendantOf( - mLeftContent)); - MOZ_ASSERT(mRightContent); - MOZ_ASSERT(mRightContent->IsElement()); - MOZ_ASSERT( - aRangeToDelete.GetEndContainer()->IsInclusiveDescendantOf(mRightContent)); + + const bool isDeletingLineBreak = + mMode == Mode::DeleteBRElement || + mMode == Mode::DeletePrecedingBRElementOfBlock || + mMode == Mode::DeletePrecedingPreformattedLineBreak; + if (!isDeletingLineBreak) { + MOZ_ASSERT(aRangeToDelete.GetStartContainer()->IsInclusiveDescendantOf( + mLeftContent)); + MOZ_ASSERT(aRangeToDelete.GetEndContainer()->IsInclusiveDescendantOf( + mRightContent)); + ExtendRangeToDeleteNonCollapsedRange(aHTMLEditor, aRangeToDelete, + aEditingHost, + ComputeRangeFor::ToDeleteTheRange); + } const bool backspaceInRightBlock = aSelectionWasCollapsed == SelectionWasCollapsed::Yes && @@ -4034,7 +4785,8 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: return deleteResult.propagateErr(); } - const bool joinInclusiveAncestorBlockElements = deleteResult.unwrap(); + const bool joinInclusiveAncestorBlockElements = + !isDeletingLineBreak && deleteResult.unwrap(); // Check endpoints for possible text deletion. We can assume that if // text node is found, we can delete to end or to beginning as @@ -4048,9 +4800,24 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: } if (!joinInclusiveAncestorBlockElements) { + // When we delete only preceding lines of the right child block, we should + // put caret into start of the right block. + if (mMode == Mode::DeletePrecedingLinesAndContentInRange) { + result.MarkAsHandled(); + if (MOZ_LIKELY(mRightContent->IsInComposedDoc())) { + pointToPutCaret = + HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>( + *mRightContent); + } + } break; } + MOZ_ASSERT(mLeftContent); + MOZ_ASSERT(mLeftContent->IsElement()); + MOZ_ASSERT(mRightContent); + MOZ_ASSERT(mRightContent->IsElement()); + AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent, *mRightContent); Result<bool, nsresult> canJoinThem = @@ -4106,6 +4873,12 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: break; } + // HandleDeleteLineBreak() should handle the new caret position by itself. + if (isDeletingLineBreak) { + result.MarkAsHandled(); + return result; + } + // If we're deleting selection (not replacing with new content) and // AutoInclusiveAncestorBlockElementsJoiner computed new caret position, we // should use it. Otherwise, we should keep the traditional behavior. @@ -4297,7 +5070,8 @@ HTMLEditor::AutoDeleteRangesHandler::DeleteParentBlocksWithTransactionIfEmpty( RefPtr<Element> editingHost = aHTMLEditor.ComputeEditingHost(); WSRunScanner wsScannerForPoint( editingHost, aPoint, BlockInlineCheck::UseComputedDisplayOutsideStyle); - if (!wsScannerForPoint.StartsFromCurrentBlockBoundary()) { + if (!wsScannerForPoint.StartsFromCurrentBlockBoundary() && + !wsScannerForPoint.StartsFromInlineEditingHostBoundary()) { // If there is visible node before the point, we shouldn't remove the // parent block. return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; @@ -4319,8 +5093,8 @@ HTMLEditor::AutoDeleteRangesHandler::DeleteParentBlocksWithTransactionIfEmpty( } // Next, check there is visible contents after the point in current block. - WSScanResult forwardScanFromPointResult = - wsScannerForPoint.ScanNextVisibleNodeOrBlockBoundaryFrom(aPoint); + const WSScanResult forwardScanFromPointResult = + wsScannerForPoint.ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(aPoint); if (forwardScanFromPointResult.Failed()) { NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed"); return NS_ERROR_FAILURE; @@ -4340,8 +5114,8 @@ HTMLEditor::AutoDeleteRangesHandler::DeleteParentBlocksWithTransactionIfEmpty( return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; } if (wsScannerForPoint.GetEndReasonContent()->GetNextSibling()) { - WSScanResult scanResult = - WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( + const WSScanResult scanResult = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( editingHost, EditorRawDOMPoint::After( *wsScannerForPoint.GetEndReasonContent()), @@ -4350,13 +5124,15 @@ HTMLEditor::AutoDeleteRangesHandler::DeleteParentBlocksWithTransactionIfEmpty( NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed"); return NS_ERROR_FAILURE; } - if (!scanResult.ReachedCurrentBlockBoundary()) { + if (!scanResult.ReachedCurrentBlockBoundary() && + !scanResult.ReachedInlineEditingHostBoundary()) { // If we couldn't reach the block's end after the invisible <br>, // that means that there is visible content. return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; } } - } else if (!forwardScanFromPointResult.ReachedCurrentBlockBoundary()) { + } else if (!forwardScanFromPointResult.ReachedCurrentBlockBoundary() && + !forwardScanFromPointResult.ReachedInlineEditingHostBoundary()) { // If we couldn't reach the block's end, the block has visible content. return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; } @@ -5169,11 +5945,12 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: aHTMLEditor.GetEditAction())) { WSRunScanner scanner(&aEditingHost, startOfRightContent, BlockInlineCheck::UseComputedDisplayStyle); - WSScanResult maybePreviousText = + const WSScanResult maybePreviousText = scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(startOfRightContent); if (maybePreviousText.IsContentEditable() && maybePreviousText.InVisibleOrCollapsibleCharacters()) { - mPointToPutCaret = maybePreviousText.Point<EditorDOMPoint>(); + mPointToPutCaret = + maybePreviousText.PointAfterReachedContent<EditorDOMPoint>(); } } return result; @@ -6798,11 +7575,12 @@ HTMLEditor::AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete( if (rangeToDelete.StartRef().GetContainer() != closestBlockAncestorOrInlineEditingHost) { for (;;) { - WSScanResult backwardScanFromStartResult = + const WSScanResult backwardScanFromStartResult = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( closestEditingHost, rangeToDelete.StartRef(), BlockInlineCheck::UseComputedDisplayOutsideStyle); - if (!backwardScanFromStartResult.ReachedCurrentBlockBoundary()) { + if (!backwardScanFromStartResult.ReachedCurrentBlockBoundary() && + !backwardScanFromStartResult.ReachedInlineEditingHostBoundary()) { break; } MOZ_ASSERT(backwardScanFromStartResult.GetContent() == @@ -6834,8 +7612,8 @@ HTMLEditor::AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete( *backwardScanFromStartResult.ElementPtr())) { break; } - rangeToDelete.SetStart( - backwardScanFromStartResult.PointAtContent<EditorRawDOMPoint>()); + rangeToDelete.SetStart(backwardScanFromStartResult + .PointAtReachedContent<EditorRawDOMPoint>()); } if (aFrameSelection && !aFrameSelection->IsValidSelectionPoint( rangeToDelete.StartRef().GetContainer())) { @@ -6856,8 +7634,8 @@ HTMLEditor::AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete( WSRunScanner wsScannerAtEnd( closestEditingHost, rangeToDelete.EndRef(), BlockInlineCheck::UseComputedDisplayOutsideStyle); - WSScanResult forwardScanFromEndResult = - wsScannerAtEnd.ScanNextVisibleNodeOrBlockBoundaryFrom( + const WSScanResult forwardScanFromEndResult = + wsScannerAtEnd.ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( rangeToDelete.EndRef()); if (forwardScanFromEndResult.ReachedBRElement()) { // XXX In my understanding, this is odd. The end reason may not be @@ -6881,7 +7659,9 @@ HTMLEditor::AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete( continue; } - if (forwardScanFromEndResult.ReachedCurrentBlockBoundary()) { + if (forwardScanFromEndResult.ReachedCurrentBlockBoundary() || + forwardScanFromEndResult.ReachedInlineEditingHostBoundary()) { + MOZ_ASSERT(forwardScanFromEndResult.ContentIsElement()); MOZ_ASSERT(forwardScanFromEndResult.GetContent() == wsScannerAtEnd.GetEndReasonContent()); // We want to keep looking up. But stop if we are crossing table @@ -6895,13 +7675,13 @@ HTMLEditor::AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete( // Don't cross flex-item/grid-item boundary to make new content inserted // into it. if (StaticPrefs::editor_block_inline_check_use_computed_style() && - forwardScanFromEndResult.ContentIsElement() && HTMLEditUtils::IsFlexOrGridItem( *forwardScanFromEndResult.ElementPtr())) { break; } rangeToDelete.SetEnd( - forwardScanFromEndResult.PointAfterContent<EditorRawDOMPoint>()); + forwardScanFromEndResult + .PointAfterReachedContent<EditorRawDOMPoint>()); continue; } diff --git a/editor/libeditor/HTMLStyleEditor.cpp b/editor/libeditor/HTMLStyleEditor.cpp index 2ca7b06c29..5aaff15ba4 100644 --- a/editor/libeditor/HTMLStyleEditor.cpp +++ b/editor/libeditor/HTMLStyleEditor.cpp @@ -1914,8 +1914,8 @@ HTMLEditor::AutoInlineStyleSetter::ExtendOrShrinkRangeToApplyTheStyle( // range to contain the <br> element. EditorDOMRange range(aRange); if (range.EndRef().IsInContentNode()) { - WSScanResult nextContentData = - WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( + const WSScanResult nextContentData = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( &aEditingHost, range.EndRef(), BlockInlineCheck::UseComputedDisplayOutsideStyle); if (nextContentData.ReachedInvisibleBRElement() && @@ -2076,7 +2076,7 @@ HTMLEditor::SplitAncestorStyledInlineElementsAtRangeEdges( return result; } tracker.FlushAndStopTracking(); - if (NS_WARN_IF(result.inspect().Handled())) { + if (result.inspect().Handled()) { auto endOfRange = result.inspect().AtSplitPoint<EditorDOMPoint>(); if (!endOfRange.IsSet()) { result.inspect().IgnoreCaretPointSuggestion(); diff --git a/editor/libeditor/WSRunObject.cpp b/editor/libeditor/WSRunObject.cpp index 7149578be1..8acd1f60e7 100644 --- a/editor/libeditor/WSRunObject.cpp +++ b/editor/libeditor/WSRunObject.cpp @@ -49,9 +49,11 @@ template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom( const EditorDOMPoint& aPoint) const; template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom( const EditorRawDOMPoint& aPoint) const; -template WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom( +template WSScanResult +WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( const EditorDOMPoint& aPoint) const; -template WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom( +template WSScanResult +WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( const EditorRawDOMPoint& aPoint) const; template EditorDOMPoint WSRunScanner::GetAfterLastVisiblePoint( Text& aTextNode, const Element* aAncestorLimiter); @@ -1292,7 +1294,9 @@ Result<InsertTextResult, nsresult> WhiteSpaceVisibilityKeeper::ReplaceText( // 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() && + else if ((textFragmentDataAtStart.StartsFromHardLineBreak() || + textFragmentDataAtStart + .StartsFromInlineEditingHostBoundary()) && isInsertionPointEqualsOrIsBeforeStartOfText) { theString.SetCharAt(HTMLEditUtils::kNBSP, 0); } @@ -1325,7 +1329,8 @@ Result<InsertTextResult, nsresult> WhiteSpaceVisibilityKeeper::ReplaceText( // 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() && + else if ((textFragmentDataAtEnd.EndsByBlockBoundary() || + textFragmentDataAtEnd.EndsByInlineEditingHostBoundary()) && isInsertionPointEqualsOrAfterEndOfText) { theString.SetCharAt(HTMLEditUtils::kNBSP, lastCharIndex); } @@ -1721,9 +1726,24 @@ template <typename PT, typename CT> WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom( const EditorDOMPointBase<PT, CT>& 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(WSScanResult::ScanDirection::Backward, + *aPoint.template ContainerAs<nsIContent>(), + WSType::InUncomposedDoc, mBlockInlineCheck); + } if (!TextFragmentDataAtStartRef().IsInitialized()) { - return WSScanResult(nullptr, WSType::UnexpectedError, mBlockInlineCheck); + return WSScanResult::Error(); } // If the range has visible text and start of the visible text is before @@ -1736,7 +1756,8 @@ WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom( // things now. Whether keep scanning editable things or not should be // considered by the caller. if (aPoint.GetChild() && !aPoint.GetChild()->IsEditable()) { - return WSScanResult(aPoint.GetChild(), WSType::SpecialContent, + return WSScanResult(WSScanResult::ScanDirection::Backward, + *aPoint.GetChild(), WSType::SpecialContent, mBlockInlineCheck); } const auto atPreviousChar = @@ -1744,35 +1765,82 @@ WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom( // When it's a non-empty text node, return it. if (atPreviousChar.IsSet() && !atPreviousChar.IsContainerEmpty()) { MOZ_ASSERT(!atPreviousChar.IsEndOfContainer()); - return WSScanResult(atPreviousChar.template NextPoint<EditorDOMPoint>(), + return WSScanResult(WSScanResult::ScanDirection::Backward, + atPreviousChar.template NextPoint<EditorDOMPoint>(), atPreviousChar.IsCharCollapsibleASCIISpaceOrNBSP() ? WSType::CollapsibleWhiteSpaces + : atPreviousChar.IsCharPreformattedNewLine() + ? WSType::PreformattedLineBreak : WSType::NonCollapsibleCharacters, mBlockInlineCheck); } } + 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(WSScanResult::ScanDirection::Backward, + TextFragmentDataAtStartRef().StartRef(), + TextFragmentDataAtStartRef().StartRawReason(), + mBlockInlineCheck); + 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(TextFragmentDataAtStartRef().GetStartReasonContent(), + return WSScanResult(WSScanResult::ScanDirection::Backward, + *TextFragmentDataAtStartRef().GetStartReasonContent(), TextFragmentDataAtStartRef().StartRawReason(), mBlockInlineCheck); } - return WSScanResult(TextFragmentDataAtStartRef().StartRef(), + if (NS_WARN_IF(!TextFragmentDataAtStartRef().StartRef().IsSet())) { + return WSScanResult::Error(); + } + return WSScanResult(WSScanResult::ScanDirection::Backward, + TextFragmentDataAtStartRef().StartRef(), TextFragmentDataAtStartRef().StartRawReason(), mBlockInlineCheck); } template <typename PT, typename CT> -WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom( +WSScanResult WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( const EditorDOMPointBase<PT, CT>& 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(WSScanResult::ScanDirection::Forward, + *aPoint.template ContainerAs<nsIContent>(), + WSType::InUncomposedDoc, mBlockInlineCheck); + } if (!TextFragmentDataAtStartRef().IsInitialized()) { - return WSScanResult(nullptr, WSType::UnexpectedError, mBlockInlineCheck); + return WSScanResult::Error(); } // If the range has visible text and aPoint equals or is before the end of the @@ -1785,32 +1853,66 @@ WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom( // things now. Whether keep scanning editable things or not should be // considered by the caller. if (aPoint.GetChild() && !aPoint.GetChild()->IsEditable()) { - return WSScanResult(aPoint.GetChild(), WSType::SpecialContent, + return WSScanResult(WSScanResult::ScanDirection::Forward, + *aPoint.GetChild(), WSType::SpecialContent, mBlockInlineCheck); } const auto atNextChar = GetInclusiveNextEditableCharPoint<EditorDOMPoint>(aPoint); // When it's a non-empty text node, return it. if (atNextChar.IsSet() && !atNextChar.IsContainerEmpty()) { - return WSScanResult(atNextChar, + return WSScanResult(WSScanResult::ScanDirection::Forward, atNextChar, !atNextChar.IsEndOfContainer() && atNextChar.IsCharCollapsibleASCIISpaceOrNBSP() ? WSType::CollapsibleWhiteSpaces + : !atNextChar.IsEndOfContainer() && + atNextChar.IsCharPreformattedNewLine() + ? WSType::PreformattedLineBreak : WSType::NonCollapsibleCharacters, mBlockInlineCheck); } } + 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(WSScanResult::ScanDirection::Forward, + TextFragmentDataAtStartRef().EndRef(), + TextFragmentDataAtStartRef().EndRawReason(), + mBlockInlineCheck); + 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(TextFragmentDataAtStartRef().GetEndReasonContent(), + return WSScanResult(WSScanResult::ScanDirection::Forward, + *TextFragmentDataAtStartRef().GetEndReasonContent(), TextFragmentDataAtStartRef().EndRawReason(), mBlockInlineCheck); } - return WSScanResult(TextFragmentDataAtStartRef().EndRef(), + if (NS_WARN_IF(!TextFragmentDataAtStartRef().EndRef().IsSet())) { + return WSScanResult::Error(); + } + return WSScanResult(WSScanResult::ScanDirection::Forward, + TextFragmentDataAtStartRef().EndRef(), TextFragmentDataAtStartRef().EndRawReason(), mBlockInlineCheck); } @@ -1944,6 +2046,7 @@ WSRunScanner::TextFragmentData::BoundaryData WSRunScanner::TextFragmentData:: const Element* aEditingHost, NoBreakingSpaceData* aNBSPData, BlockInlineCheck aBlockInlineCheck) { MOZ_ASSERT(aPoint.IsSetAndValid()); + MOZ_ASSERT(aEditableBlockParentOrTopmostEditableInlineElement.IsEditable()); if (aPoint.IsInTextNode() && !aPoint.IsStartOfContainer()) { Maybe<BoundaryData> startInTextNode = @@ -1967,14 +2070,16 @@ WSRunScanner::TextFragmentData::BoundaryData WSRunScanner::TextFragmentData:: {LeafNodeType::LeafNodeOrNonEditableNode}, aBlockInlineCheck, aEditingHost); if (!previousLeafContentOrBlock) { - // no prior node means we exhausted - // aEditableBlockParentOrTopmostEditableInlineElement - // mReasonContent can be either a block element or any non-editable - // content in this case. + // No previous content means that we reached + // aEditableBlockParentOrTopmostEditableInlineElement boundary. return BoundaryData(aPoint, const_cast<Element&>( aEditableBlockParentOrTopmostEditableInlineElement), - WSType::CurrentBlockBoundary); + HTMLEditUtils::IsBlockElement( + aEditableBlockParentOrTopmostEditableInlineElement, + aBlockInlineCheck) + ? WSType::CurrentBlockBoundary + : WSType::InlineEditingHostBoundary); } if (HTMLEditUtils::IsBlockElement(*previousLeafContentOrBlock, @@ -2088,6 +2193,7 @@ WSRunScanner::TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceEndFrom( const Element* aEditingHost, NoBreakingSpaceData* aNBSPData, BlockInlineCheck aBlockInlineCheck) { MOZ_ASSERT(aPoint.IsSetAndValid()); + MOZ_ASSERT(aEditableBlockParentOrTopmostEditableInlineElement.IsEditable()); if (aPoint.IsInTextNode() && !aPoint.IsEndOfContainer()) { Maybe<BoundaryData> endInTextNode = @@ -2111,14 +2217,16 @@ WSRunScanner::TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceEndFrom( {LeafNodeType::LeafNodeOrNonEditableNode}, aBlockInlineCheck, aEditingHost); if (!nextLeafContentOrBlock) { - // no next node means we exhausted - // aEditableBlockParentOrTopmostEditableInlineElement - // mReasonContent can be either a block element or any non-editable - // content in this case. + // No next content means that we reached + // aEditableBlockParentOrTopmostEditableInlineElement boundary. return BoundaryData(aPoint.template To<EditorDOMPoint>(), const_cast<Element&>( aEditableBlockParentOrTopmostEditableInlineElement), - WSType::CurrentBlockBoundary); + HTMLEditUtils::IsBlockElement( + aEditableBlockParentOrTopmostEditableInlineElement, + aBlockInlineCheck) + ? WSType::CurrentBlockBoundary + : WSType::InlineEditingHostBoundary); } if (HTMLEditUtils::IsBlockElement(*nextLeafContentOrBlock, @@ -2172,7 +2280,7 @@ WSRunScanner::TextFragmentData::InvisibleLeadingWhiteSpaceRangeRef() const { } // If it's start of line, there is no invisible leading white-spaces. - if (!StartsFromHardLineBreak()) { + if (!StartsFromHardLineBreak() && !StartsFromInlineEditingHostBoundary()) { mLeadingWhiteSpaceRange.emplace(); return mLeadingWhiteSpaceRange.ref(); } @@ -2202,7 +2310,8 @@ WSRunScanner::TextFragmentData::InvisibleTrailingWhiteSpaceRangeRef() const { // If it's not immediately before a block boundary nor an invisible // preformatted linefeed, there is no invisible trailing white-spaces. Note // that collapsible white-spaces before a `<br>` element is visible. - if (!EndsByBlockBoundary() && !EndsByInvisiblePreformattedLineBreak()) { + if (!EndsByBlockBoundary() && !EndsByInlineEditingHostBoundary() && + !EndsByInvisiblePreformattedLineBreak()) { mTrailingWhiteSpaceRange.emplace(); return mTrailingWhiteSpaceRange.ref(); } @@ -2332,7 +2441,7 @@ WSRunScanner::TextFragmentData::VisibleWhiteSpacesDataRef() const { return mVisibleWhiteSpacesData.ref(); } - if (!StartsFromHardLineBreak()) { + if (!StartsFromHardLineBreak() && !StartsFromInlineEditingHostBoundary()) { VisibleWhiteSpacesData visibleWhiteSpaces; if (mStart.PointRef().IsSet()) { visibleWhiteSpaces.SetStartPoint(mStart.PointRef()); @@ -2352,7 +2461,8 @@ WSRunScanner::TextFragmentData::VisibleWhiteSpacesDataRef() const { return mVisibleWhiteSpacesData.ref(); } - MOZ_ASSERT(StartsFromHardLineBreak()); + MOZ_ASSERT(StartsFromHardLineBreak() || + StartsFromInlineEditingHostBoundary()); MOZ_ASSERT(maybeHaveLeadingWhiteSpaces); VisibleWhiteSpacesData visibleWhiteSpaces; @@ -2360,7 +2470,7 @@ WSRunScanner::TextFragmentData::VisibleWhiteSpacesDataRef() const { visibleWhiteSpaces.SetStartPoint(leadingWhiteSpaceRange.EndRef()); } visibleWhiteSpaces.SetStartFromLeadingWhiteSpaces(); - if (!EndsByBlockBoundary()) { + if (!EndsByBlockBoundary() && !EndsByInlineEditingHostBoundary()) { // then no trailing ws. this normal run ends the overall ws run. if (mEnd.PointRef().IsSet()) { visibleWhiteSpaces.SetEndPoint(mEnd.PointRef()); @@ -2370,7 +2480,7 @@ WSRunScanner::TextFragmentData::VisibleWhiteSpacesDataRef() const { return mVisibleWhiteSpacesData.ref(); } - MOZ_ASSERT(EndsByBlockBoundary()); + MOZ_ASSERT(EndsByBlockBoundary() || EndsByInlineEditingHostBoundary()); if (!maybeHaveTrailingWhiteSpaces) { // normal ws runs right up to adjacent block (nbsp next to block) @@ -3401,7 +3511,8 @@ nsresult WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt( isPreviousCharCollapsibleASCIIWhiteSpace) { // First, try to insert <br> element if NBSP is at end of a block. // XXX We should stop this if there is a visible content. - if (visibleWhiteSpaces.EndsByBlockBoundary() && + if ((visibleWhiteSpaces.EndsByBlockBoundary() || + visibleWhiteSpaces.EndsByInlineEditingHostBoundary()) && aPoint.IsInContentNode()) { bool insertBRElement = HTMLEditUtils::IsBlockElement( *aPoint.template ContainerAs<nsIContent>(), @@ -4320,7 +4431,8 @@ WSRunScanner::GetRangeContainingInvisibleWhiteSpacesAtRangeBoundaries( // 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.StartsFromBlockBoundary() || + textFragmentDataAtStart.StartsFromInlineEditingHostBoundary()) && textFragmentDataAtStart.EndRef().IsInTextNode()) { result.SetStart(textFragmentDataAtStart.EndRef()); } @@ -4353,7 +4465,8 @@ WSRunScanner::GetRangeContainingInvisibleWhiteSpacesAtRangeBoundaries( // 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.EndsByBlockBoundary() || + textFragmentDataAtEnd.EndsByInlineEditingHostBoundary()) && textFragmentDataAtEnd.StartRef().IsInTextNode()) { result.SetEnd(EditorDOMPoint::AtEndOf( *textFragmentDataAtEnd.StartRef().ContainerAs<Text>())); diff --git a/editor/libeditor/WSRunObject.h b/editor/libeditor/WSRunObject.h index 9328b24eb2..bbc6a078d9 100644 --- a/editor/libeditor/WSRunObject.h +++ b/editor/libeditor/WSRunObject.h @@ -41,6 +41,8 @@ class MOZ_STACK_CLASS WSScanResult final { NotInitialized, // Could be the DOM tree is broken as like crash tests. UnexpectedError, + // The scanner cannot work in uncomposed tree, but tried to scan in it. + InUncomposedDoc, // The run is maybe collapsible white-spaces at start of a hard line. LeadingWhiteSpaces, // The run is maybe collapsible white-spaces at end of a hard line. @@ -59,6 +61,8 @@ class MOZ_STACK_CLASS WSScanResult final { OtherBlockBoundary, // Current block's boundary. CurrentBlockBoundary, + // Inline editing host boundary. + InlineEditingHostBoundary, }; friend std::ostream& operator<<(std::ostream& aStream, const WSType& aType) { @@ -67,6 +71,8 @@ class MOZ_STACK_CLASS WSScanResult final { return aStream << "WSType::NotInitialized"; case WSType::UnexpectedError: return aStream << "WSType::UnexpectedError"; + case WSType::InUncomposedDoc: + return aStream << "WSType::InUncomposedDoc"; case WSType::LeadingWhiteSpaces: return aStream << "WSType::LeadingWhiteSpaces"; case WSType::TrailingWhiteSpaces: @@ -85,90 +91,111 @@ class MOZ_STACK_CLASS WSScanResult final { return aStream << "WSType::OtherBlockBoundary"; case WSType::CurrentBlockBoundary: return aStream << "WSType::CurrentBlockBoundary"; + case WSType::InlineEditingHostBoundary: + return aStream << "WSType::InlineEditingHostBoundary"; } return aStream << "<Illegal value>"; } friend class WSRunScanner; // Because of WSType. + explicit WSScanResult(WSType aReason) : mReason(aReason) { + MOZ_ASSERT(mReason == WSType::UnexpectedError || + mReason == WSType::NotInitialized); + } + public: WSScanResult() = delete; - MOZ_NEVER_INLINE_DEBUG WSScanResult(nsIContent* aContent, WSType aReason, + enum class ScanDirection : bool { Backward, Forward }; + MOZ_NEVER_INLINE_DEBUG WSScanResult(ScanDirection aScanDirection, + nsIContent& aContent, WSType aReason, BlockInlineCheck aBlockInlineCheck) - : mContent(aContent), mReason(aReason) { + : mContent(&aContent), mReason(aReason), mDirection(aScanDirection) { + MOZ_ASSERT(aReason != WSType::CollapsibleWhiteSpaces && + aReason != WSType::NonCollapsibleCharacters && + aReason != WSType::PreformattedLineBreak); AssertIfInvalidData(aBlockInlineCheck); } - MOZ_NEVER_INLINE_DEBUG WSScanResult(const EditorDOMPoint& aPoint, + MOZ_NEVER_INLINE_DEBUG WSScanResult(ScanDirection aScanDirection, + const EditorDOMPoint& aPoint, WSType aReason, BlockInlineCheck aBlockInlineCheck) : mContent(aPoint.GetContainerAs<nsIContent>()), mOffset(Some(aPoint.Offset())), - mReason(aReason) { + mReason(aReason), + mDirection(aScanDirection) { AssertIfInvalidData(aBlockInlineCheck); } + static WSScanResult Error() { return WSScanResult(WSType::UnexpectedError); } + MOZ_NEVER_INLINE_DEBUG void AssertIfInvalidData( BlockInlineCheck aBlockInlineCheck) 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::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, - mContent && mContent->IsText()); + 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 && mContent->IsHTMLElement(nsGkAtoms::br)); + mContent->IsHTMLElement(nsGkAtoms::br)); MOZ_ASSERT_IF(mReason == WSType::PreformattedLineBreak, - mContent && mContent->IsText() && - EditorUtils::IsNewLinePreformatted(*mContent)); + EditorUtils::IsNewLinePreformatted(*mContent)); MOZ_ASSERT_IF( mReason == WSType::SpecialContent, - mContent && - ((mContent->IsText() && !mContent->IsEditable()) || - (!mContent->IsHTMLElement(nsGkAtoms::br) && - !HTMLEditUtils::IsBlockElement(*mContent, aBlockInlineCheck)))); + (mContent->IsText() && !mContent->IsEditable()) || + (!mContent->IsHTMLElement(nsGkAtoms::br) && + !HTMLEditUtils::IsBlockElement(*mContent, aBlockInlineCheck))); MOZ_ASSERT_IF(mReason == WSType::OtherBlockBoundary, - mContent && HTMLEditUtils::IsBlockElement(*mContent, - aBlockInlineCheck)); - // If mReason is WSType::CurrentBlockBoundary, mContent can be any content. - // In most cases, it's current block element which is editable. However, if - // there is no editable block parent, this is topmost editable inline - // content. Additionally, if there is no editable content, this is the - // container start of scanner and is not editable. - if (mReason == WSType::CurrentBlockBoundary) { - if (!mContent || - // Although not expected that scanning in orphan document fragment, - // it's okay. - !mContent->IsInComposedDoc() || - // This is what the most preferred result is mContent itself is a - // block. - HTMLEditUtils::IsBlockElement(*mContent, aBlockInlineCheck) || - // If mContent is not editable, we cannot check whether there is no - // block ancestor in the limiter which we don't have. Therefore, - // let's skip the ancestor check. - !mContent->IsEditable()) { - return; - } - const DebugOnly<Element*> closestAncestorEditableBlockElement = - HTMLEditUtils::GetAncestorElement( - *mContent, HTMLEditUtils::ClosestEditableBlockElement, - aBlockInlineCheck); - MOZ_ASSERT_IF( - mReason == WSType::CurrentBlockBoundary, - // There is no editable block ancestor, it's fine. - !closestAncestorEditableBlockElement || - // If we found an editable block, but mContent can be inline if - // it's an editing host (root or its parent is not editable). - !closestAncestorEditableBlockElement->GetParentElement() || - !closestAncestorEditableBlockElement->GetParentElement() - ->IsEditable()); - } + HTMLEditUtils::IsBlockElement(*mContent, aBlockInlineCheck)); + MOZ_ASSERT_IF(mReason == WSType::CurrentBlockBoundary, + mContent->IsElement()); + MOZ_ASSERT_IF(mReason == WSType::CurrentBlockBoundary, + mContent->IsEditable()); + MOZ_ASSERT_IF(mReason == WSType::CurrentBlockBoundary, + HTMLEditUtils::IsBlockElement(*mContent, aBlockInlineCheck)); + MOZ_ASSERT_IF(mReason == WSType::InlineEditingHostBoundary, + mContent->IsElement()); + MOZ_ASSERT_IF(mReason == WSType::InlineEditingHostBoundary, + mContent->IsEditable()); + MOZ_ASSERT_IF(mReason == WSType::InlineEditingHostBoundary, + !HTMLEditUtils::IsBlockElement(*mContent, aBlockInlineCheck)); + MOZ_ASSERT_IF(mReason == WSType::InlineEditingHostBoundary, + !mContent->GetParentElement() || + !mContent->GetParentElement()->IsEditable()); #endif // #ifdef DEBUG } @@ -187,6 +214,10 @@ class MOZ_STACK_CLASS WSScanResult final { return mContent && mContent->IsElement(); } + [[nodiscard]] bool ContentIsText() const { + return mContent && mContent->IsText(); + } + /** * The following accessors makes it easier to understand each callers. */ @@ -204,53 +235,65 @@ class MOZ_STACK_CLASS WSScanResult final { } /** - * Returns true if found or reached content is ediable. + * Returns true if found or reached content is editable. */ bool IsContentEditable() const { return mContent && mContent->IsEditable(); } /** - * Offset() returns meaningful value only when - * InVisibleOrCollapsibleCharacters() returns true or the scanner - * reached to start or end of its scanning range and that is same as start or - * end container which are specified when the scanner is initialized. If it's - * result of scanning backward, this offset means before the found point. - * Otherwise, i.e., scanning forward, this offset means after the found point. + * Offset_Deprecated() returns meaningful value only when + * InVisibleOrCollapsibleCharacters() returns true or the scanner reached to + * start or end of its scanning range and that is same as start or end + * container which are specified when the scanner is initialized. If it's + * result of scanning backward, this offset means the point of the found + * point. Otherwise, i.e., scanning forward, this offset means next point + * of the found point. E.g., if it reaches a collapsible white-space, this + * offset is at the first non-collapsible character after it. */ - MOZ_NEVER_INLINE_DEBUG uint32_t Offset() const { + MOZ_NEVER_INLINE_DEBUG uint32_t Offset_Deprecated() const { NS_ASSERTION(mOffset.isSome(), "Retrieved non-meaningful offset"); return mOffset.valueOr(0); } /** - * Point() and RawPoint() return the position in found visible node or - * reached block boundary. So, they return meaningful point only when - * Offset() returns meaningful value. + * Point_Deprecated() returns the position in found visible node or reached + * block boundary. So, this returns meaningful point only when + * Offset_Deprecated() returns meaningful value. */ template <typename EditorDOMPointType> - EditorDOMPointType Point() const { + EditorDOMPointType Point_Deprecated() const { NS_ASSERTION(mOffset.isSome(), "Retrieved non-meaningful point"); return EditorDOMPointType(mContent, mOffset.valueOr(0)); } /** - * PointAtContent() and RawPointAtContent() return the position of found - * visible content or reached block element. + * PointAtReachedContent() returns the position of found visible content or + * reached block element. */ template <typename EditorDOMPointType> - EditorDOMPointType PointAtContent() const { + EditorDOMPointType PointAtReachedContent() const { MOZ_ASSERT(mContent); - return EditorDOMPointType(mContent); + switch (mReason) { + case WSType::CollapsibleWhiteSpaces: + case WSType::NonCollapsibleCharacters: + case WSType::PreformattedLineBreak: + MOZ_DIAGNOSTIC_ASSERT(mOffset.isSome()); + return mDirection == ScanDirection::Forward + ? EditorDOMPointType(mContent, mOffset.valueOr(0)) + : EditorDOMPointType(mContent, + std::max(mOffset.valueOr(1), 1u) - 1); + default: + return EditorDOMPointType(mContent); + } } /** - * PointAfterContent() and RawPointAfterContent() retrun the position after - * found visible content or reached block element. + * PointAfterReachedContent() returns the position after found visible content + * or reached block element. */ template <typename EditorDOMPointType> - EditorDOMPointType PointAfterContent() const { + EditorDOMPointType PointAfterReachedContent() const { MOZ_ASSERT(mContent); - return mContent ? EditorDOMPointType::After(mContent) - : EditorDOMPointType(); + return PointAtReachedContent<EditorDOMPointType>().template NextPoint(); } /** @@ -337,6 +380,13 @@ class MOZ_STACK_CLASS WSScanResult final { } /** + * The scanner reached inline editing host boundary. + */ + [[nodiscard]] bool ReachedInlineEditingHostBoundary() const { + return mReason == WSType::InlineEditingHostBoundary; + } + + /** * The scanner reached something non-text node. */ bool ReachedSomethingNonTextContent() const { @@ -347,6 +397,7 @@ class MOZ_STACK_CLASS WSScanResult final { nsCOMPtr<nsIContent> mContent; Maybe<uint32_t> mOffset; WSType mReason; + ScanDirection mDirection = ScanDirection::Backward; }; class MOZ_STACK_CLASS WSRunScanner final { @@ -363,25 +414,29 @@ class MOZ_STACK_CLASS WSRunScanner final { aBlockInlineCheck), mBlockInlineCheck(aBlockInlineCheck) {} - // ScanNextVisibleNodeOrBlockBoundaryForwardFrom() returns the first visible - // node after aPoint. If there is no visible nodes after aPoint, returns - // topmost editable inline ancestor at end of current block. See comments - // around WSScanResult for the detail. + // ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom() returns the first visible + // node at or after aPoint. If there is no visible nodes after aPoint, + // returns topmost editable inline ancestor at end of current block. See + // comments around WSScanResult for the detail. When you reach a character, + // this returns WSScanResult both whose Point_Deprecated() and + // PointAtReachedContent() return the found character position. template <typename PT, typename CT> - WSScanResult ScanNextVisibleNodeOrBlockBoundaryFrom( + WSScanResult ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( const EditorDOMPointBase<PT, CT>& aPoint) const; template <typename PT, typename CT> - static WSScanResult ScanNextVisibleNodeOrBlockBoundary( + static WSScanResult ScanInclusiveNextVisibleNodeOrBlockBoundary( const Element* aEditingHost, const EditorDOMPointBase<PT, CT>& aPoint, BlockInlineCheck aBlockInlineCheck) { return WSRunScanner(aEditingHost, aPoint, aBlockInlineCheck) - .ScanNextVisibleNodeOrBlockBoundaryFrom(aPoint); + .ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(aPoint); } // ScanPreviousVisibleNodeOrBlockBoundaryFrom() returns the first visible node // before aPoint. If there is no visible nodes before aPoint, returns topmost // editable inline ancestor at start of current block. See comments around - // WSScanResult for the detail. + // WSScanResult for the detail. When you reach a character, this returns + // WSScanResult whose Point_Deprecated() returns next point of the found + // character and PointAtReachedContent() returns the point at found character. template <typename PT, typename CT> WSScanResult ScanPreviousVisibleNodeOrBlockBoundaryFrom( const EditorDOMPointBase<PT, CT>& aPoint) const; @@ -588,6 +643,9 @@ class MOZ_STACK_CLASS WSRunScanner final { bool StartsFromBlockBoundary() const { return TextFragmentDataAtStartRef().StartsFromBlockBoundary(); } + bool StartsFromInlineEditingHostBoundary() const { + return TextFragmentDataAtStartRef().StartsFromInlineEditingHostBoundary(); + } bool StartsFromHardLineBreak() const { return TextFragmentDataAtStartRef().StartsFromHardLineBreak(); } @@ -618,6 +676,9 @@ class MOZ_STACK_CLASS WSRunScanner final { bool EndsByBlockBoundary() const { return TextFragmentDataAtStartRef().EndsByBlockBoundary(); } + bool EndsByInlineEditingHostBoundary() const { + return TextFragmentDataAtStartRef().EndsByInlineEditingHostBoundary(); + } MOZ_NEVER_INLINE_DEBUG Element* StartReasonOtherBlockElementPtr() const { return TextFragmentDataAtStartRef().StartReasonOtherBlockElementPtr(); @@ -688,6 +749,9 @@ class MOZ_STACK_CLASS WSRunScanner final { return mRightWSType == WSType::CurrentBlockBoundary || mRightWSType == WSType::OtherBlockBoundary; } + bool EndsByInlineEditingHostBoundary() const { + return mRightWSType == WSType::InlineEditingHostBoundary; + } /** * ComparePoint() compares aPoint with the white-spaces. @@ -916,6 +980,9 @@ class MOZ_STACK_CLASS WSRunScanner final { return mReason == WSType::CurrentBlockBoundary || mReason == WSType::OtherBlockBoundary; } + bool IsInlineEditingHostBoundary() const { + return mReason == WSType::InlineEditingHostBoundary; + } bool IsHardLineBreak() const { return mReason == WSType::CurrentBlockBoundary || mReason == WSType::OtherBlockBoundary || @@ -950,8 +1017,8 @@ class MOZ_STACK_CLASS WSRunScanner final { EditorDOMPoint mPoint; // Must be one of WSType::NotInitialized, // WSType::NonCollapsibleCharacters, WSType::SpecialContent, - // WSType::BRElement, WSType::CurrentBlockBoundary or - // WSType::OtherBlockBoundary. + // WSType::BRElement, WSType::CurrentBlockBoundary, + // WSType::OtherBlockBoundary or WSType::InlineEditingHostBoundary. WSType mReason = WSType::NotInitialized; }; @@ -1027,6 +1094,9 @@ class MOZ_STACK_CLASS WSRunScanner final { return mStart.IsOtherBlockBoundary(); } bool StartsFromBlockBoundary() const { return mStart.IsBlockBoundary(); } + bool StartsFromInlineEditingHostBoundary() const { + return mStart.IsInlineEditingHostBoundary(); + } bool StartsFromHardLineBreak() const { return mStart.IsHardLineBreak(); } bool EndsByNonCollapsibleCharacters() const { return mEnd.IsNonCollapsibleCharacters(); @@ -1053,6 +1123,9 @@ class MOZ_STACK_CLASS WSRunScanner final { } bool EndsByOtherBlockElement() const { return mEnd.IsOtherBlockBoundary(); } bool EndsByBlockBoundary() const { return mEnd.IsBlockBoundary(); } + bool EndsByInlineEditingHostBoundary() const { + return mEnd.IsInlineEditingHostBoundary(); + } WSType StartRawReason() const { return mStart.RawReason(); } WSType EndRawReason() const { return mEnd.RawReason(); } @@ -1224,7 +1297,7 @@ class MOZ_STACK_CLASS WSRunScanner final { bool FollowingContentMayBecomeFirstVisibleContent( const EditorDOMPointType& aPoint) const { MOZ_ASSERT(aPoint.IsSetAndValid()); - if (!mStart.IsHardLineBreak()) { + if (!mStart.IsHardLineBreak() && !mStart.IsInlineEditingHostBoundary()) { return false; } // If the point is before start of text fragment, that means that the @@ -1260,7 +1333,7 @@ class MOZ_STACK_CLASS WSRunScanner final { MOZ_ASSERT(aPoint.IsSetAndValid()); // If this fragment is ends by block boundary, always the caller needs // additional check. - if (mEnd.IsBlockBoundary()) { + if (mEnd.IsBlockBoundary() || mEnd.IsInlineEditingHostBoundary()) { return true; } |