summaryrefslogtreecommitdiffstats
path: root/editor/libeditor/HTMLEditorDataTransfer.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /editor/libeditor/HTMLEditorDataTransfer.cpp
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--editor/libeditor/HTMLEditorDataTransfer.cpp4325
1 files changed, 4325 insertions, 0 deletions
diff --git a/editor/libeditor/HTMLEditorDataTransfer.cpp b/editor/libeditor/HTMLEditorDataTransfer.cpp
new file mode 100644
index 0000000000..70110fd6a6
--- /dev/null
+++ b/editor/libeditor/HTMLEditorDataTransfer.cpp
@@ -0,0 +1,4325 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=78: */
+/* 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 <string.h>
+
+#include "EditAction.h"
+#include "EditorDOMPoint.h"
+#include "EditorUtils.h"
+#include "HTMLEditHelpers.h"
+#include "HTMLEditUtils.h"
+#include "InternetCiter.h"
+#include "PendingStyles.h"
+#include "SelectionState.h"
+#include "WSRunObject.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/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/ArrayUtils.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 "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 "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 LeafNodeType = HTMLEditUtils::LeafNodeType;
+
+#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<StaticRange> 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;
+ }
+ uint32_t numItems = aDataTransfer.MozItemCount();
+ for (uint32_t i = 0; i < numItems; ++i) {
+ DebugOnly<nsresult> rvIgnored =
+ InsertFromDataTransfer(&aDataTransfer, i, aSourcePrincipal, aDroppedAt,
+ DeleteSelectedContent::No);
+ 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<nsresult> 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<const nsRange> range = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Create fragment for pasted HTML.
+ ErrorResult error;
+ RefPtr<DocumentFragment> 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.
+ Unused << pointToInsert.Offset();
+ EditorDOMPoint pointToPutCaret;
+ for (nsCOMPtr<nsIContent> contentToInsert = documentFragment->GetFirstChild();
+ contentToInsert; contentToInsert = documentFragment->GetFirstChild()) {
+ Result<CreateContentResult, nsresult> 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);
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertHTMLWithContextAsSubAction(aInString, u""_ns, u""_ns, u""_ns,
+ SafeToInsertData::Yes, EditorDOMPoint(),
+ DeleteSelectedContent::Yes,
+ InlineStylesAtInsertionPoint::Clear);
+ 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 explicit HTMLWithContextInserter(HTMLEditor& aHTMLEditor)
+ : mHTMLEditor(aHTMLEditor) {}
+
+ HTMLWithContextInserter() = delete;
+ HTMLWithContextInserter(const HTMLWithContextInserter&) = delete;
+ HTMLWithContextInserter(HTMLWithContextInserter&&) = delete;
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> 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<OwningNonNull<nsIContent>>& aOutArrayOfContents);
+
+ /**
+ * @return nullptr, if there's no invisible `<br>`.
+ */
+ HTMLBRElement* GetInvisibleBRElementAtPoint(
+ const EditorDOMPoint& aPointToInsert) const;
+
+ EditorDOMPoint GetNewCaretPointAfterInsertingHTML(
+ const EditorDOMPoint& aLastInsertedPoint) const;
+
+ /**
+ * @return error result or the last inserted point. The latter is only set, if
+ * content was inserted.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ InsertContents(
+ const EditorDOMPoint& aPointToInsert,
+ nsTArray<OwningNonNull<nsIContent>>& 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<nsINode>* aOutFragNode,
+ nsCOMPtr<nsINode>* aOutStartNode, nsCOMPtr<nsINode>* 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;
+};
+
+class MOZ_STACK_CLASS
+ HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator final {
+ public:
+ nsresult Run(const Document& aDocument, const nsAString& aInputString,
+ const nsAString& aContextStr, const nsAString& aInfoStr,
+ nsCOMPtr<nsINode>* aOutFragNode,
+ nsCOMPtr<nsINode>* aOutStartNode, nsCOMPtr<nsINode>* aOutEndNode,
+ SafeToInsertData aSafeToInsertData) const;
+
+ private:
+ nsresult CreateDocumentFragmentAndGetParentOfPastedHTMLInContext(
+ const Document& aDocument, const nsAString& aInputString,
+ const nsAString& aContextStr, SafeToInsertData aSafeToInsertData,
+ nsCOMPtr<nsINode>& aParentNodeOfPastedHTMLInContext,
+ RefPtr<DocumentFragment>& aDocumentFragmentToInsert) const;
+
+ static nsAtom* DetermineContextLocalNameForParsingPastedHTML(
+ const nsIContent* aParentContentOfPastedHTMLInContext);
+
+ static bool FindTargetNodeOfContextForPastedHTMLAndRemoveInsertionCookie(
+ nsINode& aStart, nsCOMPtr<nsINode>& 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<nsINode>* aOutStartNode,
+ nsCOMPtr<nsINode>* 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);
+};
+
+HTMLBRElement*
+HTMLEditor::HTMLWithContextInserter::GetInvisibleBRElementAtPoint(
+ const EditorDOMPoint& aPointToInsert) const {
+ WSRunScanner wsRunScannerAtInsertionPoint(mHTMLEditor.ComputeEditingHost(),
+ aPointToInsert);
+ if (wsRunScannerAtInsertionPoint.EndsByInvisibleBRElement()) {
+ return wsRunScannerAtInsertionPoint.EndReasonBRElementPtr();
+ }
+ return nullptr;
+}
+
+EditorDOMPoint
+HTMLEditor::HTMLWithContextInserter::GetNewCaretPointAfterInsertingHTML(
+ const EditorDOMPoint& aLastInsertedPoint) const {
+ EditorDOMPoint pointToPutCaret;
+
+ // but don't cross tables
+ nsIContent* containerContent = nullptr;
+ if (!HTMLEditUtils::IsTable(aLastInsertedPoint.GetChild())) {
+ containerContent = HTMLEditUtils::GetLastLeafContent(
+ *aLastInsertedPoint.GetChild(), {LeafNodeType::OnlyEditableLeafNode},
+ aLastInsertedPoint.GetChild()->GetAsElementOrParentElement());
+ if (containerContent) {
+ Element* mostDistantInclusiveAncestorTableElement = nullptr;
+ for (Element* maybeTableElement =
+ containerContent->GetAsElementOrParentElement();
+ maybeTableElement &&
+ maybeTableElement != aLastInsertedPoint.GetChild();
+ maybeTableElement = maybeTableElement->GetParentElement()) {
+ if (HTMLEditUtils::IsTable(maybeTableElement)) {
+ mostDistantInclusiveAncestorTableElement = maybeTableElement;
+ }
+ }
+ // If we're in table elements, we should put caret into the most ancestor
+ // table element.
+ if (mostDistantInclusiveAncestorTableElement) {
+ containerContent = mostDistantInclusiveAncestorTableElement;
+ }
+ }
+ }
+ // If we are not in table elements, we should put caret in the last inserted
+ // node.
+ if (!containerContent) {
+ containerContent = aLastInsertedPoint.GetChild();
+ }
+
+ // If the container is a text node or a container element except `<table>`
+ // element, put caret a end of it.
+ if (containerContent->IsText() ||
+ (HTMLEditUtils::IsContainerNode(*containerContent) &&
+ !HTMLEditUtils::IsTable(containerContent))) {
+ pointToPutCaret.SetToEndOf(containerContent);
+ }
+ // Otherwise, i.e., it's an atomic element, `<table>` element or data node,
+ // put caret after it.
+ else {
+ pointToPutCaret.Set(containerContent);
+ DebugOnly<bool> advanced = pointToPutCaret.AdvanceOffset();
+ NS_WARNING_ASSERTION(advanced, "Failed to advance offset from found node");
+ }
+
+ // Make sure we don't end up with selection collapsed after an invisible
+ // `<br>` element.
+ Element* editingHost = mHTMLEditor.ComputeEditingHost();
+ WSRunScanner wsRunScannerAtCaret(editingHost, pointToPutCaret);
+ if (wsRunScannerAtCaret
+ .ScanPreviousVisibleNodeOrBlockBoundaryFrom(pointToPutCaret)
+ .ReachedInvisibleBRElement()) {
+ WSRunScanner wsRunScannerAtStartReason(
+ editingHost,
+ EditorDOMPoint(wsRunScannerAtCaret.GetStartReasonContent()));
+ WSScanResult backwardScanFromPointToCaretResult =
+ wsRunScannerAtStartReason.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
+ pointToPutCaret);
+ if (backwardScanFromPointToCaretResult.InVisibleOrCollapsibleCharacters()) {
+ pointToPutCaret =
+ backwardScanFromPointToCaretResult.Point<EditorDOMPoint>();
+ } else if (backwardScanFromPointToCaretResult.ReachedSpecialContent()) {
+ // XXX In my understanding, this is odd. The end reason may not be
+ // same as the reached special content because the equality is
+ // guaranteed only when ReachedCurrentBlockBoundary() returns true.
+ // However, looks like that this code assumes that
+ // GetStartReasonContent() returns the content.
+ NS_ASSERTION(wsRunScannerAtStartReason.GetStartReasonContent() ==
+ backwardScanFromPointToCaretResult.GetContent(),
+ "Start reason is not the reached special content");
+ pointToPutCaret.SetAfter(
+ wsRunScannerAtStartReason.GetStartReasonContent());
+ }
+ }
+
+ 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) {
+ 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<EditActionResult, nsresult> 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);
+
+ Result<EditActionResult, nsresult> 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<EditActionResult, nsresult> 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<nsINode> 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<OwningNonNull<nsIContent>, 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<uint32_t>(streamStartOffset))
+ : EditorRawDOMPoint(fragmentAsNode, 0);
+ EditorRawDOMPoint streamEndPoint =
+ streamStartParent ? EditorRawDOMPoint(streamEndParent, streamEndOffset)
+ : EditorRawDOMPoint::AtEndOf(fragmentAsNode);
+
+ Unused << streamStartPoint;
+ Unused << streamEndPoint;
+
+ HTMLWithContextInserter::CollectTopMostChildContentsCompletelyInRange(
+ EditorRawDOMPoint(streamStartParent,
+ AssertedCast<uint32_t>(streamStartOffset)),
+ EditorRawDOMPoint(streamEndParent,
+ AssertedCast<uint32_t>(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::IsAnyTableElement(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<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ mHTMLEditor.ClearStyleAt(
+ EditorDOMPoint(mHTMLEditor.SelectionRef().AnchorRef()),
+ EditorInlineStyle::RemoveAllStyles(), SpecifiedStyle::Preserve);
+ 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<EditActionResult, nsresult> 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();
+ 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");
+ }
+ }
+
+ Element* editingHost =
+ mHTMLEditor.ComputeEditingHost(HTMLEditor::LimitInBodyElement::No);
+ if (NS_WARN_IF(!editingHost)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Adjust position based on the first node we are going to insert.
+ EditorDOMPoint pointToInsert =
+ HTMLEditUtils::GetBetterInsertionPointFor<EditorDOMPoint>(
+ arrayOfTopMostChildContents[0],
+ mHTMLEditor.GetFirstSelectionStartPoint<EditorRawDOMPoint>(),
+ *editingHost);
+ if (!pointToInsert.IsSet()) {
+ NS_WARNING("HTMLEditor::GetBetterInsertionPointFor() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Remove invisible `<br>` element at the point because if there is a `<br>`
+ // element at end of what we paste, it will make the existing invisible
+ // `<br>` element visible.
+ if (HTMLBRElement* invisibleBRElement =
+ GetInvisibleBRElementAtPoint(pointToInsert)) {
+ AutoEditorDOMPointChildInvalidator lockOffset(pointToInsert);
+ nsresult rv = mHTMLEditor.DeleteNodeWithTransaction(
+ MOZ_KnownLive(*invisibleBRElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed.");
+ return Err(rv);
+ }
+ }
+
+ const bool insertionPointWasInLink =
+ !!HTMLEditor::GetLinkElement(pointToInsert.GetContainer());
+
+ if (pointToInsert.IsInTextNode()) {
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ mHTMLEditor.SplitNodeDeepWithTransaction(
+ MOZ_KnownLive(*pointToInsert.ContainerAs<nsIContent>()),
+ 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<EditorDOMPoint>();
+ 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<EditorDOMPoint, nsresult> lastInsertedPoint = InsertContents(
+ pointToInsert, arrayOfTopMostChildContents, fragmentAsNode);
+ if (lastInsertedPoint.isErr()) {
+ NS_WARNING("HTMLWithContextInserter::InsertContents() failed.");
+ return lastInsertedPoint.propagateErr();
+ }
+
+ mHTMLEditor.TopLevelEditSubActionDataRef().mNeedsToCleanUpEmptyElements =
+ false;
+
+ if (MOZ_UNLIKELY(!lastInsertedPoint.inspect().IsInComposedDoc())) {
+ return EditActionResult::HandledResult();
+ }
+
+ const EditorDOMPoint pointToPutCaret =
+ GetNewCaretPointAfterInsertingHTML(lastInsertedPoint.inspect());
+ // Now collapse the selection to the end of what we just inserted.
+ rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+
+ // If we didn't start from an `<a href>` element, we should not keep
+ // caret in the link to make users type something outside the link.
+ if (insertionPointWasInLink) {
+ return EditActionResult::HandledResult();
+ }
+ RefPtr<Element> linkElement = GetLinkElement(pointToPutCaret.GetContainer());
+
+ if (!linkElement) {
+ return EditActionResult::HandledResult();
+ }
+
+ rv = MoveCaretOutsideOfLink(*linkElement, pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::MoveCaretOutsideOfLink "
+ "failed.");
+ return Err(rv);
+ }
+
+ return EditActionResult::HandledResult();
+}
+
+Result<EditorDOMPoint, nsresult>
+HTMLEditor::HTMLWithContextInserter::InsertContents(
+ const EditorDOMPoint& aPointToInsert,
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfTopMostChildContents,
+ const nsINode* aFragmentAsNode) {
+ MOZ_ASSERT(aPointToInsert.IsSetAndValidInComposedDoc());
+
+ EditorDOMPoint pointToInsert{aPointToInsert};
+
+ // Loop over the node list and paste the nodes:
+ const RefPtr<const Element> maybeNonEditableBlockElement =
+ pointToInsert.IsInContentNode()
+ ? HTMLEditUtils::GetInclusiveAncestorElement(
+ *pointToInsert.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestBlockElement)
+ : nullptr;
+
+ EditorDOMPoint lastInsertedPoint;
+ nsCOMPtr<nsIContent> insertedContextParentContent;
+ for (OwningNonNull<nsIContent>& 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?
+ // XXX If content was moved to outside of insertedContextParentContent
+ // by mutation event listeners, we will anyway duplicate it.
+ if (EditorUtils::IsDescendantOf(*content,
+ *insertedContextParentContent)) {
+ continue;
+ }
+ }
+
+ // If a `<table>` or `<tr>` element on the clipboard, and pasting it into
+ // a `<table>` or `<tr>` element, insert only the appropriate children
+ // instead.
+ bool inserted = false;
+ if (HTMLEditUtils::IsTableRow(content) &&
+ HTMLEditUtils::IsTableRow(pointToInsert.GetContainer()) &&
+ (HTMLEditUtils::IsTable(content) ||
+ HTMLEditUtils::IsTable(pointToInsert.GetContainer()))) {
+ // Move children of current node to the insertion point.
+ AutoTArray<OwningNonNull<nsIContent>, 24> children;
+ HTMLEditUtils::CollectAllChildren(*content, children);
+ EditorDOMPoint pointToPutCaret;
+ for (const OwningNonNull<nsIContent>& child : children) {
+ // MOZ_KnownLive(child) because of bug 1622253
+ Result<CreateContentResult, nsresult> moveChildResult =
+ mHTMLEditor.InsertNodeIntoProperAncestorWithTransaction<nsIContent>(
+ MOZ_KnownLive(child), pointToInsert,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(moveChildResult.isErr())) {
+ // If moving node is moved to different place, we should ignore
+ // this result and keep trying to insert next content node to same
+ // position.
+ if (moveChildResult.inspectErr() ==
+ NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE) {
+ inserted = true;
+ continue; // the inner `for` loop
+ }
+ NS_WARNING(
+ "HTMLEditor::InsertNodeIntoProperAncestorWithTransaction("
+ "SplitAtEdges::eDoNotCreateEmptyContainer) failed, maybe "
+ "ignored");
+ break; // from the inner `for` loop
+ }
+ if (MOZ_UNLIKELY(!moveChildResult.inspect().Handled())) {
+ continue;
+ }
+ inserted = true;
+ lastInsertedPoint.Set(child);
+ pointToInsert = lastInsertedPoint.NextPoint();
+ MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc());
+ CreateContentResult unwrappedMoveChildResult = moveChildResult.unwrap();
+ unwrappedMoveChildResult.MoveCaretPointTo(
+ pointToPutCaret, mHTMLEditor,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ } // end of the inner `for` loop
+
+ if (pointToPutCaret.IsSet()) {
+ nsresult rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ }
+ // 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.
+ else if (HTMLEditUtils::IsAnyListElement(content) &&
+ (HTMLEditUtils::IsAnyListElement(pointToInsert.GetContainer()) ||
+ HTMLEditUtils::IsListItem(pointToInsert.GetContainer()))) {
+ AutoTArray<OwningNonNull<nsIContent>, 24> children;
+ HTMLEditUtils::CollectAllChildren(*content, children);
+ EditorDOMPoint pointToPutCaret;
+ for (const OwningNonNull<nsIContent>& child : children) {
+ if (HTMLEditUtils::IsListItem(child) ||
+ HTMLEditUtils::IsAnyListElement(child)) {
+ // 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::IsListItem(pointToInsert.GetContainer()) &&
+ HTMLEditUtils::IsEmptyNode(*pointToInsert.GetContainer())) {
+ NS_WARNING_ASSERTION(pointToInsert.GetContainerParent(),
+ "Insertion point is out of the DOM tree");
+ if (pointToInsert.GetContainerParent()) {
+ pointToInsert.Set(pointToInsert.GetContainer());
+ MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc());
+ AutoEditorDOMPointChildInvalidator lockOffset(pointToInsert);
+ nsresult rv = mHTMLEditor.DeleteNodeWithTransaction(
+ MOZ_KnownLive(*pointToInsert.GetChild()));
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeWithTransaction() "
+ "failed, but ignored");
+ }
+ }
+ // MOZ_KnownLive(child) because of bug 1622253
+ Result<CreateContentResult, nsresult> moveChildResult =
+ mHTMLEditor
+ .InsertNodeIntoProperAncestorWithTransaction<nsIContent>(
+ MOZ_KnownLive(child), pointToInsert,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(moveChildResult.isErr())) {
+ // If moving node is moved to different place, we should ignore
+ // this result and keep trying to insert next content node to
+ // same position.
+ if (moveChildResult.inspectErr() ==
+ NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE) {
+ inserted = true;
+ continue; // the inner `for` loop
+ }
+ if (NS_WARN_IF(moveChildResult.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::InsertNodeIntoProperAncestorWithTransaction("
+ "SplitAtEdges::eDoNotCreateEmptyContainer) failed, but maybe "
+ "ignored");
+ break; // from the inner `for` loop
+ }
+ if (MOZ_UNLIKELY(!moveChildResult.inspect().Handled())) {
+ continue;
+ }
+ inserted = true;
+ lastInsertedPoint.Set(child);
+ pointToInsert = lastInsertedPoint.NextPoint();
+ MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc());
+ CreateContentResult unwrappedMoveChildResult =
+ moveChildResult.unwrap();
+ unwrappedMoveChildResult.MoveCaretPointTo(
+ pointToPutCaret, mHTMLEditor,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ }
+ // If the child of current node is not list item nor list element,
+ // we should remove it from the DOM tree.
+ else if (HTMLEditUtils::IsRemovableNode(child)) {
+ AutoEditorDOMPointChildInvalidator lockOffset(pointToInsert);
+ IgnoredErrorResult ignoredError;
+ content->RemoveChild(child, ignoredError);
+ if (MOZ_UNLIKELY(mHTMLEditor.Destroyed())) {
+ NS_WARNING(
+ "nsIContent::RemoveChild() caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "nsINode::RemoveChild() failed, but ignored");
+ } else {
+ NS_WARNING(
+ "Failed to delete the first child of a list element because the "
+ "list element non-editable");
+ break; // from the inner `for` loop
+ }
+ } // end of the inner `for` loop
+
+ if (MOZ_UNLIKELY(mHTMLEditor.Destroyed())) {
+ NS_WARNING("The editor has been destroyed");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (pointToPutCaret.IsSet()) {
+ nsresult rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ }
+ // If pasting into a `<pre>` element and current node is a `<pre>` element,
+ // move only its children.
+ else if (HTMLEditUtils::IsPre(maybeNonEditableBlockElement) &&
+ HTMLEditUtils::IsPre(content)) {
+ // Check for pre's going into pre's.
+ AutoTArray<OwningNonNull<nsIContent>, 24> children;
+ HTMLEditUtils::CollectAllChildren(*content, children);
+ EditorDOMPoint pointToPutCaret;
+ for (const OwningNonNull<nsIContent>& child : children) {
+ // MOZ_KnownLive(child) because of bug 1622253
+ Result<CreateContentResult, nsresult> moveChildResult =
+ mHTMLEditor.InsertNodeIntoProperAncestorWithTransaction<nsIContent>(
+ MOZ_KnownLive(child), pointToInsert,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(moveChildResult.isErr())) {
+ // If moving node is moved to different place, we should ignore
+ // this result and keep trying to insert next content node there.
+ if (moveChildResult.inspectErr() ==
+ NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE) {
+ inserted = true;
+ continue; // the inner `for` loop
+ }
+ if (NS_WARN_IF(moveChildResult.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return moveChildResult.propagateErr();
+ }
+ NS_WARNING(
+ "HTMLEditor::InsertNodeIntoProperAncestorWithTransaction("
+ "SplitAtEdges::eDoNotCreateEmptyContainer) failed, but maybe "
+ "ignored");
+ break; // from the inner `for` loop
+ }
+ if (MOZ_UNLIKELY(!moveChildResult.inspect().Handled())) {
+ continue;
+ }
+ CreateContentResult unwrappedMoveChildResult = moveChildResult.unwrap();
+ inserted = true;
+ lastInsertedPoint.Set(child);
+ pointToInsert = lastInsertedPoint.NextPoint();
+ MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc());
+ unwrappedMoveChildResult.MoveCaretPointTo(
+ pointToPutCaret, mHTMLEditor,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ } // end of the inner `for` loop
+
+ if (pointToPutCaret.IsSet()) {
+ nsresult rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ }
+
+ // TODO: For making the above code clearer, we should move this fallback
+ // path into a lambda and call it in each if/else-if block.
+ // If we haven't inserted current node nor its children, move current node
+ // to the insertion point.
+ if (!inserted) {
+ // MOZ_KnownLive(content) because 'aArrayOfTopMostChildContents' is
+ // guaranteed to keep it alive.
+ Result<CreateContentResult, nsresult> moveContentResult =
+ mHTMLEditor.InsertNodeIntoProperAncestorWithTransaction<nsIContent>(
+ MOZ_KnownLive(content), pointToInsert,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_LIKELY(moveContentResult.isOk())) {
+ if (MOZ_UNLIKELY(!moveContentResult.inspect().Handled())) {
+ continue;
+ }
+ lastInsertedPoint.Set(content);
+ pointToInsert = lastInsertedPoint;
+ MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc());
+ nsresult rv = moveContentResult.inspect().SuggestCaretPointTo(
+ mHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateContentResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CreateContentResult::SuggestCaretPointTo() failed, but ignored");
+ } else if (moveContentResult.inspectErr() ==
+ NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE) {
+ // Moving node is moved to different place, we should keep trying to
+ // insert the next content to same position.
+ } else {
+ NS_WARNING(
+ "HTMLEditor::InsertNodeIntoProperAncestorWithTransaction("
+ "SplitAtEdges::eDoNotCreateEmptyContainer) failed, but ignored");
+ // 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()`.
+ for (nsCOMPtr<nsIContent> childContent = content; childContent;
+ childContent = childContent->GetParent()) {
+ if (NS_WARN_IF(!childContent->GetParent()) ||
+ NS_WARN_IF(
+ childContent->GetParent()->IsHTMLElement(nsGkAtoms::body))) {
+ break; // for the inner `for` loop
+ }
+ const OwningNonNull<nsIContent> oldParentContent =
+ *childContent->GetParent();
+ Result<CreateContentResult, nsresult> moveParentResult =
+ mHTMLEditor
+ .InsertNodeIntoProperAncestorWithTransaction<nsIContent>(
+ oldParentContent, pointToInsert,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(moveParentResult.isErr())) {
+ // Moving node is moved to different place, we should keep trying to
+ // insert the next content to same position.
+ if (moveParentResult.inspectErr() ==
+ NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE) {
+ break; // from the inner `for` loop
+ }
+ if (NS_WARN_IF(moveParentResult.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::InsertNodeInToProperAncestorWithTransaction("
+ "SplitAtEdges::eDoNotCreateEmptyContainer) failed, but "
+ "ignored");
+ continue; // the inner `for` loop
+ }
+ if (MOZ_UNLIKELY(!moveParentResult.inspect().Handled())) {
+ continue;
+ }
+ insertedContextParentContent = oldParentContent;
+ pointToInsert.Set(oldParentContent);
+ MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc());
+ nsresult rv = moveParentResult.inspect().SuggestCaretPointTo(
+ mHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateContentResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CreateContentResult::SuggestCaretPointTo() failed, but ignored");
+ break; // from the inner `for` loop
+ } // end of the inner `for` loop
+ }
+ }
+ if (lastInsertedPoint.IsSet()) {
+ if (MOZ_UNLIKELY(lastInsertedPoint.GetContainer() !=
+ lastInsertedPoint.GetChild()->GetParentNode())) {
+ NS_WARNING(
+ "HTMLEditor::InsertHTMLWithContextAsSubAction() got lost insertion "
+ "point");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ pointToInsert = lastInsertedPoint.NextPoint();
+ MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc());
+ }
+ } // end of the `for` loop
+
+ return lastInsertedPoint;
+}
+
+nsresult HTMLEditor::HTMLWithContextInserter::MoveCaretOutsideOfLink(
+ Element& aLinkElement, const EditorDOMPoint& aPointToPutCaret) {
+ MOZ_ASSERT(HTMLEditUtils::IsLink(&aLinkElement));
+
+ // The reason why do that instead of just moving caret after it is, the
+ // link might have ended in an invisible `<br>` element. If so, the code
+ // above just placed selection inside that. So we need to split it instead.
+ // XXX Sounds like that it's not really expensive comparing with the reason
+ // to use SplitNodeDeepWithTransaction() here.
+ Result<SplitNodeResult, nsresult> splitLinkResult =
+ mHTMLEditor.SplitNodeDeepWithTransaction(
+ aLinkElement, aPointToPutCaret,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitLinkResult.isErr())) {
+ if (splitLinkResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction() failed, but ignored");
+ }
+
+ if (nsIContent* previousContentOfSplitPoint =
+ splitLinkResult.inspect().GetPreviousContent()) {
+ splitLinkResult.inspect().IgnoreCaretPointSuggestion();
+ nsresult rv = mHTMLEditor.CollapseSelectionTo(
+ EditorRawDOMPoint::After(*previousContentOfSplitPoint));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ return NS_OK;
+ }
+
+ nsresult rv = splitLinkResult.inspect().SuggestCaretPointTo(
+ mHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "SplitNodeResult::SuggestCaretPointTo() failed");
+ return rv;
+}
+
+// static
+Element* HTMLEditor::GetLinkElement(nsINode* aNode) {
+ if (NS_WARN_IF(!aNode)) {
+ return nullptr;
+ }
+ nsINode* node = aNode;
+ while (node) {
+ if (HTMLEditUtils::IsLink(node)) {
+ return node->AsElement();
+ }
+ node = node->GetParentNode();
+ }
+ return nullptr;
+}
+
+// static
+nsresult HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::
+ RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces(
+ nsIContent& aNode, NodesToRemove aNodesToRemove) {
+ if (aNode.TextIsOnlyWhitespace()) {
+ nsCOMPtr<nsINode> parent = aNode.GetParentNode();
+ // TODO: presumably, if the parent is a `<pre>` element, the node
+ // shouldn't be removed.
+ if (parent) {
+ if (aNodesToRemove == NodesToRemove::eAll ||
+ HTMLEditUtils::IsAnyListElement(parent)) {
+ ErrorResult error;
+ parent->RemoveChild(aNode, error);
+ NS_WARNING_ASSERTION(!error.Failed(), "nsINode::RemoveChild() failed");
+ return error.StealNSResult();
+ }
+ return NS_OK;
+ }
+ }
+
+ if (!aNode.IsHTMLElement(nsGkAtoms::pre)) {
+ nsCOMPtr<nsIContent> child = aNode.GetLastChild();
+ while (child) {
+ nsCOMPtr<nsIContent> previous = child->GetPreviousSibling();
+ nsresult rv = FragmentFromPasteCreator::
+ RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces(
+ *child, aNodesToRemove);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::"
+ "RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces"
+ "() "
+ "failed");
+ return rv;
+ }
+ child = std::move(previous);
+ }
+ }
+ return NS_OK;
+}
+
+class MOZ_STACK_CLASS HTMLEditor::HTMLTransferablePreparer {
+ public:
+ HTMLTransferablePreparer(const HTMLEditor& aHTMLEditor,
+ nsITransferable** aTransferable);
+
+ nsresult Run();
+
+ private:
+ void AddDataFlavorsInBestOrder(nsITransferable& aTransferable) const;
+
+ const HTMLEditor& mHTMLEditor;
+ nsITransferable** mTransferable;
+};
+
+HTMLEditor::HTMLTransferablePreparer::HTMLTransferablePreparer(
+ const HTMLEditor& aHTMLEditor, nsITransferable** aTransferable)
+ : mHTMLEditor{aHTMLEditor}, mTransferable{aTransferable} {
+ MOZ_ASSERT(mTransferable);
+ MOZ_ASSERT(!*mTransferable);
+}
+
+nsresult HTMLEditor::PrepareHTMLTransferable(
+ nsITransferable** aTransferable) const {
+ HTMLTransferablePreparer htmlTransferablePreparer{*this, aTransferable};
+ return htmlTransferablePreparer.Run();
+}
+
+nsresult HTMLEditor::HTMLTransferablePreparer::Run() {
+ // Create generic Transferable for getting the data
+ nsresult rv;
+ RefPtr<nsITransferable> transferable =
+ do_CreateInstance("@mozilla.org/widget/transferable;1", &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("do_CreateInstance() failed to create nsITransferable instance");
+ return rv;
+ }
+
+ if (!transferable) {
+ NS_WARNING("do_CreateInstance() returned nullptr, but ignored");
+ return NS_OK;
+ }
+
+ // Get the nsITransferable interface for getting the data from the clipboard
+ RefPtr<Document> destdoc = mHTMLEditor.GetDocument();
+ nsILoadContext* loadContext = destdoc ? destdoc->GetLoadContext() : nullptr;
+ DebugOnly<nsresult> rvIgnored = transferable->Init(loadContext);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::Init() failed, but ignored");
+
+ // See `HTMLEditor::InsertFromTransferableAtSelection`.
+ AddDataFlavorsInBestOrder(*transferable);
+
+ transferable.forget(mTransferable);
+
+ return NS_OK;
+}
+
+void HTMLEditor::HTMLTransferablePreparer::AddDataFlavorsInBestOrder(
+ nsITransferable& aTransferable) const {
+ // Create the desired DataFlavor for the type of data
+ // we want to get out of the transferable
+ // This should only happen in html editors, not plaintext
+ if (!mHTMLEditor.IsInPlaintextMode()) {
+ DebugOnly<nsresult> rvIgnored =
+ aTransferable.AddDataFlavor(kNativeHTMLMime);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kNativeHTMLMime) failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kHTMLMime);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kHTMLMime) failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kFileMime);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kFileMime) failed, but ignored");
+
+ switch (Preferences::GetInt("clipboard.paste_image_type", 1)) {
+ case 0: // prefer JPEG over PNG over GIF encoding
+ rvIgnored = aTransferable.AddDataFlavor(kJPEGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kJPEGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kJPGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kJPGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kPNGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kPNGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kGIFImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kGIFImageMime) "
+ "failed, but ignored");
+ break;
+ case 1: // prefer PNG over JPEG over GIF encoding (default)
+ default:
+ rvIgnored = aTransferable.AddDataFlavor(kPNGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kPNGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kJPEGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kJPEGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kJPGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kJPGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kGIFImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kGIFImageMime) "
+ "failed, but ignored");
+ break;
+ case 2: // prefer GIF over JPEG over PNG encoding
+ rvIgnored = aTransferable.AddDataFlavor(kGIFImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kGIFImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kJPEGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kJPEGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kJPGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kJPGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kPNGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kPNGImageMime) "
+ "failed, but ignored");
+ break;
+ }
+ }
+ DebugOnly<nsresult> rvIgnored = aTransferable.AddDataFlavor(kTextMime);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kTextMime) failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kMozTextInternal);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kMozTextInternal) failed, but ignored");
+}
+
+bool FindIntegerAfterString(const char* aLeadingString, const nsCString& aCStr,
+ int32_t& foundNumber) {
+ // first obtain offsets from cfhtml str
+ int32_t numFront = aCStr.Find(aLeadingString);
+ if (numFront == -1) {
+ return false;
+ }
+ numFront += strlen(aLeadingString);
+
+ int32_t numBack = aCStr.FindCharInSet(CRLF, numFront);
+ if (numBack == -1) {
+ return false;
+ }
+
+ nsAutoCString numStr(Substring(aCStr, numFront, numBack - numFront));
+ nsresult errorCode;
+ foundNumber = numStr.ToInteger(&errorCode);
+ return true;
+}
+
+void RemoveFragComments(nsCString& aStr) {
+ // remove the StartFragment/EndFragment comments from the str, if present
+ int32_t startCommentIndx = aStr.Find("<!--StartFragment");
+ if (startCommentIndx >= 0) {
+ int32_t startCommentEnd = aStr.Find("-->", startCommentIndx);
+ if (startCommentEnd > startCommentIndx) {
+ aStr.Cut(startCommentIndx, (startCommentEnd + 3) - startCommentIndx);
+ }
+ }
+ int32_t endCommentIndx = aStr.Find("<!--EndFragment");
+ if (endCommentIndx >= 0) {
+ int32_t endCommentEnd = aStr.Find("-->", endCommentIndx);
+ if (endCommentEnd > endCommentIndx) {
+ aStr.Cut(endCommentIndx, (endCommentEnd + 3) - endCommentIndx);
+ }
+ }
+}
+
+nsresult HTMLEditor::ParseCFHTML(const nsCString& aCfhtml,
+ char16_t** aStuffToPaste,
+ char16_t** aCfcontext) {
+ // First obtain offsets from cfhtml str.
+ int32_t startHTML, endHTML, startFragment, endFragment;
+ if (!FindIntegerAfterString("StartHTML:", aCfhtml, startHTML) ||
+ startHTML < -1) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!FindIntegerAfterString("EndHTML:", aCfhtml, endHTML) || endHTML < -1) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!FindIntegerAfterString("StartFragment:", aCfhtml, startFragment) ||
+ startFragment < 0) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!FindIntegerAfterString("EndFragment:", aCfhtml, endFragment) ||
+ startFragment < 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // The StartHTML and EndHTML markers are allowed to be -1 to include
+ // everything.
+ // See Reference: MSDN doc entitled "HTML Clipboard Format"
+ // http://msdn.microsoft.com/en-us/library/aa767917(VS.85).aspx#unknown_854
+ if (startHTML == -1) {
+ startHTML = aCfhtml.Find("<!--StartFragment-->");
+ if (startHTML == -1) {
+ return NS_OK;
+ }
+ }
+ if (endHTML == -1) {
+ const char endFragmentMarker[] = "<!--EndFragment-->";
+ endHTML = aCfhtml.Find(endFragmentMarker);
+ if (endHTML == -1) {
+ return NS_OK;
+ }
+ endHTML += ArrayLength(endFragmentMarker) - 1;
+ }
+
+ // create context string
+ nsAutoCString contextUTF8(
+ Substring(aCfhtml, startHTML, startFragment - startHTML) +
+ "<!--" kInsertCookie "-->"_ns +
+ Substring(aCfhtml, endFragment, endHTML - endFragment));
+
+ // validate startFragment
+ // make sure it's not in the middle of a HTML tag
+ // see bug #228879 for more details
+ int32_t curPos = startFragment;
+ while (curPos > startHTML) {
+ if (aCfhtml[curPos] == '>') {
+ // working backwards, the first thing we see is the end of a tag
+ // so StartFragment is good, so do nothing.
+ break;
+ }
+ if (aCfhtml[curPos] == '<') {
+ // if we are at the start, then we want to see the '<'
+ if (curPos != startFragment) {
+ // working backwards, the first thing we see is the start of a tag
+ // so StartFragment is bad, so we need to update it.
+ NS_ERROR(
+ "StartFragment byte count in the clipboard looks bad, see bug "
+ "#228879");
+ startFragment = curPos - 1;
+ }
+ break;
+ }
+ curPos--;
+ }
+
+ // create fragment string
+ nsAutoCString fragmentUTF8(
+ Substring(aCfhtml, startFragment, endFragment - startFragment));
+
+ // remove the StartFragment/EndFragment comments from the fragment, if present
+ RemoveFragComments(fragmentUTF8);
+
+ // remove the StartFragment/EndFragment comments from the context, if present
+ RemoveFragComments(contextUTF8);
+
+ // convert both strings to usc2
+ const nsString& fragUcs2Str = NS_ConvertUTF8toUTF16(fragmentUTF8);
+ const nsString& cntxtUcs2Str = NS_ConvertUTF8toUTF16(contextUTF8);
+
+ // translate platform linebreaks for fragment
+ int32_t oldLengthInChars =
+ fragUcs2Str.Length() + 1; // +1 to include null terminator
+ int32_t newLengthInChars = 0;
+ *aStuffToPaste = nsLinebreakConverter::ConvertUnicharLineBreaks(
+ fragUcs2Str.get(), nsLinebreakConverter::eLinebreakAny,
+ nsLinebreakConverter::eLinebreakContent, oldLengthInChars,
+ &newLengthInChars);
+ if (!*aStuffToPaste) {
+ NS_WARNING("nsLinebreakConverter::ConvertUnicharLineBreaks() failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ // translate platform linebreaks for context
+ oldLengthInChars =
+ cntxtUcs2Str.Length() + 1; // +1 to include null terminator
+ newLengthInChars = 0;
+ *aCfcontext = nsLinebreakConverter::ConvertUnicharLineBreaks(
+ cntxtUcs2Str.get(), nsLinebreakConverter::eLinebreakAny,
+ nsLinebreakConverter::eLinebreakContent, oldLengthInChars,
+ &newLengthInChars);
+ // it's ok for context to be empty. frag might be whole doc and contain all
+ // its context.
+
+ // we're done!
+ return NS_OK;
+}
+
+static nsresult ImgFromData(const nsACString& aType, const nsACString& aData,
+ nsString& aOutput) {
+ aOutput.AssignLiteral("<IMG src=\"data:");
+ AppendUTF8toUTF16(aType, aOutput);
+ aOutput.AppendLiteral(";base64,");
+ nsresult rv = Base64EncodeAppend(aData, aOutput);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Base64Encode() failed");
+ return rv;
+ }
+ aOutput.AppendLiteral("\" alt=\"\" >");
+ return NS_OK;
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLEditor::BlobReader)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(HTMLEditor::BlobReader)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mBlob)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mHTMLEditor)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPointToInsert)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(HTMLEditor::BlobReader)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBlob)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHTMLEditor)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPointToInsert)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+HTMLEditor::BlobReader::BlobReader(BlobImpl* aBlob, HTMLEditor* aHTMLEditor,
+ SafeToInsertData aSafeToInsertData,
+ const EditorDOMPoint& aPointToInsert,
+ DeleteSelectedContent aDeleteSelectedContent)
+ : mBlob(aBlob),
+ mHTMLEditor(aHTMLEditor),
+ // "beforeinput" event should've been dispatched before we read blob,
+ // but anyway, we need to clone dataTransfer for "input" event.
+ mDataTransfer(mHTMLEditor->GetInputEventDataTransfer()),
+ mPointToInsert(aPointToInsert),
+ mEditAction(aHTMLEditor->GetEditAction()),
+ mSafeToInsertData(aSafeToInsertData),
+ mDeleteSelectedContent(aDeleteSelectedContent),
+ mNeedsToDispatchBeforeInputEvent(
+ !mHTMLEditor->HasTriedToDispatchBeforeInputEvent()) {
+ MOZ_ASSERT(mBlob);
+ MOZ_ASSERT(mHTMLEditor);
+ MOZ_ASSERT(mHTMLEditor->IsEditActionDataAvailable());
+ MOZ_ASSERT(mDataTransfer);
+
+ // Take only offset here since it's our traditional behavior.
+ if (mPointToInsert.IsSet()) {
+ AutoEditorDOMPointChildInvalidator storeOnlyWithOffset(mPointToInsert);
+ }
+}
+
+nsresult HTMLEditor::BlobReader::OnResult(const nsACString& aResult) {
+ AutoEditActionDataSetter editActionData(*mHTMLEditor, mEditAction);
+ editActionData.InitializeDataTransfer(mDataTransfer);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_FAILURE);
+ }
+
+ if (NS_WARN_IF(mNeedsToDispatchBeforeInputEvent)) {
+ nsresult rv = editActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ } else {
+ editActionData.MarkAsBeforeInputHasBeenDispatched();
+ }
+
+ nsString blobType;
+ mBlob->GetType(blobType);
+
+ // TODO: This does not work well.
+ // * If the data is not an image file, this inserts <img> element with odd
+ // data URI (bug 1610220).
+ // * If the data is valid image file data, an <img> file is inserted with
+ // data URI, but it's not loaded (bug 1610219).
+ NS_ConvertUTF16toUTF8 type(blobType);
+ nsAutoString stuffToPaste;
+ nsresult rv = ImgFromData(type, aResult, stuffToPaste);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("ImgFormData() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<HTMLEditor> htmlEditor = std::move(mHTMLEditor);
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *htmlEditor, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ EditorDOMPoint pointToInsert = std::move(mPointToInsert);
+ rv = htmlEditor->InsertHTMLWithContextAsSubAction(
+ stuffToPaste, u""_ns, u""_ns, NS_LITERAL_STRING_FROM_CSTRING(kFileMime),
+ mSafeToInsertData, pointToInsert, mDeleteSelectedContent,
+ InlineStylesAtInsertionPoint::Preserve);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "InlineStylesAtInsertionPoint::Preserve) failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::BlobReader::OnError(const nsAString& aError) {
+ AutoTArray<nsString, 1> error;
+ error.AppendElement(aError);
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::warningFlag, "Editor"_ns, mHTMLEditor->GetDocument(),
+ nsContentUtils::eDOM_PROPERTIES, "EditorFileDropFailed", error);
+ return NS_OK;
+}
+
+class SlurpBlobEventListener final : public nsIDOMEventListener {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(SlurpBlobEventListener)
+
+ explicit SlurpBlobEventListener(HTMLEditor::BlobReader* aListener)
+ : mListener(aListener) {}
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(Event* aEvent) override;
+
+ private:
+ ~SlurpBlobEventListener() = default;
+
+ RefPtr<HTMLEditor::BlobReader> mListener;
+};
+
+NS_IMPL_CYCLE_COLLECTION(SlurpBlobEventListener, mListener)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SlurpBlobEventListener)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+ NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(SlurpBlobEventListener)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(SlurpBlobEventListener)
+
+NS_IMETHODIMP SlurpBlobEventListener::HandleEvent(Event* aEvent) {
+ EventTarget* target = aEvent->GetTarget();
+ if (!target || !mListener) {
+ return NS_OK;
+ }
+
+ RefPtr<FileReader> reader = do_QueryObject(target);
+ if (!reader) {
+ return NS_OK;
+ }
+
+ EventMessage message = aEvent->WidgetEventPtr()->mMessage;
+
+ RefPtr<HTMLEditor::BlobReader> listener(mListener);
+ if (message == eLoad) {
+ MOZ_ASSERT(reader->DataFormat() == FileReader::FILE_AS_BINARY);
+
+ // The original data has been converted from Latin1 to UTF-16, this just
+ // undoes that conversion.
+ DebugOnly<nsresult> rvIgnored =
+ listener->OnResult(NS_LossyConvertUTF16toASCII(reader->Result()));
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::BlobReader::OnResult() failed, but ignored");
+ return NS_OK;
+ }
+
+ if (message == eLoadError) {
+ nsAutoString errorMessage;
+ reader->GetError()->GetErrorMessage(errorMessage);
+ DebugOnly<nsresult> rvIgnored = listener->OnError(errorMessage);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::BlobReader::OnError() failed, but ignored");
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+// static
+nsresult HTMLEditor::SlurpBlob(Blob* aBlob, nsIGlobalObject* aGlobal,
+ BlobReader* aBlobReader) {
+ MOZ_ASSERT(aBlob);
+ MOZ_ASSERT(aGlobal);
+ MOZ_ASSERT(aBlobReader);
+
+ RefPtr<WeakWorkerRef> workerRef;
+ RefPtr<FileReader> reader = new FileReader(aGlobal, workerRef);
+
+ RefPtr<SlurpBlobEventListener> eventListener =
+ new SlurpBlobEventListener(aBlobReader);
+
+ nsresult rv = reader->AddEventListener(u"load"_ns, eventListener, false);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("FileReader::AddEventListener(load) failed");
+ return rv;
+ }
+
+ rv = reader->AddEventListener(u"error"_ns, eventListener, false);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("FileReader::AddEventListener(error) failed");
+ return rv;
+ }
+
+ ErrorResult error;
+ reader->ReadAsBinaryString(*aBlob, error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "FileReader::ReadAsBinaryString() failed");
+ return error.StealNSResult();
+}
+
+nsresult HTMLEditor::InsertObject(
+ const nsACString& aType, nsISupports* aObject,
+ SafeToInsertData aSafeToInsertData, const EditorDOMPoint& aPointToInsert,
+ DeleteSelectedContent aDeleteSelectedContent) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Check to see if we the file is actually an image.
+ nsAutoCString type(aType);
+ if (type.EqualsLiteral(kFileMime)) {
+ if (nsCOMPtr<nsIFile> file = do_QueryInterface(aObject)) {
+ nsCOMPtr<nsIMIMEService> mime = do_GetService("@mozilla.org/mime;1");
+ if (NS_WARN_IF(!mime)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = mime->GetTypeFromFile(file, type);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsIMIMEService::GetTypeFromFile() failed");
+ return rv;
+ }
+ }
+ }
+
+ nsCOMPtr<nsISupports> object = aObject;
+ if (type.EqualsLiteral(kJPEGImageMime) || type.EqualsLiteral(kJPGImageMime) ||
+ type.EqualsLiteral(kPNGImageMime) || type.EqualsLiteral(kGIFImageMime)) {
+ if (nsCOMPtr<nsIFile> file = do_QueryInterface(object)) {
+ object = new FileBlobImpl(file);
+ // Fallthrough to BlobImpl code below.
+ } else if (RefPtr<Blob> blob = do_QueryObject(object)) {
+ object = blob->Impl();
+ // Fallthrough.
+ } else if (nsCOMPtr<nsIInputStream> imageStream =
+ do_QueryInterface(object)) {
+ nsCString imageData;
+ nsresult rv = NS_ConsumeStream(imageStream, UINT32_MAX, imageData);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("NS_ConsumeStream() failed");
+ return rv;
+ }
+
+ rv = imageStream->Close();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsIInputStream::Close() failed");
+ return rv;
+ }
+
+ nsAutoString stuffToPaste;
+ rv = ImgFromData(type, imageData, stuffToPaste);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("ImgFromData() failed");
+ return rv;
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertHTMLWithContextAsSubAction(
+ stuffToPaste, u""_ns, u""_ns,
+ NS_LITERAL_STRING_FROM_CSTRING(kFileMime), aSafeToInsertData,
+ aPointToInsert, aDeleteSelectedContent,
+ InlineStylesAtInsertionPoint::Preserve);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "InlineStylesAtInsertionPoint::Preserve) failed, but ignored");
+ return NS_OK;
+ } else {
+ NS_WARNING("HTMLEditor::InsertObject: Unexpected type for image mime");
+ return NS_OK;
+ }
+ }
+
+ // We always try to insert BlobImpl even without a known image mime.
+ nsCOMPtr<BlobImpl> blob = do_QueryInterface(object);
+ if (!blob) {
+ return NS_OK;
+ }
+
+ RefPtr<BlobReader> br = new BlobReader(
+ blob, this, aSafeToInsertData, aPointToInsert, aDeleteSelectedContent);
+
+ nsCOMPtr<nsPIDOMWindowInner> inner = GetInnerWindow();
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(inner);
+ if (!global) {
+ NS_WARNING("Could not get global");
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<Blob> domBlob = Blob::Create(global, blob);
+ if (!domBlob) {
+ NS_WARNING("Blob::Create() failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = SlurpBlob(domBlob, global, br);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::::SlurpBlob() failed");
+ return rv;
+}
+
+static bool GetString(nsISupports* aData, nsAString& aText) {
+ if (nsCOMPtr<nsISupportsString> str = do_QueryInterface(aData)) {
+ DebugOnly<nsresult> rvIgnored = str->GetData(aText);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsISupportsString::GetData() failed, but ignored");
+ return !aText.IsEmpty();
+ }
+
+ return false;
+}
+
+static bool GetCString(nsISupports* aData, nsACString& aText) {
+ if (nsCOMPtr<nsISupportsCString> str = do_QueryInterface(aData)) {
+ DebugOnly<nsresult> rvIgnored = str->GetData(aText);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsISupportsString::GetData() failed, but ignored");
+ return !aText.IsEmpty();
+ }
+
+ return false;
+}
+
+nsresult HTMLEditor::InsertFromTransferableAtSelection(
+ nsITransferable* aTransferable, const nsAString& aContextStr,
+ const nsAString& aInfoStr, HavePrivateHTMLFlavor aHavePrivateHTMLFlavor) {
+ nsAutoCString bestFlavor;
+ nsCOMPtr<nsISupports> genericDataObj;
+
+ // See `HTMLTransferablePreparer::AddDataFlavorsInBestOrder`.
+ nsresult rv = aTransferable->GetAnyTransferData(
+ bestFlavor, getter_AddRefs(genericDataObj));
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "nsITransferable::GetAnyTransferData() failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+ nsAutoString flavor;
+ CopyASCIItoUTF16(bestFlavor, flavor);
+ const SafeToInsertData safeToInsertData = IsSafeToInsertData(nullptr);
+
+ if (bestFlavor.EqualsLiteral(kFileMime) ||
+ bestFlavor.EqualsLiteral(kJPEGImageMime) ||
+ bestFlavor.EqualsLiteral(kJPGImageMime) ||
+ bestFlavor.EqualsLiteral(kPNGImageMime) ||
+ bestFlavor.EqualsLiteral(kGIFImageMime)) {
+ nsresult rv = InsertObject(bestFlavor, genericDataObj, safeToInsertData,
+ EditorDOMPoint(), DeleteSelectedContent::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::InsertObject() failed");
+ return rv;
+ }
+ } else if (bestFlavor.EqualsLiteral(kNativeHTMLMime)) {
+ // note cf_html uses utf8
+ nsAutoCString cfhtml;
+ if (GetCString(genericDataObj, cfhtml)) {
+ // cfselection left emtpy for now.
+ nsString cfcontext, cffragment, cfselection;
+ nsresult rv = ParseCFHTML(cfhtml, getter_Copies(cffragment),
+ getter_Copies(cfcontext));
+ if (NS_SUCCEEDED(rv) && !cffragment.IsEmpty()) {
+ // If we have our private HTML flavor, we will only use the fragment
+ // from the CF_HTML. The rest comes from the clipboard.
+ if (aHavePrivateHTMLFlavor == HavePrivateHTMLFlavor::Yes) {
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertHTMLWithContextAsSubAction(
+ cffragment, aContextStr, aInfoStr, flavor, safeToInsertData,
+ EditorDOMPoint(), DeleteSelectedContent::Yes,
+ InlineStylesAtInsertionPoint::Clear);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "DeleteSelectedContent::Yes, "
+ "InlineStylesAtInsertionPoint::Clear) failed");
+ return rv;
+ }
+ } else {
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertHTMLWithContextAsSubAction(
+ cffragment, cfcontext, cfselection, flavor, safeToInsertData,
+ EditorDOMPoint(), DeleteSelectedContent::Yes,
+ InlineStylesAtInsertionPoint::Clear);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "DeleteSelectedContent::Yes, "
+ "InlineStylesAtInsertionPoint::Clear) failed");
+ return rv;
+ }
+ }
+ } else {
+ // In some platforms (like Linux), the clipboard might return data
+ // requested for unknown flavors (for example:
+ // application/x-moz-nativehtml). In this case, treat the data
+ // to be pasted as mere HTML to get the best chance of pasting it
+ // correctly.
+ bestFlavor.AssignLiteral(kHTMLMime);
+ // Fall through the next case
+ }
+ }
+ }
+ if (bestFlavor.EqualsLiteral(kHTMLMime) ||
+ bestFlavor.EqualsLiteral(kTextMime) ||
+ bestFlavor.EqualsLiteral(kMozTextInternal)) {
+ nsAutoString stuffToPaste;
+ if (!GetString(genericDataObj, stuffToPaste)) {
+ nsAutoCString text;
+ if (GetCString(genericDataObj, text)) {
+ CopyUTF8toUTF16(text, stuffToPaste);
+ }
+ }
+
+ if (!stuffToPaste.IsEmpty()) {
+ if (bestFlavor.EqualsLiteral(kHTMLMime)) {
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsresult rv = InsertHTMLWithContextAsSubAction(
+ stuffToPaste, aContextStr, aInfoStr, flavor, safeToInsertData,
+ EditorDOMPoint(), DeleteSelectedContent::Yes,
+ InlineStylesAtInsertionPoint::Clear);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "DeleteSelectedContent::Yes, "
+ "InlineStylesAtInsertionPoint::Clear) failed");
+ return rv;
+ }
+ } else {
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsresult rv =
+ InsertTextAsSubAction(stuffToPaste, SelectionHandling::Delete);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::InsertTextAsSubAction() failed");
+ return rv;
+ }
+ }
+ }
+ }
+ }
+
+ // Try to scroll the selection into view if the paste succeeded
+ DebugOnly<nsresult> rvIgnored = ScrollSelectionFocusIntoView();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::ScrollSelectionFocusIntoView() failed, but ignored");
+ return NS_OK;
+}
+
+static void GetStringFromDataTransfer(const DataTransfer* aDataTransfer,
+ const nsAString& aType, uint32_t aIndex,
+ nsString& aOutputString) {
+ nsCOMPtr<nsIVariant> variant;
+ DebugOnly<nsresult> rvIgnored = aDataTransfer->GetDataAtNoSecurityCheck(
+ aType, aIndex, getter_AddRefs(variant));
+ if (!variant) {
+ MOZ_ASSERT(aOutputString.IsEmpty());
+ return;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "DataTransfer::GetDataAtNoSecurityCheck() failed, but ignored");
+ variant->GetAsAString(aOutputString);
+ nsContentUtils::PlatformToDOMLineBreaks(aOutputString);
+}
+
+nsresult HTMLEditor::InsertFromDataTransfer(
+ const DataTransfer* aDataTransfer, uint32_t aIndex,
+ nsIPrincipal* aSourcePrincipal, const EditorDOMPoint& aDroppedAt,
+ DeleteSelectedContent aDeleteSelectedContent) {
+ MOZ_ASSERT(GetEditAction() == EditAction::eDrop ||
+ GetEditAction() == EditAction::ePaste);
+ MOZ_ASSERT(mPlaceholderBatch,
+ "HTMLEditor::InsertFromDataTransfer() should be called by "
+ "HandleDropEvent() or paste action and there should've already "
+ "been placeholder transaction");
+ MOZ_ASSERT_IF(GetEditAction() == EditAction::eDrop, aDroppedAt.IsSet());
+
+ ErrorResult error;
+ RefPtr<DOMStringList> types = aDataTransfer->MozTypesAt(aIndex, error);
+ if (error.Failed()) {
+ NS_WARNING("DataTransfer::MozTypesAt() failed");
+ return error.StealNSResult();
+ }
+
+ const bool hasPrivateHTMLFlavor =
+ types->Contains(NS_LITERAL_STRING_FROM_CSTRING(kHTMLContext));
+
+ const bool isPlaintextEditor = IsInPlaintextMode();
+ const SafeToInsertData safeToInsertData =
+ IsSafeToInsertData(aSourcePrincipal);
+
+ uint32_t length = types->Length();
+ for (uint32_t i = 0; i < length; i++) {
+ nsAutoString type;
+ types->Item(i, type);
+
+ if (!isPlaintextEditor) {
+ if (type.EqualsLiteral(kFileMime) || type.EqualsLiteral(kJPEGImageMime) ||
+ type.EqualsLiteral(kJPGImageMime) ||
+ type.EqualsLiteral(kPNGImageMime) ||
+ type.EqualsLiteral(kGIFImageMime)) {
+ nsCOMPtr<nsIVariant> variant;
+ DebugOnly<nsresult> rvIgnored = aDataTransfer->GetDataAtNoSecurityCheck(
+ type, aIndex, getter_AddRefs(variant));
+ if (variant) {
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "DataTransfer::GetDataAtNoSecurityCheck() failed, but ignored");
+ nsCOMPtr<nsISupports> object;
+ rvIgnored = variant->GetAsISupports(getter_AddRefs(object));
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIVariant::GetAsISupports() failed, but ignored");
+ nsresult rv = InsertObject(NS_ConvertUTF16toUTF8(type), object,
+ safeToInsertData, aDroppedAt,
+ aDeleteSelectedContent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertObject() failed");
+ return rv;
+ }
+ } else if (type.EqualsLiteral(kNativeHTMLMime)) {
+ // Windows only clipboard parsing.
+ nsAutoString text;
+ GetStringFromDataTransfer(aDataTransfer, type, aIndex, text);
+ NS_ConvertUTF16toUTF8 cfhtml(text);
+
+ nsString cfcontext, cffragment,
+ cfselection; // cfselection left emtpy for now
+
+ nsresult rv = ParseCFHTML(cfhtml, getter_Copies(cffragment),
+ getter_Copies(cfcontext));
+ if (NS_SUCCEEDED(rv) && !cffragment.IsEmpty()) {
+ if (hasPrivateHTMLFlavor) {
+ // If we have our private HTML flavor, we will only use the fragment
+ // from the CF_HTML. The rest comes from the clipboard.
+ nsAutoString contextString, infoString;
+ GetStringFromDataTransfer(
+ aDataTransfer, NS_LITERAL_STRING_FROM_CSTRING(kHTMLContext),
+ aIndex, contextString);
+ GetStringFromDataTransfer(aDataTransfer,
+ NS_LITERAL_STRING_FROM_CSTRING(kHTMLInfo),
+ aIndex, infoString);
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsresult rv = InsertHTMLWithContextAsSubAction(
+ cffragment, contextString, infoString, type, safeToInsertData,
+ aDroppedAt, aDeleteSelectedContent,
+ InlineStylesAtInsertionPoint::Clear);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "InlineStylesAtInsertionPoint::Clear) failed");
+ return rv;
+ }
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsresult rv = InsertHTMLWithContextAsSubAction(
+ cffragment, cfcontext, cfselection, type, safeToInsertData,
+ aDroppedAt, aDeleteSelectedContent,
+ InlineStylesAtInsertionPoint::Clear);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "InlineStylesAtInsertionPoint::Clear) failed");
+ return rv;
+ }
+ } else if (type.EqualsLiteral(kHTMLMime)) {
+ nsAutoString text, contextString, infoString;
+ GetStringFromDataTransfer(aDataTransfer, type, aIndex, text);
+ GetStringFromDataTransfer(aDataTransfer,
+ NS_LITERAL_STRING_FROM_CSTRING(kHTMLContext),
+ aIndex, contextString);
+ GetStringFromDataTransfer(aDataTransfer,
+ NS_LITERAL_STRING_FROM_CSTRING(kHTMLInfo),
+ aIndex, infoString);
+ if (type.EqualsLiteral(kHTMLMime)) {
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsresult rv = InsertHTMLWithContextAsSubAction(
+ text, contextString, infoString, type, safeToInsertData,
+ aDroppedAt, aDeleteSelectedContent,
+ InlineStylesAtInsertionPoint::Clear);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "InlineStylesAtInsertionPoint::Clear) failed");
+ return rv;
+ }
+ }
+ }
+
+ if (type.EqualsLiteral(kTextMime) || type.EqualsLiteral(kMozTextInternal)) {
+ nsAutoString text;
+ GetStringFromDataTransfer(aDataTransfer, type, aIndex, text);
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsresult rv = InsertTextAt(text, aDroppedAt, aDeleteSelectedContent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAt() failed");
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+// static
+HTMLEditor::HavePrivateHTMLFlavor HTMLEditor::ClipboardHasPrivateHTMLFlavor(
+ nsIClipboard* aClipboard) {
+ if (NS_WARN_IF(!aClipboard)) {
+ return HavePrivateHTMLFlavor::No;
+ }
+
+ // check the clipboard for our special kHTMLContext flavor. If that is there,
+ // we know we have our own internal html format on clipboard.
+ bool hasPrivateHTMLFlavor = false;
+ AutoTArray<nsCString, 1> flavArray = {nsDependentCString(kHTMLContext)};
+ nsresult rv = aClipboard->HasDataMatchingFlavors(
+ flavArray, nsIClipboard::kGlobalClipboard, &hasPrivateHTMLFlavor);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsIClipboard::HasDataMatchingFlavors(nsIClipboard::"
+ "kGlobalClipboard) failed");
+ return NS_SUCCEEDED(rv) && hasPrivateHTMLFlavor ? HavePrivateHTMLFlavor::Yes
+ : HavePrivateHTMLFlavor::No;
+}
+
+nsresult HTMLEditor::HandlePaste(AutoEditActionDataSetter& aEditActionData,
+ int32_t aClipboardType) {
+ aEditActionData.InitializeDataTransferWithClipboard(
+ SettingDataTransfer::eWithFormat, aClipboardType);
+ nsresult rv = aEditActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return rv;
+ }
+ rv = PasteInternal(aClipboardType);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::PasteInternal() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::PasteInternal(int32_t aClipboardType) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Get Clipboard Service
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIClipboard> clipboard =
+ do_GetService("@mozilla.org/widget/clipboard;1", &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get nsIClipboard service");
+ return rv;
+ }
+
+ // Get the nsITransferable interface for getting the data from the clipboard
+ nsCOMPtr<nsITransferable> transferable;
+ rv = PrepareHTMLTransferable(getter_AddRefs(transferable));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::PrepareHTMLTransferable() failed");
+ return rv;
+ }
+ if (!transferable) {
+ NS_WARNING("HTMLEditor::PrepareHTMLTransferable() returned nullptr");
+ return NS_ERROR_FAILURE;
+ }
+ // Get the Data from the clipboard
+ rv = clipboard->GetData(transferable, aClipboardType);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsIClipboard::GetData() failed");
+ return rv;
+ }
+
+ // XXX Why don't you check this first?
+ if (!IsModifiable()) {
+ return NS_OK;
+ }
+
+ // also get additional html copy hints, if present
+ nsAutoString contextStr, infoStr;
+
+ // If we have our internal html flavor on the clipboard, there is special
+ // context to use instead of cfhtml context.
+ const HavePrivateHTMLFlavor clipboardHasPrivateHTMLFlavor =
+ ClipboardHasPrivateHTMLFlavor(clipboard);
+ if (clipboardHasPrivateHTMLFlavor == HavePrivateHTMLFlavor::Yes) {
+ nsCOMPtr<nsITransferable> contextTransferable =
+ do_CreateInstance("@mozilla.org/widget/transferable;1");
+ if (!contextTransferable) {
+ NS_WARNING(
+ "do_CreateInstance() failed to create nsITransferable instance");
+ return NS_ERROR_FAILURE;
+ }
+ DebugOnly<nsresult> rvIgnored = contextTransferable->Init(nullptr);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::Init() failed, but ignored");
+ contextTransferable->SetIsPrivateData(transferable->GetIsPrivateData());
+ rvIgnored = contextTransferable->AddDataFlavor(kHTMLContext);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kHTMLContext) failed, but ignored");
+ rvIgnored = clipboard->GetData(contextTransferable, aClipboardType);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsIClipboard::GetData() failed, but ignored");
+ nsCOMPtr<nsISupports> contextDataObj;
+ rv = contextTransferable->GetTransferData(kHTMLContext,
+ getter_AddRefs(contextDataObj));
+ if (NS_SUCCEEDED(rv) && contextDataObj) {
+ if (nsCOMPtr<nsISupportsString> str = do_QueryInterface(contextDataObj)) {
+ DebugOnly<nsresult> rvIgnored = str->GetData(contextStr);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsISupportsString::GetData() failed, but ignored");
+ }
+ }
+
+ nsCOMPtr<nsITransferable> infoTransferable =
+ do_CreateInstance("@mozilla.org/widget/transferable;1");
+ if (!infoTransferable) {
+ NS_WARNING(
+ "do_CreateInstance() failed to create nsITransferable instance");
+ return NS_ERROR_FAILURE;
+ }
+ rvIgnored = infoTransferable->Init(nullptr);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsITransferable::Init() failed, but ignored");
+ contextTransferable->SetIsPrivateData(transferable->GetIsPrivateData());
+ rvIgnored = infoTransferable->AddDataFlavor(kHTMLInfo);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kHTMLInfo) failed, but ignored");
+ clipboard->GetData(infoTransferable, aClipboardType);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsIClipboard::GetData() failed, but ignored");
+ nsCOMPtr<nsISupports> infoDataObj;
+ rv = infoTransferable->GetTransferData(kHTMLInfo,
+ getter_AddRefs(infoDataObj));
+ if (NS_SUCCEEDED(rv) && infoDataObj) {
+ if (nsCOMPtr<nsISupportsString> str = do_QueryInterface(infoDataObj)) {
+ DebugOnly<nsresult> rvIgnored = str->GetData(infoStr);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsISupportsString::GetData() failed, but ignored");
+ }
+ }
+ }
+
+ rv = InsertFromTransferableAtSelection(transferable, contextStr, infoStr,
+ clipboardHasPrivateHTMLFlavor);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertFromTransferableAtSelection() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::HandlePasteTransferable(
+ AutoEditActionDataSetter& aEditActionData, nsITransferable& aTransferable) {
+ // InitializeDataTransfer may fetch input stream in aTransferable, so it
+ // may be invalid after calling this.
+ aEditActionData.InitializeDataTransfer(&aTransferable);
+
+ nsresult rv = aEditActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent(), failed");
+ return rv;
+ }
+
+ RefPtr<DataTransfer> dataTransfer = GetInputEventDataTransfer();
+ if (dataTransfer->HasFile() && dataTransfer->MozItemCount() > 0) {
+ // Now aTransferable has moved to DataTransfer. Use DataTransfer.
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ rv = InsertFromDataTransfer(dataTransfer, 0, nullptr, EditorDOMPoint(),
+ DeleteSelectedContent::Yes);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertFromDataTransfer("
+ "DeleteSelectedContent::Yes) failed");
+ return rv;
+ }
+
+ nsAutoString contextStr, infoStr;
+ rv = InsertFromTransferableAtSelection(&aTransferable, contextStr, infoStr,
+ HavePrivateHTMLFlavor::No);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertFromTransferableAtSelection("
+ "HavePrivateHTMLFlavor::No) failed");
+ return rv;
+}
+
+nsresult HTMLEditor::PasteNoFormattingAsAction(
+ int32_t aClipboardType, DispatchPasteEvent aDispatchPasteEvent,
+ nsIPrincipal* aPrincipal) {
+ if (IsReadonly()) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::ePaste,
+ aPrincipal);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ editActionData.InitializeDataTransferWithClipboard(
+ SettingDataTransfer::eWithoutFormat, aClipboardType);
+
+ if (aDispatchPasteEvent == DispatchPasteEvent::Yes) {
+ RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager();
+ if (NS_WARN_IF(!focusManager)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ const RefPtr<Element> focusedElement = focusManager->GetFocusedElement();
+
+ Result<ClipboardEventResult, nsresult> ret =
+ DispatchClipboardEventAndUpdateClipboard(ePasteNoFormatting,
+ aClipboardType);
+ if (MOZ_UNLIKELY(ret.isErr())) {
+ NS_WARNING(
+ "EditorBase::DispatchClipboardEventAndUpdateClipboard("
+ "ePasteNoFormatting) failed");
+ return EditorBase::ToGenericNSResult(ret.unwrapErr());
+ }
+ switch (ret.inspect()) {
+ case ClipboardEventResult::DoDefault:
+ break;
+ case ClipboardEventResult::DefaultPreventedOfPaste:
+ case ClipboardEventResult::IgnoredOrError:
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ case ClipboardEventResult::CopyOrCutHandled:
+ MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste");
+ }
+
+ // If focus is changed by a "paste" event listener, we should keep handling
+ // the "pasting" in new focused editor because Chrome works as so.
+ const RefPtr<Element> newFocusedElement = focusManager->GetFocusedElement();
+ if (MOZ_UNLIKELY(focusedElement != newFocusedElement)) {
+ // For the privacy reason, let's top handling it if new focused element is
+ // in different document.
+ if (focusManager->GetFocusedWindow() != GetWindow()) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ }
+ RefPtr<EditorBase> editorBase =
+ nsContentUtils::GetActiveEditor(GetPresContext());
+ if (!editorBase || (editorBase->IsHTMLEditor() &&
+ !editorBase->AsHTMLEditor()->IsActiveInDOMWindow())) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ }
+ if (editorBase != this) {
+ if (editorBase->IsHTMLEditor()) {
+ nsresult rv =
+ MOZ_KnownLive(editorBase->AsHTMLEditor())
+ ->PasteNoFormattingAsAction(
+ aClipboardType, DispatchPasteEvent::No, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::PasteNoFormattingAsAction("
+ "DispatchPasteEvent::No) failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ nsresult rv = editorBase->PasteAsAction(
+ aClipboardType, DispatchPasteEvent::No, aPrincipal);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::PasteAsAction(DispatchPasteEvent::No) failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ }
+ }
+
+ // Dispatch "beforeinput" event after "paste" event. And perhaps, before
+ // committing composition because if pasting is canceled, we don't need to
+ // commit the active composition.
+ nsresult rv = editActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ DebugOnly<nsresult> rvIgnored = CommitComposition();
+ if (NS_WARN_IF(Destroyed())) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CommitComposition() failed, but ignored");
+
+ // Get Clipboard Service
+ nsCOMPtr<nsIClipboard> clipboard(
+ do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get nsIClipboard service");
+ return rv;
+ }
+
+ if (!GetDocument()) {
+ NS_WARNING("Editor didn't have document, but ignored");
+ return NS_OK;
+ }
+
+ Result<nsCOMPtr<nsITransferable>, nsresult> maybeTransferable =
+ EditorUtils::CreateTransferableForPlainText(*GetDocument());
+ if (maybeTransferable.isErr()) {
+ NS_WARNING("EditorUtils::CreateTransferableForPlainText() failed");
+ return EditorBase::ToGenericNSResult(maybeTransferable.unwrapErr());
+ }
+ nsCOMPtr<nsITransferable> transferable(maybeTransferable.unwrap());
+ if (!transferable) {
+ NS_WARNING(
+ "EditorUtils::CreateTransferableForPlainText() returned nullptr, but "
+ "ignored");
+ return NS_OK;
+ }
+
+ if (!IsModifiable()) {
+ return NS_OK;
+ }
+
+ // Get the Data from the clipboard
+ rv = clipboard->GetData(transferable, aClipboardType);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsIClipboard::GetData() failed");
+ return rv;
+ }
+
+ rv = InsertFromTransferableAtSelection(transferable, u""_ns, u""_ns,
+ HavePrivateHTMLFlavor::No);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertFromTransferableAtSelection("
+ "HavePrivateHTMLFlavor::No) failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+// The following arrays contain the MIME types that we can paste. The arrays
+// are used by CanPaste() and CanPasteTransferable() below.
+
+static const char* textEditorFlavors[] = {kTextMime};
+static const char* textHtmlEditorFlavors[] = {kTextMime, kHTMLMime,
+ kJPEGImageMime, kJPGImageMime,
+ kPNGImageMime, kGIFImageMime};
+
+bool HTMLEditor::CanPaste(int32_t aClipboardType) const {
+ if (AreClipboardCommandsUnconditionallyEnabled()) {
+ return true;
+ }
+
+ // can't paste if readonly
+ if (!IsModifiable()) {
+ return false;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard(
+ do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get nsIClipboard service");
+ return false;
+ }
+
+ // Use the flavors depending on the current editor mask
+ if (IsInPlaintextMode()) {
+ AutoTArray<nsCString, ArrayLength(textEditorFlavors)> flavors;
+ flavors.AppendElements<const char*>(Span<const char*>(textEditorFlavors));
+ bool haveFlavors;
+ nsresult rv = clipboard->HasDataMatchingFlavors(flavors, aClipboardType,
+ &haveFlavors);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsIClipboard::HasDataMatchingFlavors() failed");
+ return NS_SUCCEEDED(rv) && haveFlavors;
+ }
+
+ AutoTArray<nsCString, ArrayLength(textHtmlEditorFlavors)> flavors;
+ flavors.AppendElements<const char*>(Span<const char*>(textHtmlEditorFlavors));
+ bool haveFlavors;
+ rv = clipboard->HasDataMatchingFlavors(flavors, aClipboardType, &haveFlavors);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsIClipboard::HasDataMatchingFlavors() failed");
+ return NS_SUCCEEDED(rv) && haveFlavors;
+}
+
+bool HTMLEditor::CanPasteTransferable(nsITransferable* aTransferable) {
+ // can't paste if readonly
+ if (!IsModifiable()) {
+ return false;
+ }
+
+ // If |aTransferable| is null, assume that a paste will succeed.
+ if (!aTransferable) {
+ return true;
+ }
+
+ // Peek in |aTransferable| to see if it contains a supported MIME type.
+
+ // Use the flavors depending on the current editor mask
+ const char** flavors;
+ size_t length;
+ if (IsInPlaintextMode()) {
+ flavors = textEditorFlavors;
+ length = ArrayLength(textEditorFlavors);
+ } else {
+ flavors = textHtmlEditorFlavors;
+ length = ArrayLength(textHtmlEditorFlavors);
+ }
+
+ for (size_t i = 0; i < length; i++, flavors++) {
+ nsCOMPtr<nsISupports> data;
+ nsresult rv =
+ aTransferable->GetTransferData(*flavors, getter_AddRefs(data));
+ if (NS_SUCCEEDED(rv) && data) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+nsresult HTMLEditor::HandlePasteAsQuotation(
+ AutoEditActionDataSetter& aEditActionData, int32_t aClipboardType) {
+ MOZ_ASSERT(aClipboardType == nsIClipboard::kGlobalClipboard ||
+ aClipboardType == nsIClipboard::kSelectionClipboard);
+ aEditActionData.InitializeDataTransferWithClipboard(
+ SettingDataTransfer::eWithFormat, aClipboardType);
+ if (NS_WARN_IF(!aEditActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv = aEditActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent(), failed");
+ return rv;
+ }
+
+ if (IsInPlaintextMode()) {
+ nsresult rv = PasteAsPlaintextQuotation(aClipboardType);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::PasteAsPlaintextQuotation() failed");
+ return rv;
+ }
+
+ // If it's not in plain text edit mode, paste text into new
+ // <blockquote type="cite"> element after removing selection.
+
+ {
+ // XXX Why don't we test these first?
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ UndefineCaretBidiLevel();
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertQuotation, 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 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");
+ }
+ }
+
+ // Remove Selection and create `<blockquote type="cite">` now.
+ // XXX Why don't we insert the `<blockquote>` into the DOM tree after
+ // pasting the content in clipboard into it?
+ Result<RefPtr<Element>, nsresult> blockquoteElementOrError =
+ DeleteSelectionAndCreateElement(
+ *nsGkAtoms::blockquote,
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ [](HTMLEditor&, Element& aBlockquoteElement, const EditorDOMPoint&)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ DebugOnly<nsresult> rvIgnored = aBlockquoteElement.SetAttr(
+ kNameSpaceID_None, nsGkAtoms::type, u"cite"_ns,
+ aBlockquoteElement.IsInComposedDoc());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ nsPrintfCString(
+ "Element::SetAttr(nsGkAtoms::type, \"cite\", %s) "
+ "failed, but ignored",
+ aBlockquoteElement.IsInComposedDoc() ? "true" : "false")
+ .get());
+ return NS_OK;
+ });
+ if (MOZ_UNLIKELY(blockquoteElementOrError.isErr()) ||
+ NS_WARN_IF(Destroyed())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteSelectionAndCreateElement(nsGkAtoms::blockquote) "
+ "failed");
+ return Destroyed() ? NS_ERROR_EDITOR_DESTROYED
+ : blockquoteElementOrError.unwrapErr();
+ }
+ MOZ_ASSERT(blockquoteElementOrError.inspect());
+
+ // Collapse Selection in the new `<blockquote>` element.
+ rv = CollapseSelectionToStartOf(
+ MOZ_KnownLive(*blockquoteElementOrError.inspect()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionToStartOf() failed");
+ return rv;
+ }
+
+ rv = PasteInternal(aClipboardType);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::PasteInternal() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::PasteAsPlaintextQuotation(int32_t aSelectionType) {
+ // Get Clipboard Service
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard =
+ do_GetService("@mozilla.org/widget/clipboard;1", &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get nsIClipboard service");
+ return rv;
+ }
+
+ // Create generic Transferable for getting the data
+ nsCOMPtr<nsITransferable> transferable =
+ do_CreateInstance("@mozilla.org/widget/transferable;1", &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("do_CreateInstance() failed to create nsITransferable instance");
+ return rv;
+ }
+ if (!transferable) {
+ NS_WARNING("do_CreateInstance() returned nullptr");
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<Document> destdoc = GetDocument();
+ nsILoadContext* loadContext = destdoc ? destdoc->GetLoadContext() : nullptr;
+ DebugOnly<nsresult> rvIgnored = transferable->Init(loadContext);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::Init() failed, but ignored");
+
+ // We only handle plaintext pastes here
+ rvIgnored = transferable->AddDataFlavor(kTextMime);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kTextMime) failed, but ignored");
+
+ // Get the Data from the clipboard
+ rvIgnored = clipboard->GetData(transferable, aSelectionType);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsIClipboard::GetData() failed, but ignored");
+
+ // Now we ask the transferable for the data
+ // it still owns the data, we just have a pointer to it.
+ // If it can't support a "text" output of the data the call will fail
+ nsCOMPtr<nsISupports> genericDataObj;
+ nsAutoCString flavor;
+ rv = transferable->GetAnyTransferData(flavor, getter_AddRefs(genericDataObj));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsITransferable::GetAnyTransferData() failed");
+ return rv;
+ }
+
+ if (!flavor.EqualsLiteral(kTextMime)) {
+ return NS_OK;
+ }
+
+ nsAutoString stuffToPaste;
+ if (!GetString(genericDataObj, stuffToPaste)) {
+ return NS_OK;
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertAsPlaintextQuotation(stuffToPaste, true, 0);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertAsPlaintextQuotation() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::InsertWithQuotationsAsSubAction(
+ const nsAString& aQuotedText) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ UndefineCaretBidiLevel();
+
+ // Let the citer quote it for us:
+ nsString quotedStuff;
+ InternetCiter::GetCiteString(aQuotedText, quotedStuff);
+
+ // It's best to put a blank line after the quoted text so that mails
+ // written without thinking won't be so ugly.
+ if (!aQuotedText.IsEmpty() &&
+ (aQuotedText.Last() != HTMLEditUtils::kNewLine)) {
+ quotedStuff.Append(HTMLEditUtils::kNewLine);
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertText, 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");
+
+ 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");
+ }
+ }
+
+ rv = InsertTextAsSubAction(quotedStuff, SelectionHandling::Delete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsSubAction() failed");
+ return rv;
+}
+
+NS_IMETHODIMP HTMLEditor::InsertTextWithQuotations(
+ const nsAString& aStringToInsert) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertText);
+ MOZ_ASSERT(!aStringToInsert.IsVoid());
+ editActionData.SetData(aStringToInsert);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ if (aStringToInsert.IsEmpty()) {
+ return NS_OK;
+ }
+
+ // The whole operation should be undoable in one transaction:
+ // XXX Why isn't enough to use only AutoPlaceholderBatch here?
+ AutoTransactionBatch bundleAllTransactions(*this, __FUNCTION__);
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ rv = InsertTextWithQuotationsInternal(aStringToInsert);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertTextWithQuotationsInternal() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::InsertTextWithQuotationsInternal(
+ const nsAString& aStringToInsert) {
+ MOZ_ASSERT(!aStringToInsert.IsEmpty());
+ // We're going to loop over the string, collecting up a "hunk"
+ // that's all the same type (quoted or not),
+ // Whenever the quotedness changes (or we reach the string's end)
+ // we will insert the hunk all at once, quoted or non.
+ static const char16_t cite('>');
+ bool curHunkIsQuoted = (aStringToInsert.First() == cite);
+
+ nsAString::const_iterator hunkStart, strEnd;
+ aStringToInsert.BeginReading(hunkStart);
+ aStringToInsert.EndReading(strEnd);
+
+ // In the loop below, we only look for DOM newlines (\n),
+ // because we don't have a FindChars method that can look
+ // for both \r and \n. \r is illegal in the dom anyway,
+ // but in debug builds, let's take the time to verify that
+ // there aren't any there:
+#ifdef DEBUG
+ nsAString::const_iterator dbgStart(hunkStart);
+ if (FindCharInReadable(HTMLEditUtils::kCarriageReturn, dbgStart, strEnd)) {
+ NS_ASSERTION(
+ false,
+ "Return characters in DOM! InsertTextWithQuotations may be wrong");
+ }
+#endif /* DEBUG */
+
+ // Loop over lines:
+ nsresult rv = NS_OK;
+ nsAString::const_iterator lineStart(hunkStart);
+ // We will break from inside when we run out of newlines.
+ for (;;) {
+ // Search for the end of this line (dom newlines, see above):
+ bool found = FindCharInReadable(HTMLEditUtils::kNewLine, lineStart, strEnd);
+ bool quoted = false;
+ if (found) {
+ // if there's another newline, lineStart now points there.
+ // Loop over any consecutive newline chars:
+ nsAString::const_iterator firstNewline(lineStart);
+ while (*lineStart == HTMLEditUtils::kNewLine) {
+ ++lineStart;
+ }
+ quoted = (*lineStart == cite);
+ if (quoted == curHunkIsQuoted) {
+ continue;
+ }
+ // else we're changing state, so we need to insert
+ // from curHunk to lineStart then loop around.
+
+ // But if the current hunk is quoted, then we want to make sure
+ // that any extra newlines on the end do not get included in
+ // the quoted section: blank lines flaking a quoted section
+ // should be considered unquoted, so that if the user clicks
+ // there and starts typing, the new text will be outside of
+ // the quoted block.
+ if (curHunkIsQuoted) {
+ lineStart = firstNewline;
+
+ // 'firstNewline' points to the first '\n'. We want to
+ // ensure that this first newline goes into the hunk
+ // since quoted hunks can be displayed as blocks
+ // (and the newline should become invisible in this case).
+ // So the next line needs to start at the next character.
+ lineStart++;
+ }
+ }
+
+ // If no newline found, lineStart is now strEnd and we can finish up,
+ // inserting from curHunk to lineStart then returning.
+ const nsAString& curHunk = Substring(hunkStart, lineStart);
+ nsCOMPtr<nsINode> dummyNode;
+ if (curHunkIsQuoted) {
+ rv =
+ InsertAsPlaintextQuotation(curHunk, false, getter_AddRefs(dummyNode));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertAsPlaintextQuotation() failed, "
+ "but might be ignored");
+ } else {
+ rv = InsertTextAsSubAction(curHunk, SelectionHandling::Delete);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsSubAction() failed, but might be ignored");
+ }
+ if (!found) {
+ break;
+ }
+ curHunkIsQuoted = quoted;
+ hunkStart = lineStart;
+ }
+
+ // XXX This returns the last result of InsertAsPlaintextQuotation() or
+ // InsertTextAsSubAction() in the loop. This must be a bug.
+ return rv;
+}
+
+nsresult HTMLEditor::InsertAsQuotation(const nsAString& aQuotedText,
+ nsINode** aNodeInserted) {
+ if (IsInPlaintextMode()) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertText);
+ MOZ_ASSERT(!aQuotedText.IsVoid());
+ editActionData.SetData(aQuotedText);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(
+ rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertAsPlaintextQuotation(aQuotedText, true, aNodeInserted);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertAsPlaintextQuotation() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eInsertBlockquoteElement);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsAutoString citation;
+ rv = InsertAsCitedQuotationInternal(aQuotedText, citation, false,
+ aNodeInserted);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertAsCitedQuotationInternal() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+// Insert plaintext as a quotation, with cite marks (e.g. "> ").
+// This differs from its corresponding method in TextEditor
+// in that here, quoted material is enclosed in a <pre> tag
+// in order to preserve the original line wrapping.
+nsresult HTMLEditor::InsertAsPlaintextQuotation(const nsAString& aQuotedText,
+ bool aAddCites,
+ nsINode** aNodeInserted) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (aNodeInserted) {
+ *aNodeInserted = nullptr;
+ }
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ UndefineCaretBidiLevel();
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertQuotation, 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_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");
+ }
+ }
+
+ // Wrap the inserted quote in a <span> so we can distinguish it. If we're
+ // inserting into the <body>, we use a <span> which is displayed as a block
+ // and sized to the screen using 98 viewport width units.
+ // We could use 100vw, but 98vw avoids a horizontal scroll bar where possible.
+ // All this is done to wrap overlong lines to the screen and not to the
+ // container element, the width-restricted body.
+ Result<RefPtr<Element>, nsresult> spanElementOrError =
+ DeleteSelectionAndCreateElement(
+ *nsGkAtoms::span, [](HTMLEditor&, Element& aSpanElement,
+ const EditorDOMPoint& aPointToInsert) {
+ // Add an attribute on the pre node so we'll know it's a quotation.
+ DebugOnly<nsresult> rvIgnored = aSpanElement.SetAttr(
+ kNameSpaceID_None, nsGkAtoms::mozquote, u"true"_ns,
+ aSpanElement.IsInComposedDoc());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ nsPrintfCString(
+ "Element::SetAttr(nsGkAtoms::mozquote, \"true\", %s) "
+ "failed",
+ aSpanElement.IsInComposedDoc() ? "true" : "false")
+ .get());
+ // Allow wrapping on spans so long lines get wrapped to the screen.
+ if (aPointToInsert.IsContainerHTMLElement(nsGkAtoms::body)) {
+ DebugOnly<nsresult> rvIgnored = aSpanElement.SetAttr(
+ kNameSpaceID_None, nsGkAtoms::style,
+ nsLiteralString(u"white-space: pre-wrap; display: block; "
+ u"width: 98vw;"),
+ false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr(nsGkAtoms::style, \"pre-wrap, block\", "
+ "false) failed, but ignored");
+ } else {
+ DebugOnly<nsresult> rvIgnored =
+ aSpanElement.SetAttr(kNameSpaceID_None, nsGkAtoms::style,
+ u"white-space: pre-wrap;"_ns, false);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr(nsGkAtoms::style, "
+ "\"pre-wrap\", false) failed, but ignored");
+ }
+ return NS_OK;
+ });
+ NS_WARNING_ASSERTION(spanElementOrError.isOk(),
+ "HTMLEditor::DeleteSelectionAndCreateElement(nsGkAtoms::"
+ "span) failed, but ignored");
+
+ // If this succeeded, then set selection inside the pre
+ // so the inserted text will end up there.
+ // If it failed, we don't care what the return value was,
+ // but we'll fall through and try to insert the text anyway.
+ if (spanElementOrError.isOk()) {
+ MOZ_ASSERT(spanElementOrError.inspect());
+ rv = CollapseSelectionToStartOf(
+ MOZ_KnownLive(*spanElementOrError.inspect()));
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionToStartOf() caused destroying the "
+ "editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed, but ignored");
+ }
+
+ // TODO: We should insert text at specific point rather than at selection.
+ // Then, we can do this before inserting the <span> element.
+ if (aAddCites) {
+ rv = InsertWithQuotationsAsSubAction(aQuotedText);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::InsertWithQuotationsAsSubAction() failed");
+ return rv;
+ }
+ } else {
+ rv = InsertTextAsSubAction(aQuotedText, SelectionHandling::Delete);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::InsertTextAsSubAction() failed");
+ return rv;
+ }
+ }
+
+ // XXX Why don't we check this before inserting the quoted text?
+ if (spanElementOrError.isErr()) {
+ return NS_OK;
+ }
+
+ // Set the selection to just after the inserted node:
+ EditorRawDOMPoint afterNewSpanElement(
+ EditorRawDOMPoint::After(*spanElementOrError.inspect()));
+ NS_WARNING_ASSERTION(
+ afterNewSpanElement.IsSet(),
+ "Failed to set after the new <span> element, but ignored");
+ if (afterNewSpanElement.IsSet()) {
+ nsresult rv = CollapseSelectionTo(afterNewSpanElement);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+
+ // Note that if !aAddCites, aNodeInserted isn't set.
+ // That's okay because the routines that use aAddCites
+ // don't need to know the inserted node.
+ if (aNodeInserted) {
+ spanElementOrError.unwrap().forget(aNodeInserted);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::Rewrap(bool aRespectNewlines) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eRewrap);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // Rewrap makes no sense if there's no wrap column; default to 72.
+ int32_t wrapWidth = WrapWidth();
+ if (wrapWidth <= 0) {
+ wrapWidth = 72;
+ }
+
+ nsAutoString current;
+ const bool isCollapsed = SelectionRef().IsCollapsed();
+ uint32_t flags = nsIDocumentEncoder::OutputFormatted |
+ nsIDocumentEncoder::OutputLFLineBreak;
+ if (!isCollapsed) {
+ flags |= nsIDocumentEncoder::OutputSelectionOnly;
+ }
+ rv = ComputeValueInternal(u"text/plain"_ns, flags, current);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::ComputeValueInternal(text/plain) failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (current.IsEmpty()) {
+ return NS_OK;
+ }
+
+ nsString wrapped;
+ uint32_t firstLineOffset = 0; // XXX need to reset this if there is a
+ // selection
+ InternetCiter::Rewrap(current, wrapWidth, firstLineOffset, aRespectNewlines,
+ wrapped);
+
+ if (wrapped.IsEmpty()) {
+ return NS_OK;
+ }
+
+ if (isCollapsed) {
+ DebugOnly<nsresult> rvIgnored = SelectAllInternal();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::SelectAllInternal() failed");
+ }
+
+ // The whole operation in InsertTextWithQuotationsInternal() should be
+ // undoable in one transaction.
+ // XXX Why isn't enough to use only AutoPlaceholderBatch here?
+ AutoTransactionBatch bundleAllTransactions(*this, __FUNCTION__);
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertTextWithQuotationsInternal(wrapped);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertTextWithQuotationsInternal() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP HTMLEditor::InsertAsCitedQuotation(const nsAString& aQuotedText,
+ const nsAString& aCitation,
+ bool aInsertHTML,
+ nsINode** aNodeInserted) {
+ // Don't let anyone insert HTML when we're in plaintext mode.
+ if (IsInPlaintextMode()) {
+ NS_ASSERTION(
+ !aInsertHTML,
+ "InsertAsCitedQuotation: trying to insert html into plaintext editor");
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertText);
+ MOZ_ASSERT(!aQuotedText.IsVoid());
+ editActionData.SetData(aQuotedText);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(
+ rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertAsPlaintextQuotation(aQuotedText, true, aNodeInserted);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertAsPlaintextQuotation() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eInsertBlockquoteElement);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertAsCitedQuotationInternal(aQuotedText, aCitation, aInsertHTML,
+ aNodeInserted);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertAsCitedQuotationInternal() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::InsertAsCitedQuotationInternal(
+ const nsAString& aQuotedText, const nsAString& aCitation, bool aInsertHTML,
+ nsINode** aNodeInserted) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!IsInPlaintextMode());
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ UndefineCaretBidiLevel();
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertQuotation, 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_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");
+ }
+ }
+
+ Result<RefPtr<Element>, nsresult> blockquoteElementOrError =
+ DeleteSelectionAndCreateElement(
+ *nsGkAtoms::blockquote,
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ [&aCitation](HTMLEditor&, Element& aBlockquoteElement,
+ const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ // Try to set type=cite. Ignore it if this fails.
+ DebugOnly<nsresult> rvIgnored = aBlockquoteElement.SetAttr(
+ kNameSpaceID_None, nsGkAtoms::type, u"cite"_ns,
+ aBlockquoteElement.IsInComposedDoc());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ nsPrintfCString(
+ "Element::SetAttr(nsGkAtoms::type, \"cite\", %s) failed, "
+ "but ignored",
+ aBlockquoteElement.IsInComposedDoc() ? "true" : "false")
+ .get());
+ if (!aCitation.IsEmpty()) {
+ DebugOnly<nsresult> rvIgnored = aBlockquoteElement.SetAttr(
+ kNameSpaceID_None, nsGkAtoms::cite, aCitation,
+ aBlockquoteElement.IsInComposedDoc());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ nsPrintfCString(
+ "Element::SetAttr(nsGkAtoms::cite, \"...\", %s) failed, "
+ "but ignored",
+ aBlockquoteElement.IsInComposedDoc() ? "true" : "false")
+ .get());
+ }
+ return NS_OK;
+ });
+ if (MOZ_UNLIKELY(blockquoteElementOrError.isErr() ||
+ NS_WARN_IF(Destroyed()))) {
+ NS_WARNING(
+ "HTMLEditor::DeleteSelectionAndCreateElement(nsGkAtoms::blockquote) "
+ "failed");
+ return Destroyed() ? NS_ERROR_EDITOR_DESTROYED
+ : blockquoteElementOrError.unwrapErr();
+ }
+ MOZ_ASSERT(blockquoteElementOrError.inspect());
+
+ // Set the selection inside the blockquote so aQuotedText will go there:
+ rv = CollapseSelectionTo(
+ EditorRawDOMPoint(blockquoteElementOrError.inspect(), 0u));
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+
+ // TODO: We should insert text at specific point rather than at selection.
+ // Then, we can do this before inserting the <blockquote> element.
+ if (aInsertHTML) {
+ rv = LoadHTML(aQuotedText);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::LoadHTML() failed");
+ return rv;
+ }
+ } else {
+ rv = InsertTextAsSubAction(
+ aQuotedText, SelectionHandling::Delete); // XXX ignore charset
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::LoadHTML() failed");
+ return rv;
+ }
+ }
+
+ // Set the selection to just after the inserted node:
+ EditorRawDOMPoint afterNewBlockquoteElement(
+ EditorRawDOMPoint::After(blockquoteElementOrError.inspect()));
+ NS_WARNING_ASSERTION(
+ afterNewBlockquoteElement.IsSet(),
+ "Failed to set after new <blockquote> element, but ignored");
+ if (afterNewBlockquoteElement.IsSet()) {
+ nsresult rv = CollapseSelectionTo(afterNewBlockquoteElement);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+
+ if (aNodeInserted) {
+ blockquoteElementOrError.unwrap().forget(aNodeInserted);
+ }
+
+ return NS_OK;
+}
+
+void HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::
+ RemoveHeadChildAndStealBodyChildsChildren(nsINode& aNode) {
+ nsCOMPtr<nsIContent> body, head;
+ // find the body and head nodes if any.
+ // look only at immediate children of aNode.
+ for (nsCOMPtr<nsIContent> child = aNode.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (child->IsHTMLElement(nsGkAtoms::body)) {
+ body = child;
+ } else if (child->IsHTMLElement(nsGkAtoms::head)) {
+ head = child;
+ }
+ }
+ if (head) {
+ ErrorResult ignored;
+ aNode.RemoveChild(*head, ignored);
+ }
+ if (body) {
+ nsCOMPtr<nsIContent> child = body->GetFirstChild();
+ while (child) {
+ ErrorResult ignored;
+ aNode.InsertBefore(*child, body, ignored);
+ child = body->GetFirstChild();
+ }
+
+ ErrorResult ignored;
+ aNode.RemoveChild(*body, ignored);
+ }
+}
+
+// static
+void HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::
+ RemoveIncompleteDescendantsFromInsertingFragment(nsINode& aNode) {
+ nsIContent* child = aNode.GetFirstChild();
+ while (child) {
+ bool isEmptyNodeShouldNotInserted = false;
+ if (HTMLEditUtils::IsAnyListElement(child)) {
+ // Current limitation of HTMLEditor:
+ // Cannot put caret in a list element which does not have list item
+ // element even as a descendant. I.e., HTMLEditor does not support
+ // editing in such empty list element, and does not support to delete
+ // it from outside. Therefore, HTMLWithContextInserter should not
+ // insert empty list element.
+ isEmptyNodeShouldNotInserted = HTMLEditUtils::IsEmptyNode(
+ *child,
+ {// Although we don't check relation between list item element
+ // and parent list element, but it should not be a problem in the
+ // wild because appearing such invalid list element is an edge case
+ // and anyway HTMLEditor supports editing in them.
+ EmptyCheckOption::TreatListItemAsVisible,
+ // Ignore editable state because non-editable list item element
+ // may make the list element visible. Although HTMLEditor does not
+ // support to edit list elements which have only non-editable list
+ // item elements, but it should be deleted from outside.
+ // TODO: Currently, HTMLEditor does not support deleting such list
+ // element with Backspace. We should fix this.
+ EmptyCheckOption::IgnoreEditableState});
+ }
+ // TODO: Perhaps, we should delete <table>s if they have no <td>/<th>
+ // element, or something other elements which must have specific
+ // children but they don't.
+ if (isEmptyNodeShouldNotInserted) {
+ nsIContent* nextChild = child->GetNextSibling();
+ OwningNonNull<nsIContent> 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<nsINode>& 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<nsIContent> 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<nsINode>* aOutFragNode,
+ nsCOMPtr<nsINode>* aOutStartNode, nsCOMPtr<nsINode>* 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<const Document> 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<nsINode>& aParentNodeOfPastedHTMLInContext,
+ RefPtr<DocumentFragment>& aDocumentFragmentToInsert) const {
+ // if we have context info, create a fragment for that
+ RefPtr<DocumentFragment> 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<nsIContent> parentContentOfPastedHTMLInContext =
+ nsIContent::FromNodeOrNull(aParentNodeOfPastedHTMLInContext);
+ MOZ_ASSERT_IF(aParentNodeOfPastedHTMLInContext,
+ parentContentOfPastedHTMLInContext);
+
+ nsAtom* contextLocalNameAtom =
+ FragmentFromPasteCreator::DetermineContextLocalNameForParsingPastedHTML(
+ parentContentOfPastedHTMLInContext);
+ RefPtr<DocumentFragment> 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<nsINode>* aOutFragNode, nsCOMPtr<nsINode>* aOutStartNode,
+ nsCOMPtr<nsINode>* aOutEndNode, SafeToInsertData aSafeToInsertData) const {
+ MOZ_ASSERT(aOutFragNode);
+ MOZ_ASSERT(aOutStartNode);
+ MOZ_ASSERT(aOutEndNode);
+
+ nsCOMPtr<nsINode> parentNodeOfPastedHTMLInContext;
+ RefPtr<DocumentFragment> 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<nsINode>* aOutStartNode,
+ nsCOMPtr<nsINode>* 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<Document> doc =
+ nsContentUtils::CreateInertHTMLDocument(aTargetDocument);
+ if (!doc) {
+ return NS_ERROR_FAILURE;
+ }
+ RefPtr<DocumentFragment> 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<OwningNonNull<nsIContent>>& aOutArrayOfContents) {
+ MOZ_ASSERT(aStartPoint.IsSetAndValid());
+ MOZ_ASSERT(aEndPoint.IsSetAndValid());
+
+ RefPtr<nsRange> 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<OwningNonNull<nsIContent>>& aArrayOfTopMostChildContents) {
+ EnsureBeginsOrEndsWithValidContent(StartOrEnd::start,
+ aArrayOfTopMostChildContents);
+ EnsureBeginsOrEndsWithValidContent(StartOrEnd::end,
+ aArrayOfTopMostChildContents);
+}
+
+// static
+void HTMLEditor::AutoHTMLFragmentBoundariesFixer::
+ CollectTableAndAnyListElementsOfInclusiveAncestorsAt(
+ nsIContent& aContent,
+ nsTArray<OwningNonNull<Element>>& aOutArrayOfListAndTableElements) {
+ for (Element* element = aContent.GetAsElementOrParentElement(); element;
+ element = element->GetParentElement()) {
+ if (HTMLEditUtils::IsAnyListElement(element) ||
+ HTMLEditUtils::IsTable(element)) {
+ aOutArrayOfListAndTableElements.AppendElement(*element);
+ }
+ }
+}
+
+// static
+Element* HTMLEditor::AutoHTMLFragmentBoundariesFixer::
+ GetMostDistantAncestorListOrTableElement(
+ const nsTArray<OwningNonNull<nsIContent>>& aArrayOfTopMostChildContents,
+ const nsTArray<OwningNonNull<Element>>&
+ aInclusiveAncestorsTableOrListElements) {
+ Element* lastFoundAncestorListOrTableElement = nullptr;
+ for (auto& content : aArrayOfTopMostChildContents) {
+ if (HTMLEditUtils::IsAnyTableElementButNotTable(content)) {
+ Element* tableElement =
+ HTMLEditUtils::GetClosestAncestorTableElement(*content);
+ if (!tableElement) {
+ continue;
+ }
+ // If we find a `<table>` 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 `<table>` 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 `<table>` element which is topmost list or `<table>`
+ // element at first or last of aArrayOfNodes, return it.
+ if (aInclusiveAncestorsTableOrListElements.LastElement().get() ==
+ tableElement) {
+ return tableElement;
+ }
+ // Otherwise, store the `<table>` element which is an ancestor but
+ // not topmost ancestor of first or last of aArrayOfNodes.
+ lastFoundAncestorListOrTableElement = tableElement;
+ continue;
+ }
+
+ if (!HTMLEditUtils::IsListItem(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 `<table>` element.
+ // XXX Is that really expected that this returns a `<table>` element in
+ // this case?
+ if (!aInclusiveAncestorsTableOrListElements.Contains(listElement)) {
+ return lastFoundAncestorListOrTableElement;
+ }
+ // If we find a list element which is topmost list or `<table>` 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 `<table>` 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 `<td>`, `<th>`,
+ // `<tr>`, `<thead>`, `<tbody>`, `<tfoot>` or `<caption>`.
+ // 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::IsAnyTableElement(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 `<table>` element, why don't we keep searching
+ // from its parent?
+ }
+ return nullptr;
+}
+
+bool HTMLEditor::AutoHTMLFragmentBoundariesFixer::IsReplaceableListElement(
+ Element& aListElement, nsIContent& aContentMaybeInListElement) const {
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&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::IsListItem(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<OwningNonNull<nsIContent>>&
+ aArrayOfTopMostChildContents) const {
+ MOZ_ASSERT(!aArrayOfTopMostChildContents.IsEmpty());
+
+ // Collect list elements and table related elements at first or last node
+ // in aArrayOfTopMostChildContents.
+ AutoTArray<OwningNonNull<Element>, 4> inclusiveAncestorsListOrTableElements;
+ CollectTableAndAnyListElementsOfInclusiveAncestorsAt(
+ aStartOrEnd == StartOrEnd::end
+ ? aArrayOfTopMostChildContents.LastElement()
+ : aArrayOfTopMostChildContents[0],
+ inclusiveAncestorsListOrTableElements);
+ if (inclusiveAncestorsListOrTableElements.IsEmpty()) {
+ return;
+ }
+
+ // Get most ancestor list or `<table>` element in
+ // inclusiveAncestorsListOrTableElements which contains earlier
+ // node in aArrayOfTopMostChildContents as far as possible.
+ // XXX With inclusiveAncestorsListOrTableElements, this returns a
+ // list or `<table>` 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 `<table>`. 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<nsIContent>& firstOrLastChildContent =
+ aStartOrEnd == StartOrEnd::end
+ ? aArrayOfTopMostChildContents.LastElement()
+ : aArrayOfTopMostChildContents[0];
+
+ // Find substructure of list or table that must be included in paste.
+ Element* replaceElement;
+ if (HTMLEditUtils::IsAnyListElement(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 `<table>` 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<nsIContent>& 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