From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- editor/libeditor/HTMLEditor.cpp | 7199 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 7199 insertions(+) create mode 100644 editor/libeditor/HTMLEditor.cpp (limited to 'editor/libeditor/HTMLEditor.cpp') diff --git a/editor/libeditor/HTMLEditor.cpp b/editor/libeditor/HTMLEditor.cpp new file mode 100644 index 0000000000..474ce8b841 --- /dev/null +++ b/editor/libeditor/HTMLEditor.cpp @@ -0,0 +1,7199 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "HTMLEditor.h" +#include "HTMLEditHelpers.h" +#include "HTMLEditorInlines.h" + +#include "AutoRangeArray.h" +#include "CSSEditUtils.h" +#include "EditAction.h" +#include "EditorBase.h" +#include "EditorDOMPoint.h" +#include "EditorUtils.h" +#include "ErrorList.h" +#include "HTMLEditorEventListener.h" +#include "HTMLEditUtils.h" +#include "InsertNodeTransaction.h" +#include "JoinNodesTransaction.h" +#include "JoinSplitNodeDirection.h" +#include "MoveNodeTransaction.h" +#include "PendingStyles.h" +#include "ReplaceTextTransaction.h" +#include "SplitNodeTransaction.h" +#include "WSRunObject.h" + +#include "mozilla/ComposerCommandsUpdater.h" +#include "mozilla/ContentIterator.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/EditorForwards.h" +#include "mozilla/Encoding.h" // for Encoding +#include "mozilla/IMEStateManager.h" +#include "mozilla/IntegerRange.h" // for IntegerRange +#include "mozilla/InternalMutationEvent.h" +#include "mozilla/mozInlineSpellChecker.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_editor.h" +#include "mozilla/StyleSheet.h" +#include "mozilla/StyleSheetInlines.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TextEvents.h" +#include "mozilla/TextServicesDocument.h" +#include "mozilla/ToString.h" +#include "mozilla/css/Loader.h" +#include "mozilla/dom/AncestorIterator.h" +#include "mozilla/dom/Attr.h" +#include "mozilla/dom/DocumentFragment.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/HTMLAnchorElement.h" +#include "mozilla/dom/HTMLBodyElement.h" +#include "mozilla/dom/HTMLBRElement.h" +#include "mozilla/dom/NameSpaceConstants.h" +#include "mozilla/dom/Selection.h" + +#include "nsContentList.h" +#include "nsContentUtils.h" +#include "nsCRT.h" +#include "nsDebug.h" +#include "nsDOMAttributeMap.h" +#include "nsElementTable.h" +#include "nsFocusManager.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" +#include "nsHTMLDocument.h" +#include "nsIContent.h" +#include "nsIContentInlines.h" +#include "nsIEditActionListener.h" +#include "nsIFrame.h" +#include "nsIPrincipal.h" +#include "nsISelectionController.h" +#include "nsIURI.h" +#include "nsIWidget.h" +#include "nsNetUtil.h" +#include "nsPresContext.h" +#include "nsPrintfCString.h" +#include "nsPIDOMWindow.h" +#include "nsStyledElement.h" +#include "nsTextFragment.h" +#include "nsUnicharUtils.h" + +namespace mozilla { + +using namespace dom; +using namespace widget; + +using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; +using LeafNodeType = HTMLEditUtils::LeafNodeType; +using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; +using WalkTreeOption = HTMLEditUtils::WalkTreeOption; + +// Some utilities to handle overloading of "A" tag for link and named anchor. +static bool IsLinkTag(const nsAtom& aTagName) { + return &aTagName == nsGkAtoms::href; +} + +static bool IsNamedAnchorTag(const nsAtom& aTagName) { + return &aTagName == nsGkAtoms::anchor; +} + +// Helper struct for DoJoinNodes() and DoSplitNode(). +struct MOZ_STACK_CLASS SavedRange final { + RefPtr mSelection; + nsCOMPtr mStartContainer; + nsCOMPtr mEndContainer; + uint32_t mStartOffset = 0; + uint32_t mEndOffset = 0; +}; + +/****************************************************************************** + * HTMLEditor::AutoSelectionRestorer + *****************************************************************************/ + +HTMLEditor::AutoSelectionRestorer::AutoSelectionRestorer( + HTMLEditor& aHTMLEditor) + : mHTMLEditor(nullptr) { + if (aHTMLEditor.ArePreservingSelection()) { + // We already have initialized mParentData::mSavedSelection, so this must + // be nested call. + return; + } + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + mHTMLEditor = &aHTMLEditor; + mHTMLEditor->PreserveSelectionAcrossActions(); +} + +HTMLEditor::AutoSelectionRestorer::~AutoSelectionRestorer() { + if (!mHTMLEditor || !mHTMLEditor->ArePreservingSelection()) { + return; + } + DebugOnly rvIgnored = mHTMLEditor->RestorePreservedSelection(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::RestorePreservedSelection() failed, but ignored"); +} + +void HTMLEditor::AutoSelectionRestorer::Abort() { + if (mHTMLEditor) { + mHTMLEditor->StopPreservingSelection(); + } +} + +/****************************************************************************** + * HTMLEditor + *****************************************************************************/ + +template Result +HTMLEditor::InsertNodeIntoProperAncestorWithTransaction( + nsIContent& aContentToInsert, const EditorDOMPoint& aPointToInsert, + SplitAtEdges aSplitAtEdges); +template Result +HTMLEditor::InsertNodeIntoProperAncestorWithTransaction( + Element& aContentToInsert, const EditorDOMPoint& aPointToInsert, + SplitAtEdges aSplitAtEdges); +template Result +HTMLEditor::InsertNodeIntoProperAncestorWithTransaction( + Text& aContentToInsert, const EditorDOMPoint& aPointToInsert, + SplitAtEdges aSplitAtEdges); + +HTMLEditor::InitializeInsertingElement HTMLEditor::DoNothingForNewElement = + [](HTMLEditor&, Element&, const EditorDOMPoint&) { return NS_OK; }; + +HTMLEditor::InitializeInsertingElement HTMLEditor::InsertNewBRElement = + [](HTMLEditor& aHTMLEditor, Element& aNewElement, const EditorDOMPoint&) + MOZ_CAN_RUN_SCRIPT_BOUNDARY { + MOZ_ASSERT(!aNewElement.IsInComposedDoc()); + Result createBRElementResult = + aHTMLEditor.InsertBRElement(WithTransaction::No, + EditorDOMPoint(&aNewElement, 0u)); + if (MOZ_UNLIKELY(createBRElementResult.isErr())) { + NS_WARNING_ASSERTION( + createBRElementResult.isOk(), + "HTMLEditor::InsertBRElement(WithTransaction::No) failed"); + return createBRElementResult.unwrapErr(); + } + createBRElementResult.unwrap().IgnoreCaretPointSuggestion(); + return NS_OK; + }; + +// static +Result +HTMLEditor::AppendNewElementToInsertingElement( + HTMLEditor& aHTMLEditor, const nsStaticAtom& aTagName, Element& aNewElement, + const InitializeInsertingElement& aInitializer) { + MOZ_ASSERT(!aNewElement.IsInComposedDoc()); + Result createNewElementResult = + aHTMLEditor.CreateAndInsertElement( + WithTransaction::No, const_cast(aTagName), + EditorDOMPoint(&aNewElement, 0u), aInitializer); + NS_WARNING_ASSERTION( + createNewElementResult.isOk(), + "HTMLEditor::CreateAndInsertElement(WithTransaction::No) failed"); + return createNewElementResult; +} + +// static +Result +HTMLEditor::AppendNewElementWithBRToInsertingElement( + HTMLEditor& aHTMLEditor, const nsStaticAtom& aTagName, + Element& aNewElement) { + MOZ_ASSERT(!aNewElement.IsInComposedDoc()); + Result createNewElementWithBRResult = + HTMLEditor::AppendNewElementToInsertingElement( + aHTMLEditor, aTagName, aNewElement, HTMLEditor::InsertNewBRElement); + NS_WARNING_ASSERTION( + createNewElementWithBRResult.isOk(), + "HTMLEditor::AppendNewElementToInsertingElement() failed"); + return createNewElementWithBRResult; +} + +HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributes = + [](HTMLEditor&, const Element&, const Element&, const Attr&, nsString&) { + return true; + }; +HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributesExceptId = + [](HTMLEditor&, const Element&, const Element&, const Attr& aAttr, + nsString&) { + return aAttr.NodeInfo()->NamespaceID() != kNameSpaceID_None || + aAttr.NodeInfo()->NameAtom() != nsGkAtoms::id; + }; +HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributesExceptDir = + [](HTMLEditor&, const Element&, const Element&, const Attr& aAttr, + nsString&) { + return aAttr.NodeInfo()->NamespaceID() != kNameSpaceID_None || + aAttr.NodeInfo()->NameAtom() != nsGkAtoms::dir; + }; +HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributesExceptIdAndDir = + [](HTMLEditor&, const Element&, const Element&, const Attr& aAttr, + nsString&) { + return !(aAttr.NodeInfo()->NamespaceID() == kNameSpaceID_None && + (aAttr.NodeInfo()->NameAtom() == nsGkAtoms::id || + aAttr.NodeInfo()->NameAtom() == nsGkAtoms::dir)); + }; + +static bool ShouldUseTraditionalJoinSplitDirection(const Document& aDocument) { + if (nsIPrincipal* principal = aDocument.GetPrincipalForPrefBasedHacks()) { + if (principal->IsURIInPrefList("editor.join_split_direction." + "force_use_traditional_direction")) { + return true; + } + if (principal->IsURIInPrefList("editor.join_split_direction." + "force_use_compatible_direction")) { + return false; + } + } + return !StaticPrefs:: + editor_join_split_direction_compatible_with_the_other_browsers(); +} + +HTMLEditor::HTMLEditor(const Document& aDocument) + : EditorBase(EditorBase::EditorType::HTML), + mCRInParagraphCreatesParagraph(false), + mUseGeckoTraditionalJoinSplitBehavior( + ShouldUseTraditionalJoinSplitDirection(aDocument)), + mIsObjectResizingEnabled( + StaticPrefs::editor_resizing_enabled_by_default()), + mIsResizing(false), + mPreserveRatio(false), + mResizedObjectIsAnImage(false), + mIsAbsolutelyPositioningEnabled( + StaticPrefs::editor_positioning_enabled_by_default()), + mResizedObjectIsAbsolutelyPositioned(false), + mGrabberClicked(false), + mIsMoving(false), + mSnapToGridEnabled(false), + mIsInlineTableEditingEnabled( + StaticPrefs::editor_inline_table_editing_enabled_by_default()), + mIsCSSPrefChecked(StaticPrefs::editor_use_css()), + mOriginalX(0), + mOriginalY(0), + mResizedObjectX(0), + mResizedObjectY(0), + mResizedObjectWidth(0), + mResizedObjectHeight(0), + mResizedObjectMarginLeft(0), + mResizedObjectMarginTop(0), + mResizedObjectBorderLeft(0), + mResizedObjectBorderTop(0), + mXIncrementFactor(0), + mYIncrementFactor(0), + mWidthIncrementFactor(0), + mHeightIncrementFactor(0), + mInfoXIncrement(20), + mInfoYIncrement(20), + mPositionedObjectX(0), + mPositionedObjectY(0), + mPositionedObjectWidth(0), + mPositionedObjectHeight(0), + mPositionedObjectMarginLeft(0), + mPositionedObjectMarginTop(0), + mPositionedObjectBorderLeft(0), + mPositionedObjectBorderTop(0), + mGridSize(0), + mDefaultParagraphSeparator(ParagraphSeparator::div) {} + +HTMLEditor::~HTMLEditor() { + // Collect the data of `beforeinput` event only when it's enabled because + // web apps should switch their behavior with feature detection with + // checking `onbeforeinput` or `getTargetRanges`. + if (StaticPrefs::dom_input_events_beforeinput_enabled()) { + Telemetry::Accumulate( + Telemetry::HTMLEDITORS_WITH_BEFOREINPUT_LISTENERS, + MayHaveBeforeInputEventListenersForTelemetry() ? 1 : 0); + Telemetry::Accumulate( + Telemetry::HTMLEDITORS_OVERRIDDEN_BY_BEFOREINPUT_LISTENERS, + mHasBeforeInputBeenCanceled ? 1 : 0); + Telemetry::Accumulate( + Telemetry:: + HTMLEDITORS_WITH_MUTATION_LISTENERS_WITHOUT_BEFOREINPUT_LISTENERS, + !MayHaveBeforeInputEventListenersForTelemetry() && + MayHaveMutationEventListeners() + ? 1 + : 0); + Telemetry::Accumulate( + Telemetry:: + HTMLEDITORS_WITH_MUTATION_OBSERVERS_WITHOUT_BEFOREINPUT_LISTENERS, + !MayHaveBeforeInputEventListenersForTelemetry() && + MutationObserverHasObservedNodeForTelemetry() + ? 1 + : 0); + } + + mPendingStylesToApplyToNewContent = nullptr; + + if (mDisabledLinkHandling) { + if (Document* doc = GetDocument()) { + doc->SetLinkHandlingEnabled(mOldLinkHandlingEnabled); + } + } + + RemoveEventListeners(); + + HideAnonymousEditingUIs(); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLEditor) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLEditor, EditorBase) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingStylesToApplyToNewContent) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mComposerCommandsUpdater) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mChangedRangeForTopLevelEditSubAction) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPaddingBRElementForEmptyEditor) + tmp->HideAnonymousEditingUIs(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLEditor, EditorBase) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingStylesToApplyToNewContent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mComposerCommandsUpdater) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChangedRangeForTopLevelEditSubAction) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPaddingBRElementForEmptyEditor) + + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopLeftHandle) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopHandle) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopRightHandle) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLeftHandle) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRightHandle) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomLeftHandle) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomHandle) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomRightHandle) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActivatedHandle) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizingShadow) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizingInfo) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizedObject) + + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAbsolutelyPositionedObject) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGrabber) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPositioningShadow) + + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInlineEditedCell) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddColumnBeforeButton) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRemoveColumnButton) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddColumnAfterButton) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddRowBeforeButton) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRemoveRowButton) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddRowAfterButton) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ADDREF_INHERITED(HTMLEditor, EditorBase) +NS_IMPL_RELEASE_INHERITED(HTMLEditor, EditorBase) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLEditor) + NS_INTERFACE_MAP_ENTRY(nsIHTMLEditor) + NS_INTERFACE_MAP_ENTRY(nsIHTMLObjectResizer) + NS_INTERFACE_MAP_ENTRY(nsIHTMLAbsPosEditor) + NS_INTERFACE_MAP_ENTRY(nsIHTMLInlineTableEditor) + NS_INTERFACE_MAP_ENTRY(nsITableEditor) + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) + NS_INTERFACE_MAP_ENTRY(nsIEditorMailSupport) +NS_INTERFACE_MAP_END_INHERITING(EditorBase) + +nsresult HTMLEditor::Init(Document& aDocument, + ComposerCommandsUpdater& aComposerCommandsUpdater, + uint32_t aFlags) { + MOZ_ASSERT(!mInitSucceeded, + "HTMLEditor::Init() called again without calling PreDestroy()?"); + + MOZ_DIAGNOSTIC_ASSERT(!mComposerCommandsUpdater || + mComposerCommandsUpdater == &aComposerCommandsUpdater); + mComposerCommandsUpdater = &aComposerCommandsUpdater; + + RefPtr presShell = aDocument.GetPresShell(); + if (NS_WARN_IF(!presShell)) { + return NS_ERROR_FAILURE; + } + nsresult rv = InitInternal(aDocument, nullptr, *presShell, aFlags); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::InitInternal() failed"); + return rv; + } + + // Init mutation observer + aDocument.AddMutationObserverUnlessExists(this); + + if (!mRootElement) { + UpdateRootElement(); + } + + // disable Composer-only features + if (IsMailEditor()) { + DebugOnly rvIgnored = SetAbsolutePositioningEnabled(false); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "HTMLEditor::SetAbsolutePositioningEnabled(false) failed, but ignored"); + rvIgnored = SetSnapToGridEnabled(false); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "HTMLEditor::SetSnapToGridEnabled(false) failed, but ignored"); + } + + // disable links + Document* document = GetDocument(); + if (NS_WARN_IF(!document)) { + return NS_ERROR_FAILURE; + } + if (!IsInPlaintextMode() && !IsInteractionAllowed()) { + mDisabledLinkHandling = true; + mOldLinkHandlingEnabled = document->LinkHandlingEnabled(); + document->SetLinkHandlingEnabled(false); + } + + // init the type-in state + mPendingStylesToApplyToNewContent = new PendingStyles(); + + if (!IsInteractionAllowed()) { + nsCOMPtr uaURI; + rv = NS_NewURI(getter_AddRefs(uaURI), + "resource://gre/res/EditorOverride.css"); + NS_ENSURE_SUCCESS(rv, rv); + + rv = document->LoadAdditionalStyleSheet(Document::eAgentSheet, uaURI); + NS_ENSURE_SUCCESS(rv, rv); + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eInitializing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_FAILURE; + } + + rv = InitEditorContentAndSelection(); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::InitEditorContentAndSelection() failed"); + // XXX Sholdn't we expose `NS_ERROR_EDITOR_DESTROYED` even though this + // is a public method? + return EditorBase::ToGenericNSResult(rv); + } + + // Throw away the old transaction manager if this is not the first time that + // we're initializing the editor. + ClearUndoRedo(); + EnableUndoRedo(); // FYI: Creating mTransactionManager in this call + + if (mTransactionManager) { + mTransactionManager->Attach(*this); + } + + MOZ_ASSERT(!mInitSucceeded, "HTMLEditor::Init() shouldn't be nested"); + mInitSucceeded = true; + return NS_OK; +} + +nsresult HTMLEditor::PostCreate() { + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv = PostCreateInternal(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::PostCreatInternal() failed"); + return rv; +} + +void HTMLEditor::PreDestroy() { + if (mDidPreDestroy) { + return; + } + + mInitSucceeded = false; + + // FYI: Cannot create AutoEditActionDataSetter here. However, it does not + // necessary for the methods called by the following code. + + RefPtr document = GetDocument(); + if (document) { + document->RemoveMutationObserver(this); + + if (!IsInteractionAllowed()) { + nsCOMPtr uaURI; + nsresult rv = NS_NewURI(getter_AddRefs(uaURI), + "resource://gre/res/EditorOverride.css"); + if (NS_SUCCEEDED(rv)) { + document->RemoveAdditionalStyleSheet(Document::eAgentSheet, uaURI); + } + } + } + + // Clean up after our anonymous content -- we don't want these nodes to + // stay around (which they would, since the frames have an owning reference). + PresShell* presShell = GetPresShell(); + if (presShell && presShell->IsDestroying()) { + // Just destroying PresShell now. + // We have to keep UI elements of anonymous content until PresShell + // is destroyed. + RefPtr self = this; + nsContentUtils::AddScriptRunner( + NS_NewRunnableFunction("HTMLEditor::PreDestroy", + [self]() { self->HideAnonymousEditingUIs(); })); + } else { + // PresShell is alive or already gone. + HideAnonymousEditingUIs(); + } + + mPaddingBRElementForEmptyEditor = nullptr; + + PreDestroyInternal(); +} + +NS_IMETHODIMP HTMLEditor::GetDocumentCharacterSet(nsACString& aCharacterSet) { + nsresult rv = GetDocumentCharsetInternal(aCharacterSet); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::GetDocumentCharsetInternal() failed"); + return rv; +} + +NS_IMETHODIMP HTMLEditor::SetDocumentCharacterSet( + const nsACString& aCharacterSet) { + AutoEditActionDataSetter editActionData(*this, EditAction::eSetCharacterSet); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + RefPtr document = GetDocument(); + if (NS_WARN_IF(!document)) { + return EditorBase::ToGenericNSResult(NS_ERROR_NOT_INITIALIZED); + } + // This method is scriptable, so add-ons could pass in something other + // than a canonical name. + const Encoding* encoding = Encoding::ForLabelNoReplacement(aCharacterSet); + if (!encoding) { + NS_WARNING("Encoding::ForLabelNoReplacement() failed"); + return EditorBase::ToGenericNSResult(NS_ERROR_INVALID_ARG); + } + document->SetDocumentCharacterSet(WrapNotNull(encoding)); + + // Update META charset element. + if (UpdateMetaCharsetWithTransaction(*document, aCharacterSet)) { + return NS_OK; + } + + // Set attributes to the created element + if (aCharacterSet.IsEmpty()) { + return NS_OK; + } + + RefPtr headElementList = + document->GetElementsByTagName(u"head"_ns); + if (NS_WARN_IF(!headElementList)) { + return NS_OK; + } + + nsCOMPtr primaryHeadElement = headElementList->Item(0); + if (NS_WARN_IF(!primaryHeadElement)) { + return NS_OK; + } + + // Create a new meta charset tag + Result createNewMetaElementResult = + CreateAndInsertElement( + WithTransaction::Yes, *nsGkAtoms::meta, + EditorDOMPoint(primaryHeadElement, 0), + [&aCharacterSet](HTMLEditor&, Element& aMetaElement, + const EditorDOMPoint&) { + MOZ_ASSERT(!aMetaElement.IsInComposedDoc()); + DebugOnly rvIgnored = + aMetaElement.SetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv, + u"Content-Type"_ns, false); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "Element::SetAttr(nsGkAtoms::httpEquiv, \"Content-Type\", " + "false) failed, but ignored"); + rvIgnored = + aMetaElement.SetAttr(kNameSpaceID_None, nsGkAtoms::content, + u"text/html;charset="_ns + + NS_ConvertASCIItoUTF16(aCharacterSet), + false); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + nsPrintfCString( + "Element::SetAttr(nsGkAtoms::content, " + "\"text/html;charset=%s\", false) failed, but ignored", + nsPromiseFlatCString(aCharacterSet).get()) + .get()); + return NS_OK; + }); + NS_WARNING_ASSERTION(createNewMetaElementResult.isOk(), + "HTMLEditor::CreateAndInsertElement(WithTransaction::" + "Yes, nsGkAtoms::meta) failed, but ignored"); + // Probably, we don't need to update selection in this case since we should + // not put selection into element. + createNewMetaElementResult.inspect().IgnoreCaretPointSuggestion(); + return NS_OK; +} + +bool HTMLEditor::UpdateMetaCharsetWithTransaction( + Document& aDocument, const nsACString& aCharacterSet) { + // get a list of META tags + RefPtr metaElementList = + aDocument.GetElementsByTagName(u"meta"_ns); + if (NS_WARN_IF(!metaElementList)) { + return false; + } + + for (uint32_t i = 0; i < metaElementList->Length(true); ++i) { + RefPtr metaElement = metaElementList->Item(i)->AsElement(); + MOZ_ASSERT(metaElement); + + nsAutoString currentValue; + metaElement->GetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv, currentValue); + + if (!FindInReadable(u"content-type"_ns, currentValue, + nsCaseInsensitiveStringComparator)) { + continue; + } + + metaElement->GetAttr(kNameSpaceID_None, nsGkAtoms::content, currentValue); + + constexpr auto charsetEquals = u"charset="_ns; + nsAString::const_iterator originalStart, start, end; + originalStart = currentValue.BeginReading(start); + currentValue.EndReading(end); + if (!FindInReadable(charsetEquals, start, end, + nsCaseInsensitiveStringComparator)) { + continue; + } + + // set attribute to charset=text/html + nsresult rv = SetAttributeWithTransaction( + *metaElement, *nsGkAtoms::content, + Substring(originalStart, start) + charsetEquals + + NS_ConvertASCIItoUTF16(aCharacterSet)); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::SetAttributeWithTransaction(nsGkAtoms::content) failed"); + return NS_SUCCEEDED(rv); + } + return false; +} + +NS_IMETHODIMP HTMLEditor::NotifySelectionChanged(Document* aDocument, + Selection* aSelection, + int16_t aReason, + int32_t aAmount) { + if (NS_WARN_IF(!aDocument) || NS_WARN_IF(!aSelection)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (mPendingStylesToApplyToNewContent) { + RefPtr pendingStyles = mPendingStylesToApplyToNewContent; + pendingStyles->OnSelectionChange(*this, aReason); + + // We used a class which derived from nsISelectionListener to call + // HTMLEditor::RefreshEditingUI(). The lifetime of the class was + // exactly same as mPendingStylesToApplyToNewContent. So, call it only when + // mPendingStylesToApplyToNewContent is not nullptr. + if ((aReason & (nsISelectionListener::MOUSEDOWN_REASON | + nsISelectionListener::KEYPRESS_REASON | + nsISelectionListener::SELECTALL_REASON)) && + aSelection) { + // the selection changed and we need to check if we have to + // hide and/or redisplay resizing handles + // FYI: This is an XPCOM method. So, the caller, Selection, guarantees + // the lifetime of this instance. So, don't need to grab this with + // local variable. + DebugOnly rv = RefreshEditingUI(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::RefreshEditingUI() failed, but ignored"); + } + } + + if (mComposerCommandsUpdater) { + RefPtr updater = mComposerCommandsUpdater; + updater->OnSelectionChange(); + } + + nsresult rv = EditorBase::NotifySelectionChanged(aDocument, aSelection, + aReason, aAmount); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::NotifySelectionChanged() failed"); + return rv; +} + +void HTMLEditor::UpdateRootElement() { + // Use the HTML documents body element as the editor root if we didn't + // get a root element during initialization. + + mRootElement = GetBodyElement(); + if (!mRootElement) { + RefPtr doc = GetDocument(); + if (doc) { + // If there is no HTML body element, + // we should use the document root element instead. + mRootElement = doc->GetDocumentElement(); + } + // else leave it null, for lack of anything better. + } +} + +nsresult HTMLEditor::FocusedElementOrDocumentBecomesEditable( + Document& aDocument, Element* aElement) { + // If we should've already handled focus event, selection limiter should not + // be set. Therefore, if it's set, we should do nothing here. + if (GetSelectionAncestorLimiter()) { + return NS_OK; + } + // If we should be in the design mode, we want to handle focus event fired + // on the document node. Therefore, we should emulate it here. + if (IsInDesignMode() && (!aElement || aElement->IsInDesignMode())) { + MOZ_ASSERT(&aDocument == GetDocument()); + nsresult rv = OnFocus(aDocument); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::OnFocus() failed"); + return rv; + } + + if (NS_WARN_IF(!aElement)) { + return NS_ERROR_INVALID_ARG; + } + + // Otherwise, we should've already handled focus event on the element, + // therefore, we need to emulate it here. + MOZ_ASSERT(nsFocusManager::GetFocusManager()->GetFocusedElement() == + aElement); + nsresult rv = OnFocus(*aElement); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::OnFocus() failed"); + + // Note that we don't need to call + // IMEStateManager::MaybeOnEditableStateDisabled here because + // EditorBase::OnFocus must have already been called IMEStateManager::OnFocus + // if succeeded. And perhaps, it's okay that IME is not enabled when + // HTMLEditor fails to start handling since nobody can handle composition + // events anyway... + + return rv; +} + +nsresult HTMLEditor::OnFocus(const nsINode& aOriginalEventTargetNode) { + // Before doing anything, we should check whether the original target is still + // valid focus event target because it may have already lost focus. + if (!CanKeepHandlingFocusEvent(aOriginalEventTargetNode)) { + return NS_OK; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_FAILURE; + } + + return EditorBase::OnFocus(aOriginalEventTargetNode); +} + +nsresult HTMLEditor::FocusedElementOrDocumentBecomesNotEditable( + HTMLEditor* aHTMLEditor, Document& aDocument, Element* aElement) { + nsresult rv = [&]() MOZ_CAN_RUN_SCRIPT { + // If HTMLEditor has not been created yet, we just need to adjust + // IMEStateManager. So, don't return error. + if (!aHTMLEditor) { + return NS_OK; + } + + nsIContent* const limiter = aHTMLEditor->GetSelectionAncestorLimiter(); + // The HTMLEditor has not received `focus` event so that it does not need to + // emulate `blur`. + if (!limiter) { + return NS_OK; + } + + // If we should be in the design mode, we should treat it as blur from + // the document node. + if (aHTMLEditor->IsInDesignMode() && + (!aElement || aElement->IsInDesignMode())) { + MOZ_ASSERT(aHTMLEditor->GetDocument() == &aDocument); + nsresult rv = aHTMLEditor->OnBlur(&aDocument); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::OnBlur() failed"); + return rv; + } + // If the HTMLEditor has already received `focus` event for different + // element than aElement, we'll receive `blur` event later so that we need + // to do nothing here. + if (aElement != limiter) { + return NS_OK; + } + + // Otherwise, even though the limiter keeps having focus but becomes not + // editable. From HTMLEditor point of view, this is equivalent to the + // elements gets blurred. Therefore, we should treat it as losing + // focus. + nsresult rv = aHTMLEditor->OnBlur(aElement); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::OnBlur() failed"); + return rv; + }(); + + // If the element becomes not editable without focus change, IMEStateManager + // does not have a chance to disable IME. Therefore, (even if we fail to + // handle the emulated blur above,) we should notify IMEStateManager of the + // editing state change. + RefPtr focusedElement = aElement ? aElement + : aHTMLEditor + ? aHTMLEditor->GetFocusedElement() + : nullptr; + RefPtr presContext = + focusedElement ? focusedElement->GetPresContext( + Element::PresContextFor::eForComposedDoc) + : aDocument.GetPresContext(); + if (presContext) { + IMEStateManager::MaybeOnEditableStateDisabled(*presContext, focusedElement); + } + + return rv; +} + +nsresult HTMLEditor::OnBlur(const EventTarget* aEventTarget) { + // check if something else is focused. If another element is focused, then + // we should not change the selection. + nsFocusManager* focusManager = nsFocusManager::GetFocusManager(); + if (MOZ_UNLIKELY(!focusManager)) { + return NS_OK; + } + + // If another element already has focus, we should not maintain the selection + // because we may not have the rights doing it. + if (focusManager->GetFocusedElement()) { + return NS_OK; + } + + // If it's in the designMode, and blur occurs, the target must be the + // document node. If a blur event is fired and the target is an element, it + // must be delayed blur event at initializing the `HTMLEditor`. + if (IsInDesignMode() && Element::FromEventTargetOrNull(aEventTarget)) { + return NS_OK; + } + nsresult rv = FinalizeSelection(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::FinalizeSelection() failed"); + return rv; +} + +Element* HTMLEditor::FindSelectionRoot(const nsINode& aNode) const { + MOZ_ASSERT(aNode.IsDocument() || aNode.IsContent(), + "aNode must be content or document node"); + + if (NS_WARN_IF(!aNode.IsInComposedDoc())) { + return nullptr; + } + + if (aNode.IsInDesignMode()) { + return GetDocument()->GetRootElement(); + } + + nsIContent* content = const_cast(aNode.AsContent()); + if (!content->HasFlag(NODE_IS_EDITABLE)) { + // If the content is in read-write state but is not editable itself, + // return it as the selection root. + if (content->IsElement() && + content->AsElement()->State().HasState(ElementState::READWRITE)) { + return content->AsElement(); + } + return nullptr; + } + + // For non-readonly editors we want to find the root of the editable subtree + // containing aContent. + return content->GetEditingHost(); +} + +bool HTMLEditor::IsInDesignMode() const { + // TODO: If active editing host is in a shadow tree, it means that we should + // behave exactly same as contenteditable mode because shadow tree + // content is not editable even if composed document is in design mode, + // but contenteditable elements in shoadow trees are focusable and + // their content is editable. Changing this affects to drop event + // handler and blur event handler, so please add new tests for them + // when you change here. + Document* document = GetDocument(); + return document && document->IsInDesignMode(); +} + +bool HTMLEditor::EntireDocumentIsEditable() const { + Document* document = GetDocument(); + return document && document->GetDocumentElement() && + (document->GetDocumentElement()->IsEditable() || + (document->GetBody() && document->GetBody()->IsEditable())); +} + +void HTMLEditor::CreateEventListeners() { + // Don't create the handler twice + if (!mEventListener) { + mEventListener = new HTMLEditorEventListener(); + } +} + +nsresult HTMLEditor::InstallEventListeners() { + if (NS_WARN_IF(!IsInitialized()) || NS_WARN_IF(!mEventListener)) { + return NS_ERROR_NOT_INITIALIZED; + } + + // NOTE: HTMLEditor doesn't need to initialize mEventTarget here because + // the target must be document node and it must be referenced as weak pointer. + + HTMLEditorEventListener* listener = + reinterpret_cast(mEventListener.get()); + nsresult rv = listener->Connect(this); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditorEventListener::Connect() failed"); + return rv; +} + +void HTMLEditor::RemoveEventListeners() { + if (!IsInitialized()) { + return; + } + + EditorBase::RemoveEventListeners(); +} + +void HTMLEditor::Detach( + const ComposerCommandsUpdater& aComposerCommandsUpdater) { + MOZ_DIAGNOSTIC_ASSERT_IF( + mComposerCommandsUpdater, + &aComposerCommandsUpdater == mComposerCommandsUpdater); + if (mComposerCommandsUpdater == &aComposerCommandsUpdater) { + mComposerCommandsUpdater = nullptr; + if (mTransactionManager) { + mTransactionManager->Detach(*this); + } + } +} + +NS_IMETHODIMP HTMLEditor::BeginningOfDocument() { + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv = MaybeCollapseSelectionAtFirstEditableNode(false); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(false) failed"); + return rv; +} + +NS_IMETHODIMP HTMLEditor::EndOfDocument() { + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = CollapseSelectionToEndOfLastLeafNodeOfDocument(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() failed"); + // This is low level API for embedders and chrome script so that we can return + // raw error code here. + return rv; +} + +nsresult HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() const { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // We should do nothing with the result of GetRoot() if only a part of the + // document is editable. + if (!EntireDocumentIsEditable()) { + return NS_OK; + } + + RefPtr bodyOrDocumentElement = GetRoot(); + if (NS_WARN_IF(!bodyOrDocumentElement)) { + return NS_ERROR_NULL_POINTER; + } + + auto pointToPutCaret = [&]() -> EditorRawDOMPoint { + nsCOMPtr lastLeafContent = HTMLEditUtils::GetLastLeafContent( + *bodyOrDocumentElement, {LeafNodeType::OnlyLeafNode}); + if (!lastLeafContent) { + return EditorRawDOMPoint::AtEndOf(*bodyOrDocumentElement); + } + // TODO: We should put caret into text node if it's visible. + return lastLeafContent->IsText() || + HTMLEditUtils::IsContainerNode(*lastLeafContent) + ? EditorRawDOMPoint::AtEndOf(*lastLeafContent) + : EditorRawDOMPoint(lastLeafContent); + }(); + nsresult rv = CollapseSelectionTo(pointToPutCaret); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed"); + return rv; +} + +void HTMLEditor::InitializeSelectionAncestorLimit( + nsIContent& aAncestorLimit) const { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // Hack for initializing selection. + // HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode() will try to + // collapse selection at first editable text node or inline element which + // cannot have text nodes as its children. However, selection has already + // set into the new editing host by user, we should not change it. For + // solving this issue, we should do nothing if selection range is in active + // editing host except it's not collapsed at start of the editing host since + // aSelection.SetAncestorLimiter(aAncestorLimit) will collapse selection + // at start of the new limiter if focus node of aSelection is outside of the + // editing host. However, we need to check here if selection is already + // collapsed at start of the editing host because it's possible JS to do it. + // In such case, we should not modify selection with calling + // MaybeCollapseSelectionAtFirstEditableNode(). + + // Basically, we should try to collapse selection at first editable node + // in HTMLEditor. + bool tryToCollapseSelectionAtFirstEditableNode = true; + if (SelectionRef().RangeCount() == 1 && SelectionRef().IsCollapsed()) { + Element* editingHost = ComputeEditingHost(); + const nsRange* range = SelectionRef().GetRangeAt(0); + if (range->GetStartContainer() == editingHost && !range->StartOffset()) { + // JS or user operation has already collapsed selection at start of + // the editing host. So, we don't need to try to change selection + // in this case. + tryToCollapseSelectionAtFirstEditableNode = false; + } + } + + EditorBase::InitializeSelectionAncestorLimit(aAncestorLimit); + + // XXX Do we need to check if we still need to change selection? E.g., + // we could have already lost focus while we're changing the ancestor + // limiter because it may causes "selectionchange" event. + if (tryToCollapseSelectionAtFirstEditableNode) { + DebugOnly rvIgnored = + MaybeCollapseSelectionAtFirstEditableNode(true); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(true) failed, " + "but ignored"); + } + + // If the target is a text control element, we won't handle user input + // for the `TextEditor` in it. However, we need to be open for `execCommand`. + // Therefore, we shouldn't set ancestor limit in this case. + // Note that we should do this once setting ancestor limiter for backward + // compatiblity of select events, etc. (Selection should be collapsed into + // the text control element.) + if (aAncestorLimit.HasIndependentSelection()) { + SelectionRef().SetAncestorLimiter(nullptr); + } +} + +nsresult HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode( + bool aIgnoreIfSelectionInEditingHost) const { + MOZ_ASSERT(IsEditActionDataAvailable()); + + RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); + if (NS_WARN_IF(!editingHost)) { + return NS_OK; + } + + // If selection range is already in the editing host and the range is not + // start of the editing host, we shouldn't reset selection. E.g., window + // is activated when the editor had focus before inactivated. + if (aIgnoreIfSelectionInEditingHost && SelectionRef().RangeCount() == 1) { + const nsRange* range = SelectionRef().GetRangeAt(0); + if (!range->Collapsed() || + range->GetStartContainer() != editingHost.get() || + range->StartOffset()) { + return NS_OK; + } + } + + for (nsIContent* leafContent = HTMLEditUtils::GetFirstLeafContent( + *editingHost, + {LeafNodeType::LeafNodeOrNonEditableNode, + LeafNodeType::LeafNodeOrChildBlock}, + editingHost); + leafContent;) { + // If we meet a non-editable node first, we should move caret to start + // of the container block or editing host. + if (!EditorUtils::IsEditableContent(*leafContent, EditorType::HTML)) { + MOZ_ASSERT(leafContent->GetParent()); + MOZ_ASSERT(EditorUtils::IsEditableContent(*leafContent->GetParent(), + EditorType::HTML)); + if (const Element* editableBlockElementOrInlineEditingHost = + HTMLEditUtils::GetAncestorElement( + *leafContent, + HTMLEditUtils:: + ClosestEditableBlockElementOrInlineEditingHost)) { + nsresult rv = CollapseSelectionTo( + EditorDOMPoint(editableBlockElementOrInlineEditingHost, 0)); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed"); + return rv; + } + NS_WARNING("Found leaf content did not have editable parent, why?"); + return NS_ERROR_FAILURE; + } + + // When we meet an empty inline element, we should look for a next sibling. + // For example, if current editor is: + //

+ // then, we should put caret at the
element. So, let's check if found + // node is an empty inline container element. + if (leafContent->IsElement() && + HTMLEditUtils::IsInlineElement(*leafContent) && + !HTMLEditUtils::IsNeverElementContentsEditableByUser(*leafContent) && + HTMLEditUtils::CanNodeContain(*leafContent, *nsGkAtoms::textTagName)) { + // Chromium collaps selection to start of the editing host when this is + // the last leaf content. So, we don't need special handling here. + leafContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( + *leafContent, *editingHost, + {LeafNodeType::LeafNodeOrNonEditableNode, + LeafNodeType::LeafNodeOrChildBlock}, + editingHost); + continue; + } + + if (Text* text = leafContent->GetAsText()) { + // If there is editable and visible text node, move caret at first of + // the visible character. + WSScanResult scanResultInTextNode = + WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( + editingHost, EditorRawDOMPoint(text, 0)); + if ((scanResultInTextNode.InVisibleOrCollapsibleCharacters() || + scanResultInTextNode.ReachedPreformattedLineBreak()) && + scanResultInTextNode.TextPtr() == text) { + nsresult rv = CollapseSelectionTo( + scanResultInTextNode.Point()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed"); + return rv; + } + // If it's an invisible text node, keep scanning next leaf. + leafContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( + *leafContent, *editingHost, + {LeafNodeType::LeafNodeOrNonEditableNode, + LeafNodeType::LeafNodeOrChildBlock}, + editingHost); + continue; + } + + // If there is editable
or something void element like , , + //
etc, move caret before it. + if (!HTMLEditUtils::CanNodeContain(*leafContent, *nsGkAtoms::textTagName) || + HTMLEditUtils::IsNeverElementContentsEditableByUser(*leafContent)) { + MOZ_ASSERT(leafContent->GetParent()); + if (EditorUtils::IsEditableContent(*leafContent, EditorType::HTML)) { + nsresult rv = CollapseSelectionTo(EditorDOMPoint(leafContent)); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed"); + return rv; + } + MOZ_ASSERT_UNREACHABLE( + "How do we reach editable leaf in non-editable element?"); + // But if it's not editable, let's put caret at start of editing host + // for now. + nsresult rv = CollapseSelectionTo(EditorDOMPoint(editingHost, 0)); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed"); + return rv; + } + + // If we meet non-empty block element, we need to scan its child too. + if (HTMLEditUtils::IsBlockElement(*leafContent) && + !HTMLEditUtils::IsEmptyNode( + *leafContent, {EmptyCheckOption::TreatSingleBRElementAsVisible}) && + !HTMLEditUtils::IsNeverElementContentsEditableByUser(*leafContent)) { + leafContent = HTMLEditUtils::GetFirstLeafContent( + *leafContent, + {LeafNodeType::LeafNodeOrNonEditableNode, + LeafNodeType::LeafNodeOrChildBlock}, + editingHost); + continue; + } + + // Otherwise, we must meet an empty block element or a data node like + // comment node. Let's ignore it. + leafContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( + *leafContent, *editingHost, + {LeafNodeType::LeafNodeOrNonEditableNode, + LeafNodeType::LeafNodeOrChildBlock}, + editingHost); + } + + // If there is no visible/editable node except another block element in + // current editing host, we should move caret to very first of the editing + // host. + // XXX This may not make sense, but Chromium behaves so. Therefore, the + // reason why we do this is just compatibility with Chromium. + nsresult rv = CollapseSelectionTo(EditorDOMPoint(editingHost, 0)); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed"); + return rv; +} + +bool HTMLEditor::ArePreservingSelection() const { + return IsEditActionDataAvailable() && SavedSelectionRef().RangeCount(); +} + +void HTMLEditor::PreserveSelectionAcrossActions() { + MOZ_ASSERT(IsEditActionDataAvailable()); + + SavedSelectionRef().SaveSelection(SelectionRef()); + RangeUpdaterRef().RegisterSelectionState(SavedSelectionRef()); +} + +nsresult HTMLEditor::RestorePreservedSelection() { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (!SavedSelectionRef().RangeCount()) { + // XXX Returing error when it does not store is odd because no selection + // ranges is not illegal case in general. + return NS_ERROR_FAILURE; + } + DebugOnly rvIgnored = + SavedSelectionRef().RestoreSelection(SelectionRef()); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "SelectionState::RestoreSelection() failed, but ignored"); + StopPreservingSelection(); + return NS_OK; +} + +void HTMLEditor::StopPreservingSelection() { + MOZ_ASSERT(IsEditActionDataAvailable()); + + RangeUpdaterRef().DropSelectionState(SavedSelectionRef()); + SavedSelectionRef().RemoveAllRanges(); +} + +void HTMLEditor::PreHandleMouseDown(const MouseEvent& aMouseDownEvent) { + if (mPendingStylesToApplyToNewContent) { + // mPendingStylesToApplyToNewContent will be notified of selection change + // even if aMouseDownEvent is not an acceptable event for this editor. + // Therefore, we need to notify it of this event too. + mPendingStylesToApplyToNewContent->PreHandleMouseEvent(aMouseDownEvent); + } +} + +void HTMLEditor::PreHandleMouseUp(const MouseEvent& aMouseUpEvent) { + if (mPendingStylesToApplyToNewContent) { + // mPendingStylesToApplyToNewContent will be notified of selection change + // even if aMouseUpEvent is not an acceptable event for this editor. + // Therefore, we need to notify it of this event too. + mPendingStylesToApplyToNewContent->PreHandleMouseEvent(aMouseUpEvent); + } +} + +void HTMLEditor::PreHandleSelectionChangeCommand(Command aCommand) { + if (mPendingStylesToApplyToNewContent) { + mPendingStylesToApplyToNewContent->PreHandleSelectionChangeCommand( + aCommand); + } +} + +void HTMLEditor::PostHandleSelectionChangeCommand(Command aCommand) { + if (!mPendingStylesToApplyToNewContent) { + return; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (!editActionData.CanHandle()) { + return; + } + mPendingStylesToApplyToNewContent->PostHandleSelectionChangeCommand(*this, + aCommand); +} + +nsresult HTMLEditor::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent) { + // NOTE: When you change this method, you should also change: + // * editor/libeditor/tests/test_htmleditor_keyevent_handling.html + if (NS_WARN_IF(!aKeyboardEvent)) { + return NS_ERROR_UNEXPECTED; + } + + if (IsReadonly()) { + HandleKeyPressEventInReadOnlyMode(*aKeyboardEvent); + return NS_OK; + } + + MOZ_ASSERT(aKeyboardEvent->mMessage == eKeyPress, + "HandleKeyPressEvent gets non-keypress event"); + + switch (aKeyboardEvent->mKeyCode) { + case NS_VK_META: + case NS_VK_WIN: + case NS_VK_SHIFT: + case NS_VK_CONTROL: + case NS_VK_ALT: + // FYI: This shouldn't occur since modifier key shouldn't cause eKeyPress + // event. + aKeyboardEvent->PreventDefault(); + return NS_OK; + + case NS_VK_BACK: + case NS_VK_DELETE: { + nsresult rv = EditorBase::HandleKeyPressEvent(aKeyboardEvent); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::HandleKeyPressEvent() failed"); + return rv; + } + case NS_VK_TAB: { + // Basically, "Tab" key be used only for focus navigation. + // FYI: In web apps, this is always true. + if (IsTabbable()) { + return NS_OK; + } + + // If we're in the plaintext mode, and not tabbable editor, let's + // insert a horizontal tabulation. + if (IsInPlaintextMode()) { + if (aKeyboardEvent->IsShift() || aKeyboardEvent->IsControl() || + aKeyboardEvent->IsAlt() || aKeyboardEvent->IsMeta() || + aKeyboardEvent->IsOS()) { + return NS_OK; + } + + // else we insert the tab straight through + aKeyboardEvent->PreventDefault(); + nsresult rv = OnInputText(u"\t"_ns); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::OnInputText(\\t) failed"); + return rv; + } + + // Otherwise, e.g., we're an embedding editor in chrome, we can handle + // "Tab" key as an input. + if (aKeyboardEvent->IsControl() || aKeyboardEvent->IsAlt() || + aKeyboardEvent->IsMeta() || aKeyboardEvent->IsOS()) { + return NS_OK; + } + + RefPtr selection = GetSelection(); + if (NS_WARN_IF(!selection) || NS_WARN_IF(!selection->RangeCount())) { + return NS_ERROR_FAILURE; + } + + nsINode* startContainer = selection->GetRangeAt(0)->GetStartContainer(); + MOZ_ASSERT(startContainer); + if (!startContainer->IsContent()) { + break; + } + + const Element* editableBlockElement = + HTMLEditUtils::GetInclusiveAncestorElement( + *startContainer->AsContent(), + HTMLEditUtils::ClosestEditableBlockElement); + if (!editableBlockElement) { + break; + } + + // If selection is in a table element, we need special handling. + if (HTMLEditUtils::IsAnyTableElement(editableBlockElement)) { + Result result = + HandleTabKeyPressInTable(aKeyboardEvent); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING("HTMLEditor::HandleTabKeyPressInTable() failed"); + return EditorBase::ToGenericNSResult(result.unwrapErr()); + } + if (!result.inspect().Handled()) { + return NS_OK; + } + nsresult rv = ScrollSelectionFocusIntoView(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::ScrollSelectionFocusIntoView() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + // If selection is in an list item element, treat it as indent or outdent. + if (HTMLEditUtils::IsListItem(editableBlockElement)) { + aKeyboardEvent->PreventDefault(); + if (!aKeyboardEvent->IsShift()) { + nsresult rv = IndentAsAction(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::IndentAsAction() failed"); + return EditorBase::ToGenericNSResult(rv); + } + nsresult rv = OutdentAsAction(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::OutdentAsAction() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + // If only "Tab" key is pressed in normal context, just treat it as + // horizontal tab character input. + if (aKeyboardEvent->IsShift()) { + return NS_OK; + } + aKeyboardEvent->PreventDefault(); + nsresult rv = OnInputText(u"\t"_ns); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::OnInputText(\\t) failed"); + return EditorBase::ToGenericNSResult(rv); + } + case NS_VK_RETURN: + if (!aKeyboardEvent->IsInputtingLineBreak()) { + return NS_OK; + } + aKeyboardEvent->PreventDefault(); // consumed + if (aKeyboardEvent->IsShift()) { + // Only inserts a
element. + nsresult rv = InsertLineBreakAsAction(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::InsertLineBreakAsAction() failed"); + return EditorBase::ToGenericNSResult(rv); + } + // uses rules to figure out what to insert + nsresult rv = InsertParagraphSeparatorAsAction(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::InsertParagraphSeparatorAsAction() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + if (!aKeyboardEvent->IsInputtingText()) { + // we don't PreventDefault() here or keybindings like control-x won't work + return NS_OK; + } + aKeyboardEvent->PreventDefault(); + nsAutoString str(aKeyboardEvent->mCharCode); + nsresult rv = OnInputText(str); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::OnInputText() failed"); + return rv; +} + +NS_IMETHODIMP HTMLEditor::NodeIsBlock(nsINode* aNode, bool* aIsBlock) { + *aIsBlock = aNode && aNode->IsContent() && + HTMLEditUtils::IsBlockElement(*aNode->AsContent()); + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::UpdateBaseURL() { + RefPtr document = GetDocument(); + if (NS_WARN_IF(!document)) { + return NS_ERROR_FAILURE; + } + + // Look for an HTML tag + RefPtr baseElementList = + document->GetElementsByTagName(u"base"_ns); + + // If no base tag, then set baseURL to the document's URL. This is very + // important, else relative URLs for links and images are wrong + if (!baseElementList || !baseElementList->Item(0)) { + document->SetBaseURI(document->GetDocumentURI()); + } + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::InsertLineBreak() { + // XPCOM method's InsertLineBreak() should insert paragraph separator in + // HTMLEditor. + AutoEditActionDataSetter editActionData( + *this, EditAction::eInsertParagraphSeparator); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + const RefPtr editingHost = ComputeEditingHost(); + if (!editingHost) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + + Result result = + InsertParagraphSeparatorAsSubAction(*editingHost); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING("HTMLEditor::InsertParagraphSeparatorAsSubAction() failed"); + return EditorBase::ToGenericNSResult(result.unwrapErr()); + } + return NS_OK; +} + +nsresult HTMLEditor::InsertLineBreakAsAction(nsIPrincipal* aPrincipal) { + AutoEditActionDataSetter editActionData(*this, EditAction::eInsertLineBreak, + aPrincipal); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + if (IsSelectionRangeContainerNotContent()) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + + rv = InsertLineBreakAsSubAction(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::InsertLineBreakAsSubAction() failed"); + // Don't return NS_SUCCESS_DOM_NO_OPERATION for compatibility of `execCommand` + // result of Chrome. + return NS_FAILED(rv) ? rv : NS_OK; +} + +nsresult HTMLEditor::InsertParagraphSeparatorAsAction( + nsIPrincipal* aPrincipal) { + AutoEditActionDataSetter editActionData( + *this, EditAction::eInsertParagraphSeparator, aPrincipal); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + const RefPtr editingHost = ComputeEditingHost(); + if (!editingHost) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + + Result result = + InsertParagraphSeparatorAsSubAction(*editingHost); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING("HTMLEditor::InsertParagraphSeparatorAsSubAction() failed"); + return EditorBase::ToGenericNSResult(result.unwrapErr()); + } + return NS_OK; +} + +Result HTMLEditor::HandleTabKeyPressInTable( + WidgetKeyboardEvent* aKeyboardEvent) { + MOZ_ASSERT(aKeyboardEvent); + + AutoEditActionDataSetter dummyEditActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!dummyEditActionData.CanHandle())) { + // Do nothing if we didn't find a table cell. + return EditActionResult::IgnoredResult(); + } + + // Find enclosing table cell from selection (cell may be selected element) + const RefPtr cellElement = + GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td); + if (!cellElement) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td) " + "returned nullptr"); + // Do nothing if we didn't find a table cell. + return EditActionResult::IgnoredResult(); + } + + // find enclosing table + RefPtr table = + HTMLEditUtils::GetClosestAncestorTableElement(*cellElement); + if (!table) { + NS_WARNING("HTMLEditor::GetClosestAncestorTableElement() failed"); + return EditActionResult::IgnoredResult(); + } + + // advance to next cell + // first create an iterator over the table + PostContentIterator postOrderIter; + nsresult rv = postOrderIter.Init(table); + if (NS_FAILED(rv)) { + NS_WARNING("PostContentIterator::Init() failed"); + return Err(rv); + } + // position postOrderIter at block + rv = postOrderIter.PositionAt(cellElement); + if (NS_FAILED(rv)) { + NS_WARNING("PostContentIterator::PositionAt() failed"); + return Err(rv); + } + + do { + if (aKeyboardEvent->IsShift()) { + postOrderIter.Prev(); + } else { + postOrderIter.Next(); + } + + nsCOMPtr node = postOrderIter.GetCurrentNode(); + if (node && HTMLEditUtils::IsTableCell(node) && + HTMLEditUtils::GetClosestAncestorTableElement(*node->AsElement()) == + table) { + aKeyboardEvent->PreventDefault(); + CollapseSelectionToDeepestNonTableFirstChild(node); + if (NS_WARN_IF(Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + return EditActionResult::HandledResult(); + } + } while (!postOrderIter.IsDone()); + + if (aKeyboardEvent->IsShift()) { + return EditActionResult::IgnoredResult(); + } + + // If we haven't handled it yet, then we must have run off the end of the + // table. Insert a new row. + // XXX We should investigate whether this behavior is supported by other + // browsers later. + AutoEditActionDataSetter editActionData(*this, + EditAction::eInsertTableRowElement); + rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return Err(rv); + } + rv = InsertTableRowsWithTransaction(*cellElement, 1, + InsertPosition::eAfterSelectedCell); + if (NS_WARN_IF(Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::InsertTableRowsWithTransaction(*cellElement, 1, " + "InsertPosition::eAfterSelectedCell) failed"); + return Err(rv); + } + aKeyboardEvent->PreventDefault(); + // Put selection in right place. Use table code to get selection and index + // to new row... + RefPtr tblElement, cell; + int32_t row; + rv = GetCellContext(getter_AddRefs(tblElement), getter_AddRefs(cell), nullptr, + nullptr, &row, nullptr); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::GetCellContext() failed"); + return Err(rv); + } + if (!tblElement) { + NS_WARNING("HTMLEditor::GetCellContext() didn't return table element"); + return Err(NS_ERROR_FAILURE); + } + // ...so that we can ask for first cell in that row... + cell = GetTableCellElementAt(*tblElement, row, 0); + // ...and then set selection there. (Note that normally you should use + // CollapseSelectionToDeepestNonTableFirstChild(), but we know cell is an + // empty new cell, so this works fine) + if (cell) { + nsresult rv = CollapseSelectionToStartOf(*cell); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::CollapseSelectionToStartOf() failed"); + return Err(NS_ERROR_EDITOR_DESTROYED); + } + } + if (NS_WARN_IF(Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + return EditActionResult::HandledResult(); +} + +void HTMLEditor::CollapseSelectionToDeepestNonTableFirstChild(nsINode* aNode) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + MOZ_ASSERT(aNode); + + nsCOMPtr node = aNode; + + for (nsIContent* child = node->GetFirstChild(); child; + child = child->GetFirstChild()) { + // Stop if we find a table, don't want to go into nested tables + if (HTMLEditUtils::IsTable(child) || + !HTMLEditUtils::IsContainerNode(*child)) { + break; + } + node = child; + } + + DebugOnly rvIgnored = CollapseSelectionToStartOf(*node); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::CollapseSelectionToStartOf() failed, but ignored"); +} + +nsresult HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction( + const nsAString& aSourceToInsert) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // don't do any post processing, rules get confused + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eReplaceHeadWithHTMLSource, nsIEditor::eNone, + ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return ignoredError.StealNSResult(); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + CommitComposition(); + + // Do not use AutoEditSubActionNotifier -- rules code won't let us insert in + // . Use the head node as a parent and delete/insert directly. + // XXX We're using AutoEditSubActionNotifier above... + RefPtr document = GetDocument(); + if (NS_WARN_IF(!document)) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr headElementList = + document->GetElementsByTagName(u"head"_ns); + if (NS_WARN_IF(!headElementList)) { + return NS_ERROR_FAILURE; + } + + RefPtr primaryHeadElement = headElementList->Item(0)->AsElement(); + if (NS_WARN_IF(!primaryHeadElement)) { + return NS_ERROR_FAILURE; + } + + // First, make sure there are no return chars in the source. Bad things + // happen if you insert returns (instead of dom newlines, \n) into an editor + // document. + nsAutoString inputString(aSourceToInsert); + + // Windows linebreaks: Map CRLF to LF: + inputString.ReplaceSubstring(u"\r\n"_ns, u"\n"_ns); + + // Mac linebreaks: Map any remaining CR to LF: + inputString.ReplaceSubstring(u"\r"_ns, u"\n"_ns); + + AutoPlaceholderBatch treatAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + + // Get the first range in the selection, for context: + RefPtr range = SelectionRef().GetRangeAt(0); + if (NS_WARN_IF(!range)) { + return NS_ERROR_FAILURE; + } + + ErrorResult error; + RefPtr documentFragment = + range->CreateContextualFragment(inputString, error); + + // XXXX BUG 50965: This is not returning the text between ... + // Special code is needed in JS to handle title anyway, so it doesn't matter! + + if (error.Failed()) { + NS_WARNING("nsRange::CreateContextualFragment() failed"); + return error.StealNSResult(); + } + if (NS_WARN_IF(!documentFragment)) { + NS_WARNING( + "nsRange::CreateContextualFragment() didn't create DocumentFragment"); + return NS_ERROR_FAILURE; + } + + // First delete all children in head + while (nsCOMPtr child = primaryHeadElement->GetFirstChild()) { + nsresult rv = DeleteNodeWithTransaction(*child); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + } + + // Now insert the new nodes + int32_t offsetOfNewNode = 0; + + // Loop over the contents of the fragment and move into the document + while (nsCOMPtr child = documentFragment->GetFirstChild()) { + Result insertChildContentResult = + InsertNodeWithTransaction( + *child, EditorDOMPoint(primaryHeadElement, offsetOfNewNode++)); + if (MOZ_UNLIKELY(insertChildContentResult.isErr())) { + NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); + return insertChildContentResult.unwrapErr(); + } + // We probably don't need to adjust selection here, although we've done it + // unless AutoTransactionsConserveSelection is created in a caller. + insertChildContentResult.inspect().IgnoreCaretPointSuggestion(); + } + + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::RebuildDocumentFromSource( + const nsAString& aSourceString) { + CommitComposition(); + + AutoEditActionDataSetter editActionData(*this, EditAction::eSetHTML); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + RefPtr rootElement = GetRoot(); + if (NS_WARN_IF(!rootElement)) { + return NS_ERROR_NULL_POINTER; + } + + // Find where the tag starts. + nsReadingIterator beginbody; + nsReadingIterator endbody; + aSourceString.BeginReading(beginbody); + aSourceString.EndReading(endbody); + bool foundbody = + CaseInsensitiveFindInReadable(u" beginhead; + nsReadingIterator endhead; + aSourceString.BeginReading(beginhead); + aSourceString.EndReading(endhead); + bool foundhead = + CaseInsensitiveFindInReadable(u" beginbody.get()) { + foundhead = false; + } + + nsReadingIterator beginclosehead; + nsReadingIterator endclosehead; + aSourceString.BeginReading(beginclosehead); + aSourceString.EndReading(endclosehead); + + // Find the index after "" + bool foundclosehead = CaseInsensitiveFindInReadable( + u""_ns, beginclosehead, endclosehead); + // a valid close head appears after a found head + if (foundhead && beginhead.get() > beginclosehead.get()) { + foundclosehead = false; + } + // a valid close head appears before a found body + if (foundbody && beginclosehead.get() > beginbody.get()) { + foundclosehead = false; + } + + // Time to change the document + AutoPlaceholderBatch treatAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + + nsReadingIterator endtotal; + aSourceString.EndReading(endtotal); + + if (foundhead) { + if (foundclosehead) { + nsresult rv = ReplaceHeadContentsWithSourceWithTransaction( + Substring(beginhead, beginclosehead)); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() " + "failed"); + return rv; + } + } else if (foundbody) { + nsresult rv = ReplaceHeadContentsWithSourceWithTransaction( + Substring(beginhead, beginbody)); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() " + "failed"); + return rv; + } + } else { + // XXX Without recourse to some parser/content sink/docshell hackery we + // don't really know where the head ends and the body begins so we assume + // that there is no body + nsresult rv = ReplaceHeadContentsWithSourceWithTransaction( + Substring(beginhead, endtotal)); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() " + "failed"); + return rv; + } + } + } else { + nsReadingIterator begintotal; + aSourceString.BeginReading(begintotal); + constexpr auto head = u""_ns; + if (foundclosehead) { + nsresult rv = ReplaceHeadContentsWithSourceWithTransaction( + head + Substring(begintotal, beginclosehead)); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() " + "failed"); + return rv; + } + } else if (foundbody) { + nsresult rv = ReplaceHeadContentsWithSourceWithTransaction( + head + Substring(begintotal, beginbody)); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() " + "failed"); + return rv; + } + } else { + // XXX Without recourse to some parser/content sink/docshell hackery we + // don't really know where the head ends and the body begins so we assume + // that there is no head + nsresult rv = ReplaceHeadContentsWithSourceWithTransaction(head); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() " + "failed"); + return rv; + } + } + } + + rv = SelectAll(); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::SelectAll() failed"); + return rv; + } + + if (!foundbody) { + constexpr auto body = u""_ns; + // XXX Without recourse to some parser/content sink/docshell hackery we + // don't really know where the head ends and the body begins + if (foundclosehead) { + // assume body starts after the head ends + nsresult rv = LoadHTML(body + Substring(endclosehead, endtotal)); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::LoadHTML() failed"); + return rv; + } + } else if (foundhead) { + // assume there is no body + nsresult rv = LoadHTML(body); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::LoadHTML() failed"); + return rv; + } + } else { + // assume there is no head, the entire source is body + nsresult rv = LoadHTML(body + aSourceString); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::LoadHTML() failed"); + return rv; + } + } + + RefPtr divElement = CreateElementWithDefaults(*nsGkAtoms::div); + if (!divElement) { + NS_WARNING( + "HTMLEditor::CreateElementWithDefaults(nsGkAtoms::div) failed"); + return NS_ERROR_FAILURE; + } + CloneAttributesWithTransaction(*rootElement, *divElement); + + nsresult rv = MaybeCollapseSelectionAtFirstEditableNode(false); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(false) failed"); + return rv; + } + + rv = LoadHTML(Substring(beginbody, endtotal)); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::LoadHTML() failed"); + return rv; + } + + // Now we must copy attributes user might have edited on the tag + // because InsertHTML (actually, CreateContextualFragment()) will never + // return a body node in the DOM fragment + + // We already know where " beginclosebody = beginbody; + nsReadingIterator endclosebody; + aSourceString.EndReading(endclosebody); + if (!FindInReadable(u">"_ns, beginclosebody, endclosebody)) { + NS_WARNING("'>' was not found"); + return NS_ERROR_FAILURE; + } + + // Truncate at the end of the body tag. Kludge of the year: fool the parser + // by replacing "body" with "div" so we get a node + nsAutoString bodyTag; + bodyTag.AssignLiteral("
range = SelectionRef().GetRangeAt(0); + if (NS_WARN_IF(!range)) { + return NS_ERROR_FAILURE; + } + + ErrorResult error; + RefPtr documentFragment = + range->CreateContextualFragment(bodyTag, error); + if (error.Failed()) { + NS_WARNING("nsRange::CreateContextualFragment() failed"); + return error.StealNSResult(); + } + if (!documentFragment) { + NS_WARNING( + "nsRange::CreateContextualFragment() didn't create DocumentFagement"); + return NS_ERROR_FAILURE; + } + + nsCOMPtr firstChild = documentFragment->GetFirstChild(); + if (!firstChild || !firstChild->IsElement()) { + NS_WARNING("First child of DocumentFragment was not an Element node"); + return NS_ERROR_FAILURE; + } + + // Copy all attributes from the div child to current body element + CloneAttributesWithTransaction(*rootElement, + MOZ_KnownLive(*firstChild->AsElement())); + + // place selection at first editable content + rv = MaybeCollapseSelectionAtFirstEditableNode(false); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(false) failed"); + return rv; +} + +NS_IMETHODIMP HTMLEditor::InsertElementAtSelection(Element* aElement, + bool aDeleteSelection) { + nsresult rv = InsertElementAtSelectionAsAction(aElement, aDeleteSelection); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::InsertElementAtSelectionAsAction() failed"); + return rv; +} + +nsresult HTMLEditor::InsertElementAtSelectionAsAction( + Element* aElement, bool aDeleteSelection, nsIPrincipal* aPrincipal) { + if (NS_WARN_IF(!aElement)) { + return NS_ERROR_INVALID_ARG; + } + + if (IsReadonly()) { + return NS_OK; + } + + AutoEditActionDataSetter editActionData( + *this, HTMLEditUtils::GetEditActionForInsert(*aElement), aPrincipal); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + DebugOnly rvIgnored = CommitComposition(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "EditorBase::CommitComposition() failed, but ignored"); + + { + Result result = CanHandleHTMLEditSubAction(); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); + return EditorBase::ToGenericNSResult(result.unwrapErr()); + } + if (result.inspect().Canceled()) { + return NS_OK; + } + } + + UndefineCaretBidiLevel(); + + AutoPlaceholderBatch treatAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eInsertElement, 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"); + + rv = EnsureNoPaddingBRElementForEmptyEditor(); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " + "failed, but ignored"); + + if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) { + nsresult rv = EnsureCaretNotAfterInvisibleBRElement(); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() " + "failed, but ignored"); + if (NS_SUCCEEDED(rv)) { + nsresult rv = PrepareInlineStylesForCaret(); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); + } + } + + if (aDeleteSelection) { + if (!HTMLEditUtils::IsBlockElement(*aElement)) { + // E.g., inserting an image. In this case we don't need to delete any + // inline wrappers before we do the insertion. Otherwise we let + // DeleteSelectionAndPrepareToCreateNode do the deletion for us, which + // calls DeleteSelection with aStripWrappers = eStrip. + nsresult rv = DeleteSelectionAsSubAction(eNone, eNoStrip); + if (NS_FAILED(rv)) { + NS_WARNING( + "EditorBase::DeleteSelectionAsSubAction(eNone, eNoStrip) failed"); + return EditorBase::ToGenericNSResult(rv); + } + } + + nsresult rv = DeleteSelectionAndPrepareToCreateNode(); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::DeleteSelectionAndPrepareToCreateNode() failed"); + return rv; + } + } + // If deleting, selection will be collapsed. + // so if not, we collapse it + else { + // Named Anchor is a special case, + // We collapse to insert element BEFORE the selection + // For all other tags, we insert AFTER the selection + if (HTMLEditUtils::IsNamedAnchor(aElement)) { + IgnoredErrorResult ignoredError; + SelectionRef().CollapseToStart(ignoredError); + if (NS_WARN_IF(Destroyed())) { + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION(!ignoredError.Failed(), + "Selection::CollapseToStart() failed, but ignored"); + } else { + IgnoredErrorResult ignoredError; + SelectionRef().CollapseToEnd(ignoredError); + if (NS_WARN_IF(Destroyed())) { + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION(!ignoredError.Failed(), + "Selection::CollapseToEnd() failed, but ignored"); + } + } + + if (!SelectionRef().GetAnchorNode()) { + return NS_OK; + } + + Element* editingHost = ComputeEditingHost(LimitInBodyElement::No); + if (NS_WARN_IF(!editingHost)) { + return EditorBase::ToGenericNSResult(NS_ERROR_FAILURE); + } + + EditorRawDOMPoint atAnchor(SelectionRef().AnchorRef()); + // Adjust position based on the node we are going to insert. + EditorDOMPoint pointToInsert = + HTMLEditUtils::GetBetterInsertionPointFor( + *aElement, atAnchor, *editingHost); + if (!pointToInsert.IsSet()) { + NS_WARNING("HTMLEditUtils::GetBetterInsertionPointFor() failed"); + return NS_ERROR_FAILURE; + } + + { + Result insertElementResult = + InsertNodeIntoProperAncestorWithTransaction( + *aElement, pointToInsert, + SplitAtEdges::eAllowToCreateEmptyContainer); + if (MOZ_UNLIKELY(insertElementResult.isErr())) { + NS_WARNING( + "HTMLEditor::InsertNodeIntoProperAncestorWithTransaction(" + "SplitAtEdges::eAllowToCreateEmptyContainer) failed"); + return EditorBase::ToGenericNSResult(insertElementResult.unwrapErr()); + } + insertElementResult.inspect().IgnoreCaretPointSuggestion(); + } + // Set caret after element, but check for special case + // of inserting table-related elements: set in first cell instead + if (!SetCaretInTableCell(aElement)) { + if (NS_WARN_IF(Destroyed())) { + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); + } + nsresult rv = CollapseSelectionTo(EditorRawDOMPoint::After(*aElement)); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::CollapseSelectionTo() failed"); + return EditorBase::ToGenericNSResult(rv); + } + } + + // check for inserting a whole table at the end of a block. If so insert + // a br after it. + if (!HTMLEditUtils::IsTable(aElement) || + !HTMLEditUtils::IsLastChild(*aElement, + {WalkTreeOption::IgnoreNonEditableNode})) { + return NS_OK; + } + + const auto afterElement = EditorDOMPoint::After(*aElement); + // Collapse selection to the new `
` element node after creating it. + Result insertBRElementResult = + InsertBRElement(WithTransaction::Yes, afterElement, ePrevious); + if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { + NS_WARNING( + "HTMLEditor::InsertBRElement(WithTransaction::Yes, ePrevious) failed"); + return EditorBase::ToGenericNSResult(insertBRElementResult.unwrapErr()); + } + rv = insertBRElementResult.inspect().SuggestCaretPointTo(*this, {}); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed"); + MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode()); + return EditorBase::ToGenericNSResult(rv); +} + +template +Result, nsresult> +HTMLEditor::InsertNodeIntoProperAncestorWithTransaction( + NodeType& aContentToInsert, const EditorDOMPoint& aPointToInsert, + SplitAtEdges aSplitAtEdges) { + MOZ_ASSERT(aPointToInsert.IsSetAndValidInComposedDoc()); + if (NS_WARN_IF(!aPointToInsert.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + MOZ_ASSERT(aPointToInsert.IsSetAndValid()); + + if (aContentToInsert.NodeType() == nsINode::DOCUMENT_TYPE_NODE || + aContentToInsert.NodeType() == nsINode::PROCESSING_INSTRUCTION_NODE) { + return CreateNodeResultBase::NotHandled(); + } + + // Search up the parent chain to find a suitable container. + EditorDOMPoint pointToInsert(aPointToInsert); + MOZ_ASSERT(pointToInsert.IsSet()); + while (!HTMLEditUtils::CanNodeContain(*pointToInsert.GetContainer(), + aContentToInsert)) { + // If the current parent is a root (body or table element) + // then go no further - we can't insert. + if (MOZ_UNLIKELY( + pointToInsert.IsContainerHTMLElement(nsGkAtoms::body) || + HTMLEditUtils::IsAnyTableElement(pointToInsert.GetContainer()))) { + NS_WARNING( + "There was no proper container element to insert the content node in " + "the document"); + return Err(NS_ERROR_FAILURE); + } + + // Get the next point. + pointToInsert = pointToInsert.ParentPoint(); + + if (MOZ_UNLIKELY( + !pointToInsert.IsInContentNode() || + !EditorUtils::IsEditableContent( + *pointToInsert.ContainerAs(), EditorType::HTML))) { + NS_WARNING( + "There was no proper container element to insert the content node in " + "the editing host"); + return Err(NS_ERROR_FAILURE); + } + } + + if (pointToInsert != aPointToInsert) { + // We need to split some levels above the original selection parent. + MOZ_ASSERT(pointToInsert.GetChild()); + Result splitNodeResult = + SplitNodeDeepWithTransaction(MOZ_KnownLive(*pointToInsert.GetChild()), + aPointToInsert, aSplitAtEdges); + if (MOZ_UNLIKELY(splitNodeResult.isErr())) { + NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed"); + return splitNodeResult.propagateErr(); + } + pointToInsert = + splitNodeResult.inspect().template AtSplitPoint(); + MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc()); + // Caret should be set by the caller of this method so that we don't + // need to handle it here. + splitNodeResult.inspect().IgnoreCaretPointSuggestion(); + } + + // Now we can insert the new node. + Result, nsresult> insertContentNodeResult = + InsertNodeWithTransaction(aContentToInsert, pointToInsert); + if (MOZ_LIKELY(insertContentNodeResult.isOk()) && + MOZ_UNLIKELY(NS_WARN_IF(!aContentToInsert.GetParentNode()) || + NS_WARN_IF(aContentToInsert.GetParentNode() != + pointToInsert.GetContainer()))) { + NS_WARNING( + "EditorBase::InsertNodeWithTransaction() succeeded, but the inserted " + "node was moved or removed by the web app"); + insertContentNodeResult.inspect().IgnoreCaretPointSuggestion(); + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + NS_WARNING_ASSERTION(insertContentNodeResult.isOk(), + "EditorBase::InsertNodeWithTransaction() failed"); + return insertContentNodeResult; +} + +NS_IMETHODIMP HTMLEditor::SelectElement(Element* aElement) { + if (NS_WARN_IF(!aElement)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv = SelectContentInternal(MOZ_KnownLive(*aElement)); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::SelectContentInternal() failed"); + return rv; +} + +nsresult HTMLEditor::SelectContentInternal(nsIContent& aContentToSelect) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // Must be sure that element is contained in the editing host + const RefPtr editingHost = ComputeEditingHost(); + if (NS_WARN_IF(!editingHost) || + NS_WARN_IF(!aContentToSelect.IsInclusiveDescendantOf(editingHost))) { + return NS_ERROR_FAILURE; + } + + EditorRawDOMPoint newSelectionStart(&aContentToSelect); + if (NS_WARN_IF(!newSelectionStart.IsSet())) { + return NS_ERROR_FAILURE; + } + EditorRawDOMPoint newSelectionEnd(EditorRawDOMPoint::After(aContentToSelect)); + MOZ_ASSERT(newSelectionEnd.IsSet()); + ErrorResult error; + SelectionRef().SetStartAndEndInLimiter(newSelectionStart, newSelectionEnd, + error); + NS_WARNING_ASSERTION(!error.Failed(), + "Selection::SetStartAndEndInLimiter() failed"); + return error.StealNSResult(); +} + +nsresult HTMLEditor::AppendContentToSelectionAsRange(nsIContent& aContent) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + EditorRawDOMPoint atContent(&aContent); + if (NS_WARN_IF(!atContent.IsSet())) { + return NS_ERROR_FAILURE; + } + + RefPtr range = nsRange::Create( + atContent.ToRawRangeBoundary(), + atContent.NextPoint().ToRawRangeBoundary(), IgnoreErrors()); + if (NS_WARN_IF(!range)) { + NS_WARNING("nsRange::Create() failed"); + return NS_ERROR_FAILURE; + } + + ErrorResult error; + SelectionRef().AddRangeAndSelectFramesAndNotifyListeners(*range, error); + if (NS_WARN_IF(Destroyed())) { + if (error.Failed()) { + error.SuppressException(); + } + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION(!error.Failed(), "Failed to add range to Selection"); + return error.StealNSResult(); +} + +nsresult HTMLEditor::ClearSelection() { + MOZ_ASSERT(IsEditActionDataAvailable()); + + ErrorResult error; + SelectionRef().RemoveAllRanges(error); + if (NS_WARN_IF(Destroyed())) { + if (error.Failed()) { + error.SuppressException(); + } + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION(!error.Failed(), "Selection::RemoveAllRanges() failed"); + return error.StealNSResult(); +} + +nsresult HTMLEditor::SetParagraphFormatAsAction( + const nsAString& aParagraphFormat, nsIPrincipal* aPrincipal) { + AutoEditActionDataSetter editActionData( + *this, EditAction::eInsertBlockElement, aPrincipal); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + // TODO: Computing the editing host here makes the `execCommand` in + // docshell/base/crashtests/file_432114-2.xhtml cannot run + // `DOMNodeRemoved` event listener with deleting the bogus
element. + // So that it should be rewritten with different mutation event listener + // since we'd like to stop using it. + + nsAutoString lowerCaseTagName(aParagraphFormat); + ToLowerCase(lowerCaseTagName); + RefPtr tagName = NS_Atomize(lowerCaseTagName); + MOZ_ASSERT(tagName); + if (tagName == nsGkAtoms::dd || tagName == nsGkAtoms::dt) { + // MOZ_KnownLive(tagName->AsStatic()) because nsStaticAtom instances live + // while the process is running. + Result result = + MakeOrChangeListAndListItemAsSubAction( + MOZ_KnownLive(*tagName->AsStatic()), u""_ns, + SelectAllOfCurrentList::No); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING( + "HTMLEditor::MakeOrChangeListAndListItemAsSubAction(" + "SelectAllOfCurrentList::No) failed"); + return EditorBase::ToGenericNSResult(result.unwrapErr()); + } + return NS_OK; + } + + rv = FormatBlockContainerAsSubAction(*tagName); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::FormatBlockContainerAsSubAction() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +NS_IMETHODIMP HTMLEditor::GetParagraphState(bool* aMixed, + nsAString& aFirstParagraphState) { + if (NS_WARN_IF(!aMixed)) { + return NS_ERROR_INVALID_ARG; + } + if (!mInitSucceeded) { + return NS_ERROR_NOT_INITIALIZED; + } + + ErrorResult error; + ParagraphStateAtSelection paragraphState(*this, error); + if (error.Failed()) { + NS_WARNING("ParagraphStateAtSelection failed"); + return error.StealNSResult(); + } + + *aMixed = paragraphState.IsMixed(); + if (NS_WARN_IF(!paragraphState.GetFirstParagraphStateAtSelection())) { + // XXX Odd result, but keep this behavior for now... + aFirstParagraphState.AssignASCII("x"); + } else { + paragraphState.GetFirstParagraphStateAtSelection()->ToString( + aFirstParagraphState); + } + return NS_OK; +} + +nsresult HTMLEditor::GetBackgroundColorState(bool* aMixed, + nsAString& aOutColor) { + if (NS_WARN_IF(!aMixed)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (IsCSSEnabled()) { + // if we are in CSS mode, we have to check if the containing block defines + // a background color + nsresult rv = GetCSSBackgroundColorState(aMixed, aOutColor, true); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::GetCSSBackgroundColorState() failed"); + return EditorBase::ToGenericNSResult(rv); + } + // in HTML mode, we look only at page's background + nsresult rv = GetHTMLBackgroundColorState(aMixed, aOutColor); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::GetCSSBackgroundColorState() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +NS_IMETHODIMP HTMLEditor::GetHighlightColorState(bool* aMixed, + nsAString& aOutColor) { + if (NS_WARN_IF(!aMixed)) { + return NS_ERROR_INVALID_ARG; + } + + *aMixed = false; + aOutColor.AssignLiteral("transparent"); + if (!IsCSSEnabled()) { + return NS_OK; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + // in CSS mode, text background can be added by the Text Highlight button + // we need to query the background of the selection without looking for + // the block container of the ranges in the selection + nsresult rv = GetCSSBackgroundColorState(aMixed, aOutColor, false); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::GetCSSBackgroundColorState() failed"); + return rv; +} + +nsresult HTMLEditor::GetCSSBackgroundColorState(bool* aMixed, + nsAString& aOutColor, + bool aBlockLevel) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (NS_WARN_IF(!aMixed)) { + return NS_ERROR_INVALID_ARG; + } + + *aMixed = false; + // the default background color is transparent + aOutColor.AssignLiteral("transparent"); + + RefPtr firstRange = SelectionRef().GetRangeAt(0); + if (NS_WARN_IF(!firstRange)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr startContainer = firstRange->GetStartContainer(); + if (NS_WARN_IF(!startContainer) || NS_WARN_IF(!startContainer->IsContent())) { + return NS_ERROR_FAILURE; + } + + // is the selection collapsed? + nsIContent* contentToExamine; + if (SelectionRef().IsCollapsed() || startContainer->IsText()) { + if (NS_WARN_IF(!startContainer->IsContent())) { + return NS_ERROR_FAILURE; + } + // we want to look at the startContainer and ancestors + contentToExamine = startContainer->AsContent(); + } else { + // otherwise we want to look at the first editable node after + // {startContainer,offset} and its ancestors for divs with alignment on them + contentToExamine = firstRange->GetChildAtStartOffset(); + // GetNextNode(startContainer, offset, true, address_of(contentToExamine)); + } + + if (NS_WARN_IF(!contentToExamine)) { + return NS_ERROR_FAILURE; + } + + if (aBlockLevel) { + // we are querying the block background (and not the text background), let's + // climb to the block container. Note that background color of ancestor + // of editing host may be what the caller wants to know. Therefore, we + // should ignore the editing host boundaries. + Element* const closestBlockElement = + HTMLEditUtils::GetInclusiveAncestorElement( + *contentToExamine, HTMLEditUtils::ClosestBlockElement); + if (NS_WARN_IF(!closestBlockElement)) { + return NS_OK; + } + + for (RefPtr blockElement = closestBlockElement; blockElement;) { + RefPtr nextBlockElement = HTMLEditUtils::GetAncestorElement( + *blockElement, HTMLEditUtils::ClosestBlockElement); + DebugOnly rvIgnored = CSSEditUtils::GetComputedProperty( + *blockElement, *nsGkAtoms::backgroundColor, aOutColor); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + if (MayHaveMutationEventListeners() && + NS_WARN_IF(nextBlockElement != + HTMLEditUtils::GetAncestorElement( + *blockElement, HTMLEditUtils::ClosestBlockElement))) { + return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; + } + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "CSSEditUtils::GetComputedProperty(nsGkAtoms::" + "backgroundColor) failed, but ignored"); + // look at parent if the queried color is transparent and if the node to + // examine is not the root of the document + if (!aOutColor.EqualsLiteral("transparent") && + !aOutColor.EqualsLiteral("rgba(0, 0, 0, 0)")) { + break; + } + blockElement = std::move(nextBlockElement); + } + + if (aOutColor.EqualsLiteral("transparent") || + aOutColor.EqualsLiteral("rgba(0, 0, 0, 0)")) { + // we have hit the root of the document and the color is still transparent + // ! Grumble... Let's look at the default background color because that's + // the color we are looking for + CSSEditUtils::GetDefaultBackgroundColor(aOutColor); + } + } else { + // no, we are querying the text background for the Text Highlight button + if (contentToExamine->IsText()) { + // if the node of interest is a text node, let's climb a level + contentToExamine = contentToExamine->GetParent(); + } + // Return default value due to no parent node + if (!contentToExamine) { + return NS_OK; + } + + for (RefPtr element = + contentToExamine->GetAsElementOrParentElement(); + element; element = element->GetParentElement()) { + // is the node to examine a block ? + if (HTMLEditUtils::IsBlockElement(*element)) { + // yes it is a block; in that case, the text background color is + // transparent + aOutColor.AssignLiteral("transparent"); + break; + } + + // no, it's not; let's retrieve the computed style of background-color + // for the node to examine + nsCOMPtr parentNode = element->GetParentNode(); + DebugOnly rvIgnored = CSSEditUtils::GetComputedProperty( + *element, *nsGkAtoms::backgroundColor, aOutColor); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + if (NS_WARN_IF(parentNode != element->GetParentNode())) { + return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; + } + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "CSSEditUtils::GetComputedProperty(nsGkAtoms::" + "backgroundColor) failed, but ignored"); + if (!aOutColor.EqualsLiteral("transparent")) { + break; + } + } + } + return NS_OK; +} + +nsresult HTMLEditor::GetHTMLBackgroundColorState(bool* aMixed, + nsAString& aOutColor) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // TODO: We don't handle "mixed" correctly! + if (NS_WARN_IF(!aMixed)) { + return NS_ERROR_INVALID_ARG; + } + + *aMixed = false; + aOutColor.Truncate(); + + Result, nsresult> cellOrRowOrTableElementOrError = + GetSelectedOrParentTableElement(); + if (cellOrRowOrTableElementOrError.isErr()) { + NS_WARNING("HTMLEditor::GetSelectedOrParentTableElement() returned error"); + return cellOrRowOrTableElementOrError.unwrapErr(); + } + + for (RefPtr element = cellOrRowOrTableElementOrError.unwrap(); + element; element = element->GetParentElement()) { + // We are in a cell or selected table + element->GetAttr(kNameSpaceID_None, nsGkAtoms::bgcolor, aOutColor); + + // Done if we have a color explicitly set + if (!aOutColor.IsEmpty()) { + return NS_OK; + } + + // Once we hit the body, we're done + if (element->IsHTMLElement(nsGkAtoms::body)) { + return NS_OK; + } + + // No color is set, but we need to report visible color inherited + // from nested cells/tables, so search up parent chain so that + // let's keep checking the ancestors. + } + + // If no table or cell found, get page body + Element* rootElement = GetRoot(); + if (NS_WARN_IF(!rootElement)) { + return NS_ERROR_FAILURE; + } + + rootElement->GetAttr(kNameSpaceID_None, nsGkAtoms::bgcolor, aOutColor); + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::GetListState(bool* aMixed, bool* aOL, bool* aUL, + bool* aDL) { + if (NS_WARN_IF(!aMixed) || NS_WARN_IF(!aOL) || NS_WARN_IF(!aUL) || + NS_WARN_IF(!aDL)) { + return NS_ERROR_INVALID_ARG; + } + if (!mInitSucceeded) { + return NS_ERROR_NOT_INITIALIZED; + } + + ErrorResult error; + ListElementSelectionState state(*this, error); + if (error.Failed()) { + NS_WARNING("ListElementSelectionState failed"); + return error.StealNSResult(); + } + + *aMixed = state.IsNotOneTypeListElementSelected(); + *aOL = state.IsOLElementSelected(); + *aUL = state.IsULElementSelected(); + *aDL = state.IsDLElementSelected(); + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::GetListItemState(bool* aMixed, bool* aLI, bool* aDT, + bool* aDD) { + if (NS_WARN_IF(!aMixed) || NS_WARN_IF(!aLI) || NS_WARN_IF(!aDT) || + NS_WARN_IF(!aDD)) { + return NS_ERROR_INVALID_ARG; + } + if (!mInitSucceeded) { + return NS_ERROR_NOT_INITIALIZED; + } + + ErrorResult error; + ListItemElementSelectionState state(*this, error); + if (error.Failed()) { + NS_WARNING("ListItemElementSelectionState failed"); + return error.StealNSResult(); + } + + // XXX Why do we ignore `
  • ` element selected state? + *aMixed = state.IsNotOneTypeDefinitionListItemElementSelected(); + *aLI = state.IsLIElementSelected(); + *aDT = state.IsDTElementSelected(); + *aDD = state.IsDDElementSelected(); + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::GetAlignment(bool* aMixed, + nsIHTMLEditor::EAlignment* aAlign) { + if (NS_WARN_IF(!aMixed) || NS_WARN_IF(!aAlign)) { + return NS_ERROR_INVALID_ARG; + } + if (!mInitSucceeded) { + return NS_ERROR_NOT_INITIALIZED; + } + + ErrorResult error; + AlignStateAtSelection state(*this, error); + if (error.Failed()) { + NS_WARNING("AlignStateAtSelection failed"); + return error.StealNSResult(); + } + + *aMixed = false; + *aAlign = state.AlignmentAtSelectionStart(); + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::MakeOrChangeList(const nsAString& aListType, + bool aEntireList, + const nsAString& aBulletType) { + RefPtr listTagName = NS_Atomize(aListType); + if (NS_WARN_IF(!listTagName) || NS_WARN_IF(!listTagName->IsStatic())) { + return NS_ERROR_INVALID_ARG; + } + // MOZ_KnownLive(listTagName->AsStatic()) because nsStaticAtom instances live + // while the process is running. + nsresult rv = MakeOrChangeListAsAction( + MOZ_KnownLive(*listTagName->AsStatic()), aBulletType, + aEntireList ? SelectAllOfCurrentList::Yes : SelectAllOfCurrentList::No); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::MakeOrChangeListAsAction() failed"); + return rv; +} + +nsresult HTMLEditor::MakeOrChangeListAsAction( + const nsStaticAtom& aListElementTagName, const nsAString& aBulletType, + SelectAllOfCurrentList aSelectAllOfCurrentList, nsIPrincipal* aPrincipal) { + if (NS_WARN_IF(!mInitSucceeded)) { + return NS_ERROR_NOT_INITIALIZED; + } + + AutoEditActionDataSetter editActionData( + *this, HTMLEditUtils::GetEditActionForInsert(aListElementTagName), + aPrincipal); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + Result result = + MakeOrChangeListAndListItemAsSubAction(aListElementTagName, aBulletType, + aSelectAllOfCurrentList); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING("HTMLEditor::MakeOrChangeListAndListItemAsSubAction() failed"); + return EditorBase::ToGenericNSResult(result.unwrapErr()); + } + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::RemoveList(const nsAString& aListType) { + nsresult rv = RemoveListAsAction(aListType); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::RemoveListAsAction() failed"); + return rv; +} + +nsresult HTMLEditor::RemoveListAsAction(const nsAString& aListType, + nsIPrincipal* aPrincipal) { + if (NS_WARN_IF(!mInitSucceeded)) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Note that we ignore aListType when we actually remove parent list elements. + // However, we need to set InputEvent.inputType to "insertOrderedList" or + // "insertedUnorderedList" when this is called for + // execCommand("insertorderedlist") or execCommand("insertunorderedlist"). + // Otherwise, comm-central UI may call this methods with "dl" or "". + // So, it's okay to use mismatched EditAction here if this is called in + // comm-central. + + RefPtr listAtom = NS_Atomize(aListType); + if (NS_WARN_IF(!listAtom)) { + return NS_ERROR_INVALID_ARG; + } + AutoEditActionDataSetter editActionData( + *this, HTMLEditUtils::GetEditActionForRemoveList(*listAtom), aPrincipal); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + const RefPtr editingHost = ComputeEditingHost(); + if (!editingHost) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + + rv = RemoveListAtSelectionAsSubAction(*editingHost); + NS_WARNING_ASSERTION(NS_FAILED(rv), + "HTMLEditor::RemoveListAtSelectionAsSubAction() failed"); + return rv; +} + +nsresult HTMLEditor::FormatBlockContainerAsSubAction(nsAtom& aTagName) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (NS_WARN_IF(!mInitSucceeded)) { + return NS_ERROR_NOT_INITIALIZED; + } + + MOZ_ASSERT(&aTagName != nsGkAtoms::dd && &aTagName != nsGkAtoms::dt); + + AutoPlaceholderBatch treatAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eCreateOrRemoveBlock, 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"); + + { + Result result = CanHandleHTMLEditSubAction(); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); + return result.unwrapErr(); + } + if (result.inspect().Canceled()) { + return NS_OK; + } + } + + if (IsSelectionRangeContainerNotContent()) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + + nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " + "failed, but ignored"); + + if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) { + nsresult rv = EnsureCaretNotAfterInvisibleBRElement(); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() " + "failed, but ignored"); + if (NS_SUCCEEDED(rv)) { + nsresult rv = PrepareInlineStylesForCaret(); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); + } + } + + // FormatBlockContainerWithTransaction() creates AutoSelectionRestorer. + // Therefore, even if it returns NS_OK, editor might have been destroyed + // at restoring Selection. + const RefPtr editingHost = ComputeEditingHost(); + if (MOZ_UNLIKELY(!editingHost)) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + AutoRangeArray selectionRanges(SelectionRef()); + Result, nsresult> suggestBlockElementToPutCaretOrError = + FormatBlockContainerWithTransaction(selectionRanges, aTagName, + *editingHost); + if (suggestBlockElementToPutCaretOrError.isErr()) { + NS_WARNING("HTMLEditor::FormatBlockContainerWithTransaction() failed"); + return suggestBlockElementToPutCaretOrError.unwrapErr(); + } + + if (selectionRanges.HasSavedRanges()) { + selectionRanges.RestoreFromSavedRanges(); + } + + rv = selectionRanges.ApplyTo(SelectionRef()); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + if (NS_FAILED(rv)) { + NS_WARNING("AutoRangeArray::ApplyTo(SelectionRef()) failed, but ignored"); + return rv; + } + + rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() " + "failed"); + + if (!suggestBlockElementToPutCaretOrError.inspect() || + !SelectionRef().IsCollapsed()) { + return rv; + } + const auto firstSelectionStartPoint = + GetFirstSelectionStartPoint(); + if (MOZ_UNLIKELY(!firstSelectionStartPoint.IsSet())) { + return rv; + } + Result pointInBlockElementOrError = + HTMLEditUtils::ComputePointToPutCaretInElementIfOutside< + EditorRawDOMPoint>(*suggestBlockElementToPutCaretOrError.inspect(), + firstSelectionStartPoint); + if (MOZ_UNLIKELY(pointInBlockElementOrError.isErr())) { + NS_WARNING( + "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() failed, but " + "ignored"); + return rv; + } + // Note that if the point is unset, it means that firstSelectionStartPoint is + // in the block element. + if (pointInBlockElementOrError.inspect().IsSet()) { + nsresult rvOfCollapseSelection = + CollapseSelectionTo(pointInBlockElementOrError.inspect()); + if (MOZ_UNLIKELY(rvOfCollapseSelection == NS_ERROR_EDITOR_DESTROYED)) { + NS_WARNING("EditorBase::CollapseSelectionTo() failed"); + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvOfCollapseSelection), + "EditorBase::CollapseSelectionTo() failed, but ignored"); + } + return rv; +} + +nsresult HTMLEditor::IndentAsAction(nsIPrincipal* aPrincipal) { + if (NS_WARN_IF(!mInitSucceeded)) { + return NS_ERROR_NOT_INITIALIZED; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eIndent, + aPrincipal); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + const RefPtr editingHost = ComputeEditingHost(); + if (!editingHost) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + + Result result = IndentAsSubAction(*editingHost); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING("HTMLEditor::IndentAsSubAction() failed"); + return EditorBase::ToGenericNSResult(result.unwrapErr()); + } + return NS_OK; +} + +nsresult HTMLEditor::OutdentAsAction(nsIPrincipal* aPrincipal) { + if (NS_WARN_IF(!mInitSucceeded)) { + return NS_ERROR_NOT_INITIALIZED; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eOutdent, + aPrincipal); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + const RefPtr editingHost = ComputeEditingHost(); + if (!editingHost) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + + Result result = OutdentAsSubAction(*editingHost); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING("HTMLEditor::OutdentAsSubAction() failed"); + return EditorBase::ToGenericNSResult(result.unwrapErr()); + } + return NS_OK; +} + +// TODO: IMPLEMENT ALIGNMENT! + +nsresult HTMLEditor::AlignAsAction(const nsAString& aAlignType, + nsIPrincipal* aPrincipal) { + AutoEditActionDataSetter editActionData( + *this, HTMLEditUtils::GetEditActionForAlignment(aAlignType), aPrincipal); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + const RefPtr editingHost = ComputeEditingHost(); + if (!editingHost) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + + Result result = + AlignAsSubAction(aAlignType, *editingHost); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING("HTMLEditor::AlignAsSubAction() failed"); + return EditorBase::ToGenericNSResult(result.unwrapErr()); + } + return NS_OK; +} + +Element* HTMLEditor::GetInclusiveAncestorByTagName(const nsStaticAtom& aTagName, + nsIContent& aContent) const { + MOZ_ASSERT(&aTagName != nsGkAtoms::_empty); + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return nullptr; + } + + return GetInclusiveAncestorByTagNameInternal(aTagName, aContent); +} + +Element* HTMLEditor::GetInclusiveAncestorByTagNameAtSelection( + const nsStaticAtom& aTagName) const { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(&aTagName != nsGkAtoms::_empty); + + // If no node supplied, get it from anchor node of current selection + const EditorRawDOMPoint atAnchor(SelectionRef().AnchorRef()); + if (NS_WARN_IF(!atAnchor.IsInContentNode())) { + return nullptr; + } + + // Try to get the actual selected node + nsIContent* content = nullptr; + if (atAnchor.GetContainer()->HasChildNodes() && + atAnchor.ContainerAs()) { + content = atAnchor.GetChild(); + } + // Anchor node is probably a text node - just use that + if (!content) { + content = atAnchor.ContainerAs(); + if (NS_WARN_IF(!content)) { + return nullptr; + } + } + return GetInclusiveAncestorByTagNameInternal(aTagName, *content); +} + +Element* HTMLEditor::GetInclusiveAncestorByTagNameInternal( + const nsStaticAtom& aTagName, const nsIContent& aContent) const { + MOZ_ASSERT(&aTagName != nsGkAtoms::_empty); + + Element* currentElement = aContent.GetAsElementOrParentElement(); + if (NS_WARN_IF(!currentElement)) { + MOZ_ASSERT(!aContent.GetParentNode()); + return nullptr; + } + + bool lookForLink = IsLinkTag(aTagName); + bool lookForNamedAnchor = IsNamedAnchorTag(aTagName); + for (Element* element : currentElement->InclusiveAncestorsOfType()) { + // Stop searching if parent is a body element. Note: Originally used + // IsRoot() to/ stop at table cells, but that's too messy when you are + // trying to find the parent table. + if (element->IsHTMLElement(nsGkAtoms::body)) { + return nullptr; + } + if (lookForLink) { + // Test if we have a link (an anchor with href set) + if (HTMLEditUtils::IsLink(element)) { + return element; + } + } else if (lookForNamedAnchor) { + // Test if we have a named anchor (an anchor with name set) + if (HTMLEditUtils::IsNamedAnchor(element)) { + return element; + } + } else if (&aTagName == nsGkAtoms::list_) { + // Match "ol", "ul", or "dl" for lists + if (HTMLEditUtils::IsAnyListElement(element)) { + return element; + } + } else if (&aTagName == nsGkAtoms::td) { + // Table cells are another special case: match either "td" or "th" + if (HTMLEditUtils::IsTableCell(element)) { + return element; + } + } else if (&aTagName == element->NodeInfo()->NameAtom()) { + return element; + } + } + return nullptr; +} + +NS_IMETHODIMP HTMLEditor::GetElementOrParentByTagName(const nsAString& aTagName, + nsINode* aNode, + Element** aReturn) { + if (NS_WARN_IF(aTagName.IsEmpty()) || NS_WARN_IF(!aReturn)) { + return NS_ERROR_INVALID_ARG; + } + + nsStaticAtom* tagName = EditorUtils::GetTagNameAtom(aTagName); + if (NS_WARN_IF(!tagName)) { + // We don't need to support custom elements since this is an internal API. + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + if (NS_WARN_IF(tagName == nsGkAtoms::_empty)) { + return NS_ERROR_INVALID_ARG; + } + + if (!aNode) { + AutoEditActionDataSetter dummyEditAction(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!dummyEditAction.CanHandle())) { + return NS_ERROR_NOT_AVAILABLE; + } + RefPtr parentElement = + GetInclusiveAncestorByTagNameAtSelection(*tagName); + if (!parentElement) { + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + parentElement.forget(aReturn); + return NS_OK; + } + + if (!aNode->IsContent() || !aNode->GetAsElementOrParentElement()) { + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + RefPtr parentElement = + GetInclusiveAncestorByTagName(*tagName, *aNode->AsContent()); + if (!parentElement) { + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + parentElement.forget(aReturn); + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::GetSelectedElement(const nsAString& aTagName, + nsISupports** aReturn) { + if (NS_WARN_IF(!aReturn)) { + return NS_ERROR_INVALID_ARG; + } + *aReturn = nullptr; + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + ErrorResult error; + nsStaticAtom* tagName = EditorUtils::GetTagNameAtom(aTagName); + if (!aTagName.IsEmpty() && !tagName) { + // We don't need to support custom elements becaus of internal API. + return NS_OK; + } + RefPtr selectedNode = GetSelectedElement(tagName, error); + NS_WARNING_ASSERTION(!error.Failed(), + "HTMLEditor::GetSelectedElement() failed"); + selectedNode.forget(aReturn); + return error.StealNSResult(); +} + +already_AddRefed HTMLEditor::GetSelectedElement(const nsAtom* aTagName, + ErrorResult& aRv) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + MOZ_ASSERT(!aRv.Failed()); + + // If there is no Selection or two or more selection ranges, that means that + // not only one element is selected so that return nullptr. + if (SelectionRef().RangeCount() != 1) { + return nullptr; + } + + bool isLinkTag = aTagName && IsLinkTag(*aTagName); + bool isNamedAnchorTag = aTagName && IsNamedAnchorTag(*aTagName); + + RefPtr firstRange = SelectionRef().GetRangeAt(0); + MOZ_ASSERT(firstRange); + + const RangeBoundary& startRef = firstRange->StartRef(); + if (NS_WARN_IF(!startRef.IsSet())) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + const RangeBoundary& endRef = firstRange->EndRef(); + if (NS_WARN_IF(!endRef.IsSet())) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + // Optimization for a single selected element + if (startRef.Container() == endRef.Container()) { + nsIContent* startContent = startRef.GetChildAtOffset(); + nsIContent* endContent = endRef.GetChildAtOffset(); + if (startContent && endContent && + startContent->GetNextSibling() == endContent) { + if (!aTagName) { + if (!startContent->IsElement()) { + // This means only a text node or something is selected. We should + // return nullptr in this case since no other elements are selected. + return nullptr; + } + return do_AddRef(startContent->AsElement()); + } + // Test for appropriate node type requested + if (aTagName == startContent->NodeInfo()->NameAtom() || + (isLinkTag && HTMLEditUtils::IsLink(startContent)) || + (isNamedAnchorTag && HTMLEditUtils::IsNamedAnchor(startContent))) { + MOZ_ASSERT(startContent->IsElement()); + return do_AddRef(startContent->AsElement()); + } + } + } + + if (isLinkTag && startRef.Container()->IsContent() && + endRef.Container()->IsContent()) { + // Link node must be the same for both ends of selection. + Element* parentLinkOfStart = GetInclusiveAncestorByTagNameInternal( + *nsGkAtoms::href, *startRef.Container()->AsContent()); + if (parentLinkOfStart) { + if (SelectionRef().IsCollapsed()) { + // We have just a caret in the link. + return do_AddRef(parentLinkOfStart); + } + // Link node must be the same for both ends of selection. + Element* parentLinkOfEnd = GetInclusiveAncestorByTagNameInternal( + *nsGkAtoms::href, *endRef.Container()->AsContent()); + if (parentLinkOfStart == parentLinkOfEnd) { + return do_AddRef(parentLinkOfStart); + } + } + } + + if (SelectionRef().IsCollapsed()) { + return nullptr; + } + + PostContentIterator postOrderIter; + postOrderIter.Init(firstRange); + + RefPtr lastElementInRange; + for (nsINode* lastNodeInRange = nullptr; !postOrderIter.IsDone(); + postOrderIter.Next()) { + if (lastElementInRange) { + // When any node follows an element node, not only one element is + // selected so that return nullptr. + return nullptr; + } + + // This loop ignors any non-element nodes before first element node. + // Its purpose must be that this method treats this case as selecting + // the element: + // -

    abc d[ef}

    + // because children of an element node is listed up before the element. + // However, this case must not be expected by the initial developer: + // -

    a[bc def}

    + // When we meet non-parent and non-next-sibling node of previous node, + // it means that the range across element boundary (open tag in HTML + // source). So, in this case, we should not say only the following + // element is selected. + nsINode* currentNode = postOrderIter.GetCurrentNode(); + MOZ_ASSERT(currentNode); + if (lastNodeInRange && lastNodeInRange->GetParentNode() != currentNode && + lastNodeInRange->GetNextSibling() != currentNode) { + return nullptr; + } + + lastNodeInRange = currentNode; + + lastElementInRange = Element::FromNodeOrNull(lastNodeInRange); + if (!lastElementInRange) { + continue; + } + + // And also, if it's followed by a
    element, we shouldn't treat the + // the element is selected like this case: + // -

    [def}

    + // Note that we don't need special handling for because double + // clicking it selects the element and we use the first path to handle it. + // Additionally, we have this case too: + // -

    [def}

    + // In these cases, the
    element is not listed up by PostContentIterator. + // So, we should return nullptr if next sibling is a `
    ` element or + // next sibling starts with `
    ` element. + if (nsIContent* nextSibling = lastElementInRange->GetNextSibling()) { + if (nextSibling->IsHTMLElement(nsGkAtoms::br)) { + return nullptr; + } + nsIContent* firstEditableLeaf = HTMLEditUtils::GetFirstLeafContent( + *nextSibling, {LeafNodeType::OnlyLeafNode}); + if (firstEditableLeaf && + firstEditableLeaf->IsHTMLElement(nsGkAtoms::br)) { + return nullptr; + } + } + + if (!aTagName) { + continue; + } + + if (isLinkTag && HTMLEditUtils::IsLink(lastElementInRange)) { + continue; + } + + if (isNamedAnchorTag && HTMLEditUtils::IsNamedAnchor(lastElementInRange)) { + continue; + } + + if (aTagName == lastElementInRange->NodeInfo()->NameAtom()) { + continue; + } + + // First element in the range does not match what the caller is looking + // for. + return nullptr; + } + return lastElementInRange.forget(); +} + +Result HTMLEditor::CreateAndInsertElement( + WithTransaction aWithTransaction, nsAtom& aTagName, + const EditorDOMPoint& aPointToInsert, + const InitializeInsertingElement& aInitializer) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(aPointToInsert.IsSetAndValid()); + + // XXX We need offset at new node for RangeUpdaterRef(). Therefore, we need + // to compute the offset now but this is expensive. So, if it's possible, + // we need to redesign RangeUpdaterRef() as avoiding using indices. + Unused << aPointToInsert.Offset(); + + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eCreateNode, nsIEditor::eNext, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + // TODO: This method should have a callback function which is called + // immediately after creating an element but before it's inserted into + // the DOM tree. Then, caller can init the new element's attributes + // and children **without** transactions (it'll reduce the number of + // legacy mutation events). Finally, we can get rid of + // CreatElementTransaction since we can use InsertNodeTransaction + // instead. + + auto createNewElementResult = + [&]() MOZ_CAN_RUN_SCRIPT -> Result { + RefPtr newElement = CreateHTMLContent(&aTagName); + if (MOZ_UNLIKELY(!newElement)) { + NS_WARNING("EditorBase::CreateHTMLContent() failed"); + return Err(NS_ERROR_FAILURE); + } + nsresult rv = MarkElementDirty(*newElement); + if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { + NS_WARNING("EditorBase::MarkElementDirty() caused destroying the editor"); + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::MarkElementDirty() failed, but ignored"); + rv = aInitializer(*this, *newElement, aPointToInsert); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "aInitializer failed"); + if (NS_WARN_IF(Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + RefPtr transaction = + InsertNodeTransaction::Create(*this, *newElement, aPointToInsert); + rv = aWithTransaction == WithTransaction::Yes + ? DoTransactionInternal(transaction) + : transaction->DoTransaction(); + // FYI: Transaction::DoTransaction never returns NS_ERROR_EDITOR_*. + if (MOZ_UNLIKELY(Destroyed())) { + NS_WARNING( + "InsertNodeTransaction::DoTransaction() caused destroying the " + "editor"); + return Err(NS_ERROR_EDITOR_DESTROYED); + } + if (NS_FAILED(rv)) { + NS_WARNING("InsertNodeTransaction::DoTransaction() failed"); + return Err(rv); + } + // Override the success code if new element was moved by the web apps. + if (newElement && + newElement->GetParentNode() != aPointToInsert.GetContainer()) { + NS_WARNING("The new element was not inserted into the expected node"); + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + return CreateElementResult( + std::move(newElement), + transaction->SuggestPointToPutCaret()); + }(); + + if (MOZ_UNLIKELY(createNewElementResult.isErr())) { + NS_WARNING("EditorBase::DoTransactionInternal() failed"); + // XXX Why do we do this even when DoTransaction() returned error? + DebugOnly rvIgnored = + RangeUpdaterRef().SelAdjCreateNode(aPointToInsert); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "RangeUpdater::SelAdjCreateNode() failed"); + return createNewElementResult; + } + + // If we succeeded to create and insert new element, we need to adjust + // ranges in RangeUpdaterRef(). It currently requires offset of the new + // node. So, let's call it with original offset. Note that if + // aPointToInsert stores child node, it may not be at the offset since new + // element must be inserted before the old child. Although, mutation + // observer can do anything, but currently, we don't check it. + DebugOnly rvIgnored = + RangeUpdaterRef().SelAdjCreateNode(EditorRawDOMPoint( + aPointToInsert.GetContainer(), aPointToInsert.Offset())); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "RangeUpdater::SelAdjCreateNode() failed, but ignored"); + if (MOZ_LIKELY(createNewElementResult.inspect().GetNewNode())) { + TopLevelEditSubActionDataRef().DidCreateElement( + *this, *createNewElementResult.inspect().GetNewNode()); + } + + return createNewElementResult; +} + +nsresult HTMLEditor::CopyAttributes(WithTransaction aWithTransaction, + Element& aDestElement, Element& aSrcElement, + const AttributeFilter& aFilterFunc) { + RefPtr srcAttributes = aSrcElement.Attributes(); + if (!srcAttributes->Length()) { + return NS_OK; + } + AutoTArray, 16> srcAttrs; + srcAttrs.SetCapacity(srcAttributes->Length()); + for (uint32_t i = 0; i < srcAttributes->Length(); i++) { + RefPtr attr = srcAttributes->Item(i); + if (!attr) { + break; + } + srcAttrs.AppendElement(std::move(attr)); + } + if (aWithTransaction == WithTransaction::No) { + for (const OwningNonNull& attr : srcAttrs) { + nsString value; + attr->GetValue(value); + if (!aFilterFunc(*this, aSrcElement, aDestElement, attr, value)) { + continue; + } + DebugOnly rvIgnored = + aDestElement.SetAttr(attr->NodeInfo()->NamespaceID(), + attr->NodeInfo()->NameAtom(), value, false); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Element::SetAttr() failed, but ignored"); + } + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + return NS_OK; + } + MOZ_ASSERT_UNREACHABLE("Not implemented yet, but you try to use this"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +already_AddRefed HTMLEditor::CreateElementWithDefaults( + const nsAtom& aTagName) { + // NOTE: Despite of public method, this can be called for internal use. + + // Although this creates an element, but won't change the DOM tree nor + // transaction. So, EditAtion::eNotEditing is proper value here. If + // this is called for internal when there is already AutoEditActionDataSetter + // instance, this would be initialized with its EditAction value. + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return nullptr; + } + + const nsAtom* realTagName = IsLinkTag(aTagName) || IsNamedAnchorTag(aTagName) + ? nsGkAtoms::a + : &aTagName; + + // We don't use editor's CreateElement because we don't want to go through + // the transaction system + + // New call to use instead to get proper HTML element, bug 39919 + RefPtr newElement = CreateHTMLContent(realTagName); + if (!newElement) { + return nullptr; + } + + // Mark the new element dirty, so it will be formatted + // XXX Don't we need to check the error result of setting _moz_dirty attr? + IgnoredErrorResult ignoredError; + newElement->SetAttribute(u"_moz_dirty"_ns, u""_ns, ignoredError); + NS_WARNING_ASSERTION(!ignoredError.Failed(), + "Element::SetAttribute(_moz_dirty) failed, but ignored"); + ignoredError.SuppressException(); + + // Set default values for new elements + if (realTagName == nsGkAtoms::table) { + newElement->SetAttr(nsGkAtoms::cellpadding, u"2"_ns, ignoredError); + if (ignoredError.Failed()) { + NS_WARNING("Element::SetAttr(nsGkAtoms::cellpadding, 2) failed"); + return nullptr; + } + ignoredError.SuppressException(); + + newElement->SetAttr(nsGkAtoms::cellspacing, u"2"_ns, ignoredError); + if (ignoredError.Failed()) { + NS_WARNING("Element::SetAttr(nsGkAtoms::cellspacing, 2) failed"); + return nullptr; + } + ignoredError.SuppressException(); + + newElement->SetAttr(nsGkAtoms::border, u"1"_ns, ignoredError); + if (ignoredError.Failed()) { + NS_WARNING("Element::SetAttr(nsGkAtoms::border, 1) failed"); + return nullptr; + } + } else if (realTagName == nsGkAtoms::td) { + nsresult rv = SetAttributeOrEquivalent(newElement, nsGkAtoms::valign, + u"top"_ns, true); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::SetAttributeOrEquivalent(nsGkAtoms::valign, top) " + "failed"); + return nullptr; + } + } + // ADD OTHER TAGS HERE + + return newElement.forget(); +} + +NS_IMETHODIMP HTMLEditor::CreateElementWithDefaults(const nsAString& aTagName, + Element** aReturn) { + if (NS_WARN_IF(aTagName.IsEmpty()) || NS_WARN_IF(!aReturn)) { + return NS_ERROR_INVALID_ARG; + } + + *aReturn = nullptr; + + nsStaticAtom* tagName = EditorUtils::GetTagNameAtom(aTagName); + if (NS_WARN_IF(!tagName)) { + return NS_ERROR_INVALID_ARG; + } + RefPtr newElement = + CreateElementWithDefaults(MOZ_KnownLive(*tagName)); + if (!newElement) { + NS_WARNING("HTMLEditor::CreateElementWithDefaults() failed"); + return NS_ERROR_FAILURE; + } + newElement.forget(aReturn); + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::InsertLinkAroundSelection(Element* aAnchorElement) { + nsresult rv = InsertLinkAroundSelectionAsAction(aAnchorElement); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::InsertLinkAroundSelectionAsAction() failed"); + return rv; +} + +nsresult HTMLEditor::InsertLinkAroundSelectionAsAction( + Element* aAnchorElement, nsIPrincipal* aPrincipal) { + if (NS_WARN_IF(!aAnchorElement)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eInsertLinkElement, + aPrincipal); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (SelectionRef().IsCollapsed()) { + NS_WARNING("Selection was collapsed"); + return NS_OK; + } + + // Be sure we were given an anchor element + RefPtr anchor = + HTMLAnchorElement::FromNodeOrNull(aAnchorElement); + if (!anchor) { + return NS_OK; + } + + nsAutoString rawHref; + anchor->GetAttr(kNameSpaceID_None, nsGkAtoms::href, rawHref); + editActionData.SetData(rawHref); + + nsresult rv = editActionData.MaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "MaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + nsAutoString href; + anchor->GetHref(href); + if (href.IsEmpty()) { + return NS_OK; + } + + AutoPlaceholderBatch treatAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + + // Set all attributes found on the supplied anchor element + RefPtr attributeMap = anchor->Attributes(); + if (NS_WARN_IF(!attributeMap)) { + return NS_ERROR_FAILURE; + } + + // TODO: We should stop using this loop for adding attributes to newly created + // `
    ` elements. Then, we can avoid to increate the ref- + // counter of attribute names since we can use nsStaticAtom if we don't + // need to support unknown attributes. + AutoTArray stylesToSet; + stylesToSet.SetCapacity(attributeMap->Length()); + nsString value; + for (uint32_t i : IntegerRange(attributeMap->Length())) { + RefPtr attribute = attributeMap->Item(i); + if (!attribute) { + continue; + } + + RefPtr attributeName = attribute->NodeInfo()->NameAtom(); + + MOZ_ASSERT(value.IsEmpty()); + attribute->GetValue(value); + + stylesToSet.AppendElement(EditorInlineStyleAndValue( + *nsGkAtoms::a, std::move(attributeName), std::move(value))); + } + rv = SetInlinePropertiesAsSubAction(stylesToSet); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::SetInlinePropertiesAsSubAction() failed"); + return rv; +} + +nsresult HTMLEditor::SetHTMLBackgroundColorWithTransaction( + const nsAString& aColor) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // Find a selected or enclosing table element to set background on + bool isCellSelected = false; + Result, nsresult> cellOrRowOrTableElementOrError = + GetSelectedOrParentTableElement(&isCellSelected); + if (cellOrRowOrTableElementOrError.isErr()) { + NS_WARNING("HTMLEditor::GetSelectedOrParentTableElement() failed"); + return cellOrRowOrTableElementOrError.unwrapErr(); + } + + bool setColor = !aColor.IsEmpty(); + RefPtr rootElementOfBackgroundColor = + cellOrRowOrTableElementOrError.unwrap(); + if (rootElementOfBackgroundColor) { + // Needs to set or remove background color of each selected cell elements. + // Therefore, just the cell contains selection range, we don't need to + // do this. Note that users can select each cell, but with Selection API, + // web apps can select and at same time. With , looks + // odd, though. + if (isCellSelected || rootElementOfBackgroundColor->IsAnyOfHTMLElements( + nsGkAtoms::table, nsGkAtoms::tr)) { + SelectedTableCellScanner scanner(SelectionRef()); + if (scanner.IsInTableCellSelectionMode()) { + if (setColor) { + for (const OwningNonNull& cellElement : + scanner.ElementsRef()) { + // `MOZ_KnownLive(cellElement)` is safe because of `scanner` + // is stack only class and keeps grabbing it until it's destroyed. + nsresult rv = SetAttributeWithTransaction( + MOZ_KnownLive(cellElement), *nsGkAtoms::bgcolor, aColor); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + if (NS_FAILED(rv)) { + NS_WARNING( + "EditorBase::::SetAttributeWithTransaction(nsGkAtoms::" + "bgcolor) failed"); + return rv; + } + } + return NS_OK; + } + for (const OwningNonNull& cellElement : + scanner.ElementsRef()) { + // `MOZ_KnownLive(cellElement)` is safe because of `scanner` + // is stack only class and keeps grabbing it until it's destroyed. + nsresult rv = RemoveAttributeWithTransaction( + MOZ_KnownLive(cellElement), *nsGkAtoms::bgcolor); + if (NS_FAILED(rv)) { + NS_WARNING( + "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::bgcolor)" + " failed"); + return rv; + } + } + return NS_OK; + } + } + // If we failed to find a cell, fall through to use originally-found element + } else { + // No table element -- set the background color on the body tag + rootElementOfBackgroundColor = GetRoot(); + if (NS_WARN_IF(!rootElementOfBackgroundColor)) { + return NS_ERROR_FAILURE; + } + } + // Use the editor method that goes through the transaction system + if (setColor) { + nsresult rv = SetAttributeWithTransaction(*rootElementOfBackgroundColor, + *nsGkAtoms::bgcolor, aColor); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::SetAttributeWithTransaction(nsGkAtoms::bgcolor) failed"); + return rv; + } + nsresult rv = RemoveAttributeWithTransaction(*rootElementOfBackgroundColor, + *nsGkAtoms::bgcolor); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::bgcolor) failed"); + return rv; +} + +nsresult HTMLEditor::RemoveEmptyInclusiveAncestorInlineElements( + nsIContent& aContent) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(!aContent.Length()); + + Element* editingHost = aContent.GetEditingHost(); + if (NS_WARN_IF(!editingHost)) { + return NS_ERROR_FAILURE; + } + + if (&aContent == editingHost || HTMLEditUtils::IsBlockElement(aContent) || + !EditorUtils::IsEditableContent(aContent, EditorType::HTML) || + !aContent.GetParent()) { + return NS_OK; + } + + // Don't strip wrappers if this is the only wrapper in the block. Then we'll + // add a
    later, so it won't be an empty wrapper in the end. + // XXX This is different from Blink. We should delete empty inline element + // even if it's only child of the block element. + { + const Element* editableBlockElement = HTMLEditUtils::GetAncestorElement( + aContent, HTMLEditUtils::ClosestEditableBlockElement); + if (!editableBlockElement || + HTMLEditUtils::IsEmptyNode( + *editableBlockElement, + {EmptyCheckOption::TreatSingleBRElementAsVisible})) { + return NS_OK; + } + } + + OwningNonNull content = aContent; + for (nsIContent* parentContent : aContent.AncestorsOfType()) { + if (HTMLEditUtils::IsBlockElement(*parentContent) || + parentContent->Length() != 1 || + !EditorUtils::IsEditableContent(*parentContent, EditorType::HTML) || + parentContent == editingHost) { + break; + } + content = *parentContent; + } + + nsresult rv = DeleteNodeWithTransaction(content); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DeleteNodeWithTransaction() failed"); + return rv; +} + +nsresult HTMLEditor::DeleteAllChildrenWithTransaction(Element& aElement) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // Prevent rules testing until we're done + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eDeleteNode, nsIEditor::eNext, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return ignoredError.StealNSResult(); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + while (nsCOMPtr child = aElement.GetLastChild()) { + nsresult rv = DeleteNodeWithTransaction(*child); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + } + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::DeleteNode(nsINode* aNode) { + if (NS_WARN_IF(!aNode) || NS_WARN_IF(!aNode->IsContent())) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eRemoveNode); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + rv = DeleteNodeWithTransaction(MOZ_KnownLive(*aNode->AsContent())); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DeleteNodeWithTransaction() failed"); + return rv; +} + +Result HTMLEditor::DeleteTextWithTransaction( + Text& aTextNode, uint32_t aOffset, uint32_t aLength) { + if (NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(aTextNode))) { + return Err(NS_ERROR_FAILURE); + } + + Result caretPointOrError = + EditorBase::DeleteTextWithTransaction(aTextNode, aOffset, aLength); + NS_WARNING_ASSERTION(caretPointOrError.isOk(), + "EditorBase::DeleteTextWithTransaction() failed"); + return caretPointOrError; +} + +Result HTMLEditor::ReplaceTextWithTransaction( + Text& aTextNode, uint32_t aOffset, uint32_t aLength, + const nsAString& aStringToInsert) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(aLength > 0 || !aStringToInsert.IsEmpty()); + + if (aStringToInsert.IsEmpty()) { + Result caretPointOrError = + DeleteTextWithTransaction(aTextNode, aOffset, aLength); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); + return caretPointOrError.propagateErr(); + } + return InsertTextResult(EditorDOMPointInText(&aTextNode, aOffset), + caretPointOrError.unwrap()); + } + + if (!aLength) { + RefPtr document = GetDocument(); + if (NS_WARN_IF(!document)) { + return Err(NS_ERROR_NOT_INITIALIZED); + } + Result insertTextResult = + InsertTextWithTransaction(*document, aStringToInsert, + EditorDOMPoint(&aTextNode, aOffset)); + NS_WARNING_ASSERTION(insertTextResult.isOk(), + "HTMLEditor::InsertTextWithTransaction() failed"); + return insertTextResult; + } + + if (NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(aTextNode))) { + return Err(NS_ERROR_FAILURE); + } + + // This should emulates inserting text for better undo/redo behavior. + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + // FYI: Create the insertion point before changing the DOM tree because + // the point may become invalid offset after that. + EditorDOMPointInText pointToInsert(&aTextNode, aOffset); + + RefPtr transaction = ReplaceTextTransaction::Create( + *this, aStringToInsert, aTextNode, aOffset, aLength); + MOZ_ASSERT(transaction); + + if (aLength && !mActionListeners.IsEmpty()) { + for (auto& listener : mActionListeners.Clone()) { + DebugOnly rvIgnored = + listener->WillDeleteText(&aTextNode, aOffset, aLength); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsIEditActionListener::WillDeleteText() failed, but ignored"); + } + } + + nsresult rv = DoTransactionInternal(transaction); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DoTransactionInternal() failed"); + + // Don't check whether we've been destroyed here because we need to notify + // listeners and observers below even if we've already destroyed. + + EditorDOMPointInText endOfInsertedText(&aTextNode, + aOffset + aStringToInsert.Length()); + + if (pointToInsert.IsSet()) { + auto [begin, end] = ComputeInsertedRange(pointToInsert, aStringToInsert); + if (begin.IsSet() && end.IsSet()) { + TopLevelEditSubActionDataRef().DidDeleteText( + *this, begin.To()); + TopLevelEditSubActionDataRef().DidInsertText( + *this, begin.To(), end.To()); + } + + // XXX Should we update endOfInsertedText here? + } + + if (!mActionListeners.IsEmpty()) { + for (auto& listener : mActionListeners.Clone()) { + DebugOnly rvIgnored = + listener->DidInsertText(&aTextNode, aOffset, aStringToInsert, rv); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsIEditActionListener::DidInsertText() failed, but ignored"); + } + } + + if (NS_WARN_IF(Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + + return InsertTextResult( + std::move(endOfInsertedText), + transaction->SuggestPointToPutCaret()); +} + +Result HTMLEditor::InsertTextWithTransaction( + Document& aDocument, const nsAString& aStringToInsert, + const EditorDOMPoint& aPointToInsert) { + if (NS_WARN_IF(!aPointToInsert.IsSet())) { + return Err(NS_ERROR_INVALID_ARG); + } + + // Do nothing if the node is read-only + if (MOZ_UNLIKELY(NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode( + *aPointToInsert.GetContainer())))) { + return Err(NS_ERROR_FAILURE); + } + + return EditorBase::InsertTextWithTransaction(aDocument, aStringToInsert, + aPointToInsert); +} + +Result HTMLEditor::PrepareToInsertBRElement( + const EditorDOMPoint& aPointToInsert) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (NS_WARN_IF(!aPointToInsert.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + + if (!aPointToInsert.IsInTextNode()) { + return aPointToInsert; + } + + if (aPointToInsert.IsStartOfContainer()) { + // Insert before the text node. + EditorDOMPoint pointInContainer(aPointToInsert.GetContainer()); + if (!pointInContainer.IsSet()) { + NS_WARNING("Failed to climb up the DOM tree from text node"); + return Err(NS_ERROR_FAILURE); + } + return pointInContainer; + } + + if (aPointToInsert.IsEndOfContainer()) { + // Insert after the text node. + EditorDOMPoint pointInContainer(aPointToInsert.GetContainer()); + if (NS_WARN_IF(!pointInContainer.IsSet())) { + NS_WARNING("Failed to climb up the DOM tree from text node"); + return Err(NS_ERROR_FAILURE); + } + MOZ_ALWAYS_TRUE(pointInContainer.AdvanceOffset()); + return pointInContainer; + } + + MOZ_DIAGNOSTIC_ASSERT(aPointToInsert.IsSetAndValid()); + + // Unfortunately, we need to split the text node at the offset. + Result splitTextNodeResult = + SplitNodeWithTransaction(aPointToInsert); + if (MOZ_UNLIKELY(splitTextNodeResult.isErr())) { + NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed"); + return splitTextNodeResult.propagateErr(); + } + nsresult rv = splitTextNodeResult.inspect().SuggestCaretPointTo( + *this, {SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); + if (NS_FAILED(rv)) { + NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed"); + return Err(rv); + } + + // Insert new
    before the right node. + auto atNextContent = + splitTextNodeResult.inspect().AtNextContent(); + if (MOZ_UNLIKELY(!atNextContent.IsSet())) { + NS_WARNING("The next node seems not in the DOM tree"); + return Err(NS_ERROR_FAILURE); + } + return atNextContent; +} + +Result HTMLEditor::InsertBRElement( + WithTransaction aWithTransaction, const EditorDOMPoint& aPointToInsert, + EDirection aSelect /* = eNone */) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + Result maybePointToInsert = + PrepareToInsertBRElement(aPointToInsert); + if (maybePointToInsert.isErr()) { + NS_WARNING( + nsPrintfCString("HTMLEditor::PrepareToInsertBRElement(%s) failed", + ToString(aWithTransaction).c_str()) + .get()); + return maybePointToInsert.propagateErr(); + } + MOZ_ASSERT(maybePointToInsert.inspect().IsSetAndValid()); + + Result createNewBRElementResult = + CreateAndInsertElement(aWithTransaction, *nsGkAtoms::br, + maybePointToInsert.inspect()); + if (MOZ_UNLIKELY(createNewBRElementResult.isErr())) { + NS_WARNING(nsPrintfCString("HTMLEditor::CreateAndInsertElement(%s) failed", + ToString(aWithTransaction).c_str()) + .get()); + return createNewBRElementResult.propagateErr(); + } + CreateElementResult unwrappedCreateNewBRElementResult = + createNewBRElementResult.unwrap(); + RefPtr newBRElement = + unwrappedCreateNewBRElementResult.UnwrapNewNode(); + MOZ_ASSERT(newBRElement); + + unwrappedCreateNewBRElementResult.IgnoreCaretPointSuggestion(); + switch (aSelect) { + case eNext: { + const auto pointToPutCaret = EditorDOMPoint::After( + *newBRElement, Selection::InterlinePosition::StartOfNextLine); + return CreateElementResult(std::move(newBRElement), pointToPutCaret); + } + case ePrevious: { + const auto pointToPutCaret = EditorDOMPoint( + newBRElement, Selection::InterlinePosition::StartOfNextLine); + return CreateElementResult(std::move(newBRElement), pointToPutCaret); + } + default: + NS_WARNING( + "aSelect has invalid value, the caller need to set selection " + "by itself"); + [[fallthrough]]; + case eNone: + return CreateElementResult( + std::move(newBRElement), + unwrappedCreateNewBRElementResult.UnwrapCaretPoint()); + } +} + +Result +HTMLEditor::InsertContainerWithTransaction( + nsIContent& aContentToBeWrapped, const nsAtom& aWrapperTagName, + const InitializeInsertingElement& aInitializer) { + EditorDOMPoint pointToInsertNewContainer(&aContentToBeWrapped); + if (NS_WARN_IF(!pointToInsertNewContainer.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + // aContentToBeWrapped will be moved to the new container before inserting the + // new container. So, when we insert the container, the insertion point is + // before the next sibling of aContentToBeWrapped. + // XXX If pointerToInsertNewContainer stores offset here, the offset and + // referring child node become mismatched. Although, currently this + // is not a problem since InsertNodeTransaction refers only child node. + MOZ_ALWAYS_TRUE(pointToInsertNewContainer.AdvanceOffset()); + + // Create new container. + RefPtr newContainer = CreateHTMLContent(&aWrapperTagName); + if (NS_WARN_IF(!newContainer)) { + return Err(NS_ERROR_FAILURE); + } + + if (&aInitializer != &HTMLEditor::DoNothingForNewElement) { + nsresult rv = aInitializer(*this, *newContainer, + EditorDOMPoint(&aContentToBeWrapped)); + if (NS_WARN_IF(Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + if (NS_FAILED(rv)) { + NS_WARNING("aInitializer() failed"); + return Err(rv); + } + } + + // Notify our internal selection state listener + AutoInsertContainerSelNotify selNotify(RangeUpdaterRef()); + + // Put aNode in the new container, first. + // XXX Perhaps, we should not remove the container if it's not editable. + nsresult rv = DeleteNodeWithTransaction(aContentToBeWrapped); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return Err(rv); + } + + { + // TODO: Remove AutoTransactionsConserveSelection here. It's not necessary + // in normal cases. However, it may be required for nested edit + // actions which may be caused by legacy mutation event listeners or + // chrome script. + AutoTransactionsConserveSelection conserveSelection(*this); + Result insertContentNodeResult = + InsertNodeWithTransaction(aContentToBeWrapped, + EditorDOMPoint(newContainer, 0u)); + if (MOZ_UNLIKELY(insertContentNodeResult.isErr())) { + NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); + return insertContentNodeResult.propagateErr(); + } + insertContentNodeResult.inspect().IgnoreCaretPointSuggestion(); + } + + // Put the new container where aNode was. + Result insertNewContainerElementResult = + InsertNodeWithTransaction(*newContainer, + pointToInsertNewContainer); + NS_WARNING_ASSERTION(insertNewContainerElementResult.isOk(), + "EditorBase::InsertNodeWithTransaction() failed"); + return insertNewContainerElementResult; +} + +Result +HTMLEditor::ReplaceContainerWithTransactionInternal( + Element& aOldContainer, const nsAtom& aTagName, const nsAtom& aAttribute, + const nsAString& aAttributeValue, bool aCloneAllAttributes) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(aOldContainer)) || + NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(aOldContainer))) { + return Err(NS_ERROR_FAILURE); + } + + const RefPtr newContainer = CreateHTMLContent(&aTagName); + if (NS_WARN_IF(!newContainer)) { + return Err(NS_ERROR_FAILURE); + } + + // Set or clone attribute if needed. + if (aCloneAllAttributes) { + MOZ_ASSERT(&aAttribute == nsGkAtoms::_empty); + CloneAttributesWithTransaction(*newContainer, aOldContainer); + } else if (&aAttribute != nsGkAtoms::_empty) { + nsresult rv = newContainer->SetAttr(kNameSpaceID_None, + const_cast(&aAttribute), + aAttributeValue, true); + if (NS_FAILED(rv)) { + NS_WARNING("Element::SetAttr() failed"); + return Err(NS_ERROR_FAILURE); + } + } + + const OwningNonNull parentNode = *aOldContainer.GetParentNode(); + const nsCOMPtr referenceNode = aOldContainer.GetNextSibling(); + AutoReplaceContainerSelNotify selStateNotify(RangeUpdaterRef(), aOldContainer, + *newContainer); + { + AutoTArray, 32> arrayOfChildren; + HTMLEditUtils::CollectChildren( + aOldContainer, arrayOfChildren, 0u, + // Move non-editable children too because its container, aElement, is + // editable so that all children must be removable node. + {}); + // TODO: Remove AutoTransactionsConserveSelection here. It's not necessary + // in normal cases. However, it may be required for nested edit + // actions which may be caused by legacy mutation event listeners or + // chrome script. + AutoTransactionsConserveSelection conserveSelection(*this); + // Move all children from the old container to the new container. + // For making all MoveNodeTransactions have a referenc node in the current + // parent, move nodes from last one to preceding ones. + for (const OwningNonNull& child : Reversed(arrayOfChildren)) { + Result moveChildResult = + MoveNodeWithTransaction(MOZ_KnownLive(child), // due to bug 1622253. + EditorDOMPoint(newContainer, 0u)); + if (MOZ_UNLIKELY(moveChildResult.isErr())) { + NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed"); + return moveChildResult.propagateErr(); + } + // We'll suggest new caret point which is suggested by new container + // element insertion result. Therefore, we need to do nothing here. + moveChildResult.inspect().IgnoreCaretPointSuggestion(); + } + } + + // Delete aOldContainer from the DOM tree to make it not referred by + // InsertNodeTransaction. + nsresult rv = DeleteNodeWithTransaction(aOldContainer); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return Err(rv); + } + + if (referenceNode && (!referenceNode->GetParentNode() || + parentNode != referenceNode->GetParentNode())) { + NS_WARNING( + "The reference node for insertion has been moved to different parent, " + "so we got lost the insertion point"); + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + + // Finally, insert the new node to where probably aOldContainer was. + Result insertNewContainerElementResult = + InsertNodeWithTransaction( + *newContainer, referenceNode ? EditorDOMPoint(referenceNode) + : EditorDOMPoint::AtEndOf(*parentNode)); + NS_WARNING_ASSERTION(insertNewContainerElementResult.isOk(), + "EditorBase::InsertNodeWithTransaction() failed"); + MOZ_ASSERT_IF( + insertNewContainerElementResult.isOk(), + insertNewContainerElementResult.inspect().GetNewNode() == newContainer); + return insertNewContainerElementResult; +} + +Result HTMLEditor::RemoveContainerWithTransaction( + Element& aElement) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(aElement)) || + NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(aElement))) { + return Err(NS_ERROR_FAILURE); + } + + // Notify our internal selection state listener. + AutoRemoveContainerSelNotify selNotify(RangeUpdaterRef(), + EditorRawDOMPoint(&aElement)); + + AutoTArray, 32> arrayOfChildren; + HTMLEditUtils::CollectChildren( + aElement, arrayOfChildren, 0u, + // Move non-editable children too because its container, aElement, is + // editable so that all children must be removable node. + {}); + const OwningNonNull parentNode = *aElement.GetParentNode(); + nsCOMPtr previousChild = aElement.GetPreviousSibling(); + // For making all MoveNodeTransactions have a referenc node in the current + // parent, move nodes from last one to preceding ones. + for (const OwningNonNull& child : Reversed(arrayOfChildren)) { + if (MOZ_UNLIKELY(!HTMLEditUtils::IsRemovableNode(child))) { + continue; + } + Result moveChildResult = MoveNodeWithTransaction( + MOZ_KnownLive(child), // due to bug 1622253. + previousChild ? EditorDOMPoint::After(previousChild) + : EditorDOMPoint(parentNode, 0u)); + if (MOZ_UNLIKELY(moveChildResult.isErr())) { + NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed"); + return moveChildResult.propagateErr(); + } + // If the reference node was moved to different container, try to recover + // the original position. + if (previousChild && + MOZ_UNLIKELY(previousChild->GetParentNode() != parentNode)) { + if (MOZ_UNLIKELY(child->GetParentNode() != parentNode)) { + NS_WARNING( + "Neither the reference (previous) sibling nor the moved child was " + "in the expected parent node"); + moveChildResult.inspect().IgnoreCaretPointSuggestion(); + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + previousChild = child->GetPreviousSibling(); + } + // We'll need to put caret at next sibling of aElement if nobody moves + // content nodes under the parent node except us. + moveChildResult.inspect().IgnoreCaretPointSuggestion(); + } + + if (aElement.GetParentNode() && aElement.GetParentNode() != parentNode) { + NS_WARNING( + "The removing element has already been moved to another element"); + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + + NS_WARNING_ASSERTION(!aElement.GetFirstChild(), + "The removing container still has some children, but " + "they are removed by removing the container"); + + auto GetNextSiblingOf = + [](const nsTArray>& aArrayOfMovedContent, + const nsINode& aExpectedParentNode) -> nsIContent* { + for (const OwningNonNull& movedChild : + Reversed(aArrayOfMovedContent)) { + if (movedChild != &aExpectedParentNode) { + continue; // Ignore moved node which was moved to different place + } + return movedChild->GetNextSibling(); + } + // XXX If all nodes were moved by web apps, we cannot suggest "collect" + // position without computing the index of aElement. However, I + // don't think that it's necessary for the web apps in the wild. + return nullptr; + }; + + nsCOMPtr nextSibling = + aElement.GetParentNode() ? aElement.GetNextSibling() + : GetNextSiblingOf(arrayOfChildren, *parentNode); + + nsresult rv = DeleteNodeWithTransaction(aElement); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::DeleteNodeTransaction() failed"); + return Err(rv); + } + + if (nextSibling && nextSibling->GetParentNode() != parentNode) { + nextSibling = GetNextSiblingOf(arrayOfChildren, *parentNode); + } + return nextSibling ? EditorDOMPoint(nextSibling) + : EditorDOMPoint::AtEndOf(*parentNode); +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentAppended( + nsIContent* aFirstNewContent) { + DoContentInserted(aFirstNewContent, ContentNodeIs::Appended); +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentInserted( + nsIContent* aChild) { + DoContentInserted(aChild, ContentNodeIs::Inserted); +} + +bool HTMLEditor::IsInObservedSubtree(nsIContent* aChild) { + if (!aChild) { + return false; + } + + // FIXME(emilio, bug 1596856): This should probably work if the root is in the + // same shadow tree as the child, probably? I don't know what the + // contenteditable-in-shadow-dom situation is. + if (Element* root = GetRoot()) { + // To be super safe here, check both ChromeOnlyAccess and NAC / Shadow DOM. + // That catches (also unbound) native anonymous content and ShadowDOM. + if (root->ChromeOnlyAccess() != aChild->ChromeOnlyAccess() || + root->IsInNativeAnonymousSubtree() != + aChild->IsInNativeAnonymousSubtree() || + root->IsInShadowTree() != aChild->IsInShadowTree()) { + return false; + } + } + + return !aChild->ChromeOnlyAccess() && !aChild->IsInShadowTree() && + !aChild->IsInNativeAnonymousSubtree(); +} + +void HTMLEditor::DoContentInserted(nsIContent* aChild, + ContentNodeIs aContentNodeIs) { + MOZ_ASSERT(aChild); + nsINode* container = aChild->GetParentNode(); + MOZ_ASSERT(container); + + if (!IsInObservedSubtree(aChild)) { + return; + } + + // XXX Why do we need this? This method is a helper of mutation observer. + // So, the callers of mutation observer should guarantee that this won't + // be deleted at least during the call. + RefPtr kungFuDeathGrip(this); + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return; + } + + if (ShouldReplaceRootElement()) { + UpdateRootElement(); + if (mPendingRootElementUpdatedRunner) { + return; + } + mPendingRootElementUpdatedRunner = NewRunnableMethod( + "HTMLEditor::NotifyRootChanged", this, &HTMLEditor::NotifyRootChanged); + nsContentUtils::AddScriptRunner( + do_AddRef(mPendingRootElementUpdatedRunner)); + return; + } + + // We don't need to handle our own modifications + if (!GetTopLevelEditSubAction() && container->IsEditable()) { + if (EditorUtils::IsPaddingBRElementForEmptyEditor(*aChild)) { + // Ignore insertion of the padding
    element. + return; + } + nsresult rv = OnDocumentModified(); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::OnDocumentModified() failed, but ignored"); + + // Update spellcheck for only the newly-inserted node (bug 743819) + if (mInlineSpellChecker) { + nsIContent* endContent = aChild; + if (aContentNodeIs == ContentNodeIs::Appended) { + nsIContent* child = nullptr; + for (child = aChild; child; child = child->GetNextSibling()) { + if (child->InclusiveDescendantMayNeedSpellchecking(this)) { + break; + } + } + if (!child) { + // No child needed spellchecking, return. + return; + } + + // Maybe more than 1 child was appended. + endContent = container->GetLastChild(); + } else if (!aChild->InclusiveDescendantMayNeedSpellchecking(this)) { + return; + } + + RefPtr range = nsRange::Create(aChild); + range->SelectNodesInContainer(container, aChild, endContent); + DebugOnly rvIgnored = + mInlineSpellChecker->SpellCheckRange(range); + NS_WARNING_ASSERTION( + rvIgnored == NS_ERROR_NOT_INITIALIZED || NS_SUCCEEDED(rvIgnored), + "mozInlineSpellChecker::SpellCheckRange() failed, but ignored"); + } + } +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentRemoved( + nsIContent* aChild, nsIContent* aPreviousSibling) { + if (!IsInObservedSubtree(aChild)) { + return; + } + + // XXX Why do we need to do this? This method is a mutation observer's + // method. Therefore, the caller should guarantee that this won't be + // deleted during the call. + RefPtr kungFuDeathGrip(this); + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return; + } + + if (SameCOMIdentity(aChild, mRootElement)) { + mRootElement = nullptr; + if (mPendingRootElementUpdatedRunner) { + return; + } + mPendingRootElementUpdatedRunner = NewRunnableMethod( + "HTMLEditor::NotifyRootChanged", this, &HTMLEditor::NotifyRootChanged); + nsContentUtils::AddScriptRunner( + do_AddRef(mPendingRootElementUpdatedRunner)); + return; + } + + // We don't need to handle our own modifications + if (!GetTopLevelEditSubAction() && aChild->GetParentNode()->IsEditable()) { + if (aChild && EditorUtils::IsPaddingBRElementForEmptyEditor(*aChild)) { + // Ignore removal of the padding
    element for empty editor. + return; + } + + nsresult rv = OnDocumentModified(); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::OnDocumentModified() failed, but ignored"); + } +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::CharacterDataChanged( + nsIContent* aContent, const CharacterDataChangeInfo& aInfo) { + if (!mInlineSpellChecker || !aContent->IsEditable() || + !IsInObservedSubtree(aContent) || + GetTopLevelEditSubAction() != EditSubAction::eNone) { + return; + } + + nsIContent* parent = aContent->GetParent(); + if (!parent || !parent->InclusiveDescendantMayNeedSpellchecking(this)) { + return; + } + + RefPtr range = nsRange::Create(aContent); + range->SelectNodesInContainer(parent, aContent, aContent); + DebugOnly rvIgnored = mInlineSpellChecker->SpellCheckRange(range); +} + +nsresult HTMLEditor::SelectEntireDocument() { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (!mInitSucceeded) { + return NS_ERROR_NOT_INITIALIZED; + } + + // XXX It's odd to select all of the document body if an contenteditable + // element has focus. + RefPtr bodyOrDocumentElement = GetRoot(); + if (NS_WARN_IF(!bodyOrDocumentElement)) { + return NS_ERROR_NOT_INITIALIZED; + } + + // If we're empty, don't select all children because that would select the + // padding
    element for empty editor. + if (IsEmpty()) { + nsresult rv = CollapseSelectionToStartOf(*bodyOrDocumentElement); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionToStartOf() failed"); + return rv; + } + + // Otherwise, select all children. + ErrorResult error; + SelectionRef().SelectAllChildren(*bodyOrDocumentElement, error); + if (NS_WARN_IF(Destroyed())) { + error.SuppressException(); + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION(!error.Failed(), + "Selection::SelectAllChildren() failed"); + return error.StealNSResult(); +} + +nsresult HTMLEditor::SelectAllInternal() { + MOZ_ASSERT(IsEditActionDataAvailable()); + + CommitComposition(); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + + auto GetBodyElementIfElementIsParentOfHTMLBody = + [](const Element& aElement) -> Element* { + if (!aElement.OwnerDoc()->IsHTMLDocument()) { + return const_cast(&aElement); + } + HTMLBodyElement* bodyElement = aElement.OwnerDoc()->GetBodyElement(); + return bodyElement && nsContentUtils::ContentIsFlattenedTreeDescendantOf( + bodyElement, &aElement) + ? bodyElement + : const_cast(&aElement); + }; + + nsCOMPtr selectionRootContent = + [&]() MOZ_CAN_RUN_SCRIPT -> nsIContent* { + RefPtr elementToBeSelected = [&]() -> Element* { + // If there is at least one selection range, we should compute the + // selection root from the anchor node. + if (SelectionRef().RangeCount()) { + if (nsIContent* content = + nsIContent::FromNodeOrNull(SelectionRef().GetAnchorNode())) { + if (content->IsElement()) { + return content->AsElement(); + } + if (Element* parentElement = + content->GetParentElementCrossingShadowRoot()) { + return parentElement; + } + } + } + // If no element contains a selection range, we should select all children + // of the focused element at least. + if (Element* focusedElement = GetFocusedElement()) { + return focusedElement; + } + // of the body or document element. + Element* bodyOrDocumentElement = GetRoot(); + NS_WARNING_ASSERTION(bodyOrDocumentElement, + "There was no element in the document"); + return bodyOrDocumentElement; + }(); + + // If the element to be selected is or