From 2aa4a82499d4becd2284cdb482213d541b8804dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 16:29:10 +0200 Subject: Adding upstream version 86.0.1. Signed-off-by: Daniel Baumann --- editor/libeditor/HTMLEditSubActionHandler.cpp | 9603 +++++++++++++++++++++++++ 1 file changed, 9603 insertions(+) create mode 100644 editor/libeditor/HTMLEditSubActionHandler.cpp (limited to 'editor/libeditor/HTMLEditSubActionHandler.cpp') diff --git a/editor/libeditor/HTMLEditSubActionHandler.cpp b/editor/libeditor/HTMLEditSubActionHandler.cpp new file mode 100644 index 0000000000..f6463e87c2 --- /dev/null +++ b/editor/libeditor/HTMLEditSubActionHandler.cpp @@ -0,0 +1,9603 @@ +/* -*- 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., `