/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "HTMLEditor.h" #include #include "AutoSelectionRestorer.h" #include "EditAction.h" #include "EditorBase.h" #include "EditorDOMPoint.h" #include "EditorUtils.h" #include "HTMLEditHelpers.h" #include "HTMLEditUtils.h" #include "InternetCiter.h" #include "PendingStyles.h" #include "SelectionState.h" #include "WhiteSpaceVisibilityKeeper.h" #include "WSRunScanner.h" #include "ErrorList.h" #include "mozilla/dom/Comment.h" #include "mozilla/dom/DataTransfer.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/DocumentFragment.h" #include "mozilla/dom/DOMException.h" #include "mozilla/dom/DOMStringList.h" #include "mozilla/dom/DOMStringList.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/ElementInlines.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/FileBlobImpl.h" #include "mozilla/dom/FileReader.h" #include "mozilla/dom/Selection.h" #include "mozilla/dom/StaticRange.h" #include "mozilla/dom/WorkerRef.h" #include "mozilla/Attributes.h" #include "mozilla/Base64.h" #include "mozilla/BasicEvents.h" #include "mozilla/DebugOnly.h" #include "mozilla/Maybe.h" #include "mozilla/OwningNonNull.h" #include "mozilla/Preferences.h" #include "mozilla/Result.h" #include "mozilla/StaticPrefs_editor.h" #include "mozilla/TextComposition.h" #include "nsAString.h" #include "nsCOMPtr.h" #include "nsCRTGlue.h" // for CRLF #include "nsComponentManagerUtils.h" #include "nsIScriptError.h" #include "nsContentUtils.h" #include "nsDebug.h" #include "nsDependentSubstring.h" #include "nsError.h" #include "nsFocusManager.h" #include "nsGkAtoms.h" #include "nsIClipboard.h" #include "nsIContent.h" #include "nsIDocumentEncoder.h" #include "nsIFile.h" #include "nsIInputStream.h" #include "nsIMIMEService.h" #include "nsINode.h" #include "nsIParserUtils.h" #include "nsIPrincipal.h" #include "nsISupportsImpl.h" #include "nsISupportsPrimitives.h" #include "nsISupportsUtils.h" #include "nsITransferable.h" #include "nsIVariant.h" #include "nsLinebreakConverter.h" #include "nsLiteralString.h" #include "nsNameSpaceManager.h" #include "nsNetUtil.h" #include "nsPrintfCString.h" #include "nsRange.h" #include "nsReadableUtils.h" #include "nsServiceManagerUtils.h" #include "nsStreamUtils.h" #include "nsString.h" #include "nsStringFwd.h" #include "nsStringIterator.h" #include "nsTextNode.h" #include "nsTreeSanitizer.h" #include "nsXPCOM.h" #include "nscore.h" #include "nsContentUtils.h" #include "nsQueryObject.h" class nsAtom; class nsILoadContext; namespace mozilla { using namespace dom; using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; using LeafNodeOption = HTMLEditUtils::LeafNodeOption; #define kInsertCookie "_moz_Insert Here_moz_" // some little helpers static bool FindIntegerAfterString(const char* aLeadingString, const nsCString& aCStr, int32_t& foundNumber); static void RemoveFragComments(nsCString& aStr); nsresult HTMLEditor::InsertDroppedDataTransferAsAction( AutoEditActionDataSetter& aEditActionData, DataTransfer& aDataTransfer, const EditorDOMPoint& aDroppedAt, nsIPrincipal* aSourcePrincipal) { MOZ_ASSERT(aEditActionData.GetEditAction() == EditAction::eDrop); MOZ_ASSERT(GetEditAction() == EditAction::eDrop); MOZ_ASSERT(aDroppedAt.IsSet()); MOZ_ASSERT(aDataTransfer.MozItemCount() > 0); if (IsReadonly()) { return NS_OK; } aEditActionData.InitializeDataTransfer(&aDataTransfer); RefPtr targetRange = StaticRange::Create( aDroppedAt.GetContainer(), aDroppedAt.Offset(), aDroppedAt.GetContainer(), aDroppedAt.Offset(), IgnoreErrors()); NS_WARNING_ASSERTION(targetRange && targetRange->IsPositioned(), "Why did we fail to create collapsed static range at " "dropped position?"); if (targetRange && targetRange->IsPositioned()) { aEditActionData.AppendTargetRange(*targetRange); } nsresult rv = aEditActionData.MaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "MaybeDispatchBeforeInputEvent() failed"); return rv; } if (MOZ_UNLIKELY(!aDroppedAt.IsInContentNode())) { NS_WARNING("Dropped into non-content node"); return NS_OK; } const RefPtr editingHost = ComputeEditingHost( *aDroppedAt.ContainerAs(), LimitInBodyElement::No); if (MOZ_UNLIKELY(!editingHost)) { NS_WARNING("Dropped onto non-editable node"); return NS_OK; } uint32_t numItems = aDataTransfer.MozItemCount(); for (uint32_t i = 0; i < numItems; ++i) { DebugOnly rvIgnored = InsertFromDataTransfer(&aDataTransfer, i, aSourcePrincipal, aDroppedAt, DeleteSelectedContent::No, *editingHost); if (NS_WARN_IF(Destroyed())) { return NS_OK; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "HTMLEditor::InsertFromDataTransfer(" "DeleteSelectedContent::No) failed, but ignored"); } return NS_OK; } nsresult HTMLEditor::LoadHTML(const nsAString& aInputString) { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(!mInitSucceeded)) { return NS_ERROR_NOT_INITIALIZED; } // force IME commit; set up rules sniffing and batching DebugOnly rvIgnored = CommitComposition(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "EditorBase::CommitComposition() failed, but ignored"); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eInsertHTMLSource, 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"); nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::EnsureNoPaddingBRElementForEmptyEditor() failed"); return rv; } // Delete Selection, but only if it isn't collapsed, see bug #106269 if (!SelectionRef().IsCollapsed()) { nsresult rv = DeleteSelectionAsSubAction(eNone, eStrip); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed"); return rv; } } // Get the first range in the selection, for context: RefPtr range = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!range)) { return NS_ERROR_FAILURE; } // Create fragment for pasted HTML. ErrorResult error; RefPtr documentFragment = range->CreateContextualFragment(aInputString, error); if (error.Failed()) { NS_WARNING("nsRange::CreateContextualFragment() failed"); return error.StealNSResult(); } // Put the fragment into the document at start of selection. EditorDOMPoint pointToInsert(range->StartRef()); // XXX We need to make pointToInsert store offset for keeping traditional // behavior since using only child node to pointing insertion point // changes the behavior when inserted child is moved by mutation // observer. We need to investigate what we should do here. (void)pointToInsert.Offset(); EditorDOMPoint pointToPutCaret; for (nsCOMPtr contentToInsert = documentFragment->GetFirstChild(); contentToInsert; contentToInsert = documentFragment->GetFirstChild()) { Result insertChildContentNodeResult = InsertNodeWithTransaction(*contentToInsert, pointToInsert); if (MOZ_UNLIKELY(insertChildContentNodeResult.isErr())) { NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); return insertChildContentNodeResult.unwrapErr(); } CreateContentResult unwrappedInsertChildContentNodeResult = insertChildContentNodeResult.unwrap(); unwrappedInsertChildContentNodeResult.MoveCaretPointTo( pointToPutCaret, *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); // XXX If the inserted node has been moved by mutation observer, // incrementing offset will cause odd result. Next new node // will be inserted after existing node and the offset will be // overflown from the container node. pointToInsert.Set(pointToInsert.GetContainer(), pointToInsert.Offset() + 1); if (NS_WARN_IF(!pointToInsert.Offset())) { // Append the remaining children to the container if offset is // overflown. pointToInsert.SetToEndOf(pointToInsert.GetContainer()); } } if (pointToPutCaret.IsSet()) { nsresult rv = CollapseSelectionTo(pointToPutCaret); if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING("EditorBase::CollapseSelectionTo() failed, but ignored"); return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed, but ignored"); } return NS_OK; } NS_IMETHODIMP HTMLEditor::InsertHTML(const nsAString& aInString) { nsresult rv = InsertHTMLAsAction(aInString); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::InsertHTMLAsAction() failed"); return rv; } nsresult HTMLEditor::InsertHTMLAsAction(const nsAString& aInString, nsIPrincipal* aPrincipal) { // FIXME: This should keep handling inserting HTML if the caller is // nsIHTMLEditor::InsertHTML. if (IsReadonly()) { return NS_OK; } AutoEditActionDataSetter editActionData(*this, EditAction::eInsertHTML, 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(LimitInBodyElement::No); if (NS_WARN_IF(!editingHost)) { return NS_ERROR_FAILURE; } if (editingHost->IsContentEditablePlainTextOnly()) { nsAutoString plaintextString; nsresult rv = nsContentUtils::ConvertToPlainText( aInString, plaintextString, nsIDocumentEncoder::OutputLFLineBreak, 0u /* never wrap lines*/); if (NS_FAILED(rv)) { NS_WARNING("nsContentUtils::ConvertToPlainText() failed"); return EditorBase::ToGenericNSResult(rv); } Maybe treatAsOneTransaction; const auto EnsureAutoPlaceholderBatch = [&]() { if (treatAsOneTransaction.isNothing()) { treatAsOneTransaction.emplace(*this, ScrollSelectionIntoView::Yes, __FUNCTION__); } }; if (mComposition && mComposition->CanRequsetIMEToCommitOrCancelComposition()) { EnsureAutoPlaceholderBatch(); CommitComposition(); if (NS_WARN_IF(Destroyed())) { return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_WARN_IF(editingHost != ComputeEditingHost(LimitInBodyElement::No))) { return EditorBase::ToGenericNSResult( NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } if (MOZ_LIKELY(!plaintextString.IsEmpty())) { EnsureAutoPlaceholderBatch(); rv = InsertTextAsSubAction(plaintextString, InsertTextFor::NormalText); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::InsertTextAsSubAction() failed"); } else if (!SelectionRef().IsCollapsed()) { EnsureAutoPlaceholderBatch(); rv = DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::DeleteSelectionAsSubAction() failed"); } return EditorBase::ToGenericNSResult(rv); } AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); rv = InsertHTMLWithContextAsSubAction( aInString, u""_ns, u""_ns, u""_ns, SafeToInsertData::Yes, EditorDOMPoint(), DeleteSelectedContent::Yes, InlineStylesAtInsertionPoint::Clear, *editingHost); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::InsertHTMLWithContextAsSubAction(" "SafeToInsertData::Yes, DeleteSelectedContent::Yes, " "InlineStylesAtInsertionPoint::Clear) failed"); return EditorBase::ToGenericNSResult(rv); } class MOZ_STACK_CLASS HTMLEditor::HTMLWithContextInserter final { public: MOZ_CAN_RUN_SCRIPT HTMLWithContextInserter(HTMLEditor& aHTMLEditor, const Element& aEditingHost) : mHTMLEditor(aHTMLEditor), mEditingHost(aEditingHost) {} HTMLWithContextInserter() = delete; HTMLWithContextInserter(const HTMLWithContextInserter&) = delete; HTMLWithContextInserter(HTMLWithContextInserter&&) = delete; [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result Run( const nsAString& aInputString, const nsAString& aContextStr, const nsAString& aInfoStr, SafeToInsertData aSafeToInsertData, InlineStylesAtInsertionPoint aInlineStylesAtInsertionPoint); private: class FragmentFromPasteCreator; class FragmentParser; /** * CollectTopMostChildContentsCompletelyInRange() collects topmost child * contents which are completely in the given range. * For example, if the range points a node with its container node, the * result is only the node (meaning does not include its descendants). * If the range starts start of a node and ends end of it, and if the node * does not have children, returns no nodes, otherwise, if the node has * some children, the result includes its all children (not including their * descendants). * * @param aStartPoint Start point of the range. * @param aEndPoint End point of the range. * @param aOutArrayOfContents [Out] Topmost children which are completely in * the range. */ static void CollectTopMostChildContentsCompletelyInRange( const EditorRawDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint, nsTArray>& aOutArrayOfContents); EditorDOMPoint GetNewCaretPointAfterInsertingHTML( const EditorDOMPoint& aLastInsertedPoint) const; /** * Insert nodes in aArrayOfTopMostChildContents or their children to * aPointToInsert (if the container is not a proper parent of inserting node, * this splits the ancestors). */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result InsertContents( const EditorDOMPoint& aPointToInsert, nsTArray>& aArrayOfTopMostChildContents, const nsINode* aFragmentAsNode); /** * @param aContextStr as indicated by nsITransferable's kHTMLContext. * @param aInfoStr as indicated by nsITransferable's kHTMLInfo. */ nsresult CreateDOMFragmentFromPaste( const nsAString& aInputString, const nsAString& aContextStr, const nsAString& aInfoStr, nsCOMPtr* aOutFragNode, nsCOMPtr* aOutStartNode, nsCOMPtr* aOutEndNode, uint32_t* aOutStartOffset, uint32_t* aOutEndOffset, SafeToInsertData aSafeToInsertData) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult MoveCaretOutsideOfLink( Element& aLinkElement, const EditorDOMPoint& aPointToPutCaret); // MOZ_KNOWN_LIVE because this is set only by the constructor which is // marked as MOZ_CAN_RUN_SCRIPT and this is allocated only in the stack. MOZ_KNOWN_LIVE HTMLEditor& mHTMLEditor; MOZ_KNOWN_LIVE const Element& mEditingHost; }; class MOZ_STACK_CLASS HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator final { public: nsresult Run(const Document& aDocument, const nsAString& aInputString, const nsAString& aContextStr, const nsAString& aInfoStr, nsCOMPtr* aOutFragNode, nsCOMPtr* aOutStartNode, nsCOMPtr* aOutEndNode, SafeToInsertData aSafeToInsertData) const; private: nsresult CreateDocumentFragmentAndGetParentOfPastedHTMLInContext( const Document& aDocument, const nsAString& aInputString, const nsAString& aContextStr, SafeToInsertData aSafeToInsertData, nsCOMPtr& aParentNodeOfPastedHTMLInContext, RefPtr& aDocumentFragmentToInsert) const; static nsAtom* DetermineContextLocalNameForParsingPastedHTML( const nsIContent* aParentContentOfPastedHTMLInContext); static bool FindTargetNodeOfContextForPastedHTMLAndRemoveInsertionCookie( nsINode& aStart, nsCOMPtr& aResult); static bool IsInsertionCookie(const nsIContent& aContent); /** * @param aDocumentFragmentForContext contains the merged result. */ static nsresult MergeAndPostProcessFragmentsForPastedHTMLAndContext( DocumentFragment& aDocumentFragmentForPastedHTML, DocumentFragment& aDocumentFragmentForContext, nsIContent& aTargetContentOfContextForPastedHTML); /** * @param aInfoStr as indicated by nsITransferable's kHTMLInfo. */ [[nodiscard]] static nsresult MoveStartAndEndAccordingToHTMLInfo( const nsAString& aInfoStr, nsCOMPtr* aOutStartNode, nsCOMPtr* aOutEndNode); static nsresult PostProcessFragmentForPastedHTMLWithoutContext( DocumentFragment& aDocumentFragmentForPastedHTML); static nsresult PreProcessContextDocumentFragmentForMerging( DocumentFragment& aDocumentFragmentForContext); static void RemoveHeadChildAndStealBodyChildsChildren(nsINode& aNode); /** * This is designed for a helper class to remove disturbing nodes at inserting * the HTML fragment into the DOM tree. This walks the children and if some * elements do not have enough children, e.g., list elements not having * another visible list elements nor list item elements, * will be removed. * * @param aNode Should not be a node whose mutation may be observed by * JS. */ static void RemoveIncompleteDescendantsFromInsertingFragment(nsINode& aNode); enum class NodesToRemove { eAll, eOnlyListItems /*!< List items are always block-level elements, hence such whitespace-only nodes are always invisible. */ }; static nsresult RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces( nsIContent& aNode, NodesToRemove aNodesToRemove); }; EditorDOMPoint HTMLEditor::HTMLWithContextInserter::GetNewCaretPointAfterInsertingHTML( const EditorDOMPoint& aLastInsertedPoint) const { MOZ_ASSERT(aLastInsertedPoint.IsInContentNode()); MOZ_ASSERT( HTMLEditUtils::IsSimplyEditableNode(*aLastInsertedPoint.GetContainer())); MOZ_ASSERT(aLastInsertedPoint.GetChild()); // The last inserted node may be non-editable. Then, we want to put caret // after it. nsIContent* const lastInsertedContent = aLastInsertedPoint.GetChild(); const EditorRawDOMPoint firstEditablePoint = [&]() MOZ_NEVER_INLINE_DEBUG { if (HTMLEditUtils::IsSimplyEditableNode(*lastInsertedContent)) { return aLastInsertedPoint.To(); } MOZ_ASSERT(HTMLEditUtils::IsSimplyEditableNode( *aLastInsertedPoint.GetContainer())); return aLastInsertedPoint.NextPoint(); }(); if (NS_WARN_IF(!firstEditablePoint.IsSet())) { return EditorDOMPoint(); // No editable node, why? } const EditorRawDOMPoint adjustedEditablePoint = [&]() MOZ_NEVER_INLINE_DEBUG { if (firstEditablePoint != aLastInsertedPoint) { return firstEditablePoint; } if (lastInsertedContent->IsText()) { // End of the last inserted Text. return EditorRawDOMPoint::AtEndOf(*lastInsertedContent); } if (lastInsertedContent->IsHTMLElement(nsGkAtoms::table)) { // After the . return aLastInsertedPoint.NextPoint(); } if (!HTMLEditUtils::IsContainerNode(*lastInsertedContent) || HTMLEditUtils::IsReplacedElement(*lastInsertedContent)) { // After the atomic content node like
or . return aLastInsertedPoint.NextPoint(); } // We want to put caret to the last leaf content in the inserted content. nsIContent* const lastLeaf = HTMLEditUtils::GetLastLeafContent( *lastInsertedContent, { LeafNodeOption::TreatNonEditableNodeAsLeafNode, LeafNodeOption::IgnoreInvisibleText, // FIXME: We cannot visually put caret into empty inline containers // like so that let's ignore them. }); if (!lastLeaf) { // No meaning content in lastInsertedContent, let's put caret to end of // it. return EditorRawDOMPoint::AtEndOf(*lastInsertedContent); } Element* const mostDistantInclusiveAncestorTableElement = [&]() -> Element* { Element* tableElement = nullptr; for (Element* maybeTableElement = lastLeaf->GetAsElementOrParentElement(); maybeTableElement && maybeTableElement != aLastInsertedPoint.GetChild(); maybeTableElement = maybeTableElement->GetParentElement()) { if (maybeTableElement->IsHTMLElement(nsGkAtoms::table)) { tableElement = maybeTableElement; } } return tableElement; }(); // If we're in table elements, we should put caret after the most ancestor // table element in the container of aLastInsertedPoint. if (mostDistantInclusiveAncestorTableElement) { MOZ_ASSERT(HTMLEditUtils::IsSimplyEditableNode( *mostDistantInclusiveAncestorTableElement)); MOZ_ASSERT(mostDistantInclusiveAncestorTableElement->GetParentNode()); MOZ_ASSERT(HTMLEditUtils::IsSimplyEditableNode( *mostDistantInclusiveAncestorTableElement->GetParentNode())); return EditorRawDOMPoint::After( *mostDistantInclusiveAncestorTableElement); } MOZ_ASSERT(!lastLeaf->IsHTMLElement(nsGkAtoms::table)); if (lastLeaf->IsText()) { // End of the last Text in the last inserted content. return EditorRawDOMPoint::AtEndOf(*lastLeaf); } return !HTMLEditUtils::IsContainerNode(*lastLeaf) || HTMLEditUtils::IsReplacedElement(*lastLeaf) ? EditorRawDOMPoint::After(*lastLeaf) : EditorRawDOMPoint::AtEndOf(*lastLeaf); }(); // Make sure we don't end up with selection collapsed after an invisible // `
` element. EditorDOMPoint pointToPutCaret = adjustedEditablePoint.To(); // FIXME: Handle preformatted linefeed too const WSScanResult prevVisibleThing = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( // We want to put caret to an editable point so that we need to scan // only editable nodes. {WSRunScanner::Option::OnlyEditableNodes}, pointToPutCaret); if (prevVisibleThing.ReachedBRElementFollowedByBlockBoundary()) { const WSScanResult prevVisibleThingOfBRElement = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( {WSRunScanner::Option::OnlyEditableNodes}, EditorRawDOMPoint(prevVisibleThing.BRElementPtr())); if (prevVisibleThingOfBRElement.InVisibleOrCollapsibleCharacters()) { // XXX Why not end of the text node? pointToPutCaret = prevVisibleThingOfBRElement .PointAfterReachedContent(); } else if (prevVisibleThingOfBRElement.ReachedSpecialContent() || prevVisibleThingOfBRElement .ReachedEmptyInlineContainerElement()) { pointToPutCaret = prevVisibleThingOfBRElement .PointAfterReachedContentNode(); } } return pointToPutCaret; } nsresult HTMLEditor::InsertHTMLWithContextAsSubAction( const nsAString& aInputString, const nsAString& aContextStr, const nsAString& aInfoStr, const nsAString& aFlavor, SafeToInsertData aSafeToInsertData, const EditorDOMPoint& aPointToInsert, DeleteSelectedContent aDeleteSelectedContent, InlineStylesAtInsertionPoint aInlineStylesAtInsertionPoint, const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(!mInitSucceeded)) { return NS_ERROR_NOT_INITIALIZED; } CommitComposition(); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::ePasteHTMLContent, 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"); ignoredError.SuppressException(); { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result.unwrapErr(); } if (result.inspect().Canceled()) { return NS_OK; } } // If we have a destination / target node, we want to insert there rather than // in place of the selection. Ignore aDeleteSelectedContent here if // aPointToInsert is not set since deletion will also occur later in // HTMLWithContextInserter and will be collapsed around there; this block // is intended to cover the various scenarios where we are dropping in an // editor (and may want to delete the selection before collapsing the // selection in the new destination) if (aPointToInsert.IsSet()) { nsresult rv = PrepareToInsertContent(aPointToInsert, aDeleteSelectedContent); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::PrepareToInsertContent() failed"); return rv; } aDeleteSelectedContent = DeleteSelectedContent::No; } HTMLWithContextInserter htmlWithContextInserter(*this, aEditingHost); Result result = htmlWithContextInserter.Run( aInputString, aContextStr, aInfoStr, aSafeToInsertData, aInlineStylesAtInsertionPoint); if (MOZ_UNLIKELY(result.isErr())) { return result.unwrapErr(); } // If nothing is inserted and delete selection is required, we need to // delete selection right now. if (result.inspect().Ignored() && aDeleteSelectedContent == DeleteSelectedContent::Yes) { nsresult rv = DeleteSelectionAsSubAction(eNone, eStrip); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed"); return rv; } } return NS_OK; } Result HTMLEditor::HTMLWithContextInserter::Run( const nsAString& aInputString, const nsAString& aContextStr, const nsAString& aInfoStr, SafeToInsertData aSafeToInsertData, InlineStylesAtInsertionPoint aInlineStylesAtInsertionPoint) { MOZ_ASSERT(mHTMLEditor.IsEditActionDataAvailable()); // create a dom document fragment that represents the structure to paste nsCOMPtr fragmentAsNode, streamStartParent, streamEndParent; uint32_t streamStartOffset = 0, streamEndOffset = 0; nsresult rv = CreateDOMFragmentFromPaste( aInputString, aContextStr, aInfoStr, address_of(fragmentAsNode), address_of(streamStartParent), address_of(streamEndParent), &streamStartOffset, &streamEndOffset, aSafeToInsertData); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::HTMLWithContextInserter::CreateDOMFragmentFromPaste() " "failed"); return Err(rv); } // we need to recalculate various things based on potentially new offsets // this is work to be completed at a later date (probably by jfrancis) AutoTArray, 64> arrayOfTopMostChildContents; // If we have stream start point information, lets use it and end point. // Otherwise, we should make a range all over the document fragment. EditorRawDOMPoint streamStartPoint = streamStartParent ? EditorRawDOMPoint(streamStartParent, AssertedCast(streamStartOffset)) : EditorRawDOMPoint(fragmentAsNode, 0); EditorRawDOMPoint streamEndPoint = streamStartParent ? EditorRawDOMPoint(streamEndParent, streamEndOffset) : EditorRawDOMPoint::AtEndOf(fragmentAsNode); (void)streamStartPoint; (void)streamEndPoint; HTMLWithContextInserter::CollectTopMostChildContentsCompletelyInRange( EditorRawDOMPoint(streamStartParent, AssertedCast(streamStartOffset)), EditorRawDOMPoint(streamEndParent, AssertedCast(streamEndOffset)), arrayOfTopMostChildContents); if (arrayOfTopMostChildContents.IsEmpty()) { return EditActionResult::IgnoredResult(); // Nothing to insert. } // Are there any table elements in the list? // check for table cell selection mode bool cellSelectionMode = HTMLEditUtils::IsInTableCellSelectionMode(mHTMLEditor.SelectionRef()); if (cellSelectionMode) { // do we have table content to paste? If so, we want to delete // the selected table cells and replace with new table elements; // but if not we want to delete _contents_ of cells and replace // with non-table elements. Use cellSelectionMode bool to // indicate results. if (!HTMLEditUtils::IsAnyTableElementExceptColumnElement( *arrayOfTopMostChildContents[0])) { cellSelectionMode = false; } } if (!cellSelectionMode) { rv = mHTMLEditor.DeleteSelectionAndPrepareToCreateNode(); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteSelectionAndPrepareToCreateNode() failed"); return Err(rv); } if (aInlineStylesAtInsertionPoint == InlineStylesAtInsertionPoint::Clear) { // pasting does not inherit local inline styles Result pointToPutCaretOrError = mHTMLEditor.ClearStyleAt( EditorDOMPoint(mHTMLEditor.SelectionRef().AnchorRef()), EditorInlineStyle::RemoveAllStyles(), SpecifiedStyle::Preserve, mEditingHost); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { NS_WARNING("HTMLEditor::ClearStyleAt() failed"); return pointToPutCaretOrError.propagateErr(); } if (pointToPutCaretOrError.inspect().IsSetAndValid()) { nsresult rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaretOrError.unwrap()); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::CollapseSelectionTo() failed"); return Err(rv); } } } } else { // Delete whole cells: we will replace with new table content. // Braces for artificial block to scope AutoSelectionRestorer. // Save current selection since DeleteTableCellWithTransaction() perturbs // it. { AutoSelectionRestorer restoreSelectionLater(&mHTMLEditor); rv = mHTMLEditor.DeleteTableCellWithTransaction(1); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteTableCellWithTransaction(1) failed"); return Err(rv); } } // collapse selection to beginning of deleted table content IgnoredErrorResult ignoredError; mHTMLEditor.SelectionRef().CollapseToStart(ignoredError); NS_WARNING_ASSERTION(!ignoredError.Failed(), "Selection::Collapse() failed, but ignored"); } { Result result = mHTMLEditor.CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (result.inspect().Canceled()) { return result; } } mHTMLEditor.UndefineCaretBidiLevel(); rv = mHTMLEditor.EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && mHTMLEditor.SelectionRef().IsCollapsed()) { nsresult rv = mHTMLEditor.EnsureCaretNotAfterInvisibleBRElement(mEditingHost); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() " "failed, but ignored"); if (NS_SUCCEEDED(rv)) { nsresult rv = mHTMLEditor.PrepareInlineStylesForCaret(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } // Adjust position based on the first node we are going to insert. const auto candidatePointToInsert = mHTMLEditor.GetFirstSelectionStartPoint(); if (NS_WARN_IF(!candidatePointToInsert.IsSet()) || NS_WARN_IF( !candidatePointToInsert.GetContainer()->IsInclusiveDescendantOf( &mEditingHost))) { return Err(NS_ERROR_FAILURE); } EditorDOMPoint pointToInsert = HTMLEditUtils::GetBetterInsertionPointFor( arrayOfTopMostChildContents[0], mHTMLEditor.GetFirstSelectionStartPoint()); if (!pointToInsert.IsSet()) { NS_WARNING("HTMLEditor::GetBetterInsertionPointFor() failed"); return Err(NS_ERROR_FAILURE); } Result pointToInsertOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt( mHTMLEditor, pointToInsert, {WhiteSpaceVisibilityKeeper::NormalizeOption:: StopIfFollowingWhiteSpacesStartsWithNBSP}); if (MOZ_UNLIKELY(pointToInsertOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt() failed"); return pointToInsertOrError.propagateErr(); } pointToInsert = pointToInsertOrError.unwrap(); if (NS_WARN_IF(!pointToInsert.IsSetAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } const bool insertionPointWasInLink = !!HTMLEditor::GetLinkElement(pointToInsert.GetContainer()); if (pointToInsert.IsInTextNode()) { Result splitNodeResult = mHTMLEditor.SplitNodeDeepWithTransaction( MOZ_KnownLive(*pointToInsert.ContainerAs()), pointToInsert, SplitAtEdges::eAllowToCreateEmptyContainer); if (MOZ_UNLIKELY(splitNodeResult.isErr())) { NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed"); return splitNodeResult.propagateErr(); } nsresult rv = splitNodeResult.inspect().SuggestCaretPointTo( mHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); if (NS_FAILED(rv)) { NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed"); return Err(rv); } pointToInsert = splitNodeResult.inspect().AtSplitPoint(); if (MOZ_UNLIKELY(!pointToInsert.IsSet())) { NS_WARNING( "HTMLEditor::SplitNodeDeepWithTransaction() didn't return split " "point"); return Err(NS_ERROR_FAILURE); } } { // Block only for AutoHTMLFragmentBoundariesFixer to hide it from the // following code. Note that it may modify arrayOfTopMostChildContents. AutoHTMLFragmentBoundariesFixer fixPiecesOfTablesAndLists( arrayOfTopMostChildContents); } MOZ_ASSERT(pointToInsert.GetContainer()->GetChildAt_Deprecated( pointToInsert.Offset()) == pointToInsert.GetChild()); Result insertNodeResultOrError = InsertContents(pointToInsert, arrayOfTopMostChildContents, fragmentAsNode); if (MOZ_UNLIKELY(insertNodeResultOrError.isErr())) { NS_WARNING("HTMLWithContextInserter::InsertContents() failed."); return insertNodeResultOrError.propagateErr(); } // The inserting content may contain empty container elements. However, it's // intended. Therefore, we should not clean up them in the post-processing. mHTMLEditor.TopLevelEditSubActionDataRef().mNeedsToCleanUpEmptyElements = false; CreateContentResult insertNodeResult = insertNodeResultOrError.unwrap(); if (MOZ_UNLIKELY(!insertNodeResult.Handled())) { // Even if we haven't inserted new content nodes, we "handled" to insert // them so that return "handled" state. return EditActionResult::HandledResult(); } if (MOZ_LIKELY(insertNodeResult.GetNewNode()->IsInComposedDoc())) { const auto afterLastInsertedContent = EditorRawDOMPoint(insertNodeResult.GetNewNode()) .NextPointOrAfterContainer(); if (MOZ_LIKELY(afterLastInsertedContent.IsInContentNode())) { nsresult rv = mHTMLEditor.EnsureNoFollowingUnnecessaryLineBreak( afterLastInsertedContent, // When user inserting content, the web app may expect that nothing // extant content will be deleted. Therefore, we should preserve // preformatted linefeed at least. PreservePreformattedLineBreak::Yes, PaddingForEmptyBlock::Significant, mEditingHost); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed"); return Err(rv); } } } MOZ_ASSERT(insertNodeResult.HasCaretPointSuggestion()); rv = insertNodeResult.SuggestCaretPointTo( mHTMLEditor, {SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CaretPoint::SuggestCaretPointTo() failed"); return Err(rv); } NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CaretPoint::SuggestCaretPointTo() failed, but ignored"); // If we didn't start from an `` element, we should not keep // caret in the link to make users type something outside the link. if (insertionPointWasInLink) { return EditActionResult::HandledResult(); } if (Element* const parentElement = insertNodeResult.GetNewNode()->GetParentElement()) { const RefPtr linkElement = GetLinkElement(parentElement); if (MOZ_LIKELY(!linkElement)) { return EditActionResult::HandledResult(); } nsresult rv = MoveCaretOutsideOfLink(*linkElement, insertNodeResult.CaretPointRef()); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::HTMLWithContextInserter::MoveCaretOutsideOfLink() " "failed"); return Err(rv); } } return EditActionResult::HandledResult(); } Result HTMLEditor::HTMLWithContextInserter::InsertContents( const EditorDOMPoint& aPointToInsert, nsTArray>& aArrayOfTopMostChildContents, const nsINode* aFragmentAsNode) { MOZ_ASSERT(aPointToInsert.IsSetAndValidInComposedDoc()); // Loop over the node list and paste the nodes: const RefPtr maybeNonEditableBlockElement = aPointToInsert.IsInContentNode() ? HTMLEditUtils::GetInclusiveAncestorElement( *aPointToInsert.ContainerAs(), HTMLEditUtils::ClosestBlockElement, BlockInlineCheck::UseComputedDisplayOutsideStyle) : nullptr; nsCOMPtr insertedContextParentContent; RefPtr lastInsertedContent; for (const OwningNonNull& content : aArrayOfTopMostChildContents) { if (NS_WARN_IF(content == aFragmentAsNode) || NS_WARN_IF(content->IsHTMLElement(nsGkAtoms::body))) { return Err(NS_ERROR_FAILURE); } if (insertedContextParentContent) { // If we had to insert something higher up in the paste hierarchy, // we want to skip any further paste nodes that descend from that. // Else we will paste twice. // XXX This check may be really expensive. Cannot we check whether // the node's `ownerDocument` is the `aFragmentAsNode` or not? if (EditorUtils::IsDescendantOf(*content, *insertedContextParentContent)) { continue; } // Okay, now, we finished moving nodes in insertedContextParentContent. // We can forget it now to skip the expensive check. insertedContextParentContent = nullptr; } // In the most cases, we want to move `content` into the DOM as-is. However, // in some cases, we don't want to insert content but do want to insert its // children into the existing proper container. Therefore, we will check // the `content` type and insertion point's container below. However, even // in such case, we may not be able to move only its children. Then, we // need to fall it back to the default behavior. Therefore, let's wrap the // default behavior into this lambda. const auto InsertCurrentContentToNextInsertionPoint = [&](const EditorDOMPoint& aPointToInsertContent) MOZ_CAN_RUN_SCRIPT MOZ_NEVER_INLINE_DEBUG -> Result { // MOZ_KnownLive(content) because 'aArrayOfTopMostChildContents' // guarantees its lifetime. Result moveContentResult = mHTMLEditor.InsertNodeIntoProperAncestorWithTransaction( MOZ_KnownLive(content), aPointToInsertContent, SplitAtEdges::eDoNotCreateEmptyContainer); if (MOZ_LIKELY(moveContentResult.isOk())) { moveContentResult.inspect().IgnoreCaretPointSuggestion(); if (MOZ_UNLIKELY(!moveContentResult.inspect().Handled())) { MOZ_ASSERT(aPointToInsertContent.IsSetAndValidInComposedDoc()); MOZ_ASSERT_IF(lastInsertedContent, lastInsertedContent->IsInComposedDoc()); return Ok{}; } lastInsertedContent = content; MOZ_ASSERT(lastInsertedContent->IsInComposedDoc()); return Ok{}; } // If we got unexpected DOM tree, let's abort. if (NS_WARN_IF(moveContentResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) || NS_WARN_IF(moveContentResult.inspectErr() == NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE)) { return moveContentResult.propagateErr(); } // If we the next insertion point becomes invalid, it means that we // got unexpected DOM tree which couldn't be detected by // InsertNodeIntoProperAncestorWithTransaction(). Let's abort to // avoid to move the node into unexpected position/documents. if (NS_WARN_IF(!aPointToInsertContent.IsSetAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // Assume failure means no legal parent in the document hierarchy, // try again with the parent of content in the paste hierarchy. // FYI: We cannot use `InclusiveAncestorOfType` here because of // calling `InsertNodeIntoProperAncestorWithTransaction()`. EditorDOMPoint pointToInsert = aPointToInsertContent; for (nsCOMPtr parent = content->GetParent(); parent; parent = parent->GetParent()) { if (NS_WARN_IF(parent->IsHTMLElement(nsGkAtoms::body))) { break; // for the inner `for` loop } Result moveParentResult = mHTMLEditor.InsertNodeIntoProperAncestorWithTransaction( *parent, pointToInsert, SplitAtEdges::eDoNotCreateEmptyContainer); if (MOZ_UNLIKELY(moveParentResult.isErr())) { // If we got unexpected DOM tree, let's abort. if (NS_WARN_IF(moveParentResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) || NS_WARN_IF(moveParentResult.inspectErr() == NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE)) { return moveParentResult.propagateErr(); } // If we the next insertion point becomes invalid, it means that we // got unexpected DOM tree which couldn't be detected by // InsertNodeIntoProperAncestorWithTransaction(). Let's abort to // avoid to move the node into unexpected position/documents. if (NS_WARN_IF(!pointToInsert.IsSetAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // If the parent cannot be inserted into the DOM tree, the node may be // an element to make a specific structure like a table. Then, we can // insert one of its ancestors to the inserting position. So, let's // retry with its parent. continue; // the inner `for` loop } moveParentResult.inspect().IgnoreCaretPointSuggestion(); if (MOZ_UNLIKELY(!moveParentResult.inspect().Handled())) { MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc()); MOZ_ASSERT_IF(lastInsertedContent, lastInsertedContent->IsInComposedDoc()); continue; } pointToInsert = EditorDOMPoint::After(*parent); lastInsertedContent = parent; MOZ_ASSERT(lastInsertedContent->IsInComposedDoc()); insertedContextParentContent = std::move(parent); break; // from the inner `for` loop } // end of the inner `for` loop iterating ancestors of content return Ok{}; }; // If a `
` or `` element on the clipboard, and pasting it into // a `
` or `` element, insert only the appropriate children // instead. if (HTMLEditUtils::IsTableRowElement(*content)) { EditorDOMPoint pointToInsert = lastInsertedContent ? EditorDOMPoint::After(*lastInsertedContent) : aPointToInsert; if (HTMLEditUtils::IsTableRowElement( pointToInsert.GetContainerAs()) && (content->IsHTMLElement(nsGkAtoms::table) || pointToInsert.IsContainerHTMLElement(nsGkAtoms::table))) { MOZ_ASSERT(!content->IsInComposedDoc()); bool inserted = false; for (RefPtr child = content->GetFirstChild(); child; child = content->GetFirstChild()) { Result moveChildResult = mHTMLEditor .InsertNodeIntoProperAncestorWithTransaction( *child, pointToInsert, SplitAtEdges::eDoNotCreateEmptyContainer); if (MOZ_UNLIKELY(moveChildResult.isErr())) { // If we got unexpected DOM tree, let's abort. if (NS_WARN_IF(moveChildResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) || NS_WARN_IF(moveChildResult.inspectErr() == NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE)) { return moveChildResult.propagateErr(); } // If we the next insertion point becomes invalid, it means that // we got unexpected DOM tree which couldn't be detected by // InsertNodeIntoProperAncestorWithTransaction(). Let's abort to // avoid to move the node into unexpected position/documents. if (NS_WARN_IF(!pointToInsert.IsSetAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } NS_WARNING( "HTMLEditor::InsertNodeIntoProperAncestorWithTransaction(" "SplitAtEdges::eDoNotCreateEmptyContainer) failed, but maybe " "ignored"); break; // from the inner `for` loop } moveChildResult.inspect().IgnoreCaretPointSuggestion(); if (MOZ_UNLIKELY(!moveChildResult.inspect().Handled())) { MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc()); MOZ_ASSERT_IF(lastInsertedContent, lastInsertedContent->IsInComposedDoc()); continue; } inserted = true; pointToInsert = EditorDOMPoint::After(*child); MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc()); lastInsertedContent = std::move(child); MOZ_ASSERT(lastInsertedContent->IsInComposedDoc()); } // end of the inner `for` loop iterating children of `content` if (!inserted) { Result moveContentOrParentResultOrError = InsertCurrentContentToNextInsertionPoint(pointToInsert); if (MOZ_UNLIKELY(moveContentOrParentResultOrError.isErr())) { NS_WARNING("InsertCurrentContentToNextInsertionPoint() failed"); return moveContentOrParentResultOrError.propagateErr(); } } continue; } } // if // If a list element on the clipboard, and pasting it into a list or // list item element, insert the appropriate children instead. I.e., // merge the list elements instead of pasting as a sublist. if (HTMLEditUtils::IsListElement(*content)) { EditorDOMPoint pointToInsert = lastInsertedContent ? EditorDOMPoint::After(*lastInsertedContent) : aPointToInsert; if (HTMLEditUtils::IsListElement( pointToInsert.GetContainerAs()) || HTMLEditUtils::IsListItemElement( pointToInsert.GetContainerAs())) { MOZ_ASSERT(!content->IsInComposedDoc()); bool inserted = false; for (RefPtr child = content->GetFirstChild(); child; child = content->GetFirstChild()) { // Ignore invisible nodes like `Comment` or white-space only `Text` // and invalid children of the list element. // XXX Although we should not construct invalid structure, but // shouldn't we preserve invalid children for avoiding dataloss? if (!HTMLEditUtils::IsListItemElement(*child) && !HTMLEditUtils::IsListElement(*child)) { continue; } // If we're pasting into empty list item, we should remove it // and past current node into the parent list directly. // XXX This creates invalid structure if current list item element // is not proper child of the parent element, or current node // is a list element. if (HTMLEditUtils::IsListItemElement( pointToInsert.GetContainerAs()) && HTMLEditUtils::IsRemovableNode( *pointToInsert.ContainerAs()) && HTMLEditUtils::IsEmptyNode( *pointToInsert.ContainerAs(), {EmptyCheckOption::TreatNonEditableContentAsInvisible})) { const OwningNonNull emptyListItemElement = *pointToInsert.ContainerAs(); nsCOMPtr parentNode = emptyListItemElement->GetParentNode(); MOZ_ASSERT(parentNode); nsCOMPtr nextSibling = emptyListItemElement->GetNextSibling(); nsresult rv = mHTMLEditor.DeleteNodeWithTransaction(*emptyListItemElement); if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_WARN_IF(!parentNode->IsInComposedDoc()) || NS_WARN_IF(nextSibling && nextSibling->GetParentNode() != parentNode)) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::DeleteNodeWithTransaction() " "failed, but ignored"); pointToInsert = nextSibling ? EditorDOMPoint(std::move(nextSibling)) : EditorDOMPoint::AtEndOf(std::move(parentNode)); MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc()); } NS_WARNING(nsPrintfCString("%s into %s", ToString(*child).c_str(), ToString(pointToInsert).c_str()) .get()); Result moveChildResult = mHTMLEditor .InsertNodeIntoProperAncestorWithTransaction( *child, pointToInsert, SplitAtEdges::eDoNotCreateEmptyContainer); if (MOZ_UNLIKELY(moveChildResult.isErr())) { // If we got unexpected DOM tree, let's abort. if (NS_WARN_IF(moveChildResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) || NS_WARN_IF(moveChildResult.inspectErr() == NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE)) { return moveChildResult.propagateErr(); } // If we the next insertion point becomes invalid, it means that // we got unexpected DOM tree which couldn't be detected by // InsertNodeIntoProperAncestorWithTransaction(). Let's abort to // avoid to move the node into unexpected position/documents. if (NS_WARN_IF(!pointToInsert.IsSetAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } NS_WARNING( "HTMLEditor::InsertNodeIntoProperAncestorWithTransaction(" "SplitAtEdges::eDoNotCreateEmptyContainer) failed, but maybe " "ignored"); break; // from the inner `for` loop } moveChildResult.inspect().IgnoreCaretPointSuggestion(); if (MOZ_UNLIKELY(!moveChildResult.inspect().Handled())) { MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc()); MOZ_ASSERT_IF(lastInsertedContent, lastInsertedContent->IsInComposedDoc()); continue; } inserted = true; pointToInsert = EditorDOMPoint::After(*child); MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc()); lastInsertedContent = std::move(child); MOZ_ASSERT(lastInsertedContent->IsInComposedDoc()); } // end of the inner `for` loop iterating children of `content` if (!inserted) { Result moveContentOrParentResultOrError = InsertCurrentContentToNextInsertionPoint(pointToInsert); if (MOZ_UNLIKELY(moveContentOrParentResultOrError.isErr())) { NS_WARNING("InsertCurrentContentToNextInsertionPoint() failed"); return moveContentOrParentResultOrError.propagateErr(); } } continue; } } // if
s if they have no
/ // element, or something other elements which must have specific // children but they don't. if (isEmptyNodeShouldNotInserted) { nsIContent* nextChild = child->GetNextSibling(); OwningNonNull removingChild(*child); removingChild->Remove(); child = nextChild; continue; } if (child->HasChildNodes()) { RemoveIncompleteDescendantsFromInsertingFragment(*child); } child = child->GetNextSibling(); } } // static bool HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator:: IsInsertionCookie(const nsIContent& aContent) { // Is this child the magical cookie? if (const auto* comment = Comment::FromNode(&aContent)) { nsAutoString data; comment->GetData(data); return data.EqualsLiteral(kInsertCookie); } return false; } /** * This function finds the target node that we will be pasting into. aStart is * the context that we're given and aResult will be the target. Initially, * *aResult must be nullptr. * * The target for a paste is found by either finding the node that contains * the magical comment node containing kInsertCookie or, failing that, the * firstChild of the firstChild (until we reach a leaf). */ bool HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator:: FindTargetNodeOfContextForPastedHTMLAndRemoveInsertionCookie( nsINode& aStart, nsCOMPtr& aResult) { nsIContent* firstChild = aStart.GetFirstChild(); if (!firstChild) { // If the current result is nullptr, then aStart is a leaf, and is the // fallback result. if (!aResult) { aResult = &aStart; } return false; } for (nsCOMPtr child = firstChild; child; child = child->GetNextSibling()) { if (FragmentFromPasteCreator::IsInsertionCookie(*child)) { // Yes it is! Return an error so we bubble out and short-circuit the // search. aResult = &aStart; child->Remove(); return true; } if (FindTargetNodeOfContextForPastedHTMLAndRemoveInsertionCookie(*child, aResult)) { return true; } } return false; } class MOZ_STACK_CLASS HTMLEditor::HTMLWithContextInserter::FragmentParser final { public: FragmentParser(const Document& aDocument, SafeToInsertData aSafeToInsertData); [[nodiscard]] nsresult ParseContext(const nsAString& aContextString, DocumentFragment** aFragment); [[nodiscard]] nsresult ParsePastedHTML(const nsAString& aInputString, nsAtom* aContextLocalNameAtom, DocumentFragment** aFragment); private: static nsresult ParseFragment(const nsAString& aStr, nsAtom* aContextLocalName, const Document* aTargetDoc, dom::DocumentFragment** aFragment, SafeToInsertData aSafeToInsertData); const Document& mDocument; const SafeToInsertData mSafeToInsertData; }; HTMLEditor::HTMLWithContextInserter::FragmentParser::FragmentParser( const Document& aDocument, SafeToInsertData aSafeToInsertData) : mDocument{aDocument}, mSafeToInsertData{aSafeToInsertData} {} nsresult HTMLEditor::HTMLWithContextInserter::FragmentParser::ParseContext( const nsAString& aContextStr, DocumentFragment** aFragment) { return FragmentParser::ParseFragment(aContextStr, nullptr, &mDocument, aFragment, mSafeToInsertData); } nsresult HTMLEditor::HTMLWithContextInserter::FragmentParser::ParsePastedHTML( const nsAString& aInputString, nsAtom* aContextLocalNameAtom, DocumentFragment** aFragment) { return FragmentParser::ParseFragment(aInputString, aContextLocalNameAtom, &mDocument, aFragment, mSafeToInsertData); } nsresult HTMLEditor::HTMLWithContextInserter::CreateDOMFragmentFromPaste( const nsAString& aInputString, const nsAString& aContextStr, const nsAString& aInfoStr, nsCOMPtr* aOutFragNode, nsCOMPtr* aOutStartNode, nsCOMPtr* aOutEndNode, uint32_t* aOutStartOffset, uint32_t* aOutEndOffset, SafeToInsertData aSafeToInsertData) const { if (NS_WARN_IF(!aOutFragNode) || NS_WARN_IF(!aOutStartNode) || NS_WARN_IF(!aOutEndNode) || NS_WARN_IF(!aOutStartOffset) || NS_WARN_IF(!aOutEndOffset)) { return NS_ERROR_INVALID_ARG; } RefPtr document = mHTMLEditor.GetDocument(); if (NS_WARN_IF(!document)) { return NS_ERROR_FAILURE; } FragmentFromPasteCreator fragmentFromPasteCreator; const nsresult rv = fragmentFromPasteCreator.Run( *document, aInputString, aContextStr, aInfoStr, aOutFragNode, aOutStartNode, aOutEndNode, aSafeToInsertData); *aOutStartOffset = 0; *aOutEndOffset = (*aOutEndNode)->Length(); return rv; } // static nsAtom* HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator:: DetermineContextLocalNameForParsingPastedHTML( const nsIContent* aParentContentOfPastedHTMLInContext) { if (!aParentContentOfPastedHTMLInContext) { return nsGkAtoms::body; } nsAtom* contextLocalNameAtom = aParentContentOfPastedHTMLInContext->NodeInfo()->NameAtom(); return (aParentContentOfPastedHTMLInContext->IsHTMLElement(nsGkAtoms::html)) ? nsGkAtoms::body : contextLocalNameAtom; } // static nsresult HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator:: MergeAndPostProcessFragmentsForPastedHTMLAndContext( DocumentFragment& aDocumentFragmentForPastedHTML, DocumentFragment& aDocumentFragmentForContext, nsIContent& aTargetContentOfContextForPastedHTML) { FragmentFromPasteCreator::RemoveHeadChildAndStealBodyChildsChildren( aDocumentFragmentForPastedHTML); FragmentFromPasteCreator::RemoveIncompleteDescendantsFromInsertingFragment( aDocumentFragmentForPastedHTML); // unite the two trees IgnoredErrorResult ignoredError; aTargetContentOfContextForPastedHTML.AppendChild( aDocumentFragmentForPastedHTML, ignoredError); NS_WARNING_ASSERTION(!ignoredError.Failed(), "nsINode::AppendChild() failed, but ignored"); const nsresult rv = FragmentFromPasteCreator:: RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces( aDocumentFragmentForContext, NodesToRemove::eOnlyListItems); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::" "RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces()" " failed"); return rv; } return rv; } // static nsresult HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator:: PostProcessFragmentForPastedHTMLWithoutContext( DocumentFragment& aDocumentFragmentForPastedHTML) { FragmentFromPasteCreator::RemoveHeadChildAndStealBodyChildsChildren( aDocumentFragmentForPastedHTML); FragmentFromPasteCreator::RemoveIncompleteDescendantsFromInsertingFragment( aDocumentFragmentForPastedHTML); const nsresult rv = FragmentFromPasteCreator:: RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces( aDocumentFragmentForPastedHTML, NodesToRemove::eOnlyListItems); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::" "RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces() " "failed"); return rv; } return rv; } // static nsresult HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator:: PreProcessContextDocumentFragmentForMerging( DocumentFragment& aDocumentFragmentForContext) { // The context is expected to contain text nodes only in block level // elements. Hence, if they contain only whitespace, they're invisible. const nsresult rv = FragmentFromPasteCreator:: RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces( aDocumentFragmentForContext, NodesToRemove::eAll); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::" "RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces() " "failed"); return rv; } FragmentFromPasteCreator::RemoveHeadChildAndStealBodyChildsChildren( aDocumentFragmentForContext); return rv; } nsresult HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator:: CreateDocumentFragmentAndGetParentOfPastedHTMLInContext( const Document& aDocument, const nsAString& aInputString, const nsAString& aContextStr, SafeToInsertData aSafeToInsertData, nsCOMPtr& aParentNodeOfPastedHTMLInContext, RefPtr& aDocumentFragmentToInsert) const { // if we have context info, create a fragment for that RefPtr documentFragmentForContext; FragmentParser fragmentParser{aDocument, aSafeToInsertData}; if (!aContextStr.IsEmpty()) { nsresult rv = fragmentParser.ParseContext( aContextStr, getter_AddRefs(documentFragmentForContext)); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::HTMLWithContextInserter::FragmentParser::ParseContext() " "failed"); return rv; } if (!documentFragmentForContext) { NS_WARNING( "HTMLEditor::HTMLWithContextInserter::FragmentParser::ParseContext() " "returned nullptr"); return NS_ERROR_FAILURE; } rv = FragmentFromPasteCreator::PreProcessContextDocumentFragmentForMerging( *documentFragmentForContext); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::" "PreProcessContextDocumentFragmentForMerging() failed."); return rv; } FragmentFromPasteCreator:: FindTargetNodeOfContextForPastedHTMLAndRemoveInsertionCookie( *documentFragmentForContext, aParentNodeOfPastedHTMLInContext); MOZ_ASSERT(aParentNodeOfPastedHTMLInContext); } nsCOMPtr parentContentOfPastedHTMLInContext = nsIContent::FromNodeOrNull(aParentNodeOfPastedHTMLInContext); MOZ_ASSERT_IF(aParentNodeOfPastedHTMLInContext, parentContentOfPastedHTMLInContext); nsAtom* contextLocalNameAtom = FragmentFromPasteCreator::DetermineContextLocalNameForParsingPastedHTML( parentContentOfPastedHTMLInContext); RefPtr documentFragmentForPastedHTML; nsresult rv = fragmentParser.ParsePastedHTML( aInputString, contextLocalNameAtom, getter_AddRefs(documentFragmentForPastedHTML)); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::HTMLWithContextInserter::FragmentParser::ParsePastedHTML()" " failed"); return rv; } if (!documentFragmentForPastedHTML) { NS_WARNING( "HTMLEditor::HTMLWithContextInserter::FragmentParser::ParsePastedHTML()" " returned nullptr"); return NS_ERROR_FAILURE; } if (aParentNodeOfPastedHTMLInContext) { const nsresult rv = FragmentFromPasteCreator:: MergeAndPostProcessFragmentsForPastedHTMLAndContext( *documentFragmentForPastedHTML, *documentFragmentForContext, *parentContentOfPastedHTMLInContext); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::" "MergeAndPostProcessFragmentsForPastedHTMLAndContext() failed."); return rv; } aDocumentFragmentToInsert = std::move(documentFragmentForContext); } else { const nsresult rv = PostProcessFragmentForPastedHTMLWithoutContext( *documentFragmentForPastedHTML); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::" "PostProcessFragmentForPastedHTMLWithoutContext() failed."); return rv; } aDocumentFragmentToInsert = std::move(documentFragmentForPastedHTML); } return rv; } nsresult HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::Run( const Document& aDocument, const nsAString& aInputString, const nsAString& aContextStr, const nsAString& aInfoStr, nsCOMPtr* aOutFragNode, nsCOMPtr* aOutStartNode, nsCOMPtr* aOutEndNode, SafeToInsertData aSafeToInsertData) const { MOZ_ASSERT(aOutFragNode); MOZ_ASSERT(aOutStartNode); MOZ_ASSERT(aOutEndNode); nsCOMPtr parentNodeOfPastedHTMLInContext; RefPtr documentFragmentToInsert; nsresult rv = CreateDocumentFragmentAndGetParentOfPastedHTMLInContext( aDocument, aInputString, aContextStr, aSafeToInsertData, parentNodeOfPastedHTMLInContext, documentFragmentToInsert); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::" "CreateDocumentFragmentAndGetParentOfPastedHTMLInContext() failed."); return rv; } // If there was no context, then treat all of the data we did get as the // pasted data. if (parentNodeOfPastedHTMLInContext) { *aOutEndNode = *aOutStartNode = parentNodeOfPastedHTMLInContext; } else { *aOutEndNode = *aOutStartNode = documentFragmentToInsert; } *aOutFragNode = std::move(documentFragmentToInsert); if (!aInfoStr.IsEmpty()) { const nsresult rv = FragmentFromPasteCreator::MoveStartAndEndAccordingToHTMLInfo( aInfoStr, aOutStartNode, aOutEndNode); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::" "MoveStartAndEndAccordingToHTMLInfo() failed"); return rv; } } return NS_OK; } // static nsresult HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator:: MoveStartAndEndAccordingToHTMLInfo(const nsAString& aInfoStr, nsCOMPtr* aOutStartNode, nsCOMPtr* aOutEndNode) { int32_t sep = aInfoStr.FindChar((char16_t)','); nsAutoString numstr1(Substring(aInfoStr, 0, sep)); nsAutoString numstr2( Substring(aInfoStr, sep + 1, aInfoStr.Length() - (sep + 1))); // Move the start and end children. nsresult rvIgnored; int32_t num = numstr1.ToInteger(&rvIgnored); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "nsAString::ToInteger() failed, but ignored"); while (num--) { nsINode* tmp = (*aOutStartNode)->GetFirstChild(); if (!tmp) { NS_WARNING("aOutStartNode did not have children"); return NS_ERROR_FAILURE; } *aOutStartNode = tmp; } num = numstr2.ToInteger(&rvIgnored); while (num--) { nsINode* tmp = (*aOutEndNode)->GetLastChild(); if (!tmp) { NS_WARNING("aOutEndNode did not have children"); return NS_ERROR_FAILURE; } *aOutEndNode = tmp; } return NS_OK; } // static nsresult HTMLEditor::HTMLWithContextInserter::FragmentParser::ParseFragment( const nsAString& aFragStr, nsAtom* aContextLocalName, const Document* aTargetDocument, DocumentFragment** aFragment, SafeToInsertData aSafeToInsertData) { nsAutoScriptBlockerSuppressNodeRemoved autoBlocker; nsCOMPtr doc = nsContentUtils::CreateInertHTMLDocument(aTargetDocument); if (!doc) { return NS_ERROR_FAILURE; } RefPtr fragment = new (doc->NodeInfoManager()) DocumentFragment(doc->NodeInfoManager()); nsresult rv = nsContentUtils::ParseFragmentHTML( aFragStr, fragment, aContextLocalName ? aContextLocalName : nsGkAtoms::body, kNameSpaceID_XHTML, false, true); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsContentUtils::ParseFragmentHTML() failed"); if (aSafeToInsertData == SafeToInsertData::No) { nsTreeSanitizer sanitizer(aContextLocalName ? nsIParserUtils::SanitizerAllowStyle : nsIParserUtils::SanitizerAllowComments); sanitizer.Sanitize(fragment); } fragment.forget(aFragment); return rv; } // static void HTMLEditor::HTMLWithContextInserter:: CollectTopMostChildContentsCompletelyInRange( const EditorRawDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint, nsTArray>& aOutArrayOfContents) { MOZ_ASSERT(aStartPoint.IsSetAndValid()); MOZ_ASSERT(aEndPoint.IsSetAndValid()); RefPtr range = nsRange::Create(aStartPoint.ToRawRangeBoundary(), aEndPoint.ToRawRangeBoundary(), IgnoreErrors()); if (!range) { NS_WARNING("nsRange::Create() failed"); return; } DOMSubtreeIterator iter; if (NS_FAILED(iter.Init(*range))) { NS_WARNING("DOMSubtreeIterator::Init() failed, but ignored"); return; } iter.AppendAllNodesToArray(aOutArrayOfContents); } /****************************************************************************** * HTMLEditor::AutoHTMLFragmentBoundariesFixer ******************************************************************************/ HTMLEditor::AutoHTMLFragmentBoundariesFixer::AutoHTMLFragmentBoundariesFixer( nsTArray>& aArrayOfTopMostChildContents) { EnsureBeginsOrEndsWithValidContent(StartOrEnd::start, aArrayOfTopMostChildContents); EnsureBeginsOrEndsWithValidContent(StartOrEnd::end, aArrayOfTopMostChildContents); } // static void HTMLEditor::AutoHTMLFragmentBoundariesFixer:: CollectTableAndAnyListElementsOfInclusiveAncestorsAt( nsIContent& aContent, nsTArray>& aOutArrayOfListAndTableElements) { for (Element* element = aContent.GetAsElementOrParentElement(); element; element = element->GetParentElement()) { if (HTMLEditUtils::IsListElement(*element) || element->IsHTMLElement(nsGkAtoms::table)) { aOutArrayOfListAndTableElements.AppendElement(*element); } } } // static Element* HTMLEditor::AutoHTMLFragmentBoundariesFixer:: GetMostDistantAncestorListOrTableElement( const nsTArray>& aArrayOfTopMostChildContents, const nsTArray>& aInclusiveAncestorsTableOrListElements) { Element* lastFoundAncestorListOrTableElement = nullptr; for (const OwningNonNull& content : aArrayOfTopMostChildContents) { if (HTMLEditUtils::IsAnyTableElementExceptTableElementAndColumElement( content)) { Element* tableElement = HTMLEditUtils::GetClosestAncestorTableElement(*content); if (!tableElement) { continue; } // If we find a `` element which is an ancestor of a table // related element and is not an acestor of first nor last of // aArrayOfNodes, return the last found list or `
` element. // XXX Is that really expected that this returns a list element in this // case? if (!aInclusiveAncestorsTableOrListElements.Contains(tableElement)) { return lastFoundAncestorListOrTableElement; } // If we find a `
` element which is topmost list or `
` // element at first or last of aArrayOfNodes, return it. if (aInclusiveAncestorsTableOrListElements.LastElement().get() == tableElement) { return tableElement; } // Otherwise, store the `
` element which is an ancestor but // not topmost ancestor of first or last of aArrayOfNodes. lastFoundAncestorListOrTableElement = tableElement; continue; } if (!HTMLEditUtils::IsListItemElement(*content)) { continue; } Element* listElement = HTMLEditUtils::GetClosestAncestorAnyListElement(*content); if (!listElement) { continue; } // If we find a list element which is ancestor of a list item element and // is not an acestor of first nor last of aArrayOfNodes, return the last // found list or `
` element. // XXX Is that really expected that this returns a `
` element in // this case? if (!aInclusiveAncestorsTableOrListElements.Contains(listElement)) { return lastFoundAncestorListOrTableElement; } // If we find a list element which is topmost list or `
` element at // first or last of aArrayOfNodes, return it. if (aInclusiveAncestorsTableOrListElements.LastElement().get() == listElement) { return listElement; } // Otherwise, store the list element which is an ancestor but not topmost // ancestor of first or last of aArrayOfNodes. lastFoundAncestorListOrTableElement = listElement; } // If we find only non-topmost list or `
` element, returns the last // found one (meaning bottommost one). Otherwise, nullptr. return lastFoundAncestorListOrTableElement; } Element* HTMLEditor::AutoHTMLFragmentBoundariesFixer::FindReplaceableTableElement( Element& aTableElement, nsIContent& aContentMaybeInTableElement) const { MOZ_ASSERT(aTableElement.IsHTMLElement(nsGkAtoms::table)); // Perhaps, this is designed for climbing up the DOM tree from // aContentMaybeInTableElement to aTableElement and making sure that // aContentMaybeInTableElement itself or its ancestor is a ``, ``, ``, `` or `
`, ``, // `
`. // But this looks really buggy because this loop may skip aTableElement // as the following NS_ASSERTION. We should write automated tests and // check right behavior. for (Element* element = aContentMaybeInTableElement.GetAsElementOrParentElement(); element; element = element->GetParentElement()) { if (!HTMLEditUtils::IsAnyTableElementExceptColumnElement(*element) || element->IsHTMLElement(nsGkAtoms::table)) { // XXX Perhaps, the original developer of this method assumed that // aTableElement won't be skipped because if it's assumed, we can // stop climbing up the tree in that case. NS_ASSERTION(element != &aTableElement, "The table element which is looking for is ignored"); continue; } Element* tableElement = nullptr; for (Element* maybeTableElement = element->GetParentElement(); maybeTableElement; maybeTableElement = maybeTableElement->GetParentElement()) { if (maybeTableElement->IsHTMLElement(nsGkAtoms::table)) { tableElement = maybeTableElement; break; } } if (tableElement == &aTableElement) { return element; } // XXX If we find another `` element, why don't we keep searching // from its parent? } return nullptr; } bool HTMLEditor::AutoHTMLFragmentBoundariesFixer::IsReplaceableListElement( Element& aListElement, nsIContent& aContentMaybeInListElement) const { MOZ_ASSERT(HTMLEditUtils::IsListElement(aListElement)); // Perhaps, this is designed for climbing up the DOM tree from // aContentMaybeInListElement to aListElement and making sure that // aContentMaybeInListElement itself or its ancestor is an list item. // But this looks really buggy because this loop may skip aListElement // as the following NS_ASSERTION. We should write automated tests and // check right behavior. for (Element* element = aContentMaybeInListElement.GetAsElementOrParentElement(); element; element = element->GetParentElement()) { if (!HTMLEditUtils::IsListItemElement(*element)) { // XXX Perhaps, the original developer of this method assumed that // aListElement won't be skipped because if it's assumed, we can // stop climbing up the tree in that case. NS_ASSERTION(element != &aListElement, "The list element which is looking for is ignored"); continue; } Element* listElement = HTMLEditUtils::GetClosestAncestorAnyListElement(*element); if (listElement == &aListElement) { return true; } // XXX If we find another list element, why don't we keep searching // from its parent? } return false; } void HTMLEditor::AutoHTMLFragmentBoundariesFixer:: EnsureBeginsOrEndsWithValidContent(StartOrEnd aStartOrEnd, nsTArray>& aArrayOfTopMostChildContents) const { MOZ_ASSERT(!aArrayOfTopMostChildContents.IsEmpty()); // Collect list elements and table related elements at first or last node // in aArrayOfTopMostChildContents. AutoTArray, 4> inclusiveAncestorsListOrTableElements; CollectTableAndAnyListElementsOfInclusiveAncestorsAt( aStartOrEnd == StartOrEnd::end ? aArrayOfTopMostChildContents.LastElement() : aArrayOfTopMostChildContents[0], inclusiveAncestorsListOrTableElements); if (inclusiveAncestorsListOrTableElements.IsEmpty()) { return; } // Get most ancestor list or `
` element in // inclusiveAncestorsListOrTableElements which contains earlier // node in aArrayOfTopMostChildContents as far as possible. // XXX With inclusiveAncestorsListOrTableElements, this returns a // list or `
` element which contains first or last node of // aArrayOfTopMostChildContents. However, this seems slow when // aStartOrEnd is StartOrEnd::end and only the last node is in // different list or `
`. But I'm not sure whether it's // possible case or not. We need to add tests to // test_content_iterator_subtree.html for checking how // SubtreeContentIterator works. Element* listOrTableElement = GetMostDistantAncestorListOrTableElement( aArrayOfTopMostChildContents, inclusiveAncestorsListOrTableElements); if (!listOrTableElement) { return; } // If we have pieces of tables or lists to be inserted, let's force the // insertion to deal with table elements right away, so that it doesn't // orphan some table or list contents outside the table or list. OwningNonNull& firstOrLastChildContent = aStartOrEnd == StartOrEnd::end ? aArrayOfTopMostChildContents.LastElement() : aArrayOfTopMostChildContents[0]; // Find substructure of list or table that must be included in paste. Element* replaceElement; if (HTMLEditUtils::IsListElement(*listOrTableElement)) { if (!IsReplaceableListElement(*listOrTableElement, firstOrLastChildContent)) { return; } replaceElement = listOrTableElement; } else { MOZ_ASSERT(listOrTableElement->IsHTMLElement(nsGkAtoms::table)); replaceElement = FindReplaceableTableElement(*listOrTableElement, firstOrLastChildContent); if (!replaceElement) { return; } } // If we can replace the given list element or found a table related element // in the `
` element, insert it into aArrayOfTopMostChildContents which // is tompost children to be inserted instead of descendants of them in // aArrayOfTopMostChildContents. for (size_t i = 0; i < aArrayOfTopMostChildContents.Length();) { OwningNonNull& content = aArrayOfTopMostChildContents[i]; if (content == replaceElement) { // If the element is n aArrayOfTopMostChildContents, its descendants must // not be in the array. Therefore, we don't need to optimize this case. // XXX Perhaps, we can break this loop right now. aArrayOfTopMostChildContents.RemoveElementAt(i); continue; } if (!EditorUtils::IsDescendantOf(content, *replaceElement)) { i++; continue; } // For saving number of calls of EditorUtils::IsDescendantOf(), we should // remove its siblings in the array. nsIContent* parent = content->GetParent(); aArrayOfTopMostChildContents.RemoveElementAt(i); while (i < aArrayOfTopMostChildContents.Length() && aArrayOfTopMostChildContents[i]->GetParent() == parent) { aArrayOfTopMostChildContents.RemoveElementAt(i); } } // Now replace the removed nodes with the structural parent if (aStartOrEnd == StartOrEnd::end) { aArrayOfTopMostChildContents.AppendElement(*replaceElement); } else { aArrayOfTopMostChildContents.InsertElementAt(0, *replaceElement); } } } // namespace mozilla