/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "EditorBase.h" #include "HTMLEditor.h" #include "HTMLEditorInlines.h" #include "HTMLEditorNestedClasses.h" #include #include #include "AutoClonedRangeArray.h" #include "AutoSelectionRestorer.h" #include "CSSEditUtils.h" #include "EditAction.h" #include "EditorDOMPoint.h" #include "EditorLineBreak.h" #include "EditorUtils.h" #include "HTMLEditHelpers.h" #include "HTMLEditUtils.h" #include "PendingStyles.h" // for SpecifiedStyle #include "WhiteSpaceVisibilityKeeper.h" #include "WSRunScanner.h" #include "ErrorList.h" #include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/AutoRestore.h" #include "mozilla/ContentIterator.h" #include "mozilla/EditorForwards.h" #include "mozilla/IntegerRange.h" #include "mozilla/Logging.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/Maybe.h" #include "mozilla/OwningNonNull.h" #include "mozilla/PresShell.h" #include "mozilla/StaticPrefs_editor.h" #include "mozilla/TextComposition.h" #include "mozilla/UniquePtr.h" #include "mozilla/dom/AncestorIterator.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/ElementInlines.h" #include "mozilla/dom/HTMLBRElement.h" #include "mozilla/dom/RangeBinding.h" #include "mozilla/dom/Selection.h" #include "mozilla/dom/StaticRange.h" #include "nsAtom.h" #include "nsCRT.h" #include "nsCRTGlue.h" #include "nsComponentManagerUtils.h" #include "nsContentUtils.h" #include "nsDebug.h" #include "nsError.h" #include "nsFrameSelection.h" #include "nsGkAtoms.h" #include "nsIContent.h" #include "nsIFrame.h" #include "nsINode.h" #include "nsLiteralString.h" #include "nsPrintfCString.h" #include "nsRange.h" #include "nsReadableUtils.h" #include "nsString.h" #include "nsStringFwd.h" #include "nsStyledElement.h" #include "nsTArray.h" #include "nsTextNode.h" #include "nsThreadUtils.h" class nsISupports; namespace mozilla { extern LazyLogModule gTextInputLog; // Defined in EditorBase.cpp using namespace dom; using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; using EmptyCheckOptions = HTMLEditUtils::EmptyCheckOptions; using LeafNodeOption = HTMLEditUtils::LeafNodeOption; using LeafNodeOptions = HTMLEditUtils::LeafNodeOptions; using WalkTextOption = HTMLEditUtils::WalkTextOption; using WalkTreeDirection = HTMLEditUtils::WalkTreeDirection; /******************************************************** * first some helpful functors we will use ********************************************************/ static bool IsPendingStyleCachePreservingSubAction( EditSubAction aEditSubAction) { switch (aEditSubAction) { case EditSubAction::eDeleteSelectedContent: case EditSubAction::eInsertLineBreak: case EditSubAction::eInsertParagraphSeparator: case EditSubAction::eCreateOrChangeList: case EditSubAction::eIndent: case EditSubAction::eOutdent: case EditSubAction::eSetOrClearAlignment: case EditSubAction::eCreateOrRemoveBlock: case EditSubAction::eFormatBlockForHTMLCommand: case EditSubAction::eMergeBlockContents: case EditSubAction::eRemoveList: case EditSubAction::eCreateOrChangeDefinitionListItem: case EditSubAction::eInsertElement: case EditSubAction::eInsertQuotation: case EditSubAction::eInsertQuotedText: return true; default: return false; } } template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorDOMRange& aRange); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorRawDOMRange& aRange); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorRawDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorRawDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint); nsresult HTMLEditor::InitEditorContentAndSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); // We should do nothing with the result of GetRoot() if only a part of the // document is editable. if (!EntireDocumentIsEditable()) { return NS_OK; } nsresult rv = MaybeCreatePaddingBRElementForEmptyEditor(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeCreatePaddingBRElementForEmptyEditor() failed"); return rv; } // If the selection hasn't been set up yet, set it up collapsed to the end of // our editable content. // XXX I think that this shouldn't do it in `HTMLEditor` because it maybe // removed by the web app and if they call `Selection::AddRange()` without // checking the range count, it may cause multiple selection ranges. if (!SelectionRef().RangeCount()) { nsresult rv = CollapseSelectionToEndOfLastLeafNodeOfDocument(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() " "failed"); return rv; } } if (IsPlaintextMailComposer()) { // XXX Should we do this in HTMLEditor? It's odd to guarantee that last // empty line is visible only when it's in the plain text mode. nsresult rv = EnsurePaddingBRElementInMultilineEditor(); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::EnsurePaddingBRElementInMultilineEditor() failed"); return rv; } } Element* bodyOrDocumentElement = GetRoot(); if (NS_WARN_IF(!bodyOrDocumentElement && !GetDocument())) { return NS_ERROR_FAILURE; } if (!bodyOrDocumentElement) { return NS_OK; } // FIXME: This is odd to update the DOM for making users can put caret in // empty table cells and list items. We should make it possible without // the hacky
. rv = InsertBRElementToEmptyListItemsAndTableCellsInRange( RawRangeBoundary::StartOfParent(*bodyOrDocumentElement), RawRangeBoundary::EndOfParent(*bodyOrDocumentElement)); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange() " "failed, but ignored"); return NS_OK; } void HTMLEditor::OnStartToHandleTopLevelEditSubAction( EditSubAction aTopLevelEditSubAction, nsIEditor::EDirection aDirectionOfTopLevelEditSubAction, ErrorResult& aRv) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!aRv.Failed()); EditorBase::OnStartToHandleTopLevelEditSubAction( aTopLevelEditSubAction, aDirectionOfTopLevelEditSubAction, aRv); MOZ_ASSERT(GetTopLevelEditSubAction() == aTopLevelEditSubAction); MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() == aDirectionOfTopLevelEditSubAction); if (NS_WARN_IF(Destroyed())) { aRv.Throw(NS_ERROR_EDITOR_DESTROYED); return; } if (!mInitSucceeded) { return; // We should do nothing if we're being initialized. } NS_WARNING_ASSERTION( !aRv.Failed(), "EditorBase::OnStartToHandleTopLevelEditSubAction() failed"); // Let's work with the latest layout information after (maybe) dispatching // `beforeinput` event. RefPtr document = GetDocument(); if (NS_WARN_IF(!document)) { aRv.Throw(NS_ERROR_UNEXPECTED); return; } document->FlushPendingNotifications(FlushType::Frames); if (NS_WARN_IF(Destroyed())) { aRv.Throw(NS_ERROR_EDITOR_DESTROYED); return; } // Remember where our selection was before edit action took place: const auto atCompositionStart = GetFirstIMESelectionStartPoint(); if (atCompositionStart.IsSet()) { // If there is composition string, let's remember current composition // range. TopLevelEditSubActionDataRef().mSelectedRange->StoreRange( atCompositionStart, GetLastIMESelectionEndPoint()); } else { // Get the selection location // XXX This may occur so that I think that we shouldn't throw exception // in this case. if (NS_WARN_IF(!SelectionRef().RangeCount())) { aRv.Throw(NS_ERROR_UNEXPECTED); return; } if (const nsRange* range = SelectionRef().GetRangeAt(0)) { TopLevelEditSubActionDataRef().mSelectedRange->StoreRange(*range); } } // Register with range updater to track this as we perturb the doc RangeUpdaterRef().RegisterRangeItem( *TopLevelEditSubActionDataRef().mSelectedRange); // Remember current inline styles for deletion and normal insertion ops const bool cacheInlineStyles = [&]() { switch (aTopLevelEditSubAction) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eDeleteSelectedContent: return true; default: return IsPendingStyleCachePreservingSubAction(aTopLevelEditSubAction); } }(); if (cacheInlineStyles) { const RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (NS_WARN_IF(!editingHost)) { aRv.Throw(NS_ERROR_FAILURE); return; } nsIContent* const startContainer = HTMLEditUtils::GetContentToPreserveInlineStyles( TopLevelEditSubActionDataRef() .mSelectedRange->StartPoint(), *editingHost); if (NS_WARN_IF(!startContainer)) { aRv.Throw(NS_ERROR_FAILURE); return; } if (const RefPtr startContainerElement = startContainer->GetAsElementOrParentElement()) { nsresult rv = CacheInlineStyles(*startContainerElement); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::CacheInlineStyles() failed"); aRv.Throw(rv); return; } } } // Stabilize the document against contenteditable count changes if (document->GetEditingState() == Document::EditingState::eContentEditable) { document->ChangeContentEditableCount(nullptr, +1); TopLevelEditSubActionDataRef().mRestoreContentEditableCount = true; } // Check that selection is in subtree defined by body node nsresult rv = EnsureSelectionInBodyOrDocumentElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { aRv.Throw(NS_ERROR_EDITOR_DESTROYED); return; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureSelectionInBodyOrDocumentElement() " "failed, but ignored"); } nsresult HTMLEditor::OnEndHandlingTopLevelEditSubAction() { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); nsresult rv; while (true) { if (NS_WARN_IF(Destroyed())) { rv = NS_ERROR_EDITOR_DESTROYED; break; } if (!mInitSucceeded) { rv = NS_OK; // We should do nothing if we're being initialized. break; } // Do all the tricky stuff rv = OnEndHandlingTopLevelEditSubActionInternal(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() failed"); // Perhaps, we need to do the following jobs even if the editor has been // destroyed since they adjust some states of HTML document but don't // modify the DOM tree nor Selection. // Free up selectionState range item if (TopLevelEditSubActionDataRef().mSelectedRange) { RangeUpdaterRef().DropRangeItem( *TopLevelEditSubActionDataRef().mSelectedRange); } // Reset the contenteditable count to its previous value if (TopLevelEditSubActionDataRef().mRestoreContentEditableCount) { Document* document = GetDocument(); if (NS_WARN_IF(!document)) { rv = NS_ERROR_FAILURE; break; } if (document->GetEditingState() == Document::EditingState::eContentEditable) { document->ChangeContentEditableCount(nullptr, -1); } } break; } DebugOnly rvIgnored = EditorBase::OnEndHandlingTopLevelEditSubAction(); NS_WARNING_ASSERTION( NS_FAILED(rv) || NS_SUCCEEDED(rvIgnored), "EditorBase::OnEndHandlingTopLevelEditSubAction() failed, but ignored"); MOZ_ASSERT(!GetTopLevelEditSubAction()); MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() == eNone); return rv; } nsresult HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); // If we just maintained the DOM tree for consistent behavior even after // web apps modified the DOM, we should not touch the DOM in this // post-processor. if (GetTopLevelEditSubAction() == EditSubAction::eMaintainWhiteSpaceVisibility) { return NS_OK; } nsresult rv = EnsureSelectionInBodyOrDocumentElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureSelectionInBodyOrDocumentElement() " "failed, but ignored"); if (GetTopLevelEditSubAction() == EditSubAction::eCreatePaddingBRElementForEmptyEditor) { return NS_OK; } if (TopLevelEditSubActionDataRef().mChangedRange->IsPositioned() && GetTopLevelEditSubAction() != EditSubAction::eUndo && GetTopLevelEditSubAction() != EditSubAction::eRedo) { // don't let any txns in here move the selection around behind our back. // Note that this won't prevent explicit selection setting from working. AutoTransactionsConserveSelection dontChangeMySelection(*this); { EditorDOMRange changedRange( *TopLevelEditSubActionDataRef().mChangedRange); if (changedRange.IsPositioned() && changedRange.EnsureNotInNativeAnonymousSubtree()) { bool isBlockLevelSubAction = false; switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eInsertLineBreak: case EditSubAction::eInsertParagraphSeparator: case EditSubAction::eDeleteText: { // XXX We should investigate whether this is really needed because // it seems that the following code does not handle the // white-spaces. RefPtr extendedChangedRange = CreateRangeIncludingAdjuscentWhiteSpaces(changedRange); if (extendedChangedRange) { MOZ_ASSERT(extendedChangedRange->IsPositioned()); // Use extended range temporarily. TopLevelEditSubActionDataRef().mChangedRange = std::move(extendedChangedRange); } break; } case EditSubAction::eCreateOrChangeList: case EditSubAction::eCreateOrChangeDefinitionListItem: case EditSubAction::eRemoveList: case EditSubAction::eFormatBlockForHTMLCommand: case EditSubAction::eCreateOrRemoveBlock: case EditSubAction::eIndent: case EditSubAction::eOutdent: case EditSubAction::eSetOrClearAlignment: case EditSubAction::eSetPositionToAbsolute: case EditSubAction::eSetPositionToStatic: case EditSubAction::eDecreaseZIndex: case EditSubAction::eIncreaseZIndex: isBlockLevelSubAction = true; [[fallthrough]]; default: { Element* editingHost = ComputeEditingHost(); if (MOZ_UNLIKELY(!editingHost)) { break; } RefPtr extendedChangedRange = AutoClonedRangeArray:: CreateRangeWrappingStartAndEndLinesContainingBoundaries( changedRange, GetTopLevelEditSubAction(), isBlockLevelSubAction ? BlockInlineCheck::UseHTMLDefaultStyle : BlockInlineCheck::UseComputedDisplayOutsideStyle, *editingHost); if (!extendedChangedRange) { break; } MOZ_ASSERT(extendedChangedRange->IsPositioned()); // Use extended range temporarily. TopLevelEditSubActionDataRef().mChangedRange = std::move(extendedChangedRange); break; } } } } // if we did a ranged deletion or handling backspace key, make sure we have // a place to put caret. // Note we only want to do this if the overall operation was deletion, // not if deletion was done along the way for // EditSubAction::eInsertHTMLSource, EditSubAction::eInsertText, etc. // That's why this is here rather than DeleteSelectionAsSubAction(). // However, we shouldn't insert
elements if we've already removed // empty block parents because users may want to disappear the line by // the deletion. // XXX We should make HandleDeleteSelection() store expected container // for handling this here since we cannot trust current selection is // collapsed at deleted point. if (GetTopLevelEditSubAction() == EditSubAction::eDeleteSelectedContent && TopLevelEditSubActionDataRef().mDidDeleteNonCollapsedRange && !TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks) { const auto newCaretPosition = GetFirstSelectionStartPoint(); if (!newCaretPosition.IsSet()) { NS_WARNING("There was no selection range"); return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; } Element* const editingHost = ComputeEditingHost(LimitInBodyElement::No); if (!editingHost) [[unlikely]] { return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; } Result insertPaddingBRElementResultOrError = InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded( newCaretPosition, *editingHost); if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) { NS_WARNING( "HTMLEditor::" "InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded() failed"); return insertPaddingBRElementResultOrError.unwrapErr(); } nsresult rv = insertPaddingBRElementResultOrError.unwrap().SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion}); if (NS_FAILED(rv)) { NS_WARNING("CaretPoint::SuggestCaretPointTo() failed"); return rv; } NS_WARNING_ASSERTION( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CaretPoint::SuggestCaretPointTo() failed, but ignored"); } // add in any needed
s, and remove any unneeded ones. nsresult rv = InsertBRElementToEmptyListItemsAndTableCellsInRange( TopLevelEditSubActionDataRef().mChangedRange->StartRef().AsRaw(), TopLevelEditSubActionDataRef().mChangedRange->EndRef().AsRaw()); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange()" " failed, but ignored"); // merge any adjacent text nodes switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: break; default: { nsresult rv = CollapseAdjacentTextNodes( MOZ_KnownLive(*TopLevelEditSubActionDataRef().mChangedRange)); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::CollapseAdjacentTextNodes() failed"); return rv; } break; } } // Clean up any empty nodes in the changed range unless they are inserted // intentionally. if (TopLevelEditSubActionDataRef().mNeedsToCleanUpEmptyElements) { nsresult rv = RemoveEmptyNodesIn( EditorDOMRange(*TopLevelEditSubActionDataRef().mChangedRange)); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::RemoveEmptyNodesIn() failed"); return rv; } } // Adjust selection for insert text, html paste, and delete actions if // we haven't removed new empty blocks. Note that if empty block parents // are removed, Selection should've been adjusted by the method which // did it. if (!TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks && SelectionRef().IsCollapsed()) { switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eInsertLineBreak: case EditSubAction::eInsertParagraphSeparator: case EditSubAction::ePasteHTMLContent: case EditSubAction::eInsertHTMLSource: // XXX AdjustCaretPositionAndEnsurePaddingBRElement() intentionally // does not create padding `
` element for empty editor. // Investigate which is better that whether this should does it // or wait MaybeCreatePaddingBRElementForEmptyEditor(). rv = AdjustCaretPositionAndEnsurePaddingBRElement( GetDirectionOfTopLevelEditSubAction()); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::AdjustCaretPositionAndEnsurePaddingBRElement() " "failed"); return rv; } break; default: break; } } // check for any styles which were removed inappropriately bool reapplyCachedStyle; switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eDeleteSelectedContent: reapplyCachedStyle = true; break; default: reapplyCachedStyle = IsPendingStyleCachePreservingSubAction(GetTopLevelEditSubAction()); break; } // If the selection is in empty inline HTML elements, we should delete // them unless it's inserted intentionally. if (mPlaceholderBatch && TopLevelEditSubActionDataRef().mNeedsToCleanUpEmptyElements && SelectionRef().IsCollapsed() && SelectionRef().GetFocusNode()) { RefPtr mostDistantEmptyInlineAncestor = nullptr; for (Element* ancestor : SelectionRef().GetFocusNode()->InclusiveAncestorsOfType()) { if (!ancestor->IsHTMLElement() || !HTMLEditUtils::IsRemovableFromParentNode(*ancestor) || !HTMLEditUtils::IsEmptyInlineContainer( *ancestor, {EmptyCheckOption::TreatSingleBRElementAsVisible}, BlockInlineCheck::UseComputedDisplayStyle)) { break; } mostDistantEmptyInlineAncestor = ancestor; } if (mostDistantEmptyInlineAncestor) { nsresult rv = DeleteNodeWithTransaction(*mostDistantEmptyInlineAncestor); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::DeleteNodeWithTransaction() failed at deleting " "empty inline ancestors"); return rv; } } } // But the cached inline styles should be restored from type-in-state later. if (reapplyCachedStyle) { DebugOnly rvIgnored = mPendingStylesToApplyToNewContent->UpdateSelState(*this); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "PendingStyles::UpdateSelState() failed, but ignored"); rvIgnored = ReapplyCachedStyles(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "HTMLEditor::ReapplyCachedStyles() failed, but ignored"); TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear(); } } rv = HandleInlineSpellCheck( TopLevelEditSubActionDataRef().mSelectedRange->StartPoint(), TopLevelEditSubActionDataRef().mChangedRange); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::HandleInlineSpellCheck() failed"); return rv; } // detect empty doc // XXX Need to investigate when the padding
element is removed because // I don't see the
element with testing manually. If it won't be // used, we can get rid of this cost. rv = MaybeCreatePaddingBRElementForEmptyEditor(); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() failed"); return rv; } // adjust selection HINT if needed if (!TopLevelEditSubActionDataRef().mDidExplicitlySetInterLine && SelectionRef().IsCollapsed()) { SetSelectionInterlinePosition(); } return NS_OK; } Result HTMLEditor::CanHandleHTMLEditSubAction( CheckSelectionInReplacedElement aCheckSelectionInReplacedElement /* = ::Yes */) const { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } // If there is not selection ranges, we should ignore the result. if (!SelectionRef().RangeCount()) { return EditActionResult::CanceledResult(); } const nsRange* range = SelectionRef().GetRangeAt(0); nsINode* selStartNode = range->GetStartContainer(); if (NS_WARN_IF(!selStartNode) || NS_WARN_IF(!selStartNode->IsContent())) { return Err(NS_ERROR_FAILURE); } if (!HTMLEditUtils::IsSimplyEditableNode(*selStartNode)) { return EditActionResult::CanceledResult(); } nsINode* selEndNode = range->GetEndContainer(); if (NS_WARN_IF(!selEndNode) || NS_WARN_IF(!selEndNode->IsContent())) { return Err(NS_ERROR_FAILURE); } using ReplaceOrVoidElementOption = HTMLEditUtils::ReplaceOrVoidElementOption; if (selStartNode == selEndNode) { if (aCheckSelectionInReplacedElement == CheckSelectionInReplacedElement::Yes && HTMLEditUtils::GetInclusiveAncestorReplacedOrVoidElement( *selStartNode->AsContent(), ReplaceOrVoidElementOption::LookForOnlyNonVoidReplacedElement)) { return EditActionResult::CanceledResult(); } return EditActionResult::IgnoredResult(); } if (aCheckSelectionInReplacedElement != CheckSelectionInReplacedElement::No && (HTMLEditUtils::GetInclusiveAncestorReplacedOrVoidElement( *selStartNode->AsContent(), ReplaceOrVoidElementOption::LookForOnlyNonVoidReplacedElement) || HTMLEditUtils::GetInclusiveAncestorReplacedOrVoidElement( *selEndNode->AsContent(), ReplaceOrVoidElementOption::LookForOnlyNonVoidReplacedElement))) { return EditActionResult::CanceledResult(); } if (!HTMLEditUtils::IsSimplyEditableNode(*selEndNode)) { return EditActionResult::CanceledResult(); } // If anchor node is in an HTML element which has inert attribute, we should // do nothing. // XXX HTMLEditor typically uses first range instead of anchor/focus range. // Therefore, referring first range here is more reasonable than // anchor/focus range of Selection. nsIContent* const selAnchorContent = SelectionRef().GetDirection() == eDirNext ? nsIContent::FromNode(selStartNode) : nsIContent::FromNode(selEndNode); if (selAnchorContent && HTMLEditUtils::ContentIsInert(*selAnchorContent->AsContent())) { return EditActionResult::CanceledResult(); } // XXX What does it mean the common ancestor is editable? I have no idea. // It should be in same (active) editing host, and even if it's editable, // there may be non-editable contents in the range. nsINode* commonAncestor = range->GetClosestCommonInclusiveAncestor(); if (MOZ_UNLIKELY(!commonAncestor)) { NS_WARNING( "AbstractRange::GetClosestCommonInclusiveAncestor() returned nullptr"); return Err(NS_ERROR_FAILURE); } return HTMLEditUtils::IsSimplyEditableNode(*commonAncestor) ? EditActionResult::IgnoredResult() : EditActionResult::CanceledResult(); } MOZ_CAN_RUN_SCRIPT static nsStaticAtom& MarginPropertyAtomForIndent( nsIContent& aContent) { nsAutoString direction; DebugOnly rvIgnored = CSSEditUtils::GetComputedProperty( aContent, *nsGkAtoms::direction, direction); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "CSSEditUtils::GetComputedProperty(nsGkAtoms::direction)" " failed, but ignored"); return direction.EqualsLiteral("rtl") ? *nsGkAtoms::marginRight : *nsGkAtoms::marginLeft; } nsresult HTMLEditor::EnsureCaretNotAfterInvisibleBRElement( const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(SelectionRef().IsCollapsed()); // If we are after a padding `
` element for empty last line in the same // block, then move selection to be before it const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } EditorRawDOMPoint atSelectionStart(firstRange->StartRef()); if (NS_WARN_IF(!atSelectionStart.IsSet())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(atSelectionStart.IsSetAndValid()); if (!atSelectionStart.IsInContentNode()) { return NS_OK; } const WSScanResult prevThing = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( {WSRunScanner::Option::StopAtVisibleEmptyInlineContainers}, atSelectionStart, &aEditingHost); if (!prevThing.ReachedLineBreak()) { return NS_OK; } EditorRawLineBreak unnecessaryLineBreak = prevThing.CreateEditorLineBreak(); if (!unnecessaryLineBreak.IsFollowedByBlockBoundary()) { return NS_OK; } if (!unnecessaryLineBreak.ContentRef().GetParent() || !unnecessaryLineBreak.ContentRef().IsInclusiveDescendantOf(&aEditingHost)) [[unlikely]] { return NS_OK; } if (unnecessaryLineBreak.IsPreformattedLineBreak() && NS_WARN_IF( !HTMLEditUtils::IsSimplyEditableNode( unnecessaryLineBreak.ContentRef()) && !unnecessaryLineBreak.IsPreformattedLineBreakAtStartOfText())) { // If the preceding unnecessary preformatted line break is a part of a // non-editable visible Text, we cannot put caret into it. Then, typing // something will cause a new line because the unnecessary line break // becomes visible. return NS_OK; } EditorRawDOMPoint pointToPutCaret = unnecessaryLineBreak.To(); for (nsIContent* container : pointToPutCaret.GetContainer()->InclusiveAncestorsOfType()) { if (!HTMLEditUtils::IsSimplyEditableNode(*container)) [[unlikely]] { if (NS_WARN_IF(container->GetPreviousSibling())) { // If the non-editable node is not the first child, we need to put caret // too far. Therefore, we should keep current selection. Although typing // something will cause a new line because the unnecessary line break // becomes visible. return NS_OK; } continue; } if (container != pointToPutCaret.GetContainer()) { MOZ_ASSERT(container->GetFirstChild()); MOZ_ASSERT( !HTMLEditUtils::IsSimplyEditableNode(*container->GetFirstChild())); pointToPutCaret = EditorRawDOMPoint(container, 0); } break; } MOZ_ASSERT(pointToPutCaret.IsSet()); MOZ_ASSERT( pointToPutCaret.GetContainer()->IsInclusiveDescendantOf(&aEditingHost)); // If we are here then the selection is right after a padding line break for // empty last line that is in the same block as the selection. We need to // move the selection start to be before the padding line break node. // FIXME: We should return the position instead of updating the Selection. nsresult rv = CollapseSelectionTo(pointToPutCaret); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } nsresult HTMLEditor::MaybeCreatePaddingBRElementForEmptyEditor() { MOZ_ASSERT(IsEditActionDataAvailable()); if (mPaddingBRElementForEmptyEditor) { return NS_OK; } // XXX I think that we should not insert a
element if we're for a web // content. Probably, this is required only by chrome editors such as // the mail composer of Thunderbird and the composer of SeaMonkey. const RefPtr bodyOrDocumentElement = GetRoot(); if (!bodyOrDocumentElement) { return NS_OK; } // Skip adding the padding
element for empty editor if body // is read-only. if (!HTMLEditUtils::IsSimplyEditableNode(*bodyOrDocumentElement)) { return NS_OK; } // Now we've got the body element. Iterate over the body element's children, // looking for editable content. If no editable content is found, insert the // padding
element. EditorType editorType = GetEditorType(); bool isRootEditable = EditorUtils::IsEditableContent(*bodyOrDocumentElement, editorType); for (nsIContent* child = bodyOrDocumentElement->GetFirstChild(); child; child = child->GetNextSibling()) { if (EditorUtils::IsPaddingBRElementForEmptyEditor(*child) || !isRootEditable || EditorUtils::IsEditableContent(*child, editorType) || HTMLEditUtils::IsBlockElement( *child, BlockInlineCheck::UseComputedDisplayStyle)) { return NS_OK; } } IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eCreatePaddingBRElementForEmptyEditor, nsIEditor::eNone, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return ignoredError.StealNSResult(); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); Result insertPaddingBRElementResultOrError = InsertBRElement(WithTransaction::Yes, BRElementType::PaddingForEmptyEditor, EditorDOMPoint(bodyOrDocumentElement, 0u)); if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) { NS_WARNING( "EditorBase::InsertBRElement(WithTransaction::Yes, " "BRElementType::PaddingForEmptyEditor) failed"); return insertPaddingBRElementResultOrError.propagateErr(); } CreateElementResult insertPaddingBRElementResult = insertPaddingBRElementResultOrError.unwrap(); mPaddingBRElementForEmptyEditor = HTMLBRElement::FromNode(insertPaddingBRElementResult.GetNewNode()); nsresult rv = insertPaddingBRElementResult.SuggestCaretPointTo( *this, {SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CaretPoint::SuggestCaretPointTo() failed"); return rv; } NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CaretPoint::SuggestCaretPointTo() failed, but ignored"); return NS_OK; } nsresult HTMLEditor::EnsureNoPaddingBRElementForEmptyEditor() { MOZ_ASSERT(IsEditActionDataAvailable()); if (!mPaddingBRElementForEmptyEditor) { return NS_OK; } // If we're an HTML editor, a mutation event listener may recreate padding //
element for empty editor again during the call of // DeleteNodeWithTransaction(). So, move it first. RefPtr paddingBRElement( std::move(mPaddingBRElementForEmptyEditor)); nsresult rv = DeleteNodeWithTransaction(*paddingBRElement); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::DeleteNodeWithTransaction() failed"); return rv; } nsresult HTMLEditor::ReflectPaddingBRElementForEmptyEditor() { if (NS_WARN_IF(!mRootElement)) { NS_WARNING("Failed to handle padding BR element due to no root element"); return NS_ERROR_FAILURE; } // The idea here is to see if the magic empty node has suddenly reappeared. If // it has, set our state so we remember it. There is a tradeoff between doing // here and at redo, or doing it everywhere else that might care. Since undo // and redo are relatively rare, it makes sense to take the (small) // performance hit here. nsIContent* firstLeafChild = HTMLEditUtils::GetFirstLeafContent(*mRootElement, {}); if (firstLeafChild && EditorUtils::IsPaddingBRElementForEmptyEditor(*firstLeafChild)) { mPaddingBRElementForEmptyEditor = static_cast(firstLeafChild); } else { mPaddingBRElementForEmptyEditor = nullptr; } return NS_OK; } nsresult HTMLEditor::PrepareInlineStylesForCaret() { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(SelectionRef().IsCollapsed()); // XXX This method works with the top level edit sub-action, but this // must be wrong if we are handling nested edit action. if (TopLevelEditSubActionDataRef().mDidDeleteSelection) { switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eDeleteSelectedContent: { nsresult rv = ReapplyCachedStyles(); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::ReapplyCachedStyles() failed"); return rv; } break; } default: break; } } // For most actions we want to clear the cached styles, but there are // exceptions if (!IsPendingStyleCachePreservingSubAction(GetTopLevelEditSubAction())) { TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear(); } return NS_OK; } Result HTMLEditor::HandleInsertText( const nsAString& aInsertionString, InsertTextFor aPurpose) { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_LOG( gTextInputLog, LogLevel::Info, ("%p HTMLEditor::HandleInsertText(aInsertionString=\"%s\", aPurpose=%s)", this, NS_ConvertUTF16toUTF8(aInsertionString).get(), ToString(aPurpose).c_str())); { Result result = CanHandleHTMLEditSubAction(CheckSelectionInReplacedElement::No); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (result.inspect().Canceled()) { return result; } } UndefineCaretBidiLevel(); // If the selection isn't collapsed, delete it. Don't delete existing inline // tags, because we're hopefully going to insert text (bug 787432). if (!SelectionRef().IsCollapsed() && !InsertingTextForExtantComposition(aPurpose)) { nsresult rv = DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::DeleteSelectionAsSubAction(nsIEditor::eNone, " "nsIEditor::eNoStrip) failed"); return Err(rv); } } const RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (NS_WARN_IF(!editingHost)) { return Err(NS_ERROR_FAILURE); } nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) { nsresult rv = EnsureCaretNotAfterInvisibleBRElement(*editingHost); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() " "failed, but ignored"); if (NS_SUCCEEDED(rv)) { nsresult rv = PrepareInlineStylesForCaret(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } EditorDOMPoint pointToInsert = [&]() { if (InsertingTextForExtantComposition(aPurpose)) { auto compositionStartPoint = GetFirstIMESelectionStartPoint(); if (MOZ_LIKELY(compositionStartPoint.IsSet())) { return compositionStartPoint; } } return GetFirstSelectionStartPoint(); }(); MOZ_LOG(gTextInputLog, LogLevel::Info, ("%p HTMLEditor::HandleInsertText(), pointToInsert=%s", this, ToString(pointToInsert).c_str())); if (NS_WARN_IF(!pointToInsert.IsSet())) { return Err(NS_ERROR_FAILURE); } // for every property that is set, insert a new inline style node // XXX I think that if this is second or later composition update, we should // not change the style because we won't update composition with keeping // inline elements in composing range. Result setStyleResult = CreateStyleForInsertText(pointToInsert, *editingHost); if (MOZ_UNLIKELY(setStyleResult.isErr())) { NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed"); return setStyleResult.propagateErr(); } if (setStyleResult.inspect().IsSet()) { pointToInsert = setStyleResult.unwrap(); } if (NS_WARN_IF(!pointToInsert.IsSetAndValid()) || NS_WARN_IF(!pointToInsert.IsInContentNode())) { return Err(NS_ERROR_FAILURE); } MOZ_ASSERT(pointToInsert.IsSetAndValid()); // If the point is not in an element which can contain text nodes, climb up // the DOM tree. pointToInsert = HTMLEditUtils::GetPossiblePointToInsert( pointToInsert, *nsGkAtoms::textTagName, *editingHost); if (NS_WARN_IF(!pointToInsert.IsSet())) { return Err(NS_ERROR_FAILURE); } MOZ_ASSERT(pointToInsert.IsInContentNode()); if (InsertingTextForComposition(aPurpose)) { if (aInsertionString.IsEmpty()) { // Right now the WhiteSpaceVisibilityKeeper code bails on empty strings, // but IME needs the InsertTextWithTransaction() call to still happen // since empty strings are meaningful there. Result insertEmptyTextResultOrError = InsertTextWithTransaction(aInsertionString, pointToInsert, InsertTextTo::ExistingTextNodeIfAvailable); if (MOZ_UNLIKELY(insertEmptyTextResultOrError.isErr())) { NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed"); return insertEmptyTextResultOrError.propagateErr(); } InsertTextResult insertEmptyTextResult = insertEmptyTextResultOrError.unwrap(); // InsertTextWithTransaction() doesn not suggest caret position if it's // called for IME composition. However, for the safety, let's ignore the // caret position explicitly. insertEmptyTextResult.IgnoreCaretPointSuggestion(); nsresult rv = EnsureNoFollowingUnnecessaryLineBreak( insertEmptyTextResult.EndOfInsertedTextRef(), // When user inserting text, the web app may expect that nothing // extant content will be deleted. Therefore, we should preserve // preformatted linefeed at least. PreservePreformattedLineBreak::Yes, PaddingForEmptyBlock::Unnecessary, *editingHost); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed"); return Err(rv); } const EditorDOMPoint& endOfInsertedText = insertEmptyTextResult.EndOfInsertedTextRef(); if (endOfInsertedText.IsInTextNode() && !endOfInsertedText.IsStartOfContainer()) { nsresult rv = WhiteSpaceVisibilityKeeper:: NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces( *this, endOfInsertedText.AsInText().PreviousPoint()); if (NS_FAILED(rv)) { NS_WARNING( "WhiteSpaceVisibilityKeeper::" "NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces()" " failed"); return Err(rv); } if (NS_WARN_IF( !endOfInsertedText.IsInContentNodeAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } // If we replaced non-empty composition string with an empty string, // its preceding character may be a collapsible ASCII white-space. // Therefore, we may need to insert a padding
after the white-space. Result insertPaddingBRElementResultOrError = InsertPaddingBRElementIfNeeded( insertEmptyTextResult.EndOfInsertedTextRef(), nsIEditor::eNoStrip, *editingHost); if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) { NS_WARNING( "HTMLEditor::InsertPaddingBRElementIfNeeded(eNoStrip) failed"); return insertPaddingBRElementResultOrError.propagateErr(); } insertPaddingBRElementResultOrError.unwrap().IgnoreCaretPointSuggestion(); // Then, collapse caret after the empty text inserted position, i.e., // whether the removed composition string was. if (AllowsTransactionsToChangeSelection()) { nsresult rv = CollapseSelectionTo(endOfInsertedText); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(rv); } NS_WARNING_ASSERTION( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CaretPoint::SuggestCaretPointTo() failed, but ignored"); } return EditActionResult::HandledResult(); } EditorDOMPoint endOfInsertedText; { AutoTrackDOMPoint trackPointToInsert(RangeUpdaterRef(), &pointToInsert); const auto compositionEndPoint = GetLastIMESelectionEndPoint(); Result replaceTextResult = WhiteSpaceVisibilityKeeper::InsertOrUpdateCompositionString( *this, aInsertionString, compositionEndPoint.IsSet() ? EditorDOMRange(pointToInsert, compositionEndPoint) : EditorDOMRange(pointToInsert), aPurpose, *editingHost); if (MOZ_UNLIKELY(replaceTextResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::ReplaceText() failed"); return replaceTextResult.propagateErr(); } InsertTextResult unwrappedReplaceTextResult = replaceTextResult.unwrap(); endOfInsertedText = unwrappedReplaceTextResult.EndOfInsertedTextRef(); if (InsertingTextForCommittingComposition(aPurpose)) { // If we're committing the composition, // WhiteSpaceVisibilityKeeper::InsertOrUpdateCompositionString() may // replace the last character of the composition string when it's a // white-space. Then, Selection will be moved before the last // character. So, we need to adjust Selection here. nsresult rv = unwrappedReplaceTextResult.SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CaretPoint::SuggestCaretPointTo() failed"); return Err(rv); } NS_WARNING_ASSERTION( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CaretPoint::SuggestCaretPoint() failed, but ignored"); } else { // CompositionTransaction should've set selection so that we should // ignore caret suggestion. unwrappedReplaceTextResult.IgnoreCaretPointSuggestion(); } } if (!InsertingTextForCommittingComposition(aPurpose)) { const auto newCompositionStartPoint = GetFirstIMESelectionStartPoint(); const auto newCompositionEndPoint = GetLastIMESelectionEndPoint(); if (NS_WARN_IF(!newCompositionStartPoint.IsSet()) || NS_WARN_IF(!newCompositionEndPoint.IsSet())) { // Mutation event listener has changed the DOM tree... return EditActionResult::HandledResult(); } nsresult rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd( newCompositionStartPoint.ToRawRangeBoundary(), newCompositionEndPoint.ToRawRangeBoundary()); if (NS_FAILED(rv)) { NS_WARNING("nsRange::SetStartAndEnd() failed"); return Err(rv); } } else { if (NS_WARN_IF(!endOfInsertedText.IsSetAndValidInComposedDoc()) || NS_WARN_IF(!pointToInsert.IsSetAndValidInComposedDoc())) { return EditActionResult::HandledResult(); } nsresult rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd( pointToInsert.ToRawRangeBoundary(), endOfInsertedText.ToRawRangeBoundary()); if (NS_FAILED(rv)) { NS_WARNING("nsRange::SetStartAndEnd() failed"); return Err(rv); } } return EditActionResult::HandledResult(); } MOZ_ASSERT(!InsertingTextForComposition(aPurpose)); // find where we are EditorDOMPoint currentPoint(pointToInsert); // is our text going to be PREformatted? // We remember this so that we know how to handle tabs. const bool isWhiteSpaceCollapsible = !EditorUtils::IsWhiteSpacePreformatted( *pointToInsert.ContainerAs()); const Maybe lineBreakType = GetPreferredLineBreakType( *pointToInsert.ContainerAs(), *editingHost); if (NS_WARN_IF(lineBreakType.isNothing())) { return Err(NS_ERROR_FAILURE); } // turn off the edit listener: we know how to // build the "doc changed range" ourselves, and it's // must faster to do it once here than to track all // the changes one at a time. AutoRestore disableListener( EditSubActionDataRef().mAdjustChangedRangeFromListener); EditSubActionDataRef().mAdjustChangedRangeFromListener = false; // don't change my selection in subtransactions AutoTransactionsConserveSelection dontChangeMySelection(*this); { AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToInsert); const auto GetInsertTextTo = [](int32_t aInclusiveNextLinefeedOffset, uint32_t aLineStartOffset) { if (aInclusiveNextLinefeedOffset > 0) { return aLineStartOffset > 0 // If we'll insert a
and we're inserting 2nd or later // line, we should always create new `Text` since it'll be // between 2
elements. ? InsertTextTo::AlwaysCreateNewTextNode // If we'll insert a
and we're inserting first line, // we should append text to preceding text node, but // we don't want to insert it to a a following text node // because of avoiding to split the `Text`. : InsertTextTo::ExistingTextNodeIfAvailableAndNotStart; } // If we're inserting the last line, the text should be inserted to // start of the following `Text` if there is or middle of the `Text` // at insertion position if we're inserting only the line. return InsertTextTo::ExistingTextNodeIfAvailable; }; // for efficiency, break out the pre case separately. This is because // its a lot cheaper to search the input string for only newlines than // it is to search for both tabs and newlines. if (!isWhiteSpaceCollapsible || IsPlaintextMailComposer()) { if (!aInsertionString.IsEmpty()) [[likely]] { // If the inserting string is not empty, we need to delete padding // line break after the insertion point first because X (Twitter) // expects that character data change will be notified at last. const WSScanResult nextThing = HTMLEditUtils:: ScanInclusiveNextThingWithIgnoringUnnecessaryLineBreak( currentPoint, PaddingForEmptyBlock::Unnecessary, *editingHost); if (nextThing.MaybeIgnoredLineBreak().isSome()) { const EditorLineBreak& lineBreak = nextThing.MaybeIgnoredLineBreak().ref(); // When user inserting content, the web app may expect that nothing // extant content will be deleted. Therefore, we should preserve // preformatted linefeed at least. However, we should delete it if // it's a padding for empty block for the compatibility with the other // browsers. if (lineBreak.IsHTMLBRElement() || lineBreak.IsPaddingForEmptyBlock()) { const RefPtr ancestorLimiterToDeleteEmptyInlines = lineBreak.ContentRef().IsInclusiveDescendantOf( currentPoint.GetContainer()) ? currentPoint.GetContainerOrContainerParentElement() : editingHost.get(); { AutoTrackDOMPoint trackCurrentPoint(RangeUpdaterRef(), ¤tPoint); Result deleteLineBreakResultOrError = DeleteLineBreakWithTransaction( lineBreak, nsIEditor::eStrip, *ancestorLimiterToDeleteEmptyInlines); if (deleteLineBreakResultOrError.isErr()) [[unlikely]] { NS_WARNING( "HTMLEditor::DeleteLineBreakWithTransaction() failed"); return deleteLineBreakResultOrError.propagateErr(); } } if (NS_WARN_IF(!currentPoint.IsSetAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } } } if (*lineBreakType == LineBreakType::Linefeed) { // Both Chrome and us inserts a preformatted linefeed with its own // `Text` node in various cases. However, when inserting multiline // text, we should insert a `Text` because Chrome does so and the // comment field in https://discussions.apple.com/ handles the new // `Text` to split each line into a paragraph. At that time, it's // not assumed that inserted text is split at every linefeed. MOZ_ASSERT(*lineBreakType == LineBreakType::Linefeed); Result insertTextResult = InsertTextWithTransaction( aInsertionString, currentPoint, InsertTextTo::ExistingTextNodeIfAvailable); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed"); return insertTextResult.propagateErr(); } // Ignore the caret suggestion because of `dontChangeMySelection` // above. insertTextResult.inspect().IgnoreCaretPointSuggestion(); if (insertTextResult.inspect().Handled()) { pointToInsert = currentPoint = insertTextResult.unwrap() .EndOfInsertedTextRef() .To(); } else { pointToInsert = currentPoint; } } else { MOZ_ASSERT(*lineBreakType == LineBreakType::BRElement); uint32_t nextOffset = 0; while (nextOffset < aInsertionString.Length()) { const uint32_t lineStartOffset = nextOffset; const int32_t inclusiveNextLinefeedOffset = aInsertionString.FindChar( HTMLEditUtils::kNewLine, lineStartOffset); const uint32_t lineLength = inclusiveNextLinefeedOffset != -1 ? static_cast(inclusiveNextLinefeedOffset) - lineStartOffset : aInsertionString.Length() - lineStartOffset; if (lineLength) { // lineText does not include the preformatted line break. const nsDependentSubstring lineText(aInsertionString, lineStartOffset, lineLength); Result insertTextResult = InsertTextWithTransaction( lineText, currentPoint, GetInsertTextTo(inclusiveNextLinefeedOffset, lineStartOffset)); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed"); return insertTextResult.propagateErr(); } // Ignore the caret suggestion because of `dontChangeMySelection` // above. insertTextResult.inspect().IgnoreCaretPointSuggestion(); if (insertTextResult.inspect().Handled()) { pointToInsert = currentPoint = insertTextResult.unwrap() .EndOfInsertedTextRef() .To(); } else { pointToInsert = currentPoint; } if (inclusiveNextLinefeedOffset < 0) { break; // We reached the last line } } MOZ_ASSERT(inclusiveNextLinefeedOffset >= 0); Result insertLineBreakResultOrError = InsertLineBreak(WithTransaction::Yes, *lineBreakType, currentPoint); if (MOZ_UNLIKELY(insertLineBreakResultOrError.isErr())) { NS_WARNING(nsPrintfCString("HTMLEditor::InsertLineBreak(" "WithTransaction::Yes, %s) failed", ToString(*lineBreakType).c_str()) .get()); return insertLineBreakResultOrError.propagateErr(); } CreateLineBreakResult insertLineBreakResult = insertLineBreakResultOrError.unwrap(); // We don't want to update selection here because we've blocked // InsertNodeTransaction updating selection with // dontChangeMySelection. insertLineBreakResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(!AllowsTransactionsToChangeSelection()); nextOffset = inclusiveNextLinefeedOffset + 1; pointToInsert = insertLineBreakResult.AfterLineBreak(); currentPoint.SetAfter(&insertLineBreakResult.LineBreakContentRef()); if (NS_WARN_IF(!pointToInsert.IsSetAndValidInComposedDoc()) || NS_WARN_IF(!currentPoint.IsSetAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } } } else { uint32_t nextOffset = 0; while (nextOffset < aInsertionString.Length()) { const uint32_t lineStartOffset = nextOffset; const int32_t inclusiveNextLinefeedOffset = aInsertionString.FindChar(HTMLEditUtils::kNewLine, lineStartOffset); const uint32_t lineLength = inclusiveNextLinefeedOffset != -1 ? static_cast(inclusiveNextLinefeedOffset) - lineStartOffset : aInsertionString.Length() - lineStartOffset; if (lineLength) { auto insertTextResult = [&]() MOZ_CAN_RUN_SCRIPT -> Result { // lineText does not include the preformatted line break. const nsDependentSubstring lineText(aInsertionString, lineStartOffset, lineLength); if (!lineText.Contains(u'\t')) { return WhiteSpaceVisibilityKeeper::InsertText( *this, lineText, currentPoint, GetInsertTextTo(inclusiveNextLinefeedOffset, lineStartOffset), *editingHost); } nsAutoString formattedLineText(lineText); formattedLineText.ReplaceSubstring(u"\t"_ns, u" "_ns); return WhiteSpaceVisibilityKeeper::InsertText( *this, formattedLineText, currentPoint, GetInsertTextTo(inclusiveNextLinefeedOffset, lineStartOffset), *editingHost); }(); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertText() failed"); return insertTextResult.propagateErr(); } // Ignore the caret suggestion because of `dontChangeMySelection` // above. insertTextResult.inspect().IgnoreCaretPointSuggestion(); if (insertTextResult.inspect().Handled()) { pointToInsert = currentPoint = insertTextResult.unwrap().EndOfInsertedTextRef(); } else { pointToInsert = currentPoint; } if (inclusiveNextLinefeedOffset < 0) { break; // We reached the last line } } Result insertLineBreakResultOrError = WhiteSpaceVisibilityKeeper::InsertLineBreak(*lineBreakType, *this, currentPoint); if (MOZ_UNLIKELY(insertLineBreakResultOrError.isErr())) { NS_WARNING( nsPrintfCString( "WhiteSpaceVisibilityKeeper::InsertLineBreak(%s) failed", ToString(*lineBreakType).c_str()) .get()); return insertLineBreakResultOrError.propagateErr(); } CreateLineBreakResult insertLineBreakResult = insertLineBreakResultOrError.unwrap(); // TODO: Some methods called for handling non-preformatted text use // ComputeEditingHost(). Therefore, they depend on the latest // selection. So we cannot skip updating selection here. nsresult rv = insertLineBreakResult.SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return Err(rv); } NS_WARNING_ASSERTION( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CreateElementResult::SuggestCaretPointTo() failed, but ignored"); nextOffset = inclusiveNextLinefeedOffset + 1; pointToInsert = insertLineBreakResult.AfterLineBreak(); currentPoint.SetAfter(&insertLineBreakResult.LineBreakContentRef()); if (NS_WARN_IF(!pointToInsert.IsSetAndValidInComposedDoc()) || NS_WARN_IF(!currentPoint.IsSetAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } } // After this block, pointToInsert is updated by AutoTrackDOMPoint. } if (currentPoint.IsSet()) { // If we appended a collapsible white-space to the end of the text node, // its following content may be removed by the web app. Then, we need to // keep it visible even if it becomes immediately before a block boundary. // For referring the node from our mutation observer, we need to store the // text node temporarily. if (currentPoint.IsInTextNode() && MOZ_LIKELY(!currentPoint.IsStartOfContainer()) && currentPoint.IsEndOfContainer() && currentPoint.IsPreviousCharCollapsibleASCIISpace()) { mLastCollapsibleWhiteSpaceAppendedTextNode = currentPoint.ContainerAs(); } if (!aInsertionString.IsEmpty() && aInsertionString.Last() == HTMLEditUtils::kNewLine) { Result insertPaddingLineBreakResult = InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded(currentPoint, *editingHost); if (insertPaddingLineBreakResult.isErr()) [[unlikely]] { NS_WARNING( "HTMLEditor::InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded()" " failed"); return insertPaddingLineBreakResult.propagateErr(); } if (insertPaddingLineBreakResult.inspect().HasCaretPointSuggestion()) { currentPoint = insertPaddingLineBreakResult.unwrap().UnwrapCaretPoint(); } } else { nsresult rv = EnsureNoFollowingUnnecessaryLineBreak( currentPoint, // When user inserting text, the web app may expect that nothing // extant content will be deleted. Therefore, we should preserve // preformatted linefeed at least. PreservePreformattedLineBreak::Yes, PaddingForEmptyBlock::Unnecessary, *editingHost); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed"); return Err(rv); } } currentPoint.SetInterlinePosition(InterlinePosition::EndOfLine); rv = CollapseSelectionTo(currentPoint); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Selection::Collapse() failed, but ignored"); // manually update the doc changed range so that AfterEdit will clean up // the correct portion of the document. rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd( pointToInsert.ToRawRangeBoundary(), currentPoint.ToRawRangeBoundary()); if (NS_FAILED(rv)) { NS_WARNING("nsRange::SetStartAndEnd() failed"); return Err(rv); } return EditActionResult::HandledResult(); } DebugOnly rvIgnored = SelectionRef().SetInterlinePosition(InterlinePosition::EndOfLine); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "Selection::SetInterlinePosition(InterlinePosition::" "EndOfLine) failed, but ignored"); rv = TopLevelEditSubActionDataRef().mChangedRange->CollapseTo(pointToInsert); if (NS_FAILED(rv)) { NS_WARNING("nsRange::CollapseTo() failed"); return Err(rv); } return EditActionResult::HandledResult(); } HTMLEditor::CharPointData HTMLEditor::GetPreviousCharPointDataForNormalizingWhiteSpaces( const EditorDOMPointInText& aPoint) const { MOZ_ASSERT(aPoint.IsSetAndValid()); if (!aPoint.IsStartOfContainer()) { return CharPointData::InSameTextNode( HTMLEditor::GetPreviousCharPointType(aPoint)); } const auto previousCharPoint = WSRunScanner::GetPreviousCharPoint( {WSRunScanner::Option::OnlyEditableNodes}, aPoint); if (!previousCharPoint.IsSet()) { return CharPointData::InDifferentTextNode(CharPointType::TextEnd); } return CharPointData::InDifferentTextNode( HTMLEditor::GetCharPointType(previousCharPoint)); } HTMLEditor::CharPointData HTMLEditor::GetInclusiveNextCharPointDataForNormalizingWhiteSpaces( const EditorDOMPointInText& aPoint) const { MOZ_ASSERT(aPoint.IsSetAndValid()); if (!aPoint.IsEndOfContainer()) { return CharPointData::InSameTextNode(HTMLEditor::GetCharPointType(aPoint)); } const auto nextCharPoint = WSRunScanner::GetInclusiveNextCharPoint( {WSRunScanner::Option::OnlyEditableNodes}, aPoint); if (!nextCharPoint.IsSet()) { return CharPointData::InDifferentTextNode(CharPointType::TextEnd); } return CharPointData::InDifferentTextNode( HTMLEditor::GetCharPointType(nextCharPoint)); } // static void HTMLEditor::NormalizeAllWhiteSpaceSequences( nsString& aResult, const CharPointData& aPreviousCharPointData, const CharPointData& aNextCharPointData, Linefeed aLinefeed) { MOZ_ASSERT(!aResult.IsEmpty()); const auto IsCollapsibleChar = [&](char16_t aChar) { if (aChar == HTMLEditUtils::kNewLine) { return aLinefeed == Linefeed::Preformatted; } return nsCRT::IsAsciiSpace(aChar); }; const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) { return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar); }; const uint32_t length = aResult.Length(); for (uint32_t offset = 0; offset < length; offset++) { const char16_t ch = aResult[offset]; if (!IsCollapsibleCharOrNBSP(ch)) { continue; } const CharPointData previousCharData = [&]() { if (offset) { const char16_t prevChar = aResult[offset - 1u]; return CharPointData::InSameTextNode( prevChar == HTMLEditUtils::kNewLine ? CharPointType::PreformattedLineBreak : CharPointType::VisibleChar); } return aPreviousCharPointData; }(); const uint32_t endOffset = [&]() { for (const uint32_t i : IntegerRange(offset, length)) { if (IsCollapsibleCharOrNBSP(aResult[i])) { continue; } return i; } return length; }(); const CharPointData nextCharData = [&]() { if (endOffset < length) { const char16_t nextChar = aResult[endOffset]; return CharPointData::InSameTextNode( nextChar == HTMLEditUtils::kNewLine ? CharPointType::PreformattedLineBreak : CharPointType::VisibleChar); } return aNextCharPointData; }(); HTMLEditor::ReplaceStringWithNormalizedWhiteSpaceSequence( aResult, offset, endOffset - offset, previousCharData, nextCharData); offset = endOffset; } } // static void HTMLEditor::GenerateWhiteSpaceSequence( nsString& aResult, uint32_t aLength, const CharPointData& aPreviousCharPointData, const CharPointData& aNextCharPointData) { MOZ_ASSERT(aResult.IsEmpty()); MOZ_ASSERT(aLength); aResult.SetLength(aLength); HTMLEditor::ReplaceStringWithNormalizedWhiteSpaceSequence( aResult, 0u, aLength, aPreviousCharPointData, aNextCharPointData); } // static void HTMLEditor::ReplaceStringWithNormalizedWhiteSpaceSequence( nsString& aResult, uint32_t aOffset, uint32_t aLength, const CharPointData& aPreviousCharPointData, const CharPointData& aNextCharPointData) { MOZ_ASSERT(!aResult.IsEmpty()); MOZ_ASSERT(aLength); MOZ_ASSERT(aOffset < aResult.Length()); MOZ_ASSERT(aOffset + aLength <= aResult.Length()); // For now, this method does not assume that result will be append to // white-space sequence in the text node. MOZ_ASSERT(aPreviousCharPointData.AcrossTextNodeBoundary() || !aPreviousCharPointData.IsCollapsibleWhiteSpace()); // For now, this method does not assume that the result will be inserted // into white-space sequence nor start of white-space sequence. MOZ_ASSERT(aNextCharPointData.AcrossTextNodeBoundary() || !aNextCharPointData.IsCollapsibleWhiteSpace()); if (aLength == 1) { // Even if previous/next char is in different text node, we should put // an ASCII white-space between visible characters. // XXX This means that this does not allow to put an NBSP in HTML editor // without preformatted style. However, Chrome has same issue too. if (aPreviousCharPointData.Type() == CharPointType::VisibleChar && aNextCharPointData.Type() == CharPointType::VisibleChar) { aResult.SetCharAt(HTMLEditUtils::kSpace, aOffset); return; } // If it's start or end of text, put an NBSP. if (aPreviousCharPointData.Type() == CharPointType::TextEnd || aNextCharPointData.Type() == CharPointType::TextEnd) { aResult.SetCharAt(HTMLEditUtils::kNBSP, aOffset); return; } // If the character is next to a preformatted linefeed, we need to put // an NBSP for avoiding collapsed into the linefeed. if (aPreviousCharPointData.Type() == CharPointType::PreformattedLineBreak || aNextCharPointData.Type() == CharPointType::PreformattedLineBreak) { aResult.SetCharAt(HTMLEditUtils::kNBSP, aOffset); return; } // Now, the white-space will be inserted to a white-space sequence, but not // end of text. We can put an ASCII white-space only when both sides are // not ASCII white-spaces. aResult.SetCharAt( aPreviousCharPointData.Type() == CharPointType::ASCIIWhiteSpace || aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace, aOffset); return; } // Generate pairs of NBSP and ASCII white-space. bool appendNBSP = true; // Basically, starts with an NBSP. char16_t* const lastChar = aResult.BeginWriting() + aOffset + aLength - 1; for (char16_t* iter = aResult.BeginWriting() + aOffset; iter != lastChar; iter++) { *iter = appendNBSP ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace; appendNBSP = !appendNBSP; } // If the final one is expected to an NBSP, we can put an NBSP simply. if (appendNBSP) { *lastChar = HTMLEditUtils::kNBSP; return; } // If next char point is end of text node, an ASCII white-space or // preformatted linefeed, we need to put an NBSP. *lastChar = aNextCharPointData.AcrossTextNodeBoundary() || aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace || aNextCharPointData.Type() == CharPointType::PreformattedLineBreak ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace; } HTMLEditor::NormalizedStringToInsertText HTMLEditor::NormalizeWhiteSpacesToInsertText( const EditorDOMPoint& aPointToInsert, const nsAString& aStringToInsert, NormalizeSurroundingWhiteSpaces aNormalizeSurroundingWhiteSpaces) const { MOZ_ASSERT(aPointToInsert.IsSet()); // If white-spaces are preformatted, we don't need to normalize white-spaces. if (EditorUtils::IsWhiteSpacePreformatted( *aPointToInsert.ContainerAs())) { return NormalizedStringToInsertText(aStringToInsert, aPointToInsert); } Text* const textNode = aPointToInsert.GetContainerAs(); const CharacterDataBuffer* const characterDataBuffer = textNode ? &textNode->DataBuffer() : nullptr; const bool isNewLineCollapsible = !EditorUtils::IsNewLinePreformatted( *aPointToInsert.ContainerAs()); // We don't want to make invisible things visible with this normalization. // Therefore, we need to know whether there are invisible leading and/or // trailing white-spaces in the `Text`. // Then, compute visible white-space length before/after the insertion point. // Note that these lengths may contain invisible white-spaces. const uint32_t precedingWhiteSpaceLength = [&]() { if (!textNode || !aNormalizeSurroundingWhiteSpaces || aPointToInsert.IsStartOfContainer()) { return 0u; } const auto nonWhiteSpaceOffset = HTMLEditUtils::GetPreviousNonCollapsibleCharOffset( *textNode, aPointToInsert.Offset(), {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible}); const uint32_t firstWhiteSpaceOffset = nonWhiteSpaceOffset ? *nonWhiteSpaceOffset + 1u : 0u; return aPointToInsert.Offset() - firstWhiteSpaceOffset; }(); const uint32_t followingWhiteSpaceLength = [&]() { if (!textNode || !aNormalizeSurroundingWhiteSpaces || aPointToInsert.IsEndOfContainer()) { return 0u; } MOZ_ASSERT(characterDataBuffer); const auto nonWhiteSpaceOffset = HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset( *textNode, aPointToInsert.Offset(), {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible}); MOZ_ASSERT(nonWhiteSpaceOffset.valueOr(characterDataBuffer->GetLength()) >= aPointToInsert.Offset()); return nonWhiteSpaceOffset.valueOr(characterDataBuffer->GetLength()) - aPointToInsert.Offset(); }(); // Now, we can know invisible white-space length in precedingWhiteSpaceLength // and followingWhiteSpaceLength. const uint32_t precedingInvisibleWhiteSpaceCount = textNode ? HTMLEditUtils::GetInvisibleWhiteSpaceCount( *textNode, aPointToInsert.Offset() - precedingWhiteSpaceLength, precedingWhiteSpaceLength) : 0u; MOZ_ASSERT(precedingWhiteSpaceLength >= precedingInvisibleWhiteSpaceCount); const uint32_t newPrecedingWhiteSpaceLength = precedingWhiteSpaceLength - precedingInvisibleWhiteSpaceCount; const uint32_t followingInvisibleSpaceCount = textNode ? HTMLEditUtils::GetInvisibleWhiteSpaceCount( *textNode, aPointToInsert.Offset(), followingWhiteSpaceLength) : 0u; MOZ_ASSERT(followingWhiteSpaceLength >= followingInvisibleSpaceCount); const uint32_t newFollowingWhiteSpaceLength = followingWhiteSpaceLength - followingInvisibleSpaceCount; const nsAutoString stringToInsertWithSurroundingSpaces = [&]() -> nsAutoString { if (!newPrecedingWhiteSpaceLength && !newFollowingWhiteSpaceLength) { return nsAutoString(aStringToInsert); } nsAutoString str; str.SetCapacity(aStringToInsert.Length() + newPrecedingWhiteSpaceLength + newFollowingWhiteSpaceLength); for ([[maybe_unused]] auto unused : IntegerRange(newPrecedingWhiteSpaceLength)) { str.Append(' '); } str.Append(aStringToInsert); for ([[maybe_unused]] auto unused : IntegerRange(newFollowingWhiteSpaceLength)) { str.Append(' '); } return str; }(); const uint32_t insertionOffsetInTextNode = aPointToInsert.IsInTextNode() ? aPointToInsert.Offset() : 0u; NormalizedStringToInsertText result( stringToInsertWithSurroundingSpaces, insertionOffsetInTextNode, insertionOffsetInTextNode - precedingWhiteSpaceLength, // replace start precedingWhiteSpaceLength + followingWhiteSpaceLength, // replace length newPrecedingWhiteSpaceLength, newFollowingWhiteSpaceLength); // Now, normalize the inserting string. // Note that if the caller does not want to normalize the following // white-spaces, we always need to guarantee that neither the first character // nor the last character of the insertion string is not collapsible, i.e., if // each one is a collapsible white-space, we need to replace them an NBSP to // keep the visibility of the collapsible white-spaces. Therefore, if // aNormalizeSurroundingWhiteSpaces is "No", we need to treat the insertion // string is the only characters in the `Text`. HTMLEditor::NormalizeAllWhiteSpaceSequences( result.mNormalizedString, CharPointData::InSameTextNode( !characterDataBuffer || !result.mReplaceStartOffset || !aNormalizeSurroundingWhiteSpaces ? CharPointType::TextEnd : (characterDataBuffer->CharAt(result.mReplaceStartOffset - 1u) == HTMLEditUtils::kNewLine ? CharPointType::PreformattedLineBreak : CharPointType::VisibleChar)), CharPointData::InSameTextNode( !characterDataBuffer || result.mReplaceEndOffset >= characterDataBuffer->GetLength() || !aNormalizeSurroundingWhiteSpaces ? CharPointType::TextEnd : (characterDataBuffer->CharAt(result.mReplaceEndOffset) == HTMLEditUtils::kNewLine ? CharPointType::PreformattedLineBreak : CharPointType::VisibleChar)), isNewLineCollapsible ? Linefeed::Collapsible : Linefeed::Preformatted); return result; } HTMLEditor::ReplaceWhiteSpacesData HTMLEditor::GetNormalizedStringAt( const EditorDOMPointInText& aPoint) const { MOZ_ASSERT(aPoint.IsSet()); // If white-spaces are preformatted, we don't need to normalize white-spaces. if (EditorUtils::IsWhiteSpacePreformatted(*aPoint.ContainerAs())) { return ReplaceWhiteSpacesData(); } const Text& textNode = *aPoint.ContainerAs(); const CharacterDataBuffer& characterDataBuffer = textNode.DataBuffer(); // We don't want to make invisible things visible with this normalization. // Therefore, we need to know whether there are invisible leading and/or // trailing white-spaces in the `Text`. // Then, compute visible white-space length before/after the point. // Note that these lengths may contain invisible white-spaces. const uint32_t precedingWhiteSpaceLength = [&]() { if (aPoint.IsStartOfContainer()) { return 0u; } const auto nonWhiteSpaceOffset = HTMLEditUtils::GetPreviousNonCollapsibleCharOffset( textNode, aPoint.Offset(), {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible}); const uint32_t firstWhiteSpaceOffset = nonWhiteSpaceOffset ? *nonWhiteSpaceOffset + 1u : 0u; return aPoint.Offset() - firstWhiteSpaceOffset; }(); const uint32_t followingWhiteSpaceLength = [&]() { if (aPoint.IsEndOfContainer()) { return 0u; } const auto nonWhiteSpaceOffset = HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset( textNode, aPoint.Offset(), {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible}); MOZ_ASSERT(nonWhiteSpaceOffset.valueOr(characterDataBuffer.GetLength()) >= aPoint.Offset()); return nonWhiteSpaceOffset.valueOr(characterDataBuffer.GetLength()) - aPoint.Offset(); }(); if (!precedingWhiteSpaceLength && !followingWhiteSpaceLength) { return ReplaceWhiteSpacesData(); } // Now, we can know invisible white-space length in precedingWhiteSpaceLength // and followingWhiteSpaceLength. const uint32_t precedingInvisibleWhiteSpaceCount = HTMLEditUtils::GetInvisibleWhiteSpaceCount( textNode, aPoint.Offset() - precedingWhiteSpaceLength, precedingWhiteSpaceLength); MOZ_ASSERT(precedingWhiteSpaceLength >= precedingInvisibleWhiteSpaceCount); const uint32_t newPrecedingWhiteSpaceLength = precedingWhiteSpaceLength - precedingInvisibleWhiteSpaceCount; const uint32_t followingInvisibleSpaceCount = HTMLEditUtils::GetInvisibleWhiteSpaceCount(textNode, aPoint.Offset(), followingWhiteSpaceLength); MOZ_ASSERT(followingWhiteSpaceLength >= followingInvisibleSpaceCount); const uint32_t newFollowingWhiteSpaceLength = followingWhiteSpaceLength - followingInvisibleSpaceCount; nsAutoString stringToInsertWithSurroundingSpaces; if (newPrecedingWhiteSpaceLength || newFollowingWhiteSpaceLength) { stringToInsertWithSurroundingSpaces.SetLength(newPrecedingWhiteSpaceLength + newFollowingWhiteSpaceLength); for (auto index : IntegerRange(newPrecedingWhiteSpaceLength + newFollowingWhiteSpaceLength)) { stringToInsertWithSurroundingSpaces.SetCharAt(' ', index); } } ReplaceWhiteSpacesData result( std::move(stringToInsertWithSurroundingSpaces), aPoint.Offset() - precedingWhiteSpaceLength, // replace start precedingWhiteSpaceLength + followingWhiteSpaceLength, // replace length // aPoint.Offset() after replacing the white-spaces aPoint.Offset() - precedingWhiteSpaceLength + newPrecedingWhiteSpaceLength); if (!result.mNormalizedString.IsEmpty()) { HTMLEditor::NormalizeAllWhiteSpaceSequences( result.mNormalizedString, CharPointData::InSameTextNode( !result.mReplaceStartOffset ? CharPointType::TextEnd : (characterDataBuffer.CharAt(result.mReplaceStartOffset - 1u) == HTMLEditUtils::kNewLine ? CharPointType::PreformattedLineBreak : CharPointType::VisibleChar)), CharPointData::InSameTextNode( result.mReplaceEndOffset >= characterDataBuffer.GetLength() ? CharPointType::TextEnd : (characterDataBuffer.CharAt(result.mReplaceEndOffset) == HTMLEditUtils::kNewLine ? CharPointType::PreformattedLineBreak : CharPointType::VisibleChar)), EditorUtils::IsNewLinePreformatted(textNode) ? Linefeed::Collapsible : Linefeed::Preformatted); } return result; } HTMLEditor::ReplaceWhiteSpacesData HTMLEditor::GetFollowingNormalizedStringToSplitAt( const EditorDOMPointInText& aPointToSplit) const { MOZ_ASSERT(aPointToSplit.IsSet()); if (EditorUtils::IsWhiteSpacePreformatted( *aPointToSplit.ContainerAs()) || aPointToSplit.IsEndOfContainer()) { return ReplaceWhiteSpacesData(); } const bool isNewLineCollapsible = !EditorUtils::IsNewLinePreformatted(*aPointToSplit.ContainerAs()); const auto IsPreformattedLineBreak = [&](char16_t aChar) { return !isNewLineCollapsible && aChar == HTMLEditUtils::kNewLine; }; const auto IsCollapsibleChar = [&](char16_t aChar) { return !IsPreformattedLineBreak(aChar) && nsCRT::IsAsciiSpace(aChar); }; const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) { return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar); }; const char16_t followingChar = aPointToSplit.Char(); if (!IsCollapsibleCharOrNBSP(followingChar)) { return ReplaceWhiteSpacesData(); } const uint32_t followingWhiteSpaceLength = [&]() { const auto nonWhiteSpaceOffset = HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset( *aPointToSplit.ContainerAs(), aPointToSplit.Offset(), {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible}); MOZ_ASSERT(nonWhiteSpaceOffset.valueOr( aPointToSplit.ContainerAs()->TextDataLength()) >= aPointToSplit.Offset()); return nonWhiteSpaceOffset.valueOr( aPointToSplit.ContainerAs()->TextDataLength()) - aPointToSplit.Offset(); }(); MOZ_ASSERT(followingWhiteSpaceLength); if (NS_WARN_IF(!followingWhiteSpaceLength) || (followingWhiteSpaceLength == 1u && followingChar == HTMLEditUtils::kNBSP)) { return ReplaceWhiteSpacesData(); } const uint32_t followingInvisibleSpaceCount = HTMLEditUtils::GetInvisibleWhiteSpaceCount( *aPointToSplit.ContainerAs(), aPointToSplit.Offset(), followingWhiteSpaceLength); MOZ_ASSERT(followingWhiteSpaceLength >= followingInvisibleSpaceCount); const uint32_t newFollowingWhiteSpaceLength = followingWhiteSpaceLength - followingInvisibleSpaceCount; nsAutoString followingWhiteSpaces; if (newFollowingWhiteSpaceLength) { followingWhiteSpaces.SetLength(newFollowingWhiteSpaceLength); for (const auto offset : IntegerRange(newFollowingWhiteSpaceLength)) { followingWhiteSpaces.SetCharAt(' ', offset); } } ReplaceWhiteSpacesData result(std::move(followingWhiteSpaces), aPointToSplit.Offset(), followingWhiteSpaceLength); if (!result.mNormalizedString.IsEmpty()) { const CharacterDataBuffer& characterDataBuffer = aPointToSplit.ContainerAs()->DataBuffer(); HTMLEditor::NormalizeAllWhiteSpaceSequences( result.mNormalizedString, CharPointData::InSameTextNode(CharPointType::TextEnd), CharPointData::InSameTextNode( result.mReplaceEndOffset >= characterDataBuffer.GetLength() ? CharPointType::TextEnd : (characterDataBuffer.CharAt(result.mReplaceEndOffset) == HTMLEditUtils::kNewLine ? CharPointType::PreformattedLineBreak : CharPointType::VisibleChar)), isNewLineCollapsible ? Linefeed::Collapsible : Linefeed::Preformatted); } return result; } HTMLEditor::ReplaceWhiteSpacesData HTMLEditor::GetPrecedingNormalizedStringToSplitAt( const EditorDOMPointInText& aPointToSplit) const { MOZ_ASSERT(aPointToSplit.IsSet()); if (EditorUtils::IsWhiteSpacePreformatted( *aPointToSplit.ContainerAs()) || aPointToSplit.IsStartOfContainer()) { return ReplaceWhiteSpacesData(); } const bool isNewLineCollapsible = !EditorUtils::IsNewLinePreformatted(*aPointToSplit.ContainerAs()); const auto IsPreformattedLineBreak = [&](char16_t aChar) { return !isNewLineCollapsible && aChar == HTMLEditUtils::kNewLine; }; const auto IsCollapsibleChar = [&](char16_t aChar) { return !IsPreformattedLineBreak(aChar) && nsCRT::IsAsciiSpace(aChar); }; const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) { return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar); }; const char16_t precedingChar = aPointToSplit.PreviousChar(); if (!IsCollapsibleCharOrNBSP(precedingChar)) { return ReplaceWhiteSpacesData(); } const uint32_t precedingWhiteSpaceLength = [&]() { const auto nonWhiteSpaceOffset = HTMLEditUtils::GetPreviousNonCollapsibleCharOffset( *aPointToSplit.ContainerAs(), aPointToSplit.Offset(), {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible}); const uint32_t firstWhiteSpaceOffset = nonWhiteSpaceOffset ? *nonWhiteSpaceOffset + 1u : 0u; return aPointToSplit.Offset() - firstWhiteSpaceOffset; }(); MOZ_ASSERT(precedingWhiteSpaceLength); if (NS_WARN_IF(!precedingWhiteSpaceLength) || (precedingWhiteSpaceLength == 1u && precedingChar == HTMLEditUtils::kNBSP)) { return ReplaceWhiteSpacesData(); } const uint32_t precedingInvisibleWhiteSpaceCount = HTMLEditUtils::GetInvisibleWhiteSpaceCount( *aPointToSplit.ContainerAs(), aPointToSplit.Offset() - precedingWhiteSpaceLength, precedingWhiteSpaceLength); MOZ_ASSERT(precedingWhiteSpaceLength >= precedingInvisibleWhiteSpaceCount); const uint32_t newPrecedingWhiteSpaceLength = precedingWhiteSpaceLength - precedingInvisibleWhiteSpaceCount; nsAutoString precedingWhiteSpaces; if (newPrecedingWhiteSpaceLength) { precedingWhiteSpaces.SetLength(newPrecedingWhiteSpaceLength); for (const auto offset : IntegerRange(newPrecedingWhiteSpaceLength)) { precedingWhiteSpaces.SetCharAt(' ', offset); } } ReplaceWhiteSpacesData result( std::move(precedingWhiteSpaces), aPointToSplit.Offset() - precedingWhiteSpaceLength, precedingWhiteSpaceLength); if (!result.mNormalizedString.IsEmpty()) { const CharacterDataBuffer& characterDataBuffer = aPointToSplit.ContainerAs()->DataBuffer(); HTMLEditor::NormalizeAllWhiteSpaceSequences( result.mNormalizedString, CharPointData::InSameTextNode( !result.mReplaceStartOffset ? CharPointType::TextEnd : (characterDataBuffer.CharAt(result.mReplaceStartOffset - 1u) == HTMLEditUtils::kNewLine ? CharPointType::PreformattedLineBreak : CharPointType::VisibleChar)), CharPointData::InSameTextNode(CharPointType::TextEnd), isNewLineCollapsible ? Linefeed::Collapsible : Linefeed::Preformatted); } return result; } HTMLEditor::ReplaceWhiteSpacesData HTMLEditor::GetSurroundingNormalizedStringToDelete(const Text& aTextNode, uint32_t aOffset, uint32_t aLength) const { MOZ_ASSERT(aOffset <= aTextNode.TextDataLength()); MOZ_ASSERT(aOffset + aLength <= aTextNode.TextDataLength()); if (EditorUtils::IsWhiteSpacePreformatted(aTextNode) || !aLength || (!aOffset && aLength >= aTextNode.TextDataLength())) { return ReplaceWhiteSpacesData(); } const bool isNewLineCollapsible = !EditorUtils::IsNewLinePreformatted(aTextNode); const auto IsPreformattedLineBreak = [&](char16_t aChar) { return !isNewLineCollapsible && aChar == HTMLEditUtils::kNewLine; }; const auto IsCollapsibleChar = [&](char16_t aChar) { return !IsPreformattedLineBreak(aChar) && nsCRT::IsAsciiSpace(aChar); }; const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) { return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar); }; const CharacterDataBuffer& characterDataBuffer = aTextNode.DataBuffer(); const char16_t precedingChar = aOffset ? characterDataBuffer.CharAt(aOffset - 1u) : static_cast(0); const char16_t followingChar = aOffset + aLength < characterDataBuffer.GetLength() ? characterDataBuffer.CharAt(aOffset + aLength) : static_cast(0); // If there is no surrounding white-spaces, we need to do nothing here. if (!IsCollapsibleCharOrNBSP(precedingChar) && !IsCollapsibleCharOrNBSP(followingChar)) { return ReplaceWhiteSpacesData(); } const uint32_t precedingWhiteSpaceLength = [&]() { if (!IsCollapsibleCharOrNBSP(precedingChar)) { return 0u; } const auto nonWhiteSpaceOffset = HTMLEditUtils::GetPreviousNonCollapsibleCharOffset( aTextNode, aOffset, {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible}); const uint32_t firstWhiteSpaceOffset = nonWhiteSpaceOffset ? *nonWhiteSpaceOffset + 1u : 0u; return aOffset - firstWhiteSpaceOffset; }(); const uint32_t followingWhiteSpaceLength = [&]() { if (!IsCollapsibleCharOrNBSP(followingChar)) { return 0u; } const auto nonWhiteSpaceOffset = HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset( aTextNode, aOffset + aLength, {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible}); MOZ_ASSERT(nonWhiteSpaceOffset.valueOr(characterDataBuffer.GetLength()) >= aOffset + aLength); return nonWhiteSpaceOffset.valueOr(characterDataBuffer.GetLength()) - (aOffset + aLength); }(); if (NS_WARN_IF(!precedingWhiteSpaceLength && !followingWhiteSpaceLength)) { return ReplaceWhiteSpacesData(); } const uint32_t precedingInvisibleWhiteSpaceCount = HTMLEditUtils::GetInvisibleWhiteSpaceCount( aTextNode, aOffset - precedingWhiteSpaceLength, precedingWhiteSpaceLength); MOZ_ASSERT(precedingWhiteSpaceLength >= precedingInvisibleWhiteSpaceCount); const uint32_t followingInvisibleSpaceCount = HTMLEditUtils::GetInvisibleWhiteSpaceCount(aTextNode, aOffset + aLength, followingWhiteSpaceLength); MOZ_ASSERT(followingWhiteSpaceLength >= followingInvisibleSpaceCount); // Let's try to return early if there is only one white-space around the // deleting range to avoid to run the expensive path. if (precedingWhiteSpaceLength == 1u && !precedingInvisibleWhiteSpaceCount && !followingWhiteSpaceLength) { // If there is only one ASCII space and it'll be followed by a // non-collapsible character except preformatted linebreak after deletion, // we don't need to normalize the preceding white-space. if (precedingChar == HTMLEditUtils::kSpace && followingChar && !IsPreformattedLineBreak(followingChar)) { return ReplaceWhiteSpacesData(); } // If there is only one NBSP and it'll be the last character or will be // followed by a collapsible white-space, we don't need to normalize the // preceding white-space. if (precedingChar == HTMLEditUtils::kNBSP && (!followingChar || IsPreformattedLineBreak(followingChar))) { return ReplaceWhiteSpacesData(); } } if (followingWhiteSpaceLength == 1u && !followingInvisibleSpaceCount && !precedingWhiteSpaceLength) { // If there is only one ASCII space and it'll follow by a non-collapsible // character after deletion, we don't need to normalize the following // white-space. if (followingChar == HTMLEditUtils::kSpace && precedingChar && !IsPreformattedLineBreak(precedingChar)) { return ReplaceWhiteSpacesData(); } // If there is only one NBSP and it'll be the first character or will // follow a preformatted line break, we don't need to normalize the // following white-space. if (followingChar == HTMLEditUtils::kNBSP && (!precedingChar || IsPreformattedLineBreak(precedingChar))) { return ReplaceWhiteSpacesData(); } } const uint32_t newPrecedingWhiteSpaceLength = precedingWhiteSpaceLength - precedingInvisibleWhiteSpaceCount; const uint32_t newFollowingWhiteSpaceLength = followingWhiteSpaceLength - followingInvisibleSpaceCount; nsAutoString surroundingWhiteSpaces; if (newPrecedingWhiteSpaceLength || newFollowingWhiteSpaceLength) { surroundingWhiteSpaces.SetLength(newPrecedingWhiteSpaceLength + newFollowingWhiteSpaceLength); for (const auto offset : IntegerRange(newPrecedingWhiteSpaceLength + newFollowingWhiteSpaceLength)) { surroundingWhiteSpaces.SetCharAt(' ', offset); } } ReplaceWhiteSpacesData result( std::move(surroundingWhiteSpaces), aOffset - precedingWhiteSpaceLength, precedingWhiteSpaceLength + aLength + followingWhiteSpaceLength, aOffset - precedingInvisibleWhiteSpaceCount); if (!result.mNormalizedString.IsEmpty()) { HTMLEditor::NormalizeAllWhiteSpaceSequences( result.mNormalizedString, CharPointData::InSameTextNode( !result.mReplaceStartOffset ? CharPointType::TextEnd : (characterDataBuffer.CharAt(result.mReplaceStartOffset - 1u) == HTMLEditUtils::kNewLine ? CharPointType::PreformattedLineBreak : CharPointType::VisibleChar)), CharPointData::InSameTextNode( result.mReplaceEndOffset >= characterDataBuffer.GetLength() ? CharPointType::TextEnd : (characterDataBuffer.CharAt(result.mReplaceEndOffset) == HTMLEditUtils::kNewLine ? CharPointType::PreformattedLineBreak : CharPointType::VisibleChar)), isNewLineCollapsible ? Linefeed::Collapsible : Linefeed::Preformatted); } return result; } void HTMLEditor::ExtendRangeToDeleteWithNormalizingWhiteSpaces( EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete, nsString& aNormalizedWhiteSpacesInStartNode, nsString& aNormalizedWhiteSpacesInEndNode) const { MOZ_ASSERT(aStartToDelete.IsSetAndValid()); MOZ_ASSERT(aEndToDelete.IsSetAndValid()); MOZ_ASSERT(aStartToDelete.EqualsOrIsBefore(aEndToDelete)); MOZ_ASSERT(aNormalizedWhiteSpacesInStartNode.IsEmpty()); MOZ_ASSERT(aNormalizedWhiteSpacesInEndNode.IsEmpty()); // First, check whether there is surrounding white-spaces or not, and if there // are, check whether they are collapsible or not. Note that we shouldn't // touch white-spaces in different text nodes for performance, but we need // adjacent text node's first or last character information in some cases. const auto precedingCharPoint = WSRunScanner::GetPreviousCharPoint( {WSRunScanner::Option::OnlyEditableNodes}, aStartToDelete); const auto followingCharPoint = WSRunScanner::GetInclusiveNextCharPoint( {WSRunScanner::Option::OnlyEditableNodes}, aEndToDelete); // Blink-compat: Normalize white-spaces in first node only when not removing // its last character or no text nodes follow the first node. // If removing last character of first node and there are // following text nodes, white-spaces in following text node are // normalized instead. const bool removingLastCharOfStartNode = aStartToDelete.ContainerAs() != aEndToDelete.ContainerAs() || (aEndToDelete.IsEndOfContainer() && followingCharPoint.IsSet()); const bool maybeNormalizePrecedingWhiteSpaces = !removingLastCharOfStartNode && precedingCharPoint.IsSet() && !precedingCharPoint.IsEndOfContainer() && precedingCharPoint.ContainerAs() == aStartToDelete.ContainerAs() && precedingCharPoint.IsCharCollapsibleASCIISpaceOrNBSP(); const bool maybeNormalizeFollowingWhiteSpaces = followingCharPoint.IsSet() && !followingCharPoint.IsEndOfContainer() && (followingCharPoint.ContainerAs() == aEndToDelete.ContainerAs() || removingLastCharOfStartNode) && followingCharPoint.IsCharCollapsibleASCIISpaceOrNBSP(); if (!maybeNormalizePrecedingWhiteSpaces && !maybeNormalizeFollowingWhiteSpaces) { return; // There are no white-spaces. } // Next, consider the range to normalize. EditorDOMPointInText startToNormalize, endToNormalize; if (maybeNormalizePrecedingWhiteSpaces) { Maybe previousCharOffsetOfWhiteSpaces = HTMLEditUtils::GetPreviousNonCollapsibleCharOffset( precedingCharPoint, {WalkTextOption::TreatNBSPsCollapsible}); startToNormalize.Set(precedingCharPoint.ContainerAs(), previousCharOffsetOfWhiteSpaces.isSome() ? previousCharOffsetOfWhiteSpaces.value() + 1 : 0); MOZ_ASSERT(!startToNormalize.IsEndOfContainer()); } if (maybeNormalizeFollowingWhiteSpaces) { Maybe nextCharOffsetOfWhiteSpaces = HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset( followingCharPoint, {WalkTextOption::TreatNBSPsCollapsible}); if (nextCharOffsetOfWhiteSpaces.isSome()) { endToNormalize.Set(followingCharPoint.ContainerAs(), nextCharOffsetOfWhiteSpaces.value()); } else { endToNormalize.SetToEndOf(followingCharPoint.ContainerAs()); } MOZ_ASSERT(!endToNormalize.IsStartOfContainer()); } // Next, retrieve surrounding information of white-space sequence. // If we're removing first text node's last character, we need to // normalize white-spaces starts from another text node. In this case, // we need to lie for avoiding assertion in GenerateWhiteSpaceSequence(). CharPointData previousCharPointData = removingLastCharOfStartNode ? CharPointData::InDifferentTextNode(CharPointType::TextEnd) : GetPreviousCharPointDataForNormalizingWhiteSpaces( startToNormalize.IsSet() ? startToNormalize : aStartToDelete); CharPointData nextCharPointData = GetInclusiveNextCharPointDataForNormalizingWhiteSpaces( endToNormalize.IsSet() ? endToNormalize : aEndToDelete); // Next, compute number of white-spaces in start/end node. uint32_t lengthInStartNode = 0, lengthInEndNode = 0; if (startToNormalize.IsSet()) { MOZ_ASSERT(startToNormalize.ContainerAs() == aStartToDelete.ContainerAs()); lengthInStartNode = aStartToDelete.Offset() - startToNormalize.Offset(); MOZ_ASSERT(lengthInStartNode); } if (endToNormalize.IsSet()) { lengthInEndNode = endToNormalize.ContainerAs() == aEndToDelete.ContainerAs() ? endToNormalize.Offset() - aEndToDelete.Offset() : endToNormalize.Offset(); MOZ_ASSERT(lengthInEndNode); // If we normalize white-spaces in a text node, we can replace all of them // with one ReplaceTextTransaction. if (endToNormalize.ContainerAs() == aStartToDelete.ContainerAs()) { lengthInStartNode += lengthInEndNode; lengthInEndNode = 0; } } MOZ_ASSERT(lengthInStartNode + lengthInEndNode); // Next, generate normalized white-spaces. if (!lengthInEndNode) { HTMLEditor::GenerateWhiteSpaceSequence( aNormalizedWhiteSpacesInStartNode, lengthInStartNode, previousCharPointData, nextCharPointData); } else if (!lengthInStartNode) { HTMLEditor::GenerateWhiteSpaceSequence( aNormalizedWhiteSpacesInEndNode, lengthInEndNode, previousCharPointData, nextCharPointData); } else { // For making `GenerateWhiteSpaceSequence()` simpler, we should create // whole white-space sequence first, then, copy to the out params. nsAutoString whiteSpaces; HTMLEditor::GenerateWhiteSpaceSequence( whiteSpaces, lengthInStartNode + lengthInEndNode, previousCharPointData, nextCharPointData); aNormalizedWhiteSpacesInStartNode = Substring(whiteSpaces, 0, lengthInStartNode); aNormalizedWhiteSpacesInEndNode = Substring(whiteSpaces, lengthInStartNode); MOZ_ASSERT(aNormalizedWhiteSpacesInEndNode.Length() == lengthInEndNode); } // TODO: Shrink the replacing range and string as far as possible because // this may run a lot, i.e., HTMLEditor creates ReplaceTextTransaction // a lot for normalizing white-spaces. Then, each transaction shouldn't // have all white-spaces every time because once it's normalized, we // don't need to normalize all of the sequence again, but currently // we do. // Finally, extend the range. if (startToNormalize.IsSet()) { aStartToDelete = startToNormalize; } if (endToNormalize.IsSet()) { aEndToDelete = endToNormalize; } } Result HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces( const EditorDOMPointInText& aStartToDelete, const EditorDOMPointInText& aEndToDelete, TreatEmptyTextNodes aTreatEmptyTextNodes, DeleteDirection aDeleteDirection, const Element& aEditingHost) { MOZ_ASSERT(aStartToDelete.IsSetAndValid()); MOZ_ASSERT(aEndToDelete.IsSetAndValid()); MOZ_ASSERT(aStartToDelete.EqualsOrIsBefore(aEndToDelete)); // Use nsString for these replacing string because we should avoid to copy // the buffer from auto storange to ReplaceTextTransaction. nsString normalizedWhiteSpacesInFirstNode, normalizedWhiteSpacesInLastNode; // First, check whether we need to normalize white-spaces after deleting // the given range. EditorDOMPointInText startToDelete(aStartToDelete); EditorDOMPointInText endToDelete(aEndToDelete); ExtendRangeToDeleteWithNormalizingWhiteSpaces( startToDelete, endToDelete, normalizedWhiteSpacesInFirstNode, normalizedWhiteSpacesInLastNode); // If extended range is still collapsed, i.e., the caller just wants to // normalize white-space sequence, but there is no white-spaces which need to // be replaced, we need to do nothing here. if (startToDelete == endToDelete) { return CaretPoint(aStartToDelete.To()); } // Note that the container text node of startToDelete may be removed from // the tree if it becomes empty. Therefore, we need to track the point. EditorDOMPoint newCaretPosition; if (aStartToDelete.ContainerAs() == aEndToDelete.ContainerAs()) { newCaretPosition = aEndToDelete.To(); } else if (aDeleteDirection == DeleteDirection::Forward) { newCaretPosition.SetToEndOf(aStartToDelete.ContainerAs()); } else { newCaretPosition.Set(aEndToDelete.ContainerAs(), 0u); } // Then, modify the text nodes in the range. while (true) { AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(), &newCaretPosition); // Use ReplaceTextTransaction if we need to normalize white-spaces in // the first text node. if (!normalizedWhiteSpacesInFirstNode.IsEmpty()) { EditorDOMPoint trackingEndToDelete(endToDelete.ContainerAs(), endToDelete.Offset()); { AutoTrackDOMPoint trackEndToDelete(RangeUpdaterRef(), &trackingEndToDelete); uint32_t lengthToReplaceInFirstTextNode = startToDelete.ContainerAs() == trackingEndToDelete.ContainerAs() ? trackingEndToDelete.Offset() - startToDelete.Offset() : startToDelete.ContainerAs()->TextLength() - startToDelete.Offset(); Result replaceTextResult = ReplaceTextWithTransaction( MOZ_KnownLive(*startToDelete.ContainerAs()), startToDelete.Offset(), lengthToReplaceInFirstTextNode, normalizedWhiteSpacesInFirstNode); if (MOZ_UNLIKELY(replaceTextResult.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return replaceTextResult.propagateErr(); } // We'll return computed caret point, newCaretPosition, below. replaceTextResult.unwrap().IgnoreCaretPointSuggestion(); if (startToDelete.ContainerAs() == trackingEndToDelete.ContainerAs()) { MOZ_ASSERT(normalizedWhiteSpacesInLastNode.IsEmpty()); break; // There is no more text which we need to delete. } } MOZ_ASSERT(trackingEndToDelete.IsInTextNode()); endToDelete.Set(trackingEndToDelete.ContainerAs(), trackingEndToDelete.Offset()); // If the remaining range was modified by mutation event listener, // we should stop handling the deletion. startToDelete = EditorDOMPointInText::AtEndOf(*startToDelete.ContainerAs()); } // Delete ASCII whiteSpaces in the range simpley if there are some text // nodes which we don't need to replace their text. if (normalizedWhiteSpacesInLastNode.IsEmpty() || startToDelete.ContainerAs() != endToDelete.ContainerAs()) { // If we need to replace text in the last text node, we should // delete text before its previous text node. EditorDOMPointInText endToDeleteExceptReplaceRange = normalizedWhiteSpacesInLastNode.IsEmpty() ? endToDelete : EditorDOMPointInText(endToDelete.ContainerAs(), 0); if (startToDelete != endToDeleteExceptReplaceRange) { Result caretPointOrError = DeleteTextAndTextNodesWithTransaction(startToDelete, endToDeleteExceptReplaceRange, aTreatEmptyTextNodes); if (MOZ_UNLIKELY(caretPointOrError.isErr())) { NS_WARNING( "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed"); return caretPointOrError.propagateErr(); } nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CaretPoint::SuggestCaretPointTo() failed"); return Err(rv); } NS_WARNING_ASSERTION( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CaretPoint::SuggestCaretPointTo() failed, but ignored"); if (normalizedWhiteSpacesInLastNode.IsEmpty()) { break; // There is no more text which we need to delete. } if (MaybeNodeRemovalsObservedByDevTools() && (NS_WARN_IF(!endToDeleteExceptReplaceRange.IsSetAndValid()) || NS_WARN_IF(!endToDelete.IsSetAndValid()) || NS_WARN_IF(endToDelete.IsStartOfContainer()))) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // Then, replace the text in the last text node. startToDelete = endToDeleteExceptReplaceRange; } } // Replace ASCII whiteSpaces in the range and following character in the // last text node. MOZ_ASSERT(!normalizedWhiteSpacesInLastNode.IsEmpty()); MOZ_ASSERT(startToDelete.ContainerAs() == endToDelete.ContainerAs()); Result replaceTextResult = ReplaceTextWithTransaction( MOZ_KnownLive(*startToDelete.ContainerAs()), startToDelete.Offset(), endToDelete.Offset() - startToDelete.Offset(), normalizedWhiteSpacesInLastNode); if (MOZ_UNLIKELY(replaceTextResult.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return replaceTextResult.propagateErr(); } // We'll return computed caret point, newCaretPosition, below. replaceTextResult.unwrap().IgnoreCaretPointSuggestion(); break; } if (NS_WARN_IF(!newCaretPosition.IsSetAndValid()) || NS_WARN_IF(!newCaretPosition.GetContainer()->IsInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // Look for leaf node to put caret if we remove some empty inline ancestors // at new caret position. if (!newCaretPosition.IsInTextNode()) { if (const Element* editableBlockElementOrInlineEditingHost = HTMLEditUtils::GetInclusiveAncestorElement( *newCaretPosition.ContainerAs(), HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost, BlockInlineCheck::UseComputedDisplayStyle)) { // Try to put caret next to immediately after previous editable leaf. nsIContent* previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( newCaretPosition, {LeafNodeOption::TreatNonEditableNodeAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, editableBlockElementOrInlineEditingHost); if (previousContent && HTMLEditUtils::IsSimplyEditableNode(*previousContent) && !HTMLEditUtils::IsBlockElement( *previousContent, BlockInlineCheck::UseComputedDisplayStyle)) { newCaretPosition = previousContent->IsText() || HTMLEditUtils::IsContainerNode(*previousContent) ? EditorDOMPoint::AtEndOf(*previousContent) : EditorDOMPoint::After(*previousContent); } // But if the point is very first of a block element or immediately after // a child block, look for next editable leaf instead. else if (nsIContent* nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( newCaretPosition, {LeafNodeOption::TreatNonEditableNodeAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, editableBlockElementOrInlineEditingHost)) { if (HTMLEditUtils::IsSimplyEditableNode(*nextContent) && !HTMLEditUtils::IsBlockElement( *nextContent, BlockInlineCheck::UseComputedDisplayStyle)) { newCaretPosition = nextContent->IsText() || HTMLEditUtils::IsContainerNode(*nextContent) ? EditorDOMPoint(nextContent, 0) : EditorDOMPoint(nextContent); } } } } // For compatibility with Blink, we should move caret to end of previous // text node if it's direct previous sibling of the first text node in the // range. if (newCaretPosition.IsStartOfContainer() && newCaretPosition.IsInTextNode() && newCaretPosition.GetContainer()->GetPreviousSibling() && newCaretPosition.GetContainer()->GetPreviousSibling()->IsEditable() && newCaretPosition.GetContainer()->GetPreviousSibling()->IsText()) { newCaretPosition.SetToEndOf( newCaretPosition.GetContainer()->GetPreviousSibling()->AsText()); } MOZ_ASSERT(HTMLEditUtils::IsSimplyEditableNode( *newCaretPosition.ContainerAs())); { AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), &newCaretPosition); nsresult rv = EnsureNoFollowingUnnecessaryLineBreak( newCaretPosition, PreservePreformattedLineBreak::No, PaddingForEmptyBlock::Significant, aEditingHost); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed"); return Err(rv); } if (NS_WARN_IF(!newCaretPosition.IsSet())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } if (GetTopLevelEditSubAction() == EditSubAction::eDeleteSelectedContent) { AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(), &newCaretPosition); Result insertPaddingBRElementOrError = InsertPaddingBRElementIfNeeded( newCaretPosition, aEditingHost.IsContentEditablePlainTextOnly() ? nsIEditor::eNoStrip : nsIEditor::eStrip, aEditingHost); if (MOZ_UNLIKELY(insertPaddingBRElementOrError.isErr())) { NS_WARNING("HTMLEditor::InsertPaddingBRElementIfNeeded() failed"); return insertPaddingBRElementOrError.propagateErr(); } trackingNewCaretPosition.Flush(StopTracking::Yes); if (!newCaretPosition.IsInTextNode()) { insertPaddingBRElementOrError.unwrap().MoveCaretPointTo( newCaretPosition, {SuggestCaret::OnlyIfHasSuggestion}); } else { insertPaddingBRElementOrError.unwrap().IgnoreCaretPointSuggestion(); } if (!newCaretPosition.IsSetAndValid()) { NS_WARNING("Inserting
element caused unexpected DOM tree"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } return CaretPoint(std::move(newCaretPosition)); } Result HTMLEditor::JoinTextNodesWithNormalizeWhiteSpaces(Text& aLeftText, Text& aRightText) { if (EditorUtils::IsWhiteSpacePreformatted(aLeftText)) { Result joinResultOrError = JoinNodesWithTransaction(aLeftText, aRightText); NS_WARNING_ASSERTION(joinResultOrError.isOk(), "HTMLEditor::JoinNodesWithTransaction() failed"); return joinResultOrError; } const bool isNewLinePreformatted = EditorUtils::IsNewLinePreformatted(aLeftText); const auto IsCollapsibleChar = [&](char16_t aChar) { return (aChar == HTMLEditUtils::kNewLine && !isNewLinePreformatted) || nsCRT::IsAsciiSpace(aChar); }; const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) { return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar); }; const char16_t lastLeftChar = aLeftText.DataBuffer().SafeLastChar(); char16_t firstRightChar = aRightText.DataBuffer().SafeFirstChar(); const char16_t secondRightChar = aRightText.DataBuffer().GetLength() >= 2 ? aRightText.DataBuffer().CharAt(1u) : static_cast(0); if (IsCollapsibleCharOrNBSP(firstRightChar)) { // If the right Text starts only with a collapsible white-space and it'll // follow a non-collapsible char, we should make it an ASCII white-space. if (secondRightChar && !IsCollapsibleCharOrNBSP(secondRightChar) && lastLeftChar && !IsCollapsibleChar(lastLeftChar)) { if (firstRightChar != HTMLEditUtils::kSpace) { Result replaceWhiteSpaceResultOrError = ReplaceTextWithTransaction(aRightText, 0u, 1u, u" "_ns); if (MOZ_UNLIKELY(replaceWhiteSpaceResultOrError.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return replaceWhiteSpaceResultOrError.propagateErr(); } replaceWhiteSpaceResultOrError.unwrap().IgnoreCaretPointSuggestion(); if (NS_WARN_IF(aLeftText.GetNextSibling() != &aRightText)) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } firstRightChar = HTMLEditUtils::kSpace; } } // Otherwise, normalize the white-spaces before join, i.e., it will start // with an NBSP. else { Result atFirstVisibleThingOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter( *this, EditorDOMPoint(&aRightText, 0u), {}); if (MOZ_UNLIKELY(atFirstVisibleThingOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter() failed"); return atFirstVisibleThingOrError.propagateErr(); } if (!aRightText.GetParentNode()) { return JoinNodesResult(EditorDOMPoint::AtEndOf(aLeftText), aRightText); } } } else if (IsCollapsibleCharOrNBSP(lastLeftChar) && lastLeftChar != HTMLEditUtils::kSpace && aLeftText.DataBuffer().GetLength() >= 2u) { // If the last char of the left `Text` is a single white-space but not an // ASCII space, let's replace it with an ASCII space. const char16_t secondLastChar = aLeftText.DataBuffer().CharAt(aLeftText.DataBuffer().GetLength() - 2u); if (!IsCollapsibleCharOrNBSP(secondLastChar) && !IsCollapsibleCharOrNBSP(firstRightChar)) { Result replaceWhiteSpaceResultOrError = ReplaceTextWithTransaction( aLeftText, aLeftText.DataBuffer().GetLength() - 1u, 1u, u" "_ns); if (MOZ_UNLIKELY(replaceWhiteSpaceResultOrError.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return replaceWhiteSpaceResultOrError.propagateErr(); } replaceWhiteSpaceResultOrError.unwrap().IgnoreCaretPointSuggestion(); if (NS_WARN_IF(aLeftText.GetNextSibling() != &aRightText)) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } } Result joinResultOrError = JoinNodesWithTransaction(aLeftText, aRightText); if (MOZ_UNLIKELY(joinResultOrError.isErr())) { NS_WARNING("HTMLEditor::JoinNodesWithTransaction() failed"); return joinResultOrError; } JoinNodesResult joinResult = joinResultOrError.unwrap(); const EditorDOMPointInText startOfRightTextData = joinResult.AtJoinedPoint().GetAsInText(); if (NS_WARN_IF(!startOfRightTextData.IsSet()) || (firstRightChar && (NS_WARN_IF(startOfRightTextData.IsEndOfContainer()) || NS_WARN_IF(firstRightChar != startOfRightTextData.Char())))) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return std::move(joinResult); } // static bool HTMLEditor::CanInsertLineBreak(LineBreakType aLineBreakType, const nsIContent& aContent) { if (MOZ_UNLIKELY(!HTMLEditUtils::IsSimplyEditableNode(aContent))) { return false; } if (aLineBreakType == LineBreakType::BRElement) { return HTMLEditUtils::CanNodeContain(aContent, *nsGkAtoms::br); } MOZ_ASSERT(aLineBreakType == LineBreakType::Linefeed); const Element* const container = aContent.GetAsElementOrParentElement(); return container && HTMLEditUtils::CanNodeContain(*container, *nsGkAtoms::textTagName) && EditorUtils::IsNewLinePreformatted(*container); } Result HTMLEditor::InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded( const EditorDOMPoint& aPointToInsert, const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(aPointToInsert.IsSet()); if (!aPointToInsert.IsInContentNode() || NS_WARN_IF(!aPointToInsert.GetContainerOrContainerParentElement())) [[unlikely]] { return CreateLineBreakResult::NotHandled(); } // FYI: We don't need to put
if it reaches an inline editing host because // editing host has at least one line height by default even if it's empty and // it's tested by WPT to no
element is inserted in the cases. // If the point is not start of a line, we don't need to put a line break // here. const WSScanResult previousThing = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( {WSRunScanner::Option::OnlyEditableNodes}, aPointToInsert, &aEditingHost); if (!previousThing.ReachedLineBoundary()) { return CreateLineBreakResult::NotHandled(); } // If the point is not followed by a block boundary, we don't need to put a // line break here. const WSScanResult nextThing = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( {}, aPointToInsert, // FIXME: We should not limit the scan range into the editing host. // However, WSRunScanner does not check the visibility so that // following invisible text like the conent of