/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 sw=2 et tw=80: */ /* 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 "HTMLEditor.h" #include #include #include "HTMLEditUtils.h" #include "WSRunObject.h" #include "mozilla/Assertions.h" #include "mozilla/CSSEditUtils.h" #include "mozilla/CheckedInt.h" #include "mozilla/ContentIterator.h" #include "mozilla/EditAction.h" #include "mozilla/EditorDOMPoint.h" #include "mozilla/EditorUtils.h" #include "mozilla/InternalMutationEvent.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/OwningNonNull.h" #include "mozilla/Preferences.h" #include "mozilla/RangeUtils.h" #include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_* #include "mozilla/TextComposition.h" #include "mozilla/UniquePtr.h" #include "mozilla/Unused.h" #include "mozilla/dom/AncestorIterator.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/HTMLBRElement.h" #include "mozilla/dom/RangeBinding.h" #include "mozilla/dom/Selection.h" #include "mozilla/dom/StaticRange.h" #include "mozilla/mozalloc.h" #include "nsAString.h" #include "nsAlgorithm.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 "nsHTMLDocument.h" #include "nsIContent.h" #include "nsID.h" #include "nsIFrame.h" #include "nsINode.h" #include "nsLiteralString.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" #include "nsUnicharUtils.h" // Workaround for windows headers #ifdef SetProp # undef SetProp #endif class nsISupports; namespace mozilla { using namespace dom; using ChildBlockBoundary = HTMLEditUtils::ChildBlockBoundary; using StyleDifference = HTMLEditUtils::StyleDifference; /******************************************************** * first some helpful functors we will use ********************************************************/ static bool IsStyleCachePreservingSubAction(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::eMergeBlockContents: case EditSubAction::eRemoveList: case EditSubAction::eCreateOrChangeDefinitionListItem: case EditSubAction::eInsertElement: case EditSubAction::eInsertQuotation: case EditSubAction::eInsertQuotedText: return true; default: return false; } } template void HTMLEditor::SelectBRElementIfCollapsedInEmptyBlock( RangeBoundary& aStartRef, RangeBoundary& aEndRef) const; template void HTMLEditor::SelectBRElementIfCollapsedInEmptyBlock( RawRangeBoundary& aStartRef, RangeBoundary& aEndRef) const; template void HTMLEditor::SelectBRElementIfCollapsedInEmptyBlock( RangeBoundary& aStartRef, RawRangeBoundary& aEndRef) const; template void HTMLEditor::SelectBRElementIfCollapsedInEmptyBlock( RawRangeBoundary& aStartRef, RawRangeBoundary& aEndRef) const; template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const RangeBoundary& aStartRef, const RangeBoundary& aEndRef); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const RawRangeBoundary& aStartRef, const RangeBoundary& aEndRef); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const RangeBoundary& aStartRef, const RawRangeBoundary& aEndRef); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const RawRangeBoundary& aStartRef, const RawRangeBoundary& aEndRef); template already_AddRefed HTMLEditor::CreateRangeExtendedToHardLineStartAndEnd( const RangeBoundary& aStartRef, const RangeBoundary& aEndRef, EditSubAction aEditSubAction) const; template already_AddRefed HTMLEditor::CreateRangeExtendedToHardLineStartAndEnd( const RawRangeBoundary& aStartRef, const RangeBoundary& aEndRef, EditSubAction aEditSubAction) const; template already_AddRefed HTMLEditor::CreateRangeExtendedToHardLineStartAndEnd( const RangeBoundary& aStartRef, const RawRangeBoundary& aEndRef, EditSubAction aEditSubAction) const; template already_AddRefed HTMLEditor::CreateRangeExtendedToHardLineStartAndEnd( const RawRangeBoundary& aStartRef, const RawRangeBoundary& aEndRef, EditSubAction aEditSubAction) const; template EditorDOMPoint HTMLEditor::GetWhiteSpaceEndPoint( const RangeBoundary& aPoint, ScanDirection aScanDirection); template EditorDOMPoint HTMLEditor::GetWhiteSpaceEndPoint( const RawRangeBoundary& aPoint, ScanDirection aScanDirection); template EditorDOMPoint HTMLEditor::GetCurrentHardLineStartPoint( const RangeBoundary& aPoint, EditSubAction aEditSubAction) const; template EditorDOMPoint HTMLEditor::GetCurrentHardLineStartPoint( const RawRangeBoundary& aPoint, EditSubAction aEditSubAction) const; template EditorDOMPoint HTMLEditor::GetCurrentHardLineEndPoint( const RangeBoundary& aPoint) const; template EditorDOMPoint HTMLEditor::GetCurrentHardLineEndPoint( const RawRangeBoundary& aPoint) const; template nsIContent* HTMLEditor::FindNearEditableContent( const EditorDOMPoint& aPoint, nsIEditor::EDirection aDirection); template nsIContent* HTMLEditor::FindNearEditableContent( const EditorRawDOMPoint& aPoint, nsIEditor::EDirection aDirection); nsresult HTMLEditor::InitEditorContentAndSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); nsresult rv = TextEditor::InitEditorContentAndSelection(); if (NS_FAILED(rv)) { NS_WARNING("TextEditor::InitEditorContentAndSelection() failed"); return rv; } Element* bodyOrDocumentElement = GetRoot(); if (NS_WARN_IF(!bodyOrDocumentElement && !GetDocument())) { return NS_ERROR_FAILURE; } if (!bodyOrDocumentElement) { return NS_OK; } rv = InsertBRElementToEmptyListItemsAndTableCellsInRange( RawRangeBoundary(bodyOrDocumentElement, 0u), RawRangeBoundary(bodyOrDocumentElement, bodyOrDocumentElement->GetChildCount())); 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"); // Remember where our selection was before edit action took place: if (GetCompositionStartPoint().IsSet()) { // If there is composition string, let's remember current composition // range. TopLevelEditSubActionDataRef().mSelectedRange->StoreRange( GetCompositionStartPoint(), GetCompositionEndPoint()); } 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(!SelectionRefPtr()->RangeCount())) { aRv.Throw(NS_ERROR_UNEXPECTED); return; } if (const nsRange* range = SelectionRefPtr()->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 bool cacheInlineStyles; switch (aTopLevelEditSubAction) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eDeleteSelectedContent: cacheInlineStyles = true; break; default: cacheInlineStyles = IsStyleCachePreservingSubAction(aTopLevelEditSubAction); break; } if (cacheInlineStyles) { nsCOMPtr containerContent = nsIContent::FromNodeOrNull( aDirectionOfTopLevelEditSubAction == nsIEditor::eNext ? TopLevelEditSubActionDataRef().mSelectedRange->mEndContainer : TopLevelEditSubActionDataRef().mSelectedRange->mStartContainer); if (NS_WARN_IF(!containerContent)) { aRv.Throw(NS_ERROR_FAILURE); return; } nsresult rv = CacheInlineStyles(*containerContent); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::CacheInlineStyles() failed"); aRv.Throw(rv); return; } } // Stabilize the document against contenteditable count changes Document* document = GetDocument(); if (NS_WARN_IF(!document)) { aRv.Throw(NS_ERROR_FAILURE); return; } 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() failied"); // 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()); 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"); switch (GetTopLevelEditSubAction()) { case EditSubAction::eReplaceHeadWithHTMLSource: case EditSubAction::eCreatePaddingBRElementForEmptyEditor: return NS_OK; default: break; } 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); 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( *TopLevelEditSubActionDataRef().mChangedRange); if (extendedChangedRange) { MOZ_ASSERT(extendedChangedRange->IsPositioned()); // Use extended range temporarily. TopLevelEditSubActionDataRef().mChangedRange = std::move(extendedChangedRange); } break; } default: { RefPtr extendedChangedRange = CreateRangeExtendedToHardLineStartAndEnd( *TopLevelEditSubActionDataRef().mChangedRange, GetTopLevelEditSubAction()); if (extendedChangedRange) { 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) { EditorDOMPoint newCaretPosition = EditorBase::GetStartPoint(*SelectionRefPtr()); if (!newCaretPosition.IsSet()) { NS_WARNING("There was no selection range"); return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; } nsresult rv = InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( newCaretPosition); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() " "failed"); return rv; } } // 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 selection rv = RemoveEmptyNodesIn( MOZ_KnownLive(*TopLevelEditSubActionDataRef().mChangedRange)); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::RemoveEmptyNodesIn() failed"); return rv; } // attempt to transform any unneeded nbsp's into spaces after doing various // operations switch (GetTopLevelEditSubAction()) { case EditSubAction::eDeleteSelectedContent: if (TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces) { break; } [[fallthrough]]; case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eInsertLineBreak: case EditSubAction::eInsertParagraphSeparator: case EditSubAction::ePasteHTMLContent: case EditSubAction::eInsertHTMLSource: { // TODO: Temporarily, WhiteSpaceVisibilityKeeper replaces ASCII // white-spaces with NPSPs and then, we'll replace them with ASCII // white-spaces here. We should avoid this overwriting things as // far as possible because replacing characters in text nodes // causes running mutation event listeners which are really // expensive. // Adjust end of composition string if there is composition string. EditorRawDOMPoint pointToAdjust(GetCompositionEndPoint()); if (!pointToAdjust.IsSet()) { // Otherwise, adjust current selection start point. pointToAdjust = EditorBase::GetStartPoint(*SelectionRefPtr()); if (NS_WARN_IF(!pointToAdjust.IsSet())) { return NS_ERROR_FAILURE; } } rv = WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt( *this, pointToAdjust); if (NS_FAILED(rv)) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() " "failed"); return rv; } // also do this for original selection endpoints. // XXX Hmm, if `NormalizeVisibleWhiteSpacesAt()` runs mutation event // listener and that causes changing `mSelectedRange`, what we // should do? if (NS_WARN_IF( !TopLevelEditSubActionDataRef().mSelectedRange->IsSet())) { return NS_ERROR_FAILURE; } rv = WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt( *this, TopLevelEditSubActionDataRef().mSelectedRange->StartRawPoint()); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() " "failed, but ignored"); // we only need to handle old selection endpoint if it was different // from start if (TopLevelEditSubActionDataRef().mSelectedRange->IsCollapsed()) { nsresult rv = WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt( *this, TopLevelEditSubActionDataRef().mSelectedRange->EndRawPoint()); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() " "failed, but ignored"); } break; } default: break; } // If we created a new block, make sure caret is in it. if (TopLevelEditSubActionDataRef().mNewBlockElement && SelectionRefPtr()->IsCollapsed()) { nsresult rv = EnsureCaretInBlockElement( MOZ_KnownLive(*TopLevelEditSubActionDataRef().mNewBlockElement)); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::EnsureSelectionInBlockElement() failed, but ignored"); } // 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 && SelectionRefPtr()->IsCollapsed()) { switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eDeleteSelectedContent: 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 = IsStyleCachePreservingSubAction(GetTopLevelEditSubAction()); break; } if (reapplyCachedStyle) { DebugOnly rvIgnored = mTypeInState->UpdateSelState(SelectionRefPtr()); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "TypeInState::UpdateSelState() failed, but ignored"); rv = ReapplyCachedStyles(); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::ReapplyCachedStyles() failed"); return rv; } TopLevelEditSubActionDataRef().mCachedInlineStyles->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 && SelectionRefPtr()->IsCollapsed()) { SetSelectionInterlinePosition(); } return NS_OK; } EditActionResult HTMLEditor::CanHandleHTMLEditSubAction() const { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } // If there is not selection ranges, we should ignore the result. if (!SelectionRefPtr()->RangeCount()) { return EditActionCanceled(); } const nsRange* range = SelectionRefPtr()->GetRangeAt(0); nsINode* selStartNode = range->GetStartContainer(); if (NS_WARN_IF(!selStartNode)) { return EditActionResult(NS_ERROR_FAILURE); } if (!HTMLEditUtils::IsSimplyEditableNode(*selStartNode)) { return EditActionCanceled(); } nsINode* selEndNode = range->GetEndContainer(); if (NS_WARN_IF(!selEndNode)) { return EditActionResult(NS_ERROR_FAILURE); } if (selStartNode == selEndNode) { return EditActionIgnored(); } if (!HTMLEditUtils::IsSimplyEditableNode(*selEndNode)) { return EditActionCanceled(); } // 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 (!commonAncestor) { NS_WARNING( "AbstractRange::GetClosestCommonInclusiveAncestor() returned nullptr"); return EditActionResult(NS_ERROR_FAILURE); } return HTMLEditUtils::IsSimplyEditableNode(*commonAncestor) ? EditActionIgnored() : EditActionCanceled(); } 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::EnsureCaretNotAfterPaddingBRElement() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(SelectionRefPtr()->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 = SelectionRefPtr()->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()); nsCOMPtr previousEditableContent = GetPreviousEditableHTMLNode(atSelectionStart); if (!previousEditableContent || !EditorUtils::IsPaddingBRElementForEmptyLastLine( *previousEditableContent)) { return NS_OK; } if (!atSelectionStart.IsInContentNode()) { return NS_OK; } RefPtr blockElementAtSelectionStart = HTMLEditUtils::GetInclusiveAncestorBlockElement( *atSelectionStart.ContainerAsContent()); RefPtr parentBlockElementOfPreviousEditableContent = HTMLEditUtils::GetAncestorBlockElement(*previousEditableContent); if (!blockElementAtSelectionStart || blockElementAtSelectionStart != parentBlockElementOfPreviousEditableContent) { return NS_OK; } // If we are here then the selection is right after a padding
// element 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
element. EditorRawDOMPoint atPreviousEditableContent(previousEditableContent); nsresult rv = CollapseSelectionTo(atPreviousEditableContent); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::CollapseSelectionTo() failed"); return rv; } nsresult HTMLEditor::PrepareInlineStylesForCaret() { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(SelectionRefPtr()->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 (!IsStyleCachePreservingSubAction(GetTopLevelEditSubAction())) { TopLevelEditSubActionDataRef().mCachedInlineStyles->Clear(); } return NS_OK; } EditActionResult HTMLEditor::HandleInsertText( EditSubAction aEditSubAction, const nsAString& aInsertionString) { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText || aEditSubAction == EditSubAction::eInsertTextComingFromIME); EditActionResult result = CanHandleHTMLEditSubAction(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::CanHandleHTMLEditSubAction() failed"); 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 (!SelectionRefPtr()->IsCollapsed()) { nsresult rv = DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::DeleteSelectionAsSubAction(nsIEditor::eNone, " "nsIEditor::eNoStrip) failed"); return EditActionHandled(rv); } } nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRefPtr()->IsCollapsed()) { nsresult rv = EnsureCaretNotAfterPaddingBRElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureCaretNotAfterPaddingBRElement() " "failed, but ignored"); if (NS_SUCCEEDED(rv)) { nsresult rv = PrepareInlineStylesForCaret(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } RefPtr document = GetDocument(); if (NS_WARN_IF(!document)) { return EditActionHandled(NS_ERROR_FAILURE); } RefPtr firstRange = SelectionRefPtr()->GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionHandled(NS_ERROR_FAILURE); } // for every property that is set, insert a new inline style node // XXX CreateStyleForInsertText() adjusts selection automatically, but // it should just return the insertion point instead. rv = CreateStyleForInsertText(*firstRange); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed"); return EditActionHandled(rv); } firstRange = SelectionRefPtr()->GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionHandled(NS_ERROR_FAILURE); } EditorDOMPoint pointToInsert(firstRange->StartRef()); if (NS_WARN_IF(!pointToInsert.IsSet()) || NS_WARN_IF(!pointToInsert.IsInContentNode())) { return EditActionHandled(NS_ERROR_FAILURE); } MOZ_ASSERT(pointToInsert.IsSetAndValid()); // dont put text in places that can't have it if (!pointToInsert.IsInTextNode() && !HTMLEditUtils::CanNodeContain(*pointToInsert.GetContainer(), *nsGkAtoms::textTagName)) { NS_WARNING("Selection start container couldn't have text nodes"); return EditActionHandled(NS_ERROR_FAILURE); } if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) { EditorRawDOMPoint compositionStartPoint = GetCompositionStartPoint(); if (!compositionStartPoint.IsSet()) { compositionStartPoint = pointToInsert; } 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. nsresult rv = InsertTextWithTransaction(*document, aInsertionString, compositionStartPoint); if (NS_WARN_IF(Destroyed())) { return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::InsertTextWithTransaction() failed"); return EditActionHandled(rv); } EditorRawDOMPoint compositionEndPoint = GetCompositionEndPoint(); if (!compositionEndPoint.IsSet()) { compositionEndPoint = compositionStartPoint; } nsresult rv = WhiteSpaceVisibilityKeeper::ReplaceText( *this, aInsertionString, EditorDOMRange(compositionStartPoint, compositionEndPoint)); if (NS_FAILED(rv)) { NS_WARNING("WhiteSpaceVisibilityKeeper::ReplaceText() failed"); return EditActionHandled(rv); } compositionStartPoint = GetCompositionStartPoint(); compositionEndPoint = GetCompositionEndPoint(); if (NS_WARN_IF(!compositionStartPoint.IsSet()) || NS_WARN_IF(!compositionEndPoint.IsSet())) { // Mutation event listener has changed the DOM tree... return EditActionHandled(); } rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd( compositionStartPoint.ToRawRangeBoundary(), compositionEndPoint.ToRawRangeBoundary()); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::SetStartAndEnd() failed"); return EditActionHandled(rv); } MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText); // 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. bool isPRE = EditorUtils::IsContentPreformatted(*pointToInsert.ContainerAsContent()); // 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); int32_t pos = 0; constexpr auto newlineStr = NS_LITERAL_STRING_FROM_CSTRING(LFSTR); { AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToInsert); // 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 (isPRE || IsPlaintextEditor()) { while (pos != -1 && pos < static_cast(aInsertionString.Length())) { int32_t oldPos = pos; int32_t subStrLen; pos = aInsertionString.FindChar(nsCRT::LF, oldPos); if (pos != -1) { subStrLen = pos - oldPos; // if first char is newline, then use just it if (!subStrLen) { subStrLen = 1; } } else { subStrLen = aInsertionString.Length() - oldPos; pos = aInsertionString.Length(); } nsDependentSubstring subStr(aInsertionString, oldPos, subStrLen); // is it a return? if (subStr.Equals(newlineStr)) { RefPtr brElement = InsertBRElementWithTransaction(currentPoint, nsIEditor::eNone); if (NS_WARN_IF(Destroyed())) { return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } if (!brElement) { NS_WARNING( "HTMLEditor::InsertBRElementWithTransaction(eNone) failed"); return EditActionHandled(NS_ERROR_FAILURE); } pos++; if (brElement->GetNextSibling()) { pointToInsert.Set(brElement->GetNextSibling()); } else { pointToInsert.SetToEndOf(currentPoint.GetContainer()); } // XXX In most cases, pointToInsert and currentPoint are same here. // But if the
element has been moved to different point by // mutation observer, those points become different. currentPoint.SetAfter(brElement); NS_WARNING_ASSERTION(currentPoint.IsSet(), "Failed to set after the
element"); NS_WARNING_ASSERTION(currentPoint == pointToInsert, "Perhaps,
element position has been moved " "to different point " "by mutation observer"); } else { EditorRawDOMPoint pointAfterInsertedString; nsresult rv = InsertTextWithTransaction( *document, subStr, EditorRawDOMPoint(currentPoint), &pointAfterInsertedString); if (NS_WARN_IF(Destroyed())) { return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed"); return EditActionHandled(rv); } currentPoint = pointAfterInsertedString; pointToInsert = pointAfterInsertedString; } } } else { constexpr auto tabStr = u"\t"_ns; constexpr auto spacesStr = u" "_ns; char specialChars[] = {TAB, nsCRT::LF, 0}; nsAutoString insertionString(aInsertionString); // For FindCharInSet(). while (pos != -1 && pos < static_cast(insertionString.Length())) { int32_t oldPos = pos; int32_t subStrLen; pos = insertionString.FindCharInSet(specialChars, oldPos); if (pos != -1) { subStrLen = pos - oldPos; // if first char is newline, then use just it if (!subStrLen) { subStrLen = 1; } } else { subStrLen = insertionString.Length() - oldPos; pos = insertionString.Length(); } nsDependentSubstring subStr(insertionString, oldPos, subStrLen); // is it a tab? if (subStr.Equals(tabStr)) { EditorRawDOMPoint pointAfterInsertedSpaces; nsresult rv = WhiteSpaceVisibilityKeeper::InsertText( *this, spacesStr, currentPoint, &pointAfterInsertedSpaces); if (NS_FAILED(rv)) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertText() failed"); return EditActionHandled(rv); } pos++; MOZ_ASSERT(pointAfterInsertedSpaces.IsSet()); currentPoint = pointAfterInsertedSpaces; pointToInsert = pointAfterInsertedSpaces; } // is it a return? else if (subStr.Equals(newlineStr)) { Result, nsresult> result = WhiteSpaceVisibilityKeeper::InsertBRElement(*this, currentPoint); if (result.isErr()) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed"); return EditActionHandled(result.inspectErr()); } pos++; RefPtr newBRElement = result.unwrap(); MOZ_DIAGNOSTIC_ASSERT(newBRElement); if (newBRElement->GetNextSibling()) { pointToInsert.Set(newBRElement->GetNextSibling()); } else { pointToInsert.SetToEndOf(currentPoint.GetContainer()); } currentPoint.SetAfter(newBRElement); NS_WARNING_ASSERTION(currentPoint.IsSet(), "Failed to set after the new
element"); // XXX If the newBRElement has been moved or removed by mutation // observer, we hit this assert. We need to check if // newBRElement is in expected point, though, we must have // a lot of same bugs... NS_WARNING_ASSERTION( currentPoint == pointToInsert, "Perhaps, newBRElement has been moved or removed unexpectedly"); } else { EditorRawDOMPoint pointAfterInsertedString; nsresult rv = WhiteSpaceVisibilityKeeper::InsertText( *this, subStr, currentPoint, &pointAfterInsertedString); if (NS_FAILED(rv)) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertText() failed"); return EditActionHandled(rv); } MOZ_ASSERT(pointAfterInsertedString.IsSet()); currentPoint = pointAfterInsertedString; pointToInsert = pointAfterInsertedString; } } } // After this block, pointToInsert is updated by AutoTrackDOMPoint. } IgnoredErrorResult ignoredError; SelectionRefPtr()->SetInterlinePosition(false, ignoredError); NS_WARNING_ASSERTION(!ignoredError.Failed(), "Failed to unset interline position"); if (currentPoint.IsSet()) { nsresult rv = CollapseSelectionTo(currentPoint); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionHandled(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. if (currentPoint.IsSet()) { nsresult rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd( pointToInsert.ToRawRangeBoundary(), currentPoint.ToRawRangeBoundary()); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::SetStartAndEnd() failed"); return EditActionHandled(rv); } rv = TopLevelEditSubActionDataRef().mChangedRange->CollapseTo(pointToInsert); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::CollapseTo() failed"); return EditActionHandled(rv); } EditActionResult HTMLEditor::InsertParagraphSeparatorAsSubAction() { if (NS_WARN_IF(!mInitSucceeded)) { return EditActionIgnored(NS_ERROR_NOT_INITIALIZED); } EditActionResult result = CanHandleHTMLEditSubAction(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } // XXX This may be called by execCommand() with "insertParagraph". // In such case, naming the transaction "TypingTxnName" is odd. AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName, ScrollSelectionIntoView::Yes); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eInsertParagraphSeparator, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return EditActionResult(ignoredError.StealNSResult()); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); UndefineCaretBidiLevel(); // If the selection isn't collapsed, delete it. if (!SelectionRefPtr()->IsCollapsed()) { nsresult rv = DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eStrip); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed"); return EditActionIgnored(rv); } } nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRefPtr()->IsCollapsed()) { nsresult rv = EnsureCaretNotAfterPaddingBRElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureCaretNotAfterPaddingBRElement() " "failed, but ignored"); if (NS_SUCCEEDED(rv)) { nsresult rv = PrepareInlineStylesForCaret(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } // Split any mailcites in the way. Should we abort this if we encounter // table cell boundaries? if (IsMailEditor()) { EditorDOMPoint pointToSplit(EditorBase::GetStartPoint(*SelectionRefPtr())); if (NS_WARN_IF(!pointToSplit.IsSet())) { return EditActionIgnored(NS_ERROR_FAILURE); } EditActionResult result = SplitMailCiteElements(pointToSplit); if (result.Failed()) { NS_WARNING("HTMLEditor::SplitMailCiteElements() failed"); return result; } if (result.Handled()) { return result; } } // Smart splitting rules const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionIgnored(NS_ERROR_FAILURE); } EditorDOMPoint atStartOfSelection(firstRange->StartRef()); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return EditActionIgnored(NS_ERROR_FAILURE); } MOZ_ASSERT(atStartOfSelection.IsSetAndValid()); // Do nothing if the node is read-only if (!HTMLEditUtils::IsSimplyEditableNode( *atStartOfSelection.GetContainer())) { return EditActionCanceled(); } // If the active editing host is an inline element, or if the active editing // host is the block parent itself and we're configured to use
as a // paragraph separator, just append a
. RefPtr editingHost = GetActiveEditingHost(); if (NS_WARN_IF(!editingHost)) { return EditActionIgnored(NS_ERROR_FAILURE); } // Look for the nearest parent block. However, don't return error even if // there is no block parent here because in such case, i.e., editing host // is an inline element, we should insert
simply. RefPtr blockElement = atStartOfSelection.IsInContentNode() ? HTMLEditUtils::GetInclusiveAncestorBlockElement( *atStartOfSelection.ContainerAsContent(), editingHost) : nullptr; ParagraphSeparator separator = GetDefaultParagraphSeparator(); bool insertBRElement; // If there is no block parent in the editing host, i.e., the editing host // itself is also a non-block element, we should insert a
element. if (!blockElement) { // XXX Chromium checks if the CSS box of the editing host is a block. insertBRElement = true; } // If only the editing host is block, and the default paragraph separator // is
or the editing host cannot contain a

element, we should // insert a
element. else if (editingHost == blockElement) { insertBRElement = separator == ParagraphSeparator::br || !HTMLEditUtils::CanElementContainParagraph(*editingHost); } // If the nearest block parent is a single-line container declared in // the execCommand spec and not the editing host, we should separate the // block even if the default paragraph separator is
element. else if (HTMLEditUtils::IsSingleLineContainer(*blockElement)) { insertBRElement = false; } // Otherwise, unless there is no block ancestor which can contain

// element, we shouldn't insert a
element here. else { insertBRElement = true; for (Element* blockAncestor = blockElement; blockAncestor && insertBRElement; blockAncestor = HTMLEditUtils::GetAncestorBlockElement(*blockAncestor, editingHost)) { insertBRElement = !HTMLEditUtils::CanElementContainParagraph(*blockAncestor); } } // If we cannot insert a

/

element at the selection, we should insert // a
element instead. if (insertBRElement) { nsresult rv = InsertBRElement(atStartOfSelection); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::InsertBRElement() failed"); return EditActionIgnored(rv); } return EditActionHandled(); } if (editingHost == blockElement && separator != ParagraphSeparator::br) { // Insert a new block first MOZ_ASSERT(separator == ParagraphSeparator::div || separator == ParagraphSeparator::p); // FormatBlockContainerWithTransaction() creates AutoSelectionRestorer. // Therefore, even if it returns NS_OK, editor might have been destroyed // at restoring Selection. nsresult rv = FormatBlockContainerWithTransaction( MOZ_KnownLive(HTMLEditor::ToParagraphSeparatorTagName(separator))); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED) || NS_WARN_IF(Destroyed())) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } // We warn on failure, but don't handle it, because it might be harmless. // Instead we just check that a new block was actually created. NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::FormatBlockContainerWithTransaction() " "failed, but ignored"); firstRange = SelectionRefPtr()->GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionIgnored(NS_ERROR_FAILURE); } atStartOfSelection = firstRange->StartRef(); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return EditActionIgnored(NS_ERROR_FAILURE); } MOZ_ASSERT(atStartOfSelection.IsSetAndValid()); blockElement = atStartOfSelection.IsInContentNode() ? HTMLEditUtils::GetInclusiveAncestorBlockElement( *atStartOfSelection.ContainerAsContent(), editingHost) : nullptr; if (NS_WARN_IF(!blockElement)) { return EditActionIgnored(NS_ERROR_UNEXPECTED); } if (NS_WARN_IF(blockElement == editingHost)) { // Didn't create a new block for some reason, fall back to
rv = InsertBRElement(atStartOfSelection); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::InsertBRElement() failed"); return EditActionIgnored(rv); } return EditActionHandled(); } // Now, mNewBlockElement is last created block element for wrapping inline // elements around the caret position and AfterEditInner() will move // caret into it. However, it may be different from block parent of // the caret position. E.g., FormatBlockContainerWithTransaction() may // wrap following inline elements of a
element which is next sibling // of container of the caret. So, we need to adjust mNewBlockElement here // for avoiding jumping caret to odd position. TopLevelEditSubActionDataRef().mNewBlockElement = blockElement; } // If block is empty, populate with br. (For example, imagine a div that // contains the word "text". The user selects "text" and types return. // "Text" is deleted leaving an empty block. We want to put in one br to // make block have a line. Then code further below will put in a second br.) if (IsEmptyBlockElement(*blockElement, IgnoreSingleBR::No)) { AutoEditorDOMPointChildInvalidator lockOffset(atStartOfSelection); EditorDOMPoint endOfBlockParent; endOfBlockParent.SetToEndOf(blockElement); RefPtr brElement = InsertBRElementWithTransaction(endOfBlockParent); if (NS_WARN_IF(Destroyed())) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } if (!brElement) { NS_WARNING("HTMLEditor::InsertBRElementWithTransaction() failed"); return EditActionIgnored(NS_ERROR_FAILURE); } } RefPtr listItem = GetNearestAncestorListItemElement(*blockElement); if (listItem && listItem != editingHost) { nsresult rv = HandleInsertParagraphInListItemElement( *listItem, MOZ_KnownLive(*atStartOfSelection.GetContainer()), atStartOfSelection.Offset()); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::HandleInsertParagraphInListItemElement() " "failed, but ignored"); return EditActionHandled(); } if (HTMLEditUtils::IsHeader(*blockElement)) { // Headers: close (or split) header nsresult rv = HandleInsertParagraphInHeadingElement( *blockElement, MOZ_KnownLive(*atStartOfSelection.GetContainer()), atStartOfSelection.Offset()); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::HandleInsertParagraphInHeadingElement() " "failed, but ignored"); return EditActionHandled(); } // XXX Ideally, we should take same behavior with both

container and //

container. However, we are still using
as default // paragraph separator (non-standard) and we've split only

container // long time. Therefore, some web apps may depend on this behavior like // Gmail. So, let's use traditional odd behavior only when the default // paragraph separator is
. Otherwise, take consistent behavior // between

container and

container. if ((separator == ParagraphSeparator::br && blockElement->IsHTMLElement(nsGkAtoms::p)) || (separator != ParagraphSeparator::br && blockElement->IsAnyOfHTMLElements(nsGkAtoms::p, nsGkAtoms::div))) { AutoEditorDOMPointChildInvalidator lockOffset(atStartOfSelection); // Paragraphs: special rules to look for
s EditActionResult result = HandleInsertParagraphInParagraph(*blockElement); if (result.Failed()) { NS_WARNING("HTMLEditor::HandleInsertParagraphInParagraph() failed"); return result; } if (result.Handled()) { // Now, atStartOfSelection may be invalid because the left paragraph // may have less children than its offset. For avoiding warnings of // validation of EditorDOMPoint, we should not touch it anymore. lockOffset.Cancel(); return result; } // Fall through, if HandleInsertParagraphInParagraph() didn't handle it. MOZ_ASSERT(!result.Canceled(), "HandleInsertParagraphInParagraph() canceled this edit action, " "InsertParagraphSeparatorAsSubAction() needs to handle this " "action instead"); } // If nobody handles this edit action, let's insert new
at the selection. rv = InsertBRElement(atStartOfSelection); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::InsertBRElement() failed"); return EditActionIgnored(rv); } return EditActionHandled(); } nsresult HTMLEditor::InsertBRElement(const EditorDOMPoint& aPointToBreak) { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(!aPointToBreak.IsSet())) { return NS_ERROR_INVALID_ARG; } bool brElementIsAfterBlock = false, brElementIsBeforeBlock = false; // First, insert a
element. RefPtr brElement; if (IsPlaintextEditor()) { brElement = InsertBRElementWithTransaction(aPointToBreak); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (!brElement) { NS_WARNING("HTMLEditor::InsertBRElementWithTransaction() failed"); return NS_ERROR_FAILURE; } } else { EditorDOMPoint pointToBreak(aPointToBreak); WSRunScanner wsRunScanner(*this, pointToBreak); WSScanResult backwardScanResult = wsRunScanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(pointToBreak); if (backwardScanResult.Failed()) { NS_WARNING( "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom() failed"); return NS_ERROR_FAILURE; } brElementIsAfterBlock = backwardScanResult.ReachedBlockBoundary(); WSScanResult forwardScanResult = wsRunScanner.ScanNextVisibleNodeOrBlockBoundaryFrom(pointToBreak); if (forwardScanResult.Failed()) { NS_WARNING( "WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed"); return NS_ERROR_FAILURE; } brElementIsBeforeBlock = forwardScanResult.ReachedBlockBoundary(); // If the container of the break is a link, we need to split it and // insert new
between the split links. RefPtr linkNode = HTMLEditor::GetLinkElement(pointToBreak.GetContainer()); if (linkNode) { SplitNodeResult splitLinkNodeResult = SplitNodeDeepWithTransaction( *linkNode, pointToBreak, SplitAtEdges::eDoNotCreateEmptyContainer); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (splitLinkNodeResult.Failed()) { NS_WARNING( "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::" "eDoNotCreateEmptyContainer) failed"); return splitLinkNodeResult.Rv(); } pointToBreak = splitLinkNodeResult.SplitPoint(); } Result, nsresult> result = WhiteSpaceVisibilityKeeper::InsertBRElement(*this, pointToBreak); if (result.isErr()) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed"); return result.inspectErr(); } brElement = result.unwrap(); MOZ_ASSERT(brElement); } // If the
element has already been removed from the DOM tree by a // mutation event listener, don't continue handling this. if (NS_WARN_IF(!brElement->GetParentNode())) { return NS_ERROR_FAILURE; } if (brElementIsAfterBlock && brElementIsBeforeBlock) { // We just placed a
between block boundaries. This is the one case // where we want the selection to be before the br we just placed, as the // br will be on a new line, rather than at end of prior line. // XXX brElementIsAfterBlock and brElementIsBeforeBlock were set before // modifying the DOM tree. So, now, the
element may not be // between blocks. IgnoredErrorResult ignoredError; SelectionRefPtr()->SetInterlinePosition(true, ignoredError); NS_WARNING_ASSERTION( !ignoredError.Failed(), "Selection::SetInterlinePosition(true) failed, but ignored"); nsresult rv = CollapseSelectionTo(EditorRawDOMPoint(brElement)); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::CollapseSelectionTo() failed"); return rv; } EditorDOMPoint afterBRElement(brElement); DebugOnly advanced = afterBRElement.AdvanceOffset(); NS_WARNING_ASSERTION(advanced, "Failed to advance offset after the new
element"); WSScanResult forwardScanFromAfterBRElementResult = WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(*this, afterBRElement); if (forwardScanFromAfterBRElementResult.Failed()) { NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed"); return NS_ERROR_FAILURE; } if (forwardScanFromAfterBRElementResult.ReachedBRElement()) { // The next thing after the break we inserted is another break. Move the // second break to be the first break's sibling. This will prevent them // from being in different inline nodes, which would break // SetInterlinePosition(). It will also assure that if the user clicks // away and then clicks back on their new blank line, they will still get // the style from the line above. if (brElement->GetNextSibling() != forwardScanFromAfterBRElementResult.BRElementPtr()) { MOZ_ASSERT(forwardScanFromAfterBRElementResult.BRElementPtr()); nsresult rv = MoveNodeWithTransaction( MOZ_KnownLive(*forwardScanFromAfterBRElementResult.BRElementPtr()), afterBRElement); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed"); return rv; } } } // SetInterlinePosition(true) means we want the caret to stick to the // content on the "right". We want the caret to stick to whatever is past // the break. This is because the break is on the same line we were on, // but the next content will be on the following line. // An exception to this is if the break has a next sibling that is a block // node. Then we stick to the left to avoid an uber caret. nsIContent* nextSiblingOfBRElement = brElement->GetNextSibling(); IgnoredErrorResult ignoredError; SelectionRefPtr()->SetInterlinePosition( !(nextSiblingOfBRElement && HTMLEditUtils::IsBlockElement(*nextSiblingOfBRElement)), ignoredError); NS_WARNING_ASSERTION(!ignoredError.Failed(), "Selection::SetInterlinePosition() failed, but ignored"); nsresult rv = CollapseSelectionTo(afterBRElement); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::CollapseSelectionTo() failed"); return rv; } EditActionResult HTMLEditor::SplitMailCiteElements( const EditorDOMPoint& aPointToSplit) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(aPointToSplit.IsSet()); RefPtr citeNode = GetMostAncestorMailCiteElement(*aPointToSplit.GetContainer()); if (!citeNode) { return EditActionIgnored(); } EditorDOMPoint pointToSplit(aPointToSplit); // If our selection is just before a break, nudge it to be just after it. // This does two things for us. It saves us the trouble of having to add // a break here ourselves to preserve the "blockness" of the inline span // mailquote (in the inline case), and : // it means the break won't end up making an empty line that happens to be // inside a mailquote (in either inline or block case). // The latter can confuse a user if they click there and start typing, // because being in the mailquote may affect wrapping behavior, or font // color, etc. WSScanResult forwardScanFromPointToSplitResult = WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(*this, pointToSplit); if (forwardScanFromPointToSplitResult.Failed()) { return EditActionResult(NS_ERROR_FAILURE); } // If selection start point is before a break and it's inside the mailquote, // let's split it after the visible node. if (forwardScanFromPointToSplitResult.ReachedBRElement() && forwardScanFromPointToSplitResult.BRElementPtr() != citeNode && citeNode->Contains(forwardScanFromPointToSplitResult.BRElementPtr())) { pointToSplit = forwardScanFromPointToSplitResult.PointAfterContent(); } if (NS_WARN_IF(!pointToSplit.GetContainerAsContent())) { return EditActionIgnored(NS_ERROR_FAILURE); } SplitNodeResult splitCiteNodeResult = SplitNodeDeepWithTransaction( *citeNode, pointToSplit, SplitAtEdges::eDoNotCreateEmptyContainer); if (NS_WARN_IF(Destroyed())) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } if (splitCiteNodeResult.Failed()) { NS_WARNING( "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::" "eDoNotCreateEmptyContainer) failed"); return EditActionIgnored(splitCiteNodeResult.Rv()); } pointToSplit.Clear(); // Add an invisible
to the end of current cite node (If new left cite // has not been created, we're at the end of it. Otherwise, we're still at // the right node) if it was a of style="display: block". This is // important, since when serializing the cite to plain text, the span which // caused the visual break is discarded. So the added
will guarantee // that the serializer will insert a break where the user saw one. // FYI: splitCiteNodeResult grabs the previous node with nsCOMPtr. So, it's // safe to access previousNodeOfSplitPoint even after changing the DOM // tree and/or selection even though it's raw pointer. nsIContent* previousNodeOfSplitPoint = splitCiteNodeResult.GetPreviousNode(); if (previousNodeOfSplitPoint && previousNodeOfSplitPoint->IsHTMLElement(nsGkAtoms::span) && previousNodeOfSplitPoint->GetPrimaryFrame() && previousNodeOfSplitPoint->GetPrimaryFrame()->IsBlockFrameOrSubclass()) { nsCOMPtr lastChild = previousNodeOfSplitPoint->GetLastChild(); if (lastChild && !lastChild->IsHTMLElement(nsGkAtoms::br)) { // We ignore the result here. EditorDOMPoint endOfPreviousNodeOfSplitPoint; endOfPreviousNodeOfSplitPoint.SetToEndOf(previousNodeOfSplitPoint); RefPtr invisibleBRElement = InsertBRElementWithTransaction(endOfPreviousNodeOfSplitPoint); if (NS_WARN_IF(Destroyed())) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( invisibleBRElement, "HTMLEditor::InsertBRElementWithTransaction() failed, but ignored"); } } // In most cases,
should be inserted after current cite. However, if // left cite hasn't been created because the split point was start of the // cite node,
should be inserted before the current cite. EditorDOMPoint pointToInsertBRNode(splitCiteNodeResult.SplitPoint()); RefPtr brElement = InsertBRElementWithTransaction(pointToInsertBRNode); if (NS_WARN_IF(Destroyed())) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } if (!brElement) { NS_WARNING("HTMLEditor::InsertBRElementWithTransaction() failed"); return EditActionIgnored(NS_ERROR_FAILURE); } // Now, offset of pointToInsertBRNode is invalid. Let's clear it. pointToInsertBRNode.Clear(); // Want selection before the break, and on same line. EditorDOMPoint atBRElement(brElement); { AutoEditorDOMPointChildInvalidator lockOffset(atBRElement); IgnoredErrorResult ignoredError; SelectionRefPtr()->SetInterlinePosition(true, ignoredError); NS_WARNING_ASSERTION( !ignoredError.Failed(), "Selection::SetInterlinePosition(true) failed, but ignored"); nsresult rv = CollapseSelectionTo(atBRElement); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::CollapseSelectionTo() failed"); return EditActionIgnored(rv); } } // if citeNode wasn't a block, we might also want another break before it. // We need to examine the content both before the br we just added and also // just after it. If we don't have another br or block boundary adjacent, // then we will need a 2nd br added to achieve blank line that user expects. if (HTMLEditUtils::IsInlineElement(*citeNode)) { // Use DOM point which we tried to collapse to. EditorDOMPoint pointToCreateNewBRElement(atBRElement); WSScanResult backwardScanFromPointToCreateNewBRElementResult = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( *this, pointToCreateNewBRElement); if (backwardScanFromPointToCreateNewBRElementResult.Failed()) { NS_WARNING( "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary() failed"); return EditActionResult(NS_ERROR_FAILURE); } if (backwardScanFromPointToCreateNewBRElementResult .InNormalWhiteSpacesOrText() || backwardScanFromPointToCreateNewBRElementResult .ReachedSpecialContent()) { EditorRawDOMPoint pointAfterNewBRElement( EditorRawDOMPoint::After(pointToCreateNewBRElement)); NS_WARNING_ASSERTION(pointAfterNewBRElement.IsSet(), "Failed to set to after the
node"); WSScanResult forwardScanFromPointAfterNewBRElementResult = WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( *this, pointAfterNewBRElement); if (forwardScanFromPointAfterNewBRElementResult.Failed()) { NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed"); return EditActionResult(NS_ERROR_FAILURE); } if (forwardScanFromPointAfterNewBRElementResult .InNormalWhiteSpacesOrText() || forwardScanFromPointAfterNewBRElementResult.ReachedSpecialContent() || // In case we're at the very end. forwardScanFromPointAfterNewBRElementResult .ReachedCurrentBlockBoundary()) { brElement = InsertBRElementWithTransaction(pointToCreateNewBRElement); if (NS_WARN_IF(Destroyed())) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } if (!brElement) { NS_WARNING("HTMLEditor::InsertBRElementWithTransaction() failed"); return EditActionIgnored(NS_ERROR_FAILURE); } // Now, those points may be invalid. pointToCreateNewBRElement.Clear(); pointAfterNewBRElement.Clear(); } } } // delete any empty cites if (previousNodeOfSplitPoint && IsEmptyNode(*previousNodeOfSplitPoint, true, false)) { nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*previousNodeOfSplitPoint)); if (NS_WARN_IF(Destroyed())) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return EditActionIgnored(rv); } } if (citeNode && IsEmptyNode(*citeNode, true, false)) { nsresult rv = DeleteNodeWithTransaction(*citeNode); if (NS_WARN_IF(Destroyed())) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return EditActionIgnored(rv); } } return EditActionHandled(); } HTMLEditor::CharPointData HTMLEditor::GetPreviousCharPointDataForNormalizingWhiteSpaces( const EditorDOMPointInText& aPoint) const { MOZ_ASSERT(aPoint.IsSetAndValid()); if (!aPoint.IsStartOfContainer()) { return CharPointData::InSameTextNode( HTMLEditor::GetPreviousCharPointType(aPoint)); } EditorDOMPointInText previousCharPoint = WSRunScanner::GetPreviousEditableCharPoint(*this, 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)); } EditorDOMPointInText nextCharPoint = WSRunScanner::GetInclusiveNextEditableCharPoint(*this, aPoint); if (!nextCharPoint.IsSet()) { return CharPointData::InDifferentTextNode(CharPointType::TextEnd); } return CharPointData::InDifferentTextNode( HTMLEditor::GetCharPointType(nextCharPoint)); } // static void HTMLEditor::GenerateWhiteSpaceSequence( nsAString& aResult, uint32_t aLength, const CharPointData& aPreviousCharPointData, const CharPointData& aNextCharPointData) { MOZ_ASSERT(aResult.IsEmpty()); MOZ_ASSERT(aLength); // 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.IsWhiteSpace()); // 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.IsWhiteSpace()); 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.Assign(HTMLEditUtils::kSpace); return; } // If it's start or end of text, put an NBSP. if (aPreviousCharPointData.Type() == CharPointType::TextEnd || aNextCharPointData.Type() == CharPointType::TextEnd) { aResult.Assign(HTMLEditUtils::kNBSP); 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.Assign( aPreviousCharPointData.Type() == CharPointType::ASCIIWhiteSpace || aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace); return; } // Generate pairs of NBSP and ASCII white-space. aResult.SetLength(aLength); bool appendNBSP = true; // Basically, starts with an NBSP. char16_t* lastChar = aResult.EndWriting() - 1; for (char16_t* iter = aResult.BeginWriting(); 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 or an ASCII white-space, we need to // put an NBSP. *lastChar = aNextCharPointData.AcrossTextNodeBoundary() || aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace; } void HTMLEditor::ExtendRangeToDeleteWithNormalizingWhiteSpaces( EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete, nsAString& aNormalizedWhiteSpacesInStartNode, nsAString& 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. EditorDOMPointInText precedingCharPoint = WSRunScanner::GetPreviousEditableCharPoint(*this, aStartToDelete); EditorDOMPointInText followingCharPoint = WSRunScanner::GetInclusiveNextEditableCharPoint(*this, 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.ContainerAsText() != aEndToDelete.ContainerAsText() || (aEndToDelete.IsEndOfContainer() && followingCharPoint.IsSet()); const bool maybeNormalizePrecedingWhiteSpaces = !removingLastCharOfStartNode && precedingCharPoint.IsSet() && !precedingCharPoint.IsEndOfContainer() && precedingCharPoint.ContainerAsText() == aStartToDelete.ContainerAsText() && precedingCharPoint.IsCharASCIISpaceOrNBSP() && !EditorUtils::IsContentPreformatted( *precedingCharPoint.ContainerAsText()); const bool maybeNormalizeFollowingWhiteSpaces = followingCharPoint.IsSet() && !followingCharPoint.IsEndOfContainer() && (followingCharPoint.ContainerAsText() == aEndToDelete.ContainerAsText() || removingLastCharOfStartNode) && followingCharPoint.IsCharASCIISpaceOrNBSP() && !EditorUtils::IsContentPreformatted( *followingCharPoint.ContainerAsText()); if (!maybeNormalizePrecedingWhiteSpaces && !maybeNormalizeFollowingWhiteSpaces) { return; // There are no white-spaces. } // Next, consider the range to normalize. EditorDOMPointInText startToNormalize, endToNormalize; if (maybeNormalizePrecedingWhiteSpaces) { Maybe previousCharOffsetOfWhiteSpaces = HTMLEditUtils::GetPreviousCharOffsetExceptWhiteSpaces( precedingCharPoint); startToNormalize.Set(precedingCharPoint.ContainerAsText(), previousCharOffsetOfWhiteSpaces.isSome() ? previousCharOffsetOfWhiteSpaces.value() + 1 : 0); MOZ_ASSERT(!startToNormalize.IsEndOfContainer()); } if (maybeNormalizeFollowingWhiteSpaces) { Maybe nextCharOffsetOfWhiteSpaces = HTMLEditUtils::GetInclusiveNextCharOffsetExceptWhiteSpaces( followingCharPoint); if (nextCharOffsetOfWhiteSpaces.isSome()) { endToNormalize.Set(followingCharPoint.ContainerAsText(), nextCharOffsetOfWhiteSpaces.value()); } else { endToNormalize.SetToEndOf(followingCharPoint.ContainerAsText()); } 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.ContainerAsText() == aStartToDelete.ContainerAsText()); lengthInStartNode = aStartToDelete.Offset() - startToNormalize.Offset(); MOZ_ASSERT(lengthInStartNode); } if (endToNormalize.IsSet()) { lengthInEndNode = endToNormalize.ContainerAsText() == aEndToDelete.ContainerAsText() ? 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.ContainerAsText() == aStartToDelete.ContainerAsText()) { 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) { 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 EditorDOMPoint(aStartToDelete); } // 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.ContainerAsText() == aEndToDelete.ContainerAsText()) { newCaretPosition = aEndToDelete; } else if (aDeleteDirection == DeleteDirection::Forward) { newCaretPosition.SetToEndOf(aStartToDelete.ContainerAsText()); } else { newCaretPosition.Set(aEndToDelete.ContainerAsText(), 0); } // 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.ContainerAsText(), endToDelete.Offset()); { AutoTrackDOMPoint trackEndToDelete(RangeUpdaterRef(), &trackingEndToDelete); uint32_t lengthToReplaceInFirstTextNode = startToDelete.ContainerAsText() == trackingEndToDelete.ContainerAsText() ? trackingEndToDelete.Offset() - startToDelete.Offset() : startToDelete.ContainerAsText()->TextLength() - startToDelete.Offset(); nsresult rv = ReplaceTextWithTransaction( MOZ_KnownLive(*startToDelete.ContainerAsText()), startToDelete.Offset(), lengthToReplaceInFirstTextNode, normalizedWhiteSpacesInFirstNode); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return Err(rv); } if (startToDelete.ContainerAsText() == trackingEndToDelete.ContainerAsText()) { MOZ_ASSERT(normalizedWhiteSpacesInLastNode.IsEmpty()); break; // There is no more text which we need to delete. } } if (MayHaveMutationEventListeners( NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED) && (NS_WARN_IF(!trackingEndToDelete.IsSetAndValid()) || NS_WARN_IF(!trackingEndToDelete.IsInTextNode()))) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } MOZ_ASSERT(trackingEndToDelete.IsInTextNode()); endToDelete.Set(trackingEndToDelete.ContainerAsText(), trackingEndToDelete.Offset()); // If the remaining range was modified by mutation event listener, // we should stop handling the deletion. startToDelete = EditorDOMPointInText::AtEndOf(*startToDelete.ContainerAsText()); if (MayHaveMutationEventListeners( NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED) && NS_WARN_IF(!startToDelete.IsBefore(endToDelete))) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } // 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.ContainerAsText() != endToDelete.ContainerAsText()) { // 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.ContainerAsText(), 0); if (startToDelete != endToDeleteExceptReplaceRange) { nsresult rv = DeleteTextAndTextNodesWithTransaction( startToDelete, endToDeleteExceptReplaceRange, aTreatEmptyTextNodes); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed"); return Err(rv); } if (normalizedWhiteSpacesInLastNode.IsEmpty()) { break; // There is no more text which we need to delete. } if (MayHaveMutationEventListeners( NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED | NS_EVENT_BITS_MUTATION_NODEREMOVED | NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT | NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED) && (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.ContainerAsText() == endToDelete.ContainerAsText()); nsresult rv = ReplaceTextWithTransaction( MOZ_KnownLive(*startToDelete.ContainerAsText()), startToDelete.Offset(), endToDelete.Offset() - startToDelete.Offset(), normalizedWhiteSpacesInLastNode); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return Err(rv); } break; } if (!newCaretPosition.IsSetAndValid() || !newCaretPosition.GetContainer()->IsInComposedDoc()) { NS_WARNING( "HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces() got lost " "the modifying line"); 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 (nsIContent* currentBlock = HTMLEditUtils:: GetInclusiveAncestorEditableBlockElementOrInlineEditingHost( *newCaretPosition.ContainerAsContent())) { Element* editingHost = GetActiveEditingHost(); // Try to put caret next to immediately after previous editable leaf. nsIContent* previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( newCaretPosition, *currentBlock, editingHost); if (previousContent && !HTMLEditUtils::IsBlockElement(*previousContent)) { 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, *currentBlock, editingHost)) { 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()->IsText()) { newCaretPosition.SetToEndOf( newCaretPosition.GetContainer()->GetPreviousSibling()->AsText()); } { AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(), &newCaretPosition); nsresult rv = InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( newCaretPosition); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() failed"); return Err(rv); } } if (!newCaretPosition.IsSetAndValid()) { NS_WARNING("Inserting
element caused unexpected DOM tree"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return newCaretPosition; } nsresult HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( const EditorDOMPoint& aPointToInsert) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(aPointToInsert.IsSet()); if (!aPointToInsert.GetContainerAsContent()) { return NS_OK; } // If container of the point is not in a block, we don't need to put a // `
` element here. if (!HTMLEditUtils::IsBlockElement(*aPointToInsert.ContainerAsContent())) { return NS_OK; } WSRunScanner wsRunScanner(*this, aPointToInsert); // If the point is not start of a hard line, we don't need to put a `
` // element here. if (!wsRunScanner.StartsFromHardLineBreak()) { return NS_OK; } // If the point is not end of a hard line or the hard line does not end with // block boundary, we don't need to put a `
` element here. if (!wsRunScanner.EndsByBlockBoundary()) { return NS_OK; } // If we cannot insert a `
` element here, do nothing. if (!HTMLEditUtils::CanNodeContain(*aPointToInsert.GetContainer(), *nsGkAtoms::br)) { return NS_OK; } RefPtr brElement = InsertBRElementWithTransaction(aPointToInsert, nsIEditor::ePrevious); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(brElement, "HTMLEditor::InsertBRElementWithTransaction() failed"); return brElement ? NS_OK : NS_ERROR_FAILURE; } EditorDOMPoint HTMLEditor::GetGoodCaretPointFor( nsIContent& aContent, nsIEditor::EDirection aDirectionAndAmount) const { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(aDirectionAndAmount == nsIEditor::eNext || aDirectionAndAmount == nsIEditor::eNextWord || aDirectionAndAmount == nsIEditor::ePrevious || aDirectionAndAmount == nsIEditor::ePreviousWord || aDirectionAndAmount == nsIEditor::eToBeginningOfLine || aDirectionAndAmount == nsIEditor::eToEndOfLine); bool goingForward = (aDirectionAndAmount == nsIEditor::eNext || aDirectionAndAmount == nsIEditor::eNextWord || aDirectionAndAmount == nsIEditor::eToEndOfLine); // XXX Why don't we check whether the candidate position is enable or not? // When the result is not editable point, caret will be enclosed in // the non-editable content. // If we can put caret in aContent, return start or end in it. if (aContent.IsText() || HTMLEditUtils::IsContainerNode(aContent) || NS_WARN_IF(!aContent.GetParentNode())) { return EditorDOMPoint(&aContent, goingForward ? 0 : aContent.Length()); } // If we are going forward, put caret at aContent itself. if (goingForward) { return EditorDOMPoint(&aContent); } // If we are going backward, put caret to next node unless aContent is an // invisible `
` element. // XXX Shouldn't we put caret to first leaf of the next node? if (!aContent.IsHTMLElement(nsGkAtoms::br) || IsVisibleBRElement(&aContent)) { EditorDOMPoint ret(EditorDOMPoint::After(aContent)); NS_WARNING_ASSERTION(ret.IsSet(), "Failed to set after aContent"); return ret; } // Otherwise, we should put caret at the invisible `
` element. return EditorDOMPoint(&aContent); } EditActionResult HTMLEditor::MakeOrChangeListAndListItemAsSubAction( nsAtom& aListElementOrListItemElementTagName, const nsAString& aBulletType, SelectAllOfCurrentList aSelectAllOfCurrentList) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(&aListElementOrListItemElementTagName == nsGkAtoms::ul || &aListElementOrListItemElementTagName == nsGkAtoms::ol || &aListElementOrListItemElementTagName == nsGkAtoms::dl || &aListElementOrListItemElementTagName == nsGkAtoms::dd || &aListElementOrListItemElementTagName == nsGkAtoms::dt); if (NS_WARN_IF(!mInitSucceeded)) { return EditActionIgnored(NS_ERROR_NOT_INITIALIZED); } EditActionResult result = CanHandleHTMLEditSubAction(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (IsSelectionRangeContainerNotContent()) { NS_WARNING("Some selection containers are not content node, but ignored"); return EditActionIgnored(); } AutoPlaceholderBatch treatAsOneTransaction(*this, ScrollSelectionIntoView::Yes); // XXX EditSubAction::eCreateOrChangeDefinitionListItem and // EditSubAction::eCreateOrChangeList are treated differently in // HTMLEditor::MaybeSplitElementsAtEveryBRElement(). Only when // EditSubAction::eCreateOrChangeList, it splits inline nodes. // Currently, it shouldn't be done when we called for formatting // `
` or `
` by // HTMLEditor::MakeDefinitionListItemWithTransaction(). But this // difference may be a bug. We should investigate this later. IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, &aListElementOrListItemElementTagName == nsGkAtoms::dd || &aListElementOrListItemElementTagName == nsGkAtoms::dt ? EditSubAction::eCreateOrChangeDefinitionListItem : EditSubAction::eCreateOrChangeList, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return EditActionResult(ignoredError.StealNSResult()); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRefPtr()->IsCollapsed()) { nsresult rv = EnsureCaretNotAfterPaddingBRElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureCaretNotAfterPaddingBRElement() " "failed, but ignored"); if (NS_SUCCEEDED(rv)) { nsresult rv = PrepareInlineStylesForCaret(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } nsAtom* listTagName = nullptr; nsAtom* listItemTagName = nullptr; if (&aListElementOrListItemElementTagName == nsGkAtoms::ul || &aListElementOrListItemElementTagName == nsGkAtoms::ol) { listTagName = &aListElementOrListItemElementTagName; listItemTagName = nsGkAtoms::li; } else if (&aListElementOrListItemElementTagName == nsGkAtoms::dl) { listTagName = &aListElementOrListItemElementTagName; listItemTagName = nsGkAtoms::dd; } else if (&aListElementOrListItemElementTagName == nsGkAtoms::dd || &aListElementOrListItemElementTagName == nsGkAtoms::dt) { listTagName = nsGkAtoms::dl; listItemTagName = &aListElementOrListItemElementTagName; } else { NS_WARNING( "aListElementOrListItemElementTagName was neither list element name " "nor " "definition listitem element name"); return EditActionResult(NS_ERROR_INVALID_ARG); } // Expands selection range to include the immediate block parent, and then // further expands to include any ancestors whose children are all in the // range. if (!SelectionRefPtr()->IsCollapsed()) { nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeExtendSelectionToHardLineEdgesForBlockEditAction() " "failed"); return EditActionResult(rv); } } // ChangeSelectedHardLinesToList() creates AutoSelectionRestorer. // Therefore, even if it returns NS_OK, editor might have been destroyed // at restoring Selection. result = ChangeSelectedHardLinesToList(MOZ_KnownLive(*listTagName), MOZ_KnownLive(*listItemTagName), aBulletType, aSelectAllOfCurrentList); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::ChangeSelectedHardLinesToList() failed"); return result; } EditActionResult HTMLEditor::ChangeSelectedHardLinesToList( nsAtom& aListElementTagName, nsAtom& aListItemElementTagName, const nsAString& aBulletType, SelectAllOfCurrentList aSelectAllOfCurrentList) { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); AutoSelectionRestorer restoreSelectionLater(*this); AutoTArray, 64> arrayOfContents; Element* parentListElement = aSelectAllOfCurrentList == SelectAllOfCurrentList::Yes ? GetParentListElementAtSelection() : nullptr; if (parentListElement) { arrayOfContents.AppendElement( OwningNonNull(*parentListElement)); } else { AutoTransactionsConserveSelection dontChangeMySelection(*this); nsresult rv = SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges( arrayOfContents, EditSubAction::eCreateOrChangeList, CollectNonEditableNodes::No); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges(" "eCreateOrChangeList, CollectNonEditableNodes::No) failed"); return EditActionResult(rv); } } // check if all our nodes are
s, or empty inlines bool bOnlyBreaks = true; for (auto& content : arrayOfContents) { // if content is not a Break or empty inline, we're done if (!content->IsHTMLElement(nsGkAtoms::br) && !IsEmptyInlineNode(content)) { bOnlyBreaks = false; break; } } // if no nodes, we make empty list. Ditto if the user tried to make a list // of some # of breaks. if (arrayOfContents.IsEmpty() || bOnlyBreaks) { // if only breaks, delete them if (bOnlyBreaks) { for (auto& content : arrayOfContents) { // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content)); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return EditActionResult(rv); } } } const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionResult(NS_ERROR_FAILURE); } EditorDOMPoint atStartOfSelection(firstRange->StartRef()); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return EditActionResult(NS_ERROR_FAILURE); } // Make sure we can put a list here. if (!HTMLEditUtils::CanNodeContain(*atStartOfSelection.GetContainer(), aListElementTagName)) { return EditActionCanceled(); } SplitNodeResult splitAtSelectionStartResult = MaybeSplitAncestorsForInsertWithTransaction(aListElementTagName, atStartOfSelection); if (splitAtSelectionStartResult.Failed()) { NS_WARNING( "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() failed"); return EditActionResult(splitAtSelectionStartResult.Rv()); } RefPtr theList = CreateNodeWithTransaction( aListElementTagName, splitAtSelectionStartResult.SplitPoint()); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (!theList) { NS_WARNING("EditorBase::CreateNodeWithTransaction() failed"); return EditActionResult(NS_ERROR_FAILURE); } RefPtr theListItem = CreateNodeWithTransaction( aListItemElementTagName, EditorDOMPoint(theList, 0)); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (!theListItem) { NS_WARNING("EditorBase::CreateNodeWithTransaction() failed"); return EditActionResult(NS_ERROR_FAILURE); } // remember our new block for postprocessing TopLevelEditSubActionDataRef().mNewBlockElement = theListItem; // Put selection in new list item and don't restore the Selection. restoreSelectionLater.Abort(); nsresult rv = CollapseSelectionToStartOf(*theListItem); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::CollapseSelectionToStartOf() failed"); return EditActionResult(rv); } // if there is only one node in the array, and it is a list, div, or // blockquote, then look inside of it until we find inner list or content. if (arrayOfContents.Length() == 1) { if (Element* deepestDivBlockquoteOrListElement = GetDeepestEditableOnlyChildDivBlockquoteOrListElement( arrayOfContents[0])) { if (deepestDivBlockquoteOrListElement->IsAnyOfHTMLElements( nsGkAtoms::div, nsGkAtoms::blockquote)) { arrayOfContents.Clear(); CollectChildren(*deepestDivBlockquoteOrListElement, arrayOfContents, 0, CollectListChildren::No, CollectTableChildren::No, CollectNonEditableNodes::Yes); } else { arrayOfContents.ReplaceElementAt( 0, OwningNonNull(*deepestDivBlockquoteOrListElement)); } } } // Ok, now go through all the nodes and put then in the list, // or whatever is approriate. Wohoo! uint32_t countOfCollectedContents = arrayOfContents.Length(); RefPtr curList, prevListItem; for (uint32_t i = 0; i < countOfCollectedContents; i++) { // here's where we actually figure out what to do OwningNonNull content = arrayOfContents[i]; // make sure we don't assemble content that is in different table cells // into the same list. respect table cell boundaries when listifying. if (curList && HTMLEditor::NodesInDifferentTableElements(*curList, content)) { curList = nullptr; } // If current node is a `
` element, delete it and forget previous // list item element. // If current node is an empty inline node, just delete it. if (EditorUtils::IsEditableContent(content, EditorType::HTML) && (content->IsHTMLElement(nsGkAtoms::br) || IsEmptyInlineNode(content))) { nsresult rv = DeleteNodeWithTransaction(*content); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return EditActionResult(rv); } if (content->IsHTMLElement(nsGkAtoms::br)) { prevListItem = nullptr; } continue; } if (HTMLEditUtils::IsAnyListElement(content)) { // If we met a list element and current list element is not a descendant // of the list, append current node to end of the current list element. // Then, wrap it with list item element and delete the old container. if (curList && !EditorUtils::IsDescendantOf(*content, *curList)) { nsresult rv = MoveNodeToEndWithTransaction(*content, *curList); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return EditActionResult(rv); } CreateElementResult convertListTypeResult = ChangeListElementType(MOZ_KnownLive(*content->AsElement()), aListElementTagName, aListItemElementTagName); if (convertListTypeResult.Failed()) { NS_WARNING("HTMLEditor::ChangeListElementType() failed"); return EditActionResult(convertListTypeResult.Rv()); } rv = RemoveBlockContainerWithTransaction( MOZ_KnownLive(*convertListTypeResult.GetNewNode())); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::RemoveBlockContainerWithTransaction() failed"); return EditActionResult(rv); } prevListItem = nullptr; continue; } // If current list element is in found list element or we've not met a // list element, convert current list element to proper type. CreateElementResult convertListTypeResult = ChangeListElementType(MOZ_KnownLive(*content->AsElement()), aListElementTagName, aListItemElementTagName); if (convertListTypeResult.Failed()) { NS_WARNING("HTMLEditor::ChangeListElementType() failed"); return EditActionResult(convertListTypeResult.Rv()); } curList = convertListTypeResult.forget(); prevListItem = nullptr; continue; } EditorDOMPoint atContent(content); if (NS_WARN_IF(!atContent.IsSet())) { return EditActionResult(NS_ERROR_FAILURE); } MOZ_ASSERT(atContent.IsSetAndValid()); if (HTMLEditUtils::IsListItem(content)) { // If current list item element is not in proper list element, we need // to conver the list element. if (!atContent.IsContainerHTMLElement(&aListElementTagName)) { // If we've not met a list element or current node is not in current // list element, insert a list element at current node and set // current list element to the new one. if (!curList || EditorUtils::IsDescendantOf(*content, *curList)) { if (NS_WARN_IF(!atContent.GetContainerAsContent())) { return EditActionResult(NS_ERROR_FAILURE); } ErrorResult error; nsCOMPtr newLeftNode = SplitNodeWithTransaction(atContent, error); if (NS_WARN_IF(Destroyed())) { error.SuppressException(); return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (error.Failed()) { NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed"); return EditActionResult(error.StealNSResult()); } curList = CreateNodeWithTransaction( aListElementTagName, EditorDOMPoint(atContent.GetContainer())); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (!curList) { NS_WARNING("EditorBase::CreateNodeWithTransaction() failed"); return EditActionResult(NS_ERROR_FAILURE); } } // Then, move current node into current list element. nsresult rv = MoveNodeToEndWithTransaction(*content, *curList); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return EditActionResult(rv); } // Convert list item type if current node is different list item type. if (!content->IsHTMLElement(&aListItemElementTagName)) { RefPtr newListItemElement = ReplaceContainerWithTransaction( MOZ_KnownLive(*content->AsElement()), aListItemElementTagName); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (!newListItemElement) { NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed"); return EditActionResult(NS_ERROR_FAILURE); } } } else { // If we've not met a list element, set current list element to the // parent of current list item element. if (!curList) { curList = atContent.GetContainerAsElement(); NS_WARNING_ASSERTION( HTMLEditUtils::IsAnyListElement(curList), "Current list item parent is not a list element"); } // If current list item element is not a child of current list element, // move it into current list item. else if (atContent.GetContainer() != curList) { nsresult rv = MoveNodeToEndWithTransaction(*content, *curList); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return EditActionResult(rv); } } // Then, if current list item element is not proper type for current // list element, convert list item element to proper element. if (!content->IsHTMLElement(&aListItemElementTagName)) { RefPtr newListItemElement = ReplaceContainerWithTransaction( MOZ_KnownLive(*content->AsElement()), aListItemElementTagName); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (!newListItemElement) { NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed"); return EditActionResult(NS_ERROR_FAILURE); } } } Element* element = Element::FromNode(content); if (NS_WARN_IF(!element)) { return EditActionResult(NS_ERROR_FAILURE); } // If bullet type is specified, set list type attribute. // XXX Cannot we set type attribute before inserting the list item // element into the DOM tree? if (!aBulletType.IsEmpty()) { nsresult rv = SetAttributeWithTransaction( MOZ_KnownLive(*element), *nsGkAtoms::type, aBulletType); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::SetAttributeWithTransaction(nsGkAtoms::type) " "failed"); return EditActionResult(rv); } continue; } // Otherwise, remove list type attribute if there is. if (!element->HasAttr(nsGkAtoms::type)) { continue; } nsresult rv = RemoveAttributeWithTransaction(MOZ_KnownLive(*element), *nsGkAtoms::type); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::type) " "failed"); return EditActionResult(rv); } continue; } MOZ_ASSERT(!HTMLEditUtils::IsAnyListElement(content) && !HTMLEditUtils::IsListItem(content)); // If current node is a `
` element, replace it in the array with // its children. // XXX I think that this should be done when we collect the nodes above. // Then, we can change this `for` loop to ranged-for loop. if (content->IsHTMLElement(nsGkAtoms::div)) { prevListItem = nullptr; CollectChildren(*content, arrayOfContents, i + 1, CollectListChildren::Yes, CollectTableChildren::Yes, CollectNonEditableNodes::Yes); nsresult rv = RemoveContainerWithTransaction(MOZ_KnownLive(*content->AsElement())); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed"); return EditActionResult(rv); } // Extend the loop length to handle all children collected here. countOfCollectedContents = arrayOfContents.Length(); continue; } // If we've not met a list element, create a list element and make it // current list element. if (!curList) { SplitNodeResult splitCurNodeResult = MaybeSplitAncestorsForInsertWithTransaction(aListElementTagName, atContent); if (splitCurNodeResult.Failed()) { NS_WARNING( "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() failed"); return EditActionResult(splitCurNodeResult.Rv()); } prevListItem = nullptr; curList = CreateNodeWithTransaction(aListElementTagName, splitCurNodeResult.SplitPoint()); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (!curList) { NS_WARNING("EditorBase::CreateNodeWithTransaction() failed"); return EditActionResult(NS_ERROR_FAILURE); } // Set new block element of top level edit sub-action to the new list // element for setting selection into it. // XXX This must be wrong. If we're handling nested edit action, // we shouldn't overwrite the new block element. TopLevelEditSubActionDataRef().mNewBlockElement = curList; // atContent is now referring the right node with mOffset but // referring the left node with mRef. So, invalidate it now. atContent.Clear(); } // If we're currently handling contents of a list item and current node // is not a block element, move current node into the list item. if (HTMLEditUtils::IsInlineElement(content) && prevListItem) { nsresult rv = MoveNodeToEndWithTransaction(*content, *prevListItem); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return EditActionResult(rv); } continue; } // If current node is a paragraph, that means that it does not contain // block children so that we can just replace it with new list item // element and move it into current list element. // XXX This is too rough handling. If web apps modifies DOM tree directly, // any elements can have block elements as children. if (content->IsHTMLElement(nsGkAtoms::p)) { RefPtr newListItemElement = ReplaceContainerWithTransaction( MOZ_KnownLive(*content->AsElement()), aListItemElementTagName); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (!newListItemElement) { NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed"); return EditActionResult(NS_ERROR_FAILURE); } prevListItem = nullptr; nsresult rv = MoveNodeToEndWithTransaction(*newListItemElement, *curList); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return EditActionResult(rv); } // XXX Why don't we set `type` attribute here?? continue; } // If current node is not a paragraph, wrap current node with new list // item element and move it into current list element. RefPtr newListItemElement = InsertContainerWithTransaction(*content, aListItemElementTagName); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (!newListItemElement) { NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed"); return EditActionResult(NS_ERROR_FAILURE); } // If current node is not a block element, new list item should have // following inline nodes too. if (HTMLEditUtils::IsInlineElement(content)) { prevListItem = newListItemElement; } else { prevListItem = nullptr; } nsresult rv = MoveNodeToEndWithTransaction(*newListItemElement, *curList); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return EditActionResult(rv); } // XXX Why don't we set `type` attribute here?? } return EditActionHandled(); } nsresult HTMLEditor::RemoveListAtSelectionAsSubAction() { MOZ_ASSERT(IsEditActionDataAvailable()); EditActionResult result = CanHandleHTMLEditSubAction(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result.Rv(); } AutoPlaceholderBatch treatAsOneTransaction(*this, ScrollSelectionIntoView::Yes); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eRemoveList, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return ignoredError.StealNSResult(); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); if (!SelectionRefPtr()->IsCollapsed()) { nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeExtendSelectionToHardLineEdgesForBlockEditAction() " "failed"); return rv; } } AutoSelectionRestorer restoreSelectionLater(*this); AutoTArray, 64> arrayOfContents; { AutoTransactionsConserveSelection dontChangeMySelection(*this); nsresult rv = SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges( arrayOfContents, EditSubAction::eCreateOrChangeList, CollectNonEditableNodes::No); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges(" "eCreateOrChangeList, CollectNonEditableNodes::No) failed"); return rv; } } // Remove all non-editable nodes. Leave them be. // XXX SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges() // should return only editable contents when it's called with // CollectNonEditableNodes::No. for (int32_t i = arrayOfContents.Length() - 1; i >= 0; i--) { OwningNonNull& content = arrayOfContents[i]; if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) { arrayOfContents.RemoveElementAt(i); } } // Only act on lists or list items in the array for (auto& content : arrayOfContents) { // here's where we actually figure out what to do if (HTMLEditUtils::IsListItem(content)) { // unlist this listitem nsresult rv = LiftUpListItemElement(MOZ_KnownLive(*content->AsElement()), LiftUpFromAllParentListElements::Yes); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::LiftUpListItemElement(LiftUpFromAllParentListElements:" ":Yes) failed"); return rv; } continue; } if (HTMLEditUtils::IsAnyListElement(content)) { // node is a list, move list items out nsresult rv = DestroyListStructureRecursively(MOZ_KnownLive(*content->AsElement())); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DestroyListStructureRecursively() failed"); return rv; } continue; } } return NS_OK; } nsresult HTMLEditor::FormatBlockContainerWithTransaction(nsAtom& blockType) { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); if (!SelectionRefPtr()->IsCollapsed()) { nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeExtendSelectionToHardLineEdgesForBlockEditAction() " "failed"); return rv; } } AutoSelectionRestorer restoreSelectionLater(*this); AutoTransactionsConserveSelection dontChangeMySelection(*this); AutoTArray, 64> arrayOfContents; nsresult rv = SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges( arrayOfContents, EditSubAction::eCreateOrRemoveBlock, CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges(" "eCreateOrRemoveBlock, CollectNonEditableNodes::Yes) failed"); return rv; } // If there is no visible and editable nodes in the edit targets, make an // empty block. // XXX Isn't this odd if there are only non-editable visible nodes? if (IsEmptyOneHardLine(arrayOfContents)) { const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } EditorDOMPoint pointToInsertBlock(firstRange->StartRef()); if (&blockType == nsGkAtoms::normal || &blockType == nsGkAtoms::_empty) { if (!pointToInsertBlock.IsInContentNode()) { NS_WARNING( "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find " "block parent because container of the point is not content"); return NS_ERROR_FAILURE; } // We are removing blocks (going to "body text") RefPtr blockElement = HTMLEditUtils::GetInclusiveAncestorBlockElement( *pointToInsertBlock.ContainerAsContent()); if (!blockElement) { NS_WARNING( "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find " "block parent"); return NS_ERROR_FAILURE; } if (!HTMLEditUtils::IsFormatNode(blockElement)) { return NS_OK; } // If the first editable node after selection is a br, consume it. // Otherwise it gets pushed into a following block after the split, // which is visually bad. nsCOMPtr brContent = GetNextEditableHTMLNode(pointToInsertBlock); if (brContent && brContent->IsHTMLElement(nsGkAtoms::br)) { AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock); rv = DeleteNodeWithTransaction(*brContent); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return rv; } } // Do the splits! SplitNodeResult splitNodeResult = SplitNodeDeepWithTransaction( *blockElement, pointToInsertBlock, SplitAtEdges::eDoNotCreateEmptyContainer); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (splitNodeResult.Failed()) { NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed"); return splitNodeResult.Rv(); } EditorDOMPoint pointToInsertBRNode(splitNodeResult.SplitPoint()); // Put a
element at the split point brContent = InsertBRElementWithTransaction(pointToInsertBRNode); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (!brContent) { NS_WARNING("HTMLEditor::InsertBRElementWithTransaction() failed"); return NS_ERROR_FAILURE; } // Don't restore the selection restoreSelectionLater.Abort(); // Put selection at the split point nsresult rv = CollapseSelectionTo(EditorRawDOMPoint(brContent)); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::CollapseSelectionTo() failed"); return rv; } // We are making a block. Consume a br, if needed. nsCOMPtr brNode = GetNextEditableHTMLNodeInBlock(pointToInsertBlock); if (brNode && brNode->IsHTMLElement(nsGkAtoms::br)) { AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock); rv = DeleteNodeWithTransaction(*brNode); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return rv; } // We don't need to act on this node any more arrayOfContents.RemoveElement(brNode); } // Make sure we can put a block here. SplitNodeResult splitNodeResult = MaybeSplitAncestorsForInsertWithTransaction(blockType, pointToInsertBlock); if (splitNodeResult.Failed()) { NS_WARNING( "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() failed"); return splitNodeResult.Rv(); } RefPtr block = CreateNodeWithTransaction(blockType, splitNodeResult.SplitPoint()); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (!block) { NS_WARNING("CreateNodeWithTransaction() failed"); return NS_ERROR_FAILURE; } // Remember our new block for postprocessing TopLevelEditSubActionDataRef().mNewBlockElement = block; // Delete anything that was in the list of nodes while (!arrayOfContents.IsEmpty()) { OwningNonNull& content = arrayOfContents[0]; // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content)); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return rv; } arrayOfContents.RemoveElementAt(0); } // Don't restore the selection restoreSelectionLater.Abort(); // Put selection in new block rv = CollapseSelectionToStartOf(*block); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::CollapseSelectionToStartOf() failed"); return rv; } // Okay, now go through all the nodes and make the right kind of blocks, or // whatever is approriate. Woohoo! Note: blockquote is handled a little // differently. if (&blockType == nsGkAtoms::blockquote) { nsresult rv = MoveNodesIntoNewBlockquoteElement(arrayOfContents); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::MoveNodesIntoNewBlockquoteElement() failed"); return rv; } if (&blockType == nsGkAtoms::normal || &blockType == nsGkAtoms::_empty) { nsresult rv = RemoveBlockContainerElements(arrayOfContents); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::RemoveBlockContainerElements() failed"); return rv; } rv = CreateOrChangeBlockContainerElement(arrayOfContents, blockType); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::CreateOrChangeBlockContainerElement() failed"); return rv; } nsresult HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); if (!SelectionRefPtr()->IsCollapsed()) { return NS_OK; } const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } const RangeBoundary& atStartOfSelection = firstRange->StartRef(); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return NS_ERROR_FAILURE; } if (!atStartOfSelection.Container()->IsElement()) { return NS_OK; } OwningNonNull startContainerElement = *atStartOfSelection.Container()->AsElement(); nsresult rv = InsertPaddingBRElementForEmptyLastLineIfNeeded(startContainerElement); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::InsertPaddingBRElementForEmptyLastLineIfNeeded() failed"); return rv; } EditActionResult HTMLEditor::IndentAsSubAction() { MOZ_ASSERT(IsEditActionDataAvailable()); AutoPlaceholderBatch treatAsOneTransaction(*this, ScrollSelectionIntoView::Yes); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eIndent, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return EditActionResult(ignoredError.StealNSResult()); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); EditActionResult result = CanHandleHTMLEditSubAction(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (IsSelectionRangeContainerNotContent()) { NS_WARNING("Some selection containers are not content node, but ignored"); return EditActionIgnored(); } result |= HandleIndentAtSelection(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::HandleIndentAtSelection() failed"); return result; } if (IsSelectionRangeContainerNotContent()) { NS_WARNING("Mutation event listener might have changed selection"); return EditActionHandled(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } nsresult rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() failed"); return result.SetResult(rv); } // Helper for Handle[CSS|HTML]IndentAtSelectionInternal nsresult HTMLEditor::IndentListChild(RefPtr* aCurList, const EditorDOMPoint& aCurPoint, nsIContent& aContent) { MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(aCurPoint.GetContainer()), "unexpected container"); MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); // some logic for putting list items into nested lists... // Check for whether we should join a list that follows aContent. // We do this if the next element is a list, and the list is of the // same type (li/ol) as aContent was a part it. if (nsIContent* nextEditableSibling = GetNextHTMLSibling(&aContent, SkipWhiteSpace::Yes)) { if (HTMLEditUtils::IsAnyListElement(nextEditableSibling) && aCurPoint.GetContainer()->NodeInfo()->NameAtom() == nextEditableSibling->NodeInfo()->NameAtom() && aCurPoint.GetContainer()->NodeInfo()->NamespaceID() == nextEditableSibling->NodeInfo()->NamespaceID()) { nsresult rv = MoveNodeWithTransaction( aContent, EditorDOMPoint(nextEditableSibling, 0)); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EdigtorBase::MoveNodeWithTransaction() failed"); return rv; } } // Check for whether we should join a list that preceeds aContent. // We do this if the previous element is a list, and the list is of // the same type (li/ol) as aContent was a part of. if (nsCOMPtr previousEditableSibling = GetPriorHTMLSibling(&aContent, SkipWhiteSpace::Yes)) { if (HTMLEditUtils::IsAnyListElement(previousEditableSibling) && aCurPoint.GetContainer()->NodeInfo()->NameAtom() == previousEditableSibling->NodeInfo()->NameAtom() && aCurPoint.GetContainer()->NodeInfo()->NamespaceID() == previousEditableSibling->NodeInfo()->NamespaceID()) { nsresult rv = MoveNodeToEndWithTransaction(aContent, *previousEditableSibling); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::MoveNodeToEndWithTransaction() failed"); return rv; } } // check to see if aCurList is still appropriate. Which it is if // aContent is still right after it in the same list. nsIContent* previousEditableSibling = *aCurList ? GetPriorHTMLSibling(&aContent, SkipWhiteSpace::Yes) : nullptr; if (!*aCurList || (previousEditableSibling && previousEditableSibling != *aCurList)) { nsAtom* containerName = aCurPoint.GetContainer()->NodeInfo()->NameAtom(); // Create a new nested list of correct type. SplitNodeResult splitNodeResult = MaybeSplitAncestorsForInsertWithTransaction( MOZ_KnownLive(*containerName), aCurPoint); if (splitNodeResult.Failed()) { NS_WARNING( "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() failed"); return splitNodeResult.Rv(); } *aCurList = CreateNodeWithTransaction(MOZ_KnownLive(*containerName), splitNodeResult.SplitPoint()); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (!*aCurList) { NS_WARNING("EditorBase::CreateNodeWithTransaction() failed"); return NS_ERROR_FAILURE; } // aCurList is now the correct thing to put aContent in // remember our new block for postprocessing TopLevelEditSubActionDataRef().mNewBlockElement = *aCurList; } // tuck the node into the end of the active list RefPtr container = *aCurList; nsresult rv = MoveNodeToEndWithTransaction(aContent, *container); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::MoveNodeToEndWithTransaction() failed"); return rv; } EditActionResult HTMLEditor::HandleIndentAtSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRefPtr()->IsCollapsed()) { nsresult rv = EnsureCaretNotAfterPaddingBRElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureCaretNotAfterPaddingBRElement() " "failed, but ignored"); if (NS_SUCCEEDED(rv)) { nsresult rv = PrepareInlineStylesForCaret(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } if (IsSelectionRangeContainerNotContent()) { NS_WARNING("Mutation event listener might have changed the selection"); return EditActionHandled(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } if (IsCSSEnabled()) { nsresult rv = HandleCSSIndentAtSelection(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::HandleCSSIndentAtSelection() failed"); return EditActionHandled(rv); } rv = HandleHTMLIndentAtSelection(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::HandleHTMLIndent() failed"); return EditActionHandled(rv); } nsresult HTMLEditor::HandleCSSIndentAtSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); if (!SelectionRefPtr()->IsCollapsed()) { nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeExtendSelectionToHardLineEdgesForBlockEditAction() " "failed"); return rv; } } // HandleCSSIndentAtSelectionInternal() creates AutoSelectionRestorer. // Therefore, even if it returns NS_OK, editor might have been destroyed // at restoring Selection. nsresult rv = HandleCSSIndentAtSelectionInternal(); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::HandleCSSIndentAtSelectionInternal() failed"); return rv; } nsresult HTMLEditor::HandleCSSIndentAtSelectionInternal() { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); AutoSelectionRestorer restoreSelectionLater(*this); AutoTArray, 64> arrayOfContents; // short circuit: detect case of collapsed selection inside an
  • . // just sublist that
  • . This prevents bug 97797. if (SelectionRefPtr()->IsCollapsed()) { EditorRawDOMPoint atCaret(EditorBase::GetStartPoint(*SelectionRefPtr())); if (NS_WARN_IF(!atCaret.IsSet())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(atCaret.IsInContentNode()); Element* blockElement = HTMLEditUtils::GetInclusiveAncestorBlockElement( *atCaret.ContainerAsContent()); if (blockElement && HTMLEditUtils::IsListItem(blockElement)) { arrayOfContents.AppendElement(*blockElement); } } if (arrayOfContents.IsEmpty()) { nsresult rv = SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges( arrayOfContents, EditSubAction::eIndent, CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges(" "eIndent, CollectNonEditableNodes::Yes) failed"); return rv; } } // If there is no visible and editable nodes in the edit targets, make an // empty block. // XXX Isn't this odd if there are only non-editable visible nodes? if (IsEmptyOneHardLine(arrayOfContents)) { // get selection location const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } EditorDOMPoint atStartOfSelection(firstRange->StartRef()); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return NS_ERROR_FAILURE; } // make sure we can put a block here SplitNodeResult splitNodeResult = MaybeSplitAncestorsForInsertWithTransaction(*nsGkAtoms::div, atStartOfSelection); if (splitNodeResult.Failed()) { NS_WARNING( "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction(nsGkAtoms::" "div) failed"); return splitNodeResult.Rv(); } RefPtr theBlock = CreateNodeWithTransaction( *nsGkAtoms::div, splitNodeResult.SplitPoint()); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (!theBlock) { NS_WARNING( "EditorBase::CreateNodeWithTransaction(nsGkAtoms::div) failed"); return NS_ERROR_FAILURE; } // remember our new block for postprocessing TopLevelEditSubActionDataRef().mNewBlockElement = theBlock; nsresult rv = ChangeMarginStart(*theBlock, ChangeMargin::Increase); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::ChangeMarginStart() failed, but ignored"); // delete anything that was in the list of nodes // XXX We don't need to remove the nodes from the array for performance. while (!arrayOfContents.IsEmpty()) { OwningNonNull& content = arrayOfContents[0]; // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content)); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return rv; } arrayOfContents.RemoveElementAt(0); } // Don't restore the selection restoreSelectionLater.Abort(); // put selection in new block rv = CollapseSelectionToStartOf(*theBlock); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::CollapseSelectionToStartOf() failed"); return rv; } // Ok, now go through all the nodes and put them in a blockquote, // or whatever is appropriate. RefPtr curList, curQuote; for (OwningNonNull& content : arrayOfContents) { // Here's where we actually figure out what to do. EditorDOMPoint atContent(content); if (NS_WARN_IF(!atContent.IsSet())) { continue; } // Ignore all non-editable nodes. Leave them be. // XXX We ignore non-editable nodes here, but not so in the above block. if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) { continue; } if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) { // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. nsresult rv = IndentListChild(&curList, atContent, MOZ_KnownLive(content)); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::IndentListChild() failed"); return rv; } continue; } // Not a list item. if (HTMLEditUtils::IsBlockElement(content)) { nsresult rv = ChangeMarginStart(MOZ_KnownLive(*content->AsElement()), ChangeMargin::Increase); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::ChangeMarginStart() failed, but ignored"); curQuote = nullptr; continue; } if (!curQuote) { // First, check that our element can contain a div. if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(), *nsGkAtoms::div)) { return NS_OK; // cancelled } SplitNodeResult splitNodeResult = MaybeSplitAncestorsForInsertWithTransaction(*nsGkAtoms::div, atContent); if (splitNodeResult.Failed()) { NS_WARNING( "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction(nsGkAtoms:" ":div) failed"); return splitNodeResult.Rv(); } curQuote = CreateNodeWithTransaction(*nsGkAtoms::div, splitNodeResult.SplitPoint()); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (!curQuote) { NS_WARNING( "EditorBase::CreateNodeWithTransaction(nsGkAtoms::div) failed"); return NS_ERROR_FAILURE; } nsresult rv = ChangeMarginStart(*curQuote, ChangeMargin::Increase); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::ChangeMarginStart() failed, but ignored"); // remember our new block for postprocessing TopLevelEditSubActionDataRef().mNewBlockElement = curQuote; // curQuote is now the correct thing to put content in } // tuck the node into the end of the active blockquote // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. nsresult rv = MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *curQuote); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return rv; } } return NS_OK; } nsresult HTMLEditor::HandleHTMLIndentAtSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); if (!SelectionRefPtr()->IsCollapsed()) { nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeExtendSelectionToHardLineEdgesForBlockEditAction() " "failed"); return rv; } } // HandleHTMLIndentAtSelectionInternal() creates AutoSelectionRestorer. // Therefore, even if it returns NS_OK, editor might have been destroyed // at restoring Selection. nsresult rv = HandleHTMLIndentAtSelectionInternal(); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::HandleHTMLIndentAtSelectionInternal() failed"); return rv; } nsresult HTMLEditor::HandleHTMLIndentAtSelectionInternal() { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); AutoSelectionRestorer restoreSelectionLater(*this); // convert the selection ranges into "promoted" selection ranges: // this basically just expands the range to include the immediate // block parent, and then further expands to include any ancestors // whose children are all in the range AutoTArray, 4> arrayOfRanges; GetSelectionRangesExtendedToHardLineStartAndEnd(arrayOfRanges, EditSubAction::eIndent); // use these ranges to construct a list of nodes to act on. AutoTArray, 64> arrayOfContents; nsresult rv = SplitInlinesAndCollectEditTargetNodes( arrayOfRanges, arrayOfContents, EditSubAction::eIndent, CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::SplitInlinesAndCollectEditTargetNodes(eIndent, " "CollectNonEditableNodes::Yes) failed"); return rv; } // If there is no visible and editable nodes in the edit targets, make an // empty block. // XXX Isn't this odd if there are only non-editable visible nodes? if (IsEmptyOneHardLine(arrayOfContents)) { const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } EditorDOMPoint atStartOfSelection(firstRange->StartRef()); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return NS_ERROR_FAILURE; } // Make sure we can put a block here. SplitNodeResult splitNodeResult = MaybeSplitAncestorsForInsertWithTransaction(*nsGkAtoms::blockquote, atStartOfSelection); if (splitNodeResult.Failed()) { NS_WARNING( "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction(nsGkAtoms::" "blockquote) failed"); return splitNodeResult.Rv(); } RefPtr theBlock = CreateNodeWithTransaction( *nsGkAtoms::blockquote, splitNodeResult.SplitPoint()); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (!theBlock) { NS_WARNING("EditorBase::CreateNodeWithTransaction() failed"); return NS_ERROR_FAILURE; } // remember our new block for postprocessing TopLevelEditSubActionDataRef().mNewBlockElement = theBlock; // delete anything that was in the list of nodes // XXX We don't need to remove the nodes from the array for performance. while (!arrayOfContents.IsEmpty()) { OwningNonNull& content = arrayOfContents[0]; // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content)); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return rv; } arrayOfContents.RemoveElementAt(0); } // Don't restore the selection restoreSelectionLater.Abort(); nsresult rv = CollapseSelectionToStartOf(*theBlock); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::CollapseSelectionToStartOf() failed"); return rv; } // Ok, now go through all the nodes and put them in a blockquote, // or whatever is appropriate. Wohoo! RefPtr curList, curQuote, indentedLI; for (OwningNonNull& content : arrayOfContents) { // Here's where we actually figure out what to do. EditorDOMPoint atContent(content); if (NS_WARN_IF(!atContent.IsSet())) { continue; } // Ignore all non-editable nodes. Leave them be. // XXX We ignore non-editable nodes here, but not so in the above block. if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) { continue; } if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) { // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. nsresult rv = IndentListChild(&curList, atContent, MOZ_KnownLive(content)); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::IndentListChild() failed"); return rv; } // forget curQuote, if any curQuote = nullptr; continue; } // Not a list item, use blockquote? // if we are inside a list item, we don't want to blockquote, we want // to sublist the list item. We may have several nodes listed in the // array of nodes to act on, that are in the same list item. Since // we only want to indent that li once, we must keep track of the most // recent indented list item, and not indent it if we find another node // to act on that is still inside the same li. if (RefPtr listItem = GetNearestAncestorListItemElement(content)) { if (indentedLI == listItem) { // already indented this list item continue; } // check to see if curList is still appropriate. Which it is if // content is still right after it in the same list. nsIContent* previousEditableSibling = curList ? GetPriorHTMLSibling(listItem) : nullptr; if (!curList || (previousEditableSibling && previousEditableSibling != curList)) { EditorDOMPoint atListItem(listItem); if (NS_WARN_IF(!listItem)) { return NS_ERROR_FAILURE; } nsAtom* containerName = atListItem.GetContainer()->NodeInfo()->NameAtom(); // Create a new nested list of correct type. SplitNodeResult splitNodeResult = MaybeSplitAncestorsForInsertWithTransaction( MOZ_KnownLive(*containerName), atListItem); if (splitNodeResult.Failed()) { NS_WARNING( "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() " "failed"); return splitNodeResult.Rv(); } curList = CreateNodeWithTransaction(MOZ_KnownLive(*containerName), splitNodeResult.SplitPoint()); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (!curList) { NS_WARNING("HTMLEditor::CreateNodeWithTransaction() failed"); return NS_ERROR_FAILURE; } } rv = MoveNodeToEndWithTransaction(*listItem, *curList); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return rv; } // remember we indented this li indentedLI = listItem; continue; } // need to make a blockquote to put things in if we haven't already, // or if this node doesn't go in blockquote we used earlier. // One reason it might not go in prio blockquote is if we are now // in a different table cell. if (curQuote && HTMLEditor::NodesInDifferentTableElements(*curQuote, content)) { curQuote = nullptr; } if (!curQuote) { // First, check that our element can contain a blockquote. if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(), *nsGkAtoms::blockquote)) { return NS_OK; // cancelled } SplitNodeResult splitNodeResult = MaybeSplitAncestorsForInsertWithTransaction(*nsGkAtoms::blockquote, atContent); if (splitNodeResult.Failed()) { NS_WARNING( "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction(nsGkAtoms:" ":blockquote) failed"); return splitNodeResult.Rv(); } curQuote = CreateNodeWithTransaction(*nsGkAtoms::blockquote, splitNodeResult.SplitPoint()); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (!curQuote) { NS_WARNING( "EditorBase::CreateNodeWithTransaction(nsGkAtoms::blockquote) " "failed"); return NS_ERROR_FAILURE; } // remember our new block for postprocessing TopLevelEditSubActionDataRef().mNewBlockElement = curQuote; // curQuote is now the correct thing to put curNode in } // tuck the node into the end of the active blockquote // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. rv = MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *curQuote); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return rv; } // forget curList, if any curList = nullptr; } return NS_OK; } EditActionResult HTMLEditor::OutdentAsSubAction() { MOZ_ASSERT(IsEditActionDataAvailable()); AutoPlaceholderBatch treatAsOneTransaction(*this, ScrollSelectionIntoView::Yes); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eOutdent, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return EditActionResult(ignoredError.StealNSResult()); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); EditActionResult result = CanHandleHTMLEditSubAction(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (IsSelectionRangeContainerNotContent()) { NS_WARNING("Some selection containers are not content node, but ignored"); return EditActionIgnored(); } result |= HandleOutdentAtSelection(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::HandleOutdentAtSelection() failed"); return result; } if (IsSelectionRangeContainerNotContent()) { NS_WARNING("Mutation event listener might have changed the selection"); return EditActionHandled(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } nsresult rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() " "failed"); return result.SetResult(rv); } EditActionResult HTMLEditor::HandleOutdentAtSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); if (!SelectionRefPtr()->IsCollapsed()) { nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction(); if (NS_WARN_IF(NS_FAILED(rv))) { return EditActionHandled(rv); } } // HandleOutdentAtSelectionInternal() creates AutoSelectionRestorer. // Therefore, even if it returns NS_OK, the editor might have been destroyed // at restoring Selection. SplitRangeOffFromNodeResult outdentResult = HandleOutdentAtSelectionInternal(); if (NS_WARN_IF(Destroyed())) { return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } if (outdentResult.Failed()) { NS_WARNING("HTMLEditor::HandleOutdentAtSelectionInternal() failed"); return EditActionHandled(outdentResult.Rv()); } // Make sure selection didn't stick to last piece of content in old bq (only // a problem for collapsed selections) if (!outdentResult.GetLeftContent() && !outdentResult.GetRightContent()) { return EditActionHandled(); } if (!SelectionRefPtr()->IsCollapsed()) { return EditActionHandled(); } // Push selection past end of left element of last split indented element. if (outdentResult.GetLeftContent()) { const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionHandled(); } const RangeBoundary& atStartOfSelection = firstRange->StartRef(); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return EditActionHandled(NS_ERROR_FAILURE); } if (atStartOfSelection.Container() == outdentResult.GetLeftContent() || EditorUtils::IsDescendantOf(*atStartOfSelection.Container(), *outdentResult.GetLeftContent())) { // Selection is inside the left node - push it past it. EditorRawDOMPoint afterRememberedLeftBQ( EditorRawDOMPoint::After(*outdentResult.GetLeftContent())); NS_WARNING_ASSERTION( afterRememberedLeftBQ.IsSet(), "Failed to set after remembered left blockquote element"); nsresult rv = CollapseSelectionTo(afterRememberedLeftBQ); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::CollapseSelectionTo() failed, but ignored"); } } // And pull selection before beginning of right element of last split // indented element. if (outdentResult.GetRightContent()) { const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionHandled(); } const RangeBoundary& atStartOfSelection = firstRange->StartRef(); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return EditActionHandled(NS_ERROR_FAILURE); } if (atStartOfSelection.Container() == outdentResult.GetRightContent() || EditorUtils::IsDescendantOf(*atStartOfSelection.Container(), *outdentResult.GetRightContent())) { // Selection is inside the right element - push it before it. EditorRawDOMPoint atRememberedRightBQ(outdentResult.GetRightContent()); nsresult rv = CollapseSelectionTo(atRememberedRightBQ); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::CollapseSelectionTo() failed, but ignored"); } } return EditActionHandled(); } SplitRangeOffFromNodeResult HTMLEditor::HandleOutdentAtSelectionInternal() { MOZ_ASSERT(IsEditActionDataAvailable()); AutoSelectionRestorer restoreSelectionLater(*this); bool useCSS = IsCSSEnabled(); // Convert the selection ranges into "promoted" selection ranges: this // basically just expands the range to include the immediate block parent, // and then further expands to include any ancestors whose children are all // in the range AutoTArray, 64> arrayOfContents; nsresult rv = SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges( arrayOfContents, EditSubAction::eOutdent, CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges() " "failed"); return SplitRangeOffFromNodeResult(rv); } nsCOMPtr leftContentOfLastOutdented; nsCOMPtr middleContentOfLastOutdented; nsCOMPtr rightContentOfLastOutdented; RefPtr indentedParentElement; nsCOMPtr firstContentToBeOutdented, lastContentToBeOutdented; BlockIndentedWith indentedParentIndentedWith = BlockIndentedWith::HTML; for (OwningNonNull& content : arrayOfContents) { // Here's where we actually figure out what to do EditorDOMPoint atContent(content); if (!atContent.IsSet()) { continue; } // If it's a `
    `, remove it to outdent its children. if (content->IsHTMLElement(nsGkAtoms::blockquote)) { // If we've already found an ancestor block element indented, we need to // split it and remove the block element first. if (indentedParentElement) { MOZ_ASSERT(indentedParentElement == content); SplitRangeOffFromNodeResult outdentResult = OutdentPartOfBlock( *indentedParentElement, *firstContentToBeOutdented, *lastContentToBeOutdented, indentedParentIndentedWith); if (outdentResult.Failed()) { NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed"); return outdentResult; } leftContentOfLastOutdented = outdentResult.GetLeftContent(); middleContentOfLastOutdented = outdentResult.GetMiddleContent(); rightContentOfLastOutdented = outdentResult.GetRightContent(); indentedParentElement = nullptr; firstContentToBeOutdented = nullptr; lastContentToBeOutdented = nullptr; indentedParentIndentedWith = BlockIndentedWith::HTML; } nsresult rv = RemoveBlockContainerWithTransaction( MOZ_KnownLive(*content->AsElement())); if (NS_WARN_IF(Destroyed())) { return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed"); return SplitRangeOffFromNodeResult(rv); } continue; } // If we're using CSS and the node is a block element, check its start // margin whether it's indented with CSS. if (useCSS && HTMLEditUtils::IsBlockElement(content)) { nsStaticAtom& marginProperty = MarginPropertyAtomForIndent(MOZ_KnownLive(content)); if (NS_WARN_IF(Destroyed())) { return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED); } nsAutoString value; DebugOnly rvIgnored = CSSEditUtils::GetSpecifiedProperty(content, marginProperty, value); if (NS_WARN_IF(Destroyed())) { return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "CSSEditUtils::GetSpecifiedProperty() failed, but ignored"); float startMargin = 0; RefPtr unit; CSSEditUtils::ParseLength(value, &startMargin, getter_AddRefs(unit)); // If indented with CSS, we should decrease the start mergin. if (startMargin > 0) { nsresult rv = ChangeMarginStart(MOZ_KnownLive(*content->AsElement()), ChangeMargin::Decrease); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::ChangeMarginStart(ChangeMargin::" "Decrease) failed, but ignored"); continue; } } // If it's a list item, we should treat as that it "indents" its children. if (HTMLEditUtils::IsListItem(content)) { // If it is a list item, that means we are not outdenting whole list. // XXX I don't understand this sentence... We may meet parent list // element, no? if (indentedParentElement) { SplitRangeOffFromNodeResult outdentResult = OutdentPartOfBlock( *indentedParentElement, *firstContentToBeOutdented, *lastContentToBeOutdented, indentedParentIndentedWith); if (NS_WARN_IF(outdentResult.Failed())) { return outdentResult; } leftContentOfLastOutdented = outdentResult.GetLeftContent(); middleContentOfLastOutdented = outdentResult.GetMiddleContent(); rightContentOfLastOutdented = outdentResult.GetRightContent(); indentedParentElement = nullptr; firstContentToBeOutdented = nullptr; lastContentToBeOutdented = nullptr; indentedParentIndentedWith = BlockIndentedWith::HTML; } // XXX `content` could become different element since // `OutdentPartOfBlock()` may run mutation event listeners. rv = LiftUpListItemElement(MOZ_KnownLive(*content->AsElement()), LiftUpFromAllParentListElements::No); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::LiftUpListItemElement(LiftUpFromAllParentListElements:" ":No) failed"); return SplitRangeOffFromNodeResult(rv); } continue; } // If we've found an ancestor block element which indents its children // and the current node is NOT a descendant of it, we should remove it to // outdent its children. Otherwise, i.e., current node is a descendant of // it, we meet new node which should be outdented when the indented parent // is removed. if (indentedParentElement) { if (EditorUtils::IsDescendantOf(*content, *indentedParentElement)) { // Extend the range to be outdented at removing the // indentedParentElement. lastContentToBeOutdented = content; continue; } SplitRangeOffFromNodeResult outdentResult = OutdentPartOfBlock( *indentedParentElement, *firstContentToBeOutdented, *lastContentToBeOutdented, indentedParentIndentedWith); if (outdentResult.Failed()) { NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed"); return outdentResult; } leftContentOfLastOutdented = outdentResult.GetLeftContent(); middleContentOfLastOutdented = outdentResult.GetMiddleContent(); rightContentOfLastOutdented = outdentResult.GetRightContent(); indentedParentElement = nullptr; firstContentToBeOutdented = nullptr; lastContentToBeOutdented = nullptr; // curBlockIndentedWith = HTMLEditor::BlockIndentedWith::HTML; // Then, we need to look for next indentedParentElement. } indentedParentIndentedWith = BlockIndentedWith::HTML; RefPtr editingHost = GetActiveEditingHost(); for (nsCOMPtr parentContent = content->GetParent(); parentContent && !parentContent->IsHTMLElement(nsGkAtoms::body) && parentContent != editingHost && (parentContent->IsHTMLElement(nsGkAtoms::table) || !HTMLEditUtils::IsAnyTableElement(parentContent)); parentContent = parentContent->GetParent()) { // If we reach a `
    ` ancestor, it should be split at next // time at least for outdenting current node. if (parentContent->IsHTMLElement(nsGkAtoms::blockquote)) { indentedParentElement = parentContent->AsElement(); firstContentToBeOutdented = content; lastContentToBeOutdented = content; break; } if (!useCSS) { continue; } nsCOMPtr grandParentNode = parentContent->GetParentNode(); nsStaticAtom& marginProperty = MarginPropertyAtomForIndent(MOZ_KnownLive(content)); if (NS_WARN_IF(Destroyed())) { return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_WARN_IF(grandParentNode != parentContent->GetParentNode())) { return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } nsAutoString value; DebugOnly rvIgnored = CSSEditUtils::GetSpecifiedProperty( *parentContent, marginProperty, value); if (NS_WARN_IF(Destroyed())) { return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "CSSEditUtils::GetSpecifiedProperty() failed, but ignored"); // XXX Now, editing host may become different element. If so, shouldn't // we stop this handling? float startMargin; RefPtr unit; CSSEditUtils::ParseLength(value, &startMargin, getter_AddRefs(unit)); // If we reach a block element which indents its children with start // margin, we should remove it at next time. if (startMargin > 0 && !(HTMLEditUtils::IsAnyListElement(atContent.GetContainer()) && HTMLEditUtils::IsAnyListElement(content))) { indentedParentElement = parentContent->AsElement(); firstContentToBeOutdented = content; lastContentToBeOutdented = content; indentedParentIndentedWith = BlockIndentedWith::CSS; break; } } if (indentedParentElement) { continue; } // If we don't have any block elements which indents current node and // both current node and its parent are list element, remove current // node to move all its children to the parent list. // XXX This is buggy. When both lists' item types are different, // we create invalid tree. E.g., `