summaryrefslogtreecommitdiffstats
path: root/editor/libeditor/HTMLEditorDataTransfer.cpp
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /editor/libeditor/HTMLEditorDataTransfer.cpp
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'editor/libeditor/HTMLEditorDataTransfer.cpp')
-rw-r--r--editor/libeditor/HTMLEditorDataTransfer.cpp2409
1 files changed, 2409 insertions, 0 deletions
diff --git a/editor/libeditor/HTMLEditorDataTransfer.cpp b/editor/libeditor/HTMLEditorDataTransfer.cpp
new file mode 100644
index 000000000..b9cd8adb9
--- /dev/null
+++ b/editor/libeditor/HTMLEditorDataTransfer.cpp
@@ -0,0 +1,2409 @@
+/* -*- 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 "mozilla/HTMLEditor.h"
+
+#include <string.h>
+
+#include "HTMLEditUtils.h"
+#include "TextEditUtils.h"
+#include "WSRunObject.h"
+#include "mozilla/dom/DataTransfer.h"
+#include "mozilla/dom/DocumentFragment.h"
+#include "mozilla/dom/DOMStringList.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Base64.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/EditorUtils.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/SelectionState.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 "nsGkAtoms.h"
+#include "nsIClipboard.h"
+#include "nsIContent.h"
+#include "nsIContentFilter.h"
+#include "nsIDOMComment.h"
+#include "nsIDOMDocument.h"
+#include "nsIDOMDocumentFragment.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMHTMLAnchorElement.h"
+#include "nsIDOMHTMLEmbedElement.h"
+#include "nsIDOMHTMLFrameElement.h"
+#include "nsIDOMHTMLIFrameElement.h"
+#include "nsIDOMHTMLImageElement.h"
+#include "nsIDOMHTMLInputElement.h"
+#include "nsIDOMHTMLLinkElement.h"
+#include "nsIDOMHTMLObjectElement.h"
+#include "nsIDOMHTMLScriptElement.h"
+#include "nsIDOMNode.h"
+#include "nsIDocument.h"
+#include "nsIEditor.h"
+#include "nsIEditorIMESupport.h"
+#include "nsIEditorMailSupport.h"
+#include "nsIEditRules.h"
+#include "nsIFile.h"
+#include "nsIInputStream.h"
+#include "nsIMIMEService.h"
+#include "nsNameSpaceManager.h"
+#include "nsINode.h"
+#include "nsIParserUtils.h"
+#include "nsIPlaintextEditor.h"
+#include "nsISupportsImpl.h"
+#include "nsISupportsPrimitives.h"
+#include "nsISupportsUtils.h"
+#include "nsITransferable.h"
+#include "nsIURI.h"
+#include "nsIVariant.h"
+#include "nsLinebreakConverter.h"
+#include "nsLiteralString.h"
+#include "nsNetUtil.h"
+#include "nsRange.h"
+#include "nsReadableUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStreamUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsStringIterator.h"
+#include "nsSubstringTuple.h"
+#include "nsTreeSanitizer.h"
+#include "nsXPCOM.h"
+#include "nscore.h"
+#include "nsContentUtils.h"
+
+class nsIAtom;
+class nsILoadContext;
+class nsISupports;
+
+namespace mozilla {
+
+using namespace dom;
+
+#define kInsertCookie "_moz_Insert Here_moz_"
+
+// some little helpers
+static bool FindIntegerAfterString(const char* aLeadingString,
+ nsCString& aCStr, int32_t& foundNumber);
+static nsresult RemoveFragComments(nsCString& theStr);
+static void RemoveBodyAndHead(nsINode& aNode);
+static nsresult FindTargetNode(nsIDOMNode* aStart,
+ nsCOMPtr<nsIDOMNode>& aResult);
+
+nsresult
+HTMLEditor::LoadHTML(const nsAString& aInputString)
+{
+ NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED);
+
+ // force IME commit; set up rules sniffing and batching
+ ForceCompositionEnd();
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::loadHTML, nsIEditor::eNext);
+
+ // Get selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ TextRulesInfo ruleInfo(EditAction::loadHTML);
+ bool cancel, handled;
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cancel) {
+ return NS_OK; // rules canceled the operation
+ }
+
+ if (!handled) {
+ // Delete Selection, but only if it isn't collapsed, see bug #106269
+ if (!selection->Collapsed()) {
+ rv = DeleteSelection(eNone, eStrip);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Get the first range in the selection, for context:
+ RefPtr<nsRange> range = selection->GetRangeAt(0);
+ NS_ENSURE_TRUE(range, NS_ERROR_NULL_POINTER);
+
+ // create fragment for pasted html
+ nsCOMPtr<nsIDOMDocumentFragment> docfrag;
+ rv = range->CreateContextualFragment(aInputString, getter_AddRefs(docfrag));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // put the fragment into the document
+ nsCOMPtr<nsIDOMNode> parent;
+ rv = range->GetStartContainer(getter_AddRefs(parent));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER);
+ int32_t childOffset;
+ rv = range->GetStartOffset(&childOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDOMNode> nodeToInsert;
+ docfrag->GetFirstChild(getter_AddRefs(nodeToInsert));
+ while (nodeToInsert) {
+ rv = InsertNode(nodeToInsert, parent, childOffset++);
+ NS_ENSURE_SUCCESS(rv, rv);
+ docfrag->GetFirstChild(getter_AddRefs(nodeToInsert));
+ }
+ }
+
+ return rules->DidDoAction(selection, &ruleInfo, rv);
+}
+
+NS_IMETHODIMP
+HTMLEditor::InsertHTML(const nsAString& aInString)
+{
+ const nsAFlatString& empty = EmptyString();
+
+ return InsertHTMLWithContext(aInString, empty, empty, empty,
+ nullptr, nullptr, 0, true);
+}
+
+nsresult
+HTMLEditor::InsertHTMLWithContext(const nsAString& aInputString,
+ const nsAString& aContextStr,
+ const nsAString& aInfoStr,
+ const nsAString& aFlavor,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestNode,
+ int32_t aDestOffset,
+ bool aDeleteSelection)
+{
+ return DoInsertHTMLWithContext(aInputString, aContextStr, aInfoStr,
+ aFlavor, aSourceDoc, aDestNode, aDestOffset, aDeleteSelection,
+ /* trusted input */ true, /* clear style */ false);
+}
+
+nsresult
+HTMLEditor::DoInsertHTMLWithContext(const nsAString& aInputString,
+ const nsAString& aContextStr,
+ const nsAString& aInfoStr,
+ const nsAString& aFlavor,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestNode,
+ int32_t aDestOffset,
+ bool aDeleteSelection,
+ bool aTrustedInput,
+ bool aClearStyle)
+{
+ NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED);
+
+ // Prevent the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+
+ // force IME commit; set up rules sniffing and batching
+ ForceCompositionEnd();
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::htmlPaste, nsIEditor::eNext);
+
+ // Get selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_STATE(selection);
+
+ // create a dom document fragment that represents the structure to paste
+ nsCOMPtr<nsIDOMNode> fragmentAsNode, streamStartParent, streamEndParent;
+ int32_t streamStartOffset = 0, streamEndOffset = 0;
+
+ nsresult rv = CreateDOMFragmentFromPaste(aInputString, aContextStr, aInfoStr,
+ address_of(fragmentAsNode),
+ address_of(streamStartParent),
+ address_of(streamEndParent),
+ &streamStartOffset,
+ &streamEndOffset,
+ aTrustedInput);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDOMNode> targetNode;
+ int32_t targetOffset=0;
+
+ if (!aDestNode) {
+ // if caller didn't provide the destination/target node,
+ // fetch the paste insertion point from our selection
+ rv = GetStartNodeAndOffset(selection, getter_AddRefs(targetNode), &targetOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!targetNode || !IsEditable(targetNode)) {
+ return NS_ERROR_FAILURE;
+ }
+ } else {
+ targetNode = aDestNode;
+ targetOffset = aDestOffset;
+ }
+
+ bool doContinue = true;
+
+ rv = DoContentFilterCallback(aFlavor, aSourceDoc, aDeleteSelection,
+ (nsIDOMNode **)address_of(fragmentAsNode),
+ (nsIDOMNode **)address_of(streamStartParent),
+ &streamStartOffset,
+ (nsIDOMNode **)address_of(streamEndParent),
+ &streamEndOffset,
+ (nsIDOMNode **)address_of(targetNode),
+ &targetOffset, &doContinue);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(doContinue, NS_OK);
+
+ // if we have a destination / target node, we want to insert there
+ // rather than in place of the selection
+ // ignore aDeleteSelection here if no aDestNode since deletion will
+ // also occur later; 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 (aDestNode) {
+ if (aDeleteSelection) {
+ // Use an auto tracker so that our drop point is correctly
+ // positioned after the delete.
+ AutoTrackDOMPoint tracker(mRangeUpdater, &targetNode, &targetOffset);
+ rv = DeleteSelection(eNone, eStrip);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = selection->Collapse(targetNode, targetOffset);
+ NS_ENSURE_SUCCESS(rv, 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)
+
+ // make a list of what nodes in docFrag we need to move
+ nsTArray<OwningNonNull<nsINode>> nodeList;
+ nsCOMPtr<nsINode> fragmentAsNodeNode = do_QueryInterface(fragmentAsNode);
+ NS_ENSURE_STATE(fragmentAsNodeNode || !fragmentAsNode);
+ nsCOMPtr<nsINode> streamStartParentNode =
+ do_QueryInterface(streamStartParent);
+ NS_ENSURE_STATE(streamStartParentNode || !streamStartParent);
+ nsCOMPtr<nsINode> streamEndParentNode =
+ do_QueryInterface(streamEndParent);
+ NS_ENSURE_STATE(streamEndParentNode || !streamEndParent);
+ CreateListOfNodesToPaste(*static_cast<DocumentFragment*>(fragmentAsNodeNode.get()),
+ nodeList,
+ streamStartParentNode, streamStartOffset,
+ streamEndParentNode, streamEndOffset);
+
+ if (nodeList.IsEmpty()) {
+ // We aren't inserting anything, but if aDeleteSelection is set, we do want
+ // to delete everything.
+ if (aDeleteSelection) {
+ return DeleteSelection(eNone, eStrip);
+ }
+ return NS_OK;
+ }
+
+ // Are there any table elements in the list?
+ // node and offset for insertion
+ nsCOMPtr<nsIDOMNode> parentNode;
+ int32_t offsetOfNewNode;
+
+ // check for table cell selection mode
+ bool cellSelectionMode = false;
+ nsCOMPtr<nsIDOMElement> cell;
+ rv = GetFirstSelectedCell(nullptr, getter_AddRefs(cell));
+ if (NS_SUCCEEDED(rv) && cell) {
+ cellSelectionMode = true;
+ }
+
+ 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::IsTableElement(nodeList[0])) {
+ cellSelectionMode = false;
+ }
+ }
+
+ if (!cellSelectionMode) {
+ rv = DeleteSelectionAndPrepareToCreateNode();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aClearStyle) {
+ // pasting does not inherit local inline styles
+ nsCOMPtr<nsINode> tmpNode = selection->GetAnchorNode();
+ int32_t tmpOffset = static_cast<int32_t>(selection->AnchorOffset());
+ rv = ClearStyle(address_of(tmpNode), &tmpOffset, nullptr, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else {
+ // Delete whole cells: we will replace with new table content.
+
+ // Braces for artificial block to scope AutoSelectionRestorer.
+ // Save current selection since DeleteTableCell() perturbs it.
+ {
+ AutoSelectionRestorer selectionRestorer(selection, this);
+ rv = DeleteTableCell(1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // collapse selection to beginning of deleted table content
+ selection->CollapseToStart();
+ }
+
+ // give rules a chance to handle or cancel
+ TextRulesInfo ruleInfo(EditAction::insertElement);
+ bool cancel, handled;
+ rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cancel) {
+ return NS_OK; // rules canceled the operation
+ }
+
+ if (!handled) {
+ // The rules code (WillDoAction above) might have changed the selection.
+ // refresh our memory...
+ rv = GetStartNodeAndOffset(selection, getter_AddRefs(parentNode), &offsetOfNewNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(parentNode, NS_ERROR_FAILURE);
+
+ // Adjust position based on the first node we are going to insert.
+ NormalizeEOLInsertPosition(nodeList[0], address_of(parentNode),
+ &offsetOfNewNode);
+
+ // if there are any invisible br's after our insertion point, remove them.
+ // this is because if there is a br at end of what we paste, it will make
+ // the invisible br visible.
+ WSRunObject wsObj(this, parentNode, offsetOfNewNode);
+ if (wsObj.mEndReasonNode &&
+ TextEditUtils::IsBreak(wsObj.mEndReasonNode) &&
+ !IsVisBreak(wsObj.mEndReasonNode)) {
+ rv = DeleteNode(wsObj.mEndReasonNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Remember if we are in a link.
+ bool bStartedInLink = IsInLink(parentNode);
+
+ // Are we in a text node? If so, split it.
+ if (IsTextNode(parentNode)) {
+ nsCOMPtr<nsIContent> parentContent = do_QueryInterface(parentNode);
+ NS_ENSURE_STATE(parentContent || !parentNode);
+ offsetOfNewNode = SplitNodeDeep(*parentContent, *parentContent,
+ offsetOfNewNode);
+ NS_ENSURE_STATE(offsetOfNewNode != -1);
+ nsCOMPtr<nsIDOMNode> temp;
+ rv = parentNode->GetParentNode(getter_AddRefs(temp));
+ NS_ENSURE_SUCCESS(rv, rv);
+ parentNode = temp;
+ }
+
+ // build up list of parents of first node in list that are either
+ // lists or tables. First examine front of paste node list.
+ nsTArray<OwningNonNull<Element>> startListAndTableArray;
+ GetListAndTableParents(StartOrEnd::start, nodeList,
+ startListAndTableArray);
+
+ // remember number of lists and tables above us
+ int32_t highWaterMark = -1;
+ if (!startListAndTableArray.IsEmpty()) {
+ highWaterMark = DiscoverPartialListsAndTables(nodeList,
+ startListAndTableArray);
+ }
+
+ // if we have pieces of tables or lists to be inserted, let's force the paste
+ // to deal with table elements right away, so that it doesn't orphan some
+ // table or list contents outside the table or list.
+ if (highWaterMark >= 0) {
+ ReplaceOrphanedStructure(StartOrEnd::start, nodeList,
+ startListAndTableArray, highWaterMark);
+ }
+
+ // Now go through the same process again for the end of the paste node list.
+ nsTArray<OwningNonNull<Element>> endListAndTableArray;
+ GetListAndTableParents(StartOrEnd::end, nodeList, endListAndTableArray);
+ highWaterMark = -1;
+
+ // remember number of lists and tables above us
+ if (!endListAndTableArray.IsEmpty()) {
+ highWaterMark = DiscoverPartialListsAndTables(nodeList,
+ endListAndTableArray);
+ }
+
+ // don't orphan partial list or table structure
+ if (highWaterMark >= 0) {
+ ReplaceOrphanedStructure(StartOrEnd::end, nodeList,
+ endListAndTableArray, highWaterMark);
+ }
+
+ // Loop over the node list and paste the nodes:
+ nsCOMPtr<nsIDOMNode> parentBlock, lastInsertNode, insertedContextParent;
+ nsCOMPtr<nsINode> parentNodeNode = do_QueryInterface(parentNode);
+ NS_ENSURE_STATE(parentNodeNode || !parentNode);
+ if (IsBlockNode(parentNodeNode)) {
+ parentBlock = parentNode;
+ } else {
+ parentBlock = GetBlockNodeParent(parentNode);
+ }
+
+ int32_t listCount = nodeList.Length();
+ for (int32_t j = 0; j < listCount; j++) {
+ bool bDidInsert = false;
+ nsCOMPtr<nsIDOMNode> curNode = nodeList[j]->AsDOMNode();
+
+ NS_ENSURE_TRUE(curNode, NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(curNode != fragmentAsNode, NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(!TextEditUtils::IsBody(curNode), NS_ERROR_FAILURE);
+
+ if (insertedContextParent) {
+ // 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.
+ if (EditorUtils::IsDescendantOf(curNode, insertedContextParent)) {
+ continue;
+ }
+ }
+
+ // give the user a hand on table element insertion. if they have
+ // a table or table row on the clipboard, and are trying to insert
+ // into a table or table row, insert the appropriate children instead.
+ if (HTMLEditUtils::IsTableRow(curNode) &&
+ HTMLEditUtils::IsTableRow(parentNode) &&
+ (HTMLEditUtils::IsTable(curNode) ||
+ HTMLEditUtils::IsTable(parentNode))) {
+ nsCOMPtr<nsIDOMNode> child;
+ curNode->GetFirstChild(getter_AddRefs(child));
+ while (child) {
+ rv = InsertNodeAtPoint(child, address_of(parentNode), &offsetOfNewNode, true);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+
+ bDidInsert = true;
+ lastInsertNode = child;
+ offsetOfNewNode++;
+
+ curNode->GetFirstChild(getter_AddRefs(child));
+ }
+ }
+ // give the user a hand on list insertion. if they have
+ // a list on the clipboard, and are trying to insert
+ // into a list or list item, insert the appropriate children instead,
+ // ie, merge the lists instead of pasting in a sublist.
+ else if (HTMLEditUtils::IsList(curNode) &&
+ (HTMLEditUtils::IsList(parentNode) ||
+ HTMLEditUtils::IsListItem(parentNode))) {
+ nsCOMPtr<nsIDOMNode> child, tmp;
+ curNode->GetFirstChild(getter_AddRefs(child));
+ while (child) {
+ if (HTMLEditUtils::IsListItem(child) ||
+ HTMLEditUtils::IsList(child)) {
+ // Check if we are pasting into empty list item. If so
+ // delete it and paste into parent list instead.
+ if (HTMLEditUtils::IsListItem(parentNode)) {
+ bool isEmpty;
+ rv = IsEmptyNode(parentNode, &isEmpty, true);
+ if (NS_SUCCEEDED(rv) && isEmpty) {
+ int32_t newOffset;
+ nsCOMPtr<nsIDOMNode> listNode = GetNodeLocation(parentNode, &newOffset);
+ if (listNode) {
+ DeleteNode(parentNode);
+ parentNode = listNode;
+ offsetOfNewNode = newOffset;
+ }
+ }
+ }
+ rv = InsertNodeAtPoint(child, address_of(parentNode), &offsetOfNewNode, true);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+
+ bDidInsert = true;
+ lastInsertNode = child;
+ offsetOfNewNode++;
+ } else {
+ curNode->RemoveChild(child, getter_AddRefs(tmp));
+ }
+ curNode->GetFirstChild(getter_AddRefs(child));
+ }
+ } else if (parentBlock && HTMLEditUtils::IsPre(parentBlock) &&
+ HTMLEditUtils::IsPre(curNode)) {
+ // Check for pre's going into pre's.
+ nsCOMPtr<nsIDOMNode> child;
+ curNode->GetFirstChild(getter_AddRefs(child));
+ while (child) {
+ rv = InsertNodeAtPoint(child, address_of(parentNode), &offsetOfNewNode, true);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+
+ bDidInsert = true;
+ lastInsertNode = child;
+ offsetOfNewNode++;
+
+ curNode->GetFirstChild(getter_AddRefs(child));
+ }
+ }
+
+ if (!bDidInsert || NS_FAILED(rv)) {
+ // try to insert
+ rv = InsertNodeAtPoint(curNode, address_of(parentNode), &offsetOfNewNode, true);
+ if (NS_SUCCEEDED(rv)) {
+ bDidInsert = true;
+ lastInsertNode = curNode;
+ }
+
+ // Assume failure means no legal parent in the document hierarchy,
+ // try again with the parent of curNode in the paste hierarchy.
+ nsCOMPtr<nsIDOMNode> parent;
+ while (NS_FAILED(rv) && curNode) {
+ curNode->GetParentNode(getter_AddRefs(parent));
+ if (parent && !TextEditUtils::IsBody(parent)) {
+ rv = InsertNodeAtPoint(parent, address_of(parentNode), &offsetOfNewNode, true);
+ if (NS_SUCCEEDED(rv)) {
+ bDidInsert = true;
+ insertedContextParent = parent;
+ lastInsertNode = GetChildAt(parentNode, offsetOfNewNode);
+ }
+ }
+ curNode = parent;
+ }
+ }
+ if (lastInsertNode) {
+ parentNode = GetNodeLocation(lastInsertNode, &offsetOfNewNode);
+ offsetOfNewNode++;
+ }
+ }
+
+ // Now collapse the selection to the end of what we just inserted:
+ if (lastInsertNode) {
+ // set selection to the end of what we just pasted.
+ nsCOMPtr<nsIDOMNode> selNode, tmp, highTable;
+ int32_t selOffset;
+
+ // but don't cross tables
+ if (!HTMLEditUtils::IsTable(lastInsertNode)) {
+ nsCOMPtr<nsINode> lastInsertNode_ = do_QueryInterface(lastInsertNode);
+ NS_ENSURE_STATE(lastInsertNode_ || !lastInsertNode);
+ selNode = GetAsDOMNode(GetLastEditableLeaf(*lastInsertNode_));
+ tmp = selNode;
+ while (tmp && tmp != lastInsertNode) {
+ if (HTMLEditUtils::IsTable(tmp)) {
+ highTable = tmp;
+ }
+ nsCOMPtr<nsIDOMNode> parent = tmp;
+ tmp->GetParentNode(getter_AddRefs(parent));
+ tmp = parent;
+ }
+ if (highTable) {
+ selNode = highTable;
+ }
+ }
+ if (!selNode) {
+ selNode = lastInsertNode;
+ }
+ if (IsTextNode(selNode) ||
+ (IsContainer(selNode) && !HTMLEditUtils::IsTable(selNode))) {
+ rv = GetLengthOfDOMNode(selNode, (uint32_t&)selOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // We need to find a container for selection. Look up.
+ tmp = selNode;
+ selNode = GetNodeLocation(tmp, &selOffset);
+ // selNode might be null in case a mutation listener removed
+ // the stuff we just inserted from the DOM.
+ NS_ENSURE_STATE(selNode);
+ ++selOffset; // want to be *after* last leaf node in paste
+ }
+
+ // make sure we don't end up with selection collapsed after an invisible break node
+ WSRunObject wsRunObj(this, selNode, selOffset);
+ nsCOMPtr<nsINode> visNode;
+ int32_t outVisOffset=0;
+ WSType visType;
+ nsCOMPtr<nsINode> selNode_(do_QueryInterface(selNode));
+ wsRunObj.PriorVisibleNode(selNode_, selOffset, address_of(visNode),
+ &outVisOffset, &visType);
+ if (visType == WSType::br) {
+ // we are after a break. Is it visible? Despite the name,
+ // PriorVisibleNode does not make that determination for breaks.
+ // It also may not return the break in visNode. We have to pull it
+ // out of the WSRunObject's state.
+ if (!IsVisBreak(wsRunObj.mStartReasonNode)) {
+ // don't leave selection past an invisible break;
+ // reset {selNode,selOffset} to point before break
+ selNode = GetNodeLocation(GetAsDOMNode(wsRunObj.mStartReasonNode), &selOffset);
+ // we want to be inside any inline style prior to break
+ WSRunObject wsRunObj(this, selNode, selOffset);
+ selNode_ = do_QueryInterface(selNode);
+ wsRunObj.PriorVisibleNode(selNode_, selOffset, address_of(visNode),
+ &outVisOffset, &visType);
+ if (visType == WSType::text || visType == WSType::normalWS) {
+ selNode = GetAsDOMNode(visNode);
+ selOffset = outVisOffset; // PriorVisibleNode already set offset to _after_ the text or ws
+ } else if (visType == WSType::special) {
+ // prior visible thing is an image or some other non-text thingy.
+ // We want to be right after it.
+ selNode = GetNodeLocation(GetAsDOMNode(wsRunObj.mStartReasonNode), &selOffset);
+ ++selOffset;
+ }
+ }
+ }
+ selection->Collapse(selNode, selOffset);
+
+ // if we just pasted a link, discontinue link style
+ nsCOMPtr<nsIDOMNode> link;
+ if (!bStartedInLink && IsInLink(selNode, address_of(link))) {
+ // so, if we just pasted a link, I split it. Why do that instead of just
+ // nudging selection point beyond it? Because it might have ended in a BR
+ // that is not visible. If so, the code above just placed selection
+ // inside that. So I split it instead.
+ nsCOMPtr<nsIContent> linkContent = do_QueryInterface(link);
+ NS_ENSURE_STATE(linkContent || !link);
+ nsCOMPtr<nsIContent> selContent = do_QueryInterface(selNode);
+ NS_ENSURE_STATE(selContent || !selNode);
+ nsCOMPtr<nsIContent> leftLink;
+ SplitNodeDeep(*linkContent, *selContent, selOffset,
+ EmptyContainers::no, getter_AddRefs(leftLink));
+ if (leftLink) {
+ selNode = GetNodeLocation(GetAsDOMNode(leftLink), &selOffset);
+ selection->Collapse(selNode, selOffset+1);
+ }
+ }
+ }
+ }
+
+ return rules->DidDoAction(selection, &ruleInfo, rv);
+}
+
+NS_IMETHODIMP
+HTMLEditor::AddInsertionListener(nsIContentFilter* aListener)
+{
+ NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER);
+
+ // don't let a listener be added more than once
+ if (!mContentFilters.Contains(aListener)) {
+ mContentFilters.AppendElement(*aListener);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::RemoveInsertionListener(nsIContentFilter* aListener)
+{
+ NS_ENSURE_TRUE(aListener, NS_ERROR_FAILURE);
+
+ mContentFilters.RemoveElement(aListener);
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::DoContentFilterCallback(const nsAString& aFlavor,
+ nsIDOMDocument* sourceDoc,
+ bool aWillDeleteSelection,
+ nsIDOMNode** aFragmentAsNode,
+ nsIDOMNode** aFragStartNode,
+ int32_t* aFragStartOffset,
+ nsIDOMNode** aFragEndNode,
+ int32_t* aFragEndOffset,
+ nsIDOMNode** aTargetNode,
+ int32_t* aTargetOffset,
+ bool* aDoContinue)
+{
+ *aDoContinue = true;
+
+ for (auto& listener : mContentFilters) {
+ if (!*aDoContinue) {
+ break;
+ }
+ listener->NotifyOfInsertion(aFlavor, nullptr, sourceDoc,
+ aWillDeleteSelection, aFragmentAsNode,
+ aFragStartNode, aFragStartOffset,
+ aFragEndNode, aFragEndOffset, aTargetNode,
+ aTargetOffset, aDoContinue);
+ }
+
+ return NS_OK;
+}
+
+bool
+HTMLEditor::IsInLink(nsIDOMNode* aNode,
+ nsCOMPtr<nsIDOMNode>* outLink)
+{
+ NS_ENSURE_TRUE(aNode, false);
+ if (outLink) {
+ *outLink = nullptr;
+ }
+ nsCOMPtr<nsIDOMNode> tmp, node = aNode;
+ while (node) {
+ if (HTMLEditUtils::IsLink(node)) {
+ if (outLink) {
+ *outLink = node;
+ }
+ return true;
+ }
+ tmp = node;
+ tmp->GetParentNode(getter_AddRefs(node));
+ }
+ return false;
+}
+
+nsresult
+HTMLEditor::StripFormattingNodes(nsIContent& aNode,
+ bool aListOnly)
+{
+ if (aNode.TextIsOnlyWhitespace()) {
+ nsCOMPtr<nsINode> parent = aNode.GetParentNode();
+ if (parent) {
+ if (!aListOnly || HTMLEditUtils::IsList(parent)) {
+ ErrorResult rv;
+ parent->RemoveChild(aNode, rv);
+ return rv.StealNSResult();
+ }
+ return NS_OK;
+ }
+ }
+
+ if (!aNode.IsHTMLElement(nsGkAtoms::pre)) {
+ nsCOMPtr<nsIContent> child = aNode.GetLastChild();
+ while (child) {
+ nsCOMPtr<nsIContent> previous = child->GetPreviousSibling();
+ nsresult rv = StripFormattingNodes(*child, aListOnly);
+ NS_ENSURE_SUCCESS(rv, rv);
+ child = previous.forget();
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::PrepareTransferable(nsITransferable** aTransferable)
+{
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::PrepareHTMLTransferable(nsITransferable** aTransferable)
+{
+ // Create generic Transferable for getting the data
+ nsresult rv = CallCreateInstance("@mozilla.org/widget/transferable;1", aTransferable);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the nsITransferable interface for getting the data from the clipboard
+ if (aTransferable) {
+ nsCOMPtr<nsIDocument> destdoc = GetDocument();
+ nsILoadContext* loadContext = destdoc ? destdoc->GetLoadContext() : nullptr;
+ (*aTransferable)->Init(loadContext);
+
+ // 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 (!IsPlaintextEditor()) {
+ (*aTransferable)->AddDataFlavor(kNativeHTMLMime);
+ (*aTransferable)->AddDataFlavor(kHTMLMime);
+ (*aTransferable)->AddDataFlavor(kFileMime);
+
+ switch (Preferences::GetInt("clipboard.paste_image_type", 1)) {
+ case 0: // prefer JPEG over PNG over GIF encoding
+ (*aTransferable)->AddDataFlavor(kJPEGImageMime);
+ (*aTransferable)->AddDataFlavor(kJPGImageMime);
+ (*aTransferable)->AddDataFlavor(kPNGImageMime);
+ (*aTransferable)->AddDataFlavor(kGIFImageMime);
+ break;
+ case 1: // prefer PNG over JPEG over GIF encoding (default)
+ default:
+ (*aTransferable)->AddDataFlavor(kPNGImageMime);
+ (*aTransferable)->AddDataFlavor(kJPEGImageMime);
+ (*aTransferable)->AddDataFlavor(kJPGImageMime);
+ (*aTransferable)->AddDataFlavor(kGIFImageMime);
+ break;
+ case 2: // prefer GIF over JPEG over PNG encoding
+ (*aTransferable)->AddDataFlavor(kGIFImageMime);
+ (*aTransferable)->AddDataFlavor(kJPEGImageMime);
+ (*aTransferable)->AddDataFlavor(kJPGImageMime);
+ (*aTransferable)->AddDataFlavor(kPNGImageMime);
+ break;
+ }
+ }
+ (*aTransferable)->AddDataFlavor(kUnicodeMime);
+ (*aTransferable)->AddDataFlavor(kMozTextInternal);
+ }
+
+ return NS_OK;
+}
+
+bool
+FindIntegerAfterString(const char* aLeadingString,
+ 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;
+}
+
+nsresult
+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("-->", false, startCommentIndx);
+ if (startCommentEnd > startCommentIndx) {
+ aStr.Cut(startCommentIndx, (startCommentEnd + 3) - startCommentIndx);
+ }
+ }
+ int32_t endCommentIndx = aStr.Find("<!--EndFragment");
+ if (endCommentIndx >= 0) {
+ int32_t endCommentEnd = aStr.Find("-->", false, endCommentIndx);
+ if (endCommentEnd > endCommentIndx) {
+ aStr.Cut(endCommentIndx, (endCommentEnd + 3) - endCommentIndx);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::ParseCFHTML(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) +
+ NS_LITERAL_CSTRING("<!--" kInsertCookie "-->") +
+ 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 nsAFlatString& fragUcs2Str = NS_ConvertUTF8toUTF16(fragmentUTF8);
+ const nsAFlatString& 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);
+ NS_ENSURE_TRUE(*aStuffToPaste, 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)
+{
+ nsAutoCString data64;
+ nsresult rv = Base64Encode(aData, data64);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aOutput.AssignLiteral("<IMG src=\"data:");
+ AppendUTF8toUTF16(aType, aOutput);
+ aOutput.AppendLiteral(";base64,");
+ if (!AppendASCIItoUTF16(data64, aOutput, fallible_t())) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ aOutput.AppendLiteral("\" alt=\"\" >");
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(HTMLEditor::BlobReader, nsIEditorBlobListener)
+
+HTMLEditor::BlobReader::BlobReader(BlobImpl* aBlob,
+ HTMLEditor* aHTMLEditor,
+ bool aIsSafe,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection)
+ : mBlob(aBlob)
+ , mHTMLEditor(aHTMLEditor)
+ , mIsSafe(aIsSafe)
+ , mSourceDoc(aSourceDoc)
+ , mDestinationNode(aDestinationNode)
+ , mDestOffset(aDestOffset)
+ , mDoDeleteSelection(aDoDeleteSelection)
+{
+ MOZ_ASSERT(mBlob);
+ MOZ_ASSERT(mHTMLEditor);
+ MOZ_ASSERT(mDestinationNode);
+}
+
+NS_IMETHODIMP
+HTMLEditor::BlobReader::OnResult(const nsACString& aResult)
+{
+ nsString blobType;
+ mBlob->GetType(blobType);
+
+ NS_ConvertUTF16toUTF8 type(blobType);
+ nsAutoString stuffToPaste;
+ nsresult rv = ImgFromData(type, aResult, stuffToPaste);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoEditBatch beginBatching(mHTMLEditor);
+ rv = mHTMLEditor->DoInsertHTMLWithContext(stuffToPaste, EmptyString(),
+ EmptyString(),
+ NS_LITERAL_STRING(kFileMime),
+ mSourceDoc,
+ mDestinationNode, mDestOffset,
+ mDoDeleteSelection,
+ mIsSafe, false);
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::BlobReader::OnError(const nsAString& aError)
+{
+ nsCOMPtr<nsINode> destNode = do_QueryInterface(mDestinationNode);
+ const nsPromiseFlatString& flat = PromiseFlatString(aError);
+ const char16_t* error = flat.get();
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_CSTRING("Editor"),
+ destNode->OwnerDoc(),
+ nsContentUtils::eDOM_PROPERTIES,
+ "EditorFileDropFailed",
+ &error, 1);
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::InsertObject(const nsACString& aType,
+ nsISupports* aObject,
+ bool aIsSafe,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection)
+{
+ nsresult rv;
+
+ if (nsCOMPtr<BlobImpl> blob = do_QueryInterface(aObject)) {
+ RefPtr<BlobReader> br = new BlobReader(blob, this, aIsSafe, aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection);
+ nsCOMPtr<nsIEditorUtils> utils =
+ do_GetService("@mozilla.org/editor-utils;1");
+ NS_ENSURE_TRUE(utils, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsINode> node = do_QueryInterface(aDestinationNode);
+ MOZ_ASSERT(node);
+
+ nsCOMPtr<nsIDOMBlob> domBlob = Blob::Create(node->GetOwnerGlobal(), blob);
+ NS_ENSURE_TRUE(domBlob, NS_ERROR_FAILURE);
+
+ return utils->SlurpBlob(domBlob, node->OwnerDoc()->GetWindow(), br);
+ }
+
+ nsAutoCString type(aType);
+
+ // Check to see if we can insert an image file
+ bool insertAsImage = false;
+ nsCOMPtr<nsIFile> fileObj;
+ if (type.EqualsLiteral(kFileMime)) {
+ fileObj = do_QueryInterface(aObject);
+ if (fileObj) {
+ // Accept any image type fed to us
+ if (nsContentUtils::IsFileImage(fileObj, type)) {
+ insertAsImage = true;
+ } else {
+ // Reset type.
+ type.AssignLiteral(kFileMime);
+ }
+ }
+ }
+
+ if (type.EqualsLiteral(kJPEGImageMime) ||
+ type.EqualsLiteral(kJPGImageMime) ||
+ type.EqualsLiteral(kPNGImageMime) ||
+ type.EqualsLiteral(kGIFImageMime) ||
+ insertAsImage) {
+ nsCString imageData;
+ if (insertAsImage) {
+ rv = nsContentUtils::SlurpFileToString(fileObj, imageData);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ nsCOMPtr<nsIInputStream> imageStream = do_QueryInterface(aObject);
+ NS_ENSURE_TRUE(imageStream, NS_ERROR_FAILURE);
+
+ rv = NS_ConsumeStream(imageStream, UINT32_MAX, imageData);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = imageStream->Close();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsAutoString stuffToPaste;
+ rv = ImgFromData(type, imageData, stuffToPaste);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoEditBatch beginBatching(this);
+ rv = DoInsertHTMLWithContext(stuffToPaste, EmptyString(), EmptyString(),
+ NS_LITERAL_STRING(kFileMime),
+ aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection,
+ aIsSafe, false);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::InsertFromTransferable(nsITransferable* transferable,
+ nsIDOMDocument* aSourceDoc,
+ const nsAString& aContextStr,
+ const nsAString& aInfoStr,
+ bool havePrivateHTMLFlavor,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection)
+{
+ nsresult rv = NS_OK;
+ nsAutoCString bestFlavor;
+ nsCOMPtr<nsISupports> genericDataObj;
+ uint32_t len = 0;
+ if (NS_SUCCEEDED(
+ transferable->GetAnyTransferData(bestFlavor,
+ getter_AddRefs(genericDataObj),
+ &len))) {
+ AutoTransactionsConserveSelection dontSpazMySelection(this);
+ nsAutoString flavor;
+ flavor.AssignWithConversion(bestFlavor);
+ nsAutoString stuffToPaste;
+ bool isSafe = IsSafeToInsertData(aSourceDoc);
+
+ if (bestFlavor.EqualsLiteral(kFileMime) ||
+ bestFlavor.EqualsLiteral(kJPEGImageMime) ||
+ bestFlavor.EqualsLiteral(kJPGImageMime) ||
+ bestFlavor.EqualsLiteral(kPNGImageMime) ||
+ bestFlavor.EqualsLiteral(kGIFImageMime)) {
+ rv = InsertObject(bestFlavor, genericDataObj, isSafe,
+ aSourceDoc, aDestinationNode, aDestOffset, aDoDeleteSelection);
+ } else if (bestFlavor.EqualsLiteral(kNativeHTMLMime)) {
+ // note cf_html uses utf8, hence use length = len, not len/2 as in flavors below
+ nsCOMPtr<nsISupportsCString> textDataObj = do_QueryInterface(genericDataObj);
+ if (textDataObj && len > 0) {
+ nsAutoCString cfhtml;
+ textDataObj->GetData(cfhtml);
+ NS_ASSERTION(cfhtml.Length() <= (len), "Invalid length!");
+ nsXPIDLString cfcontext, cffragment, cfselection; // cfselection left emtpy for now
+
+ rv = ParseCFHTML(cfhtml, getter_Copies(cffragment), getter_Copies(cfcontext));
+ if (NS_SUCCEEDED(rv) && !cffragment.IsEmpty()) {
+ AutoEditBatch beginBatching(this);
+ // If we have our private HTML flavor, we will only use the fragment
+ // from the CF_HTML. The rest comes from the clipboard.
+ if (havePrivateHTMLFlavor) {
+ rv = DoInsertHTMLWithContext(cffragment,
+ aContextStr, aInfoStr, flavor,
+ aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection,
+ isSafe);
+ } else {
+ rv = DoInsertHTMLWithContext(cffragment,
+ cfcontext, cfselection, flavor,
+ aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection,
+ isSafe);
+
+ }
+ } 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(kUnicodeMime) ||
+ bestFlavor.EqualsLiteral(kMozTextInternal)) {
+ nsCOMPtr<nsISupportsString> textDataObj = do_QueryInterface(genericDataObj);
+ if (textDataObj && len > 0) {
+ nsAutoString text;
+ textDataObj->GetData(text);
+ NS_ASSERTION(text.Length() <= (len/2), "Invalid length!");
+ stuffToPaste.Assign(text.get(), len / 2);
+ } else {
+ nsCOMPtr<nsISupportsCString> textDataObj(do_QueryInterface(genericDataObj));
+ if (textDataObj && len > 0) {
+ nsAutoCString text;
+ textDataObj->GetData(text);
+ NS_ASSERTION(text.Length() <= len, "Invalid length!");
+ stuffToPaste.Assign(NS_ConvertUTF8toUTF16(Substring(text, 0, len)));
+ }
+ }
+
+ if (!stuffToPaste.IsEmpty()) {
+ AutoEditBatch beginBatching(this);
+ if (bestFlavor.EqualsLiteral(kHTMLMime)) {
+ rv = DoInsertHTMLWithContext(stuffToPaste,
+ aContextStr, aInfoStr, flavor,
+ aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection,
+ isSafe);
+ } else {
+ rv = InsertTextAt(stuffToPaste, aDestinationNode, aDestOffset, aDoDeleteSelection);
+ }
+ }
+ }
+ }
+
+ // Try to scroll the selection into view if the paste succeeded
+ if (NS_SUCCEEDED(rv)) {
+ ScrollSelectionIntoView(false);
+ }
+ return rv;
+}
+
+static void
+GetStringFromDataTransfer(nsIDOMDataTransfer* aDataTransfer,
+ const nsAString& aType,
+ int32_t aIndex,
+ nsAString& aOutputString)
+{
+ nsCOMPtr<nsIVariant> variant;
+ DataTransfer::Cast(aDataTransfer)->GetDataAtNoSecurityCheck(aType, aIndex, getter_AddRefs(variant));
+ if (variant) {
+ variant->GetAsAString(aOutputString);
+ }
+}
+
+nsresult
+HTMLEditor::InsertFromDataTransfer(DataTransfer* aDataTransfer,
+ int32_t aIndex,
+ nsIDOMDocument* aSourceDoc,
+ nsIDOMNode* aDestinationNode,
+ int32_t aDestOffset,
+ bool aDoDeleteSelection)
+{
+ ErrorResult rv;
+ RefPtr<DOMStringList> types = aDataTransfer->MozTypesAt(aIndex, rv);
+ if (rv.Failed()) {
+ return rv.StealNSResult();
+ }
+
+ bool hasPrivateHTMLFlavor = types->Contains(NS_LITERAL_STRING(kHTMLContext));
+
+ bool isText = IsPlaintextEditor();
+ bool isSafe = IsSafeToInsertData(aSourceDoc);
+
+ uint32_t length = types->Length();
+ for (uint32_t t = 0; t < length; t++) {
+ nsAutoString type;
+ types->Item(t, type);
+
+ if (!isText) {
+ if (type.EqualsLiteral(kFileMime) ||
+ type.EqualsLiteral(kJPEGImageMime) ||
+ type.EqualsLiteral(kJPGImageMime) ||
+ type.EqualsLiteral(kPNGImageMime) ||
+ type.EqualsLiteral(kGIFImageMime)) {
+ nsCOMPtr<nsIVariant> variant;
+ DataTransfer::Cast(aDataTransfer)->GetDataAtNoSecurityCheck(type, aIndex, getter_AddRefs(variant));
+ if (variant) {
+ nsCOMPtr<nsISupports> object;
+ variant->GetAsISupports(getter_AddRefs(object));
+ return InsertObject(NS_ConvertUTF16toUTF8(type), object, isSafe,
+ aSourceDoc, aDestinationNode, aDestOffset, aDoDeleteSelection);
+ }
+ } else if (type.EqualsLiteral(kNativeHTMLMime)) {
+ // Windows only clipboard parsing.
+ nsAutoString text;
+ GetStringFromDataTransfer(aDataTransfer, type, aIndex, text);
+ NS_ConvertUTF16toUTF8 cfhtml(text);
+
+ nsXPIDLString cfcontext, cffragment, cfselection; // cfselection left emtpy for now
+
+ nsresult rv = ParseCFHTML(cfhtml, getter_Copies(cffragment), getter_Copies(cfcontext));
+ if (NS_SUCCEEDED(rv) && !cffragment.IsEmpty()) {
+ AutoEditBatch beginBatching(this);
+
+ 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(kHTMLContext), aIndex, contextString);
+ GetStringFromDataTransfer(aDataTransfer, NS_LITERAL_STRING(kHTMLInfo), aIndex, infoString);
+ return DoInsertHTMLWithContext(cffragment,
+ contextString, infoString, type,
+ aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection,
+ isSafe);
+ } else {
+ return DoInsertHTMLWithContext(cffragment,
+ cfcontext, cfselection, type,
+ aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection,
+ isSafe);
+ }
+ }
+ } else if (type.EqualsLiteral(kHTMLMime)) {
+ nsAutoString text, contextString, infoString;
+ GetStringFromDataTransfer(aDataTransfer, type, aIndex, text);
+ GetStringFromDataTransfer(aDataTransfer, NS_LITERAL_STRING(kHTMLContext), aIndex, contextString);
+ GetStringFromDataTransfer(aDataTransfer, NS_LITERAL_STRING(kHTMLInfo), aIndex, infoString);
+
+ AutoEditBatch beginBatching(this);
+ if (type.EqualsLiteral(kHTMLMime)) {
+ return DoInsertHTMLWithContext(text,
+ contextString, infoString, type,
+ aSourceDoc,
+ aDestinationNode, aDestOffset,
+ aDoDeleteSelection,
+ isSafe);
+ }
+ }
+ }
+
+ if (type.EqualsLiteral(kTextMime) ||
+ type.EqualsLiteral(kMozTextInternal)) {
+ nsAutoString text;
+ GetStringFromDataTransfer(aDataTransfer, type, aIndex, text);
+
+ AutoEditBatch beginBatching(this);
+ return InsertTextAt(text, aDestinationNode, aDestOffset, aDoDeleteSelection);
+ }
+ }
+
+ return NS_OK;
+}
+
+bool
+HTMLEditor::HavePrivateHTMLFlavor(nsIClipboard* aClipboard)
+{
+ // check the clipboard for our special kHTMLContext flavor. If that is there, we know
+ // we have our own internal html format on clipboard.
+
+ NS_ENSURE_TRUE(aClipboard, false);
+ bool bHavePrivateHTMLFlavor = false;
+
+ const char* flavArray[] = { kHTMLContext };
+
+ if (NS_SUCCEEDED(
+ aClipboard->HasDataMatchingFlavors(flavArray,
+ ArrayLength(flavArray),
+ nsIClipboard::kGlobalClipboard,
+ &bHavePrivateHTMLFlavor))) {
+ return bHavePrivateHTMLFlavor;
+ }
+
+ return false;
+}
+
+
+NS_IMETHODIMP
+HTMLEditor::Paste(int32_t aSelectionType)
+{
+ if (!FireClipboardEvent(ePaste, aSelectionType)) {
+ return NS_OK;
+ }
+
+ // Get Clipboard Service
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the nsITransferable interface for getting the data from the clipboard
+ nsCOMPtr<nsITransferable> trans;
+ rv = PrepareHTMLTransferable(getter_AddRefs(trans));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(trans, NS_ERROR_FAILURE);
+ // Get the Data from the clipboard
+ rv = clipboard->GetData(trans, aSelectionType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ 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.
+ bool bHavePrivateHTMLFlavor = HavePrivateHTMLFlavor(clipboard);
+ if (bHavePrivateHTMLFlavor) {
+ nsCOMPtr<nsISupports> contextDataObj, infoDataObj;
+ uint32_t contextLen, infoLen;
+ nsCOMPtr<nsISupportsString> textDataObj;
+
+ nsCOMPtr<nsITransferable> contextTrans =
+ do_CreateInstance("@mozilla.org/widget/transferable;1");
+ NS_ENSURE_TRUE(contextTrans, NS_ERROR_NULL_POINTER);
+ contextTrans->Init(nullptr);
+ contextTrans->AddDataFlavor(kHTMLContext);
+ clipboard->GetData(contextTrans, aSelectionType);
+ contextTrans->GetTransferData(kHTMLContext, getter_AddRefs(contextDataObj), &contextLen);
+
+ nsCOMPtr<nsITransferable> infoTrans =
+ do_CreateInstance("@mozilla.org/widget/transferable;1");
+ NS_ENSURE_TRUE(infoTrans, NS_ERROR_NULL_POINTER);
+ infoTrans->Init(nullptr);
+ infoTrans->AddDataFlavor(kHTMLInfo);
+ clipboard->GetData(infoTrans, aSelectionType);
+ infoTrans->GetTransferData(kHTMLInfo, getter_AddRefs(infoDataObj), &infoLen);
+
+ if (contextDataObj) {
+ nsAutoString text;
+ textDataObj = do_QueryInterface(contextDataObj);
+ textDataObj->GetData(text);
+ NS_ASSERTION(text.Length() <= (contextLen/2), "Invalid length!");
+ contextStr.Assign(text.get(), contextLen / 2);
+ }
+
+ if (infoDataObj) {
+ nsAutoString text;
+ textDataObj = do_QueryInterface(infoDataObj);
+ textDataObj->GetData(text);
+ NS_ASSERTION(text.Length() <= (infoLen/2), "Invalid length!");
+ infoStr.Assign(text.get(), infoLen / 2);
+ }
+ }
+
+ // handle transferable hooks
+ nsCOMPtr<nsIDOMDocument> domdoc;
+ GetDocument(getter_AddRefs(domdoc));
+ if (!EditorHookUtils::DoInsertionHook(domdoc, nullptr, trans)) {
+ return NS_OK;
+ }
+
+ return InsertFromTransferable(trans, nullptr, contextStr, infoStr, bHavePrivateHTMLFlavor,
+ nullptr, 0, true);
+}
+
+NS_IMETHODIMP
+HTMLEditor::PasteTransferable(nsITransferable* aTransferable)
+{
+ // Use an invalid value for the clipboard type as data comes from aTransferable
+ // and we don't currently implement a way to put that in the data transfer yet.
+ if (!FireClipboardEvent(ePaste, nsIClipboard::kGlobalClipboard)) {
+ return NS_OK;
+ }
+
+ // handle transferable hooks
+ nsCOMPtr<nsIDOMDocument> domdoc = GetDOMDocument();
+ if (!EditorHookUtils::DoInsertionHook(domdoc, nullptr, aTransferable)) {
+ return NS_OK;
+ }
+
+ nsAutoString contextStr, infoStr;
+ return InsertFromTransferable(aTransferable, nullptr, contextStr, infoStr, false,
+ nullptr, 0, true);
+}
+
+/**
+ * HTML PasteNoFormatting. Ignore any HTML styles and formating in paste source.
+ */
+NS_IMETHODIMP
+HTMLEditor::PasteNoFormatting(int32_t aSelectionType)
+{
+ if (!FireClipboardEvent(ePaste, aSelectionType)) {
+ return NS_OK;
+ }
+
+ ForceCompositionEnd();
+
+ // Get Clipboard Service
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the nsITransferable interface for getting the data from the clipboard.
+ // use TextEditor::PrepareTransferable() to force unicode plaintext data.
+ nsCOMPtr<nsITransferable> trans;
+ rv = TextEditor::PrepareTransferable(getter_AddRefs(trans));
+ if (NS_SUCCEEDED(rv) && trans) {
+ // Get the Data from the clipboard
+ if (NS_SUCCEEDED(clipboard->GetData(trans, aSelectionType)) &&
+ IsModifiable()) {
+ const nsAFlatString& empty = EmptyString();
+ rv = InsertFromTransferable(trans, nullptr, empty, empty, false, nullptr, 0,
+ true);
+ }
+ }
+
+ return 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[] = { kUnicodeMime };
+static const char* textHtmlEditorFlavors[] = { kUnicodeMime, kHTMLMime,
+ kJPEGImageMime, kJPGImageMime,
+ kPNGImageMime, kGIFImageMime };
+
+NS_IMETHODIMP
+HTMLEditor::CanPaste(int32_t aSelectionType,
+ bool* aCanPaste)
+{
+ NS_ENSURE_ARG_POINTER(aCanPaste);
+ *aCanPaste = false;
+
+ // can't paste if readonly
+ if (!IsModifiable()) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool haveFlavors;
+
+ // Use the flavors depending on the current editor mask
+ if (IsPlaintextEditor()) {
+ rv = clipboard->HasDataMatchingFlavors(textEditorFlavors,
+ ArrayLength(textEditorFlavors),
+ aSelectionType, &haveFlavors);
+ } else {
+ rv = clipboard->HasDataMatchingFlavors(textHtmlEditorFlavors,
+ ArrayLength(textHtmlEditorFlavors),
+ aSelectionType, &haveFlavors);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aCanPaste = haveFlavors;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLEditor::CanPasteTransferable(nsITransferable* aTransferable,
+ bool* aCanPaste)
+{
+ NS_ENSURE_ARG_POINTER(aCanPaste);
+
+ // can't paste if readonly
+ if (!IsModifiable()) {
+ *aCanPaste = false;
+ return NS_OK;
+ }
+
+ // If |aTransferable| is null, assume that a paste will succeed.
+ if (!aTransferable) {
+ *aCanPaste = true;
+ return NS_OK;
+ }
+
+ // Peek in |aTransferable| to see if it contains a supported MIME type.
+
+ // Use the flavors depending on the current editor mask
+ const char ** flavors;
+ unsigned length;
+ if (IsPlaintextEditor()) {
+ flavors = textEditorFlavors;
+ length = ArrayLength(textEditorFlavors);
+ } else {
+ flavors = textHtmlEditorFlavors;
+ length = ArrayLength(textHtmlEditorFlavors);
+ }
+
+ for (unsigned int i = 0; i < length; i++, flavors++) {
+ nsCOMPtr<nsISupports> data;
+ uint32_t dataLen;
+ nsresult rv = aTransferable->GetTransferData(*flavors,
+ getter_AddRefs(data),
+ &dataLen);
+ if (NS_SUCCEEDED(rv) && data) {
+ *aCanPaste = true;
+ return NS_OK;
+ }
+ }
+
+ *aCanPaste = false;
+ return NS_OK;
+}
+
+/**
+ * HTML PasteAsQuotation: Paste in a blockquote type=cite.
+ */
+NS_IMETHODIMP
+HTMLEditor::PasteAsQuotation(int32_t aSelectionType)
+{
+ if (IsPlaintextEditor()) {
+ return PasteAsPlaintextQuotation(aSelectionType);
+ }
+
+ nsAutoString citation;
+ return PasteAsCitedQuotation(citation, aSelectionType);
+}
+
+NS_IMETHODIMP
+HTMLEditor::PasteAsCitedQuotation(const nsAString& aCitation,
+ int32_t aSelectionType)
+{
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::insertQuotation,
+ nsIEditor::eNext);
+
+ // get selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ // give rules a chance to handle or cancel
+ TextRulesInfo ruleInfo(EditAction::insertElement);
+ bool cancel, handled;
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cancel || handled) {
+ return NS_OK; // rules canceled the operation
+ }
+
+ nsCOMPtr<Element> newNode =
+ DeleteSelectionAndCreateElement(*nsGkAtoms::blockquote);
+ NS_ENSURE_TRUE(newNode, NS_ERROR_NULL_POINTER);
+
+ // Try to set type=cite. Ignore it if this fails.
+ newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::type,
+ NS_LITERAL_STRING("cite"), true);
+
+ // Set the selection to the underneath the node we just inserted:
+ rv = selection->Collapse(newNode, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Ensure that the inserted <blockquote> has a frame to make it IsEditable.
+ FlushFrames();
+
+ return Paste(aSelectionType);
+}
+
+/**
+ * Paste a plaintext quotation.
+ */
+NS_IMETHODIMP
+HTMLEditor::PasteAsPlaintextQuotation(int32_t aSelectionType)
+{
+ // Get Clipboard Service
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create generic Transferable for getting the data
+ nsCOMPtr<nsITransferable> trans =
+ do_CreateInstance("@mozilla.org/widget/transferable;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(trans, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDocument> destdoc = GetDocument();
+ nsILoadContext* loadContext = destdoc ? destdoc->GetLoadContext() : nullptr;
+ trans->Init(loadContext);
+
+ // We only handle plaintext pastes here
+ trans->AddDataFlavor(kUnicodeMime);
+
+ // Get the Data from the clipboard
+ clipboard->GetData(trans, aSelectionType);
+
+ // 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;
+ uint32_t len = 0;
+ nsAutoCString flav;
+ rv = trans->GetAnyTransferData(flav, getter_AddRefs(genericDataObj), &len);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (flav.EqualsLiteral(kUnicodeMime)) {
+ nsCOMPtr<nsISupportsString> textDataObj = do_QueryInterface(genericDataObj);
+ if (textDataObj && len > 0) {
+ nsAutoString stuffToPaste;
+ textDataObj->GetData(stuffToPaste);
+ NS_ASSERTION(stuffToPaste.Length() <= (len/2), "Invalid length!");
+ AutoEditBatch beginBatching(this);
+ rv = InsertAsPlaintextQuotation(stuffToPaste, true, 0);
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::InsertTextWithQuotations(const nsAString& aStringToInsert)
+{
+ AutoEditBatch beginBatching(this);
+ // The whole operation should be undoable in one transaction:
+ BeginTransaction();
+
+ // 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('\r', 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('\n', 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 == '\n') {
+ ++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<nsIDOMNode> dummyNode;
+ if (curHunkIsQuoted) {
+ rv = InsertAsPlaintextQuotation(curHunk, false,
+ getter_AddRefs(dummyNode));
+ } else {
+ rv = InsertText(curHunk);
+ }
+ if (!found) {
+ break;
+ }
+ curHunkIsQuoted = quoted;
+ hunkStart = lineStart;
+ }
+
+ EndTransaction();
+
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::InsertAsQuotation(const nsAString& aQuotedText,
+ nsIDOMNode** aNodeInserted)
+{
+ if (IsPlaintextEditor()) {
+ return InsertAsPlaintextQuotation(aQuotedText, true, aNodeInserted);
+ }
+
+ nsAutoString citation;
+ return InsertAsCitedQuotation(aQuotedText, citation, false,
+ aNodeInserted);
+}
+
+// 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.
+NS_IMETHODIMP
+HTMLEditor::InsertAsPlaintextQuotation(const nsAString& aQuotedText,
+ bool aAddCites,
+ nsIDOMNode** aNodeInserted)
+{
+ // get selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::insertQuotation,
+ nsIEditor::eNext);
+
+ // give rules a chance to handle or cancel
+ TextRulesInfo ruleInfo(EditAction::insertElement);
+ bool cancel, handled;
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cancel || handled) {
+ return NS_OK; // rules canceled the operation
+ }
+
+ // 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.
+ nsCOMPtr<Element> newNode =
+ DeleteSelectionAndCreateElement(*nsGkAtoms::span);
+
+ // 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 (newNode) {
+ // Add an attribute on the pre node so we'll know it's a quotation.
+ newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::mozquote,
+ NS_LITERAL_STRING("true"), true);
+ // Allow wrapping on spans so long lines get wrapped to the screen.
+ nsCOMPtr<nsINode> parent = newNode->GetParentNode();
+ if (parent && parent->IsHTMLElement(nsGkAtoms::body)) {
+ newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::style,
+ NS_LITERAL_STRING("white-space: pre-wrap; display: block; width: 98vw;"),
+ true);
+ } else {
+ newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::style,
+ NS_LITERAL_STRING("white-space: pre-wrap;"), true);
+ }
+
+ // and set the selection inside it:
+ selection->Collapse(newNode, 0);
+ }
+
+ // Ensure that the inserted <span> has a frame to make it IsEditable.
+ FlushFrames();
+
+ if (aAddCites) {
+ rv = TextEditor::InsertAsQuotation(aQuotedText, aNodeInserted);
+ } else {
+ rv = TextEditor::InsertText(aQuotedText);
+ }
+ // 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 && NS_SUCCEEDED(rv)) {
+ *aNodeInserted = GetAsDOMNode(newNode);
+ NS_IF_ADDREF(*aNodeInserted);
+ }
+
+ // Set the selection to just after the inserted node:
+ if (NS_SUCCEEDED(rv) && newNode) {
+ nsCOMPtr<nsINode> parent = newNode->GetParentNode();
+ int32_t offset = parent ? parent->IndexOf(newNode) : -1;
+ if (parent) {
+ selection->Collapse(parent, offset + 1);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLEditor::StripCites()
+{
+ return TextEditor::StripCites();
+}
+
+NS_IMETHODIMP
+HTMLEditor::Rewrap(bool aRespectNewlines)
+{
+ return TextEditor::Rewrap(aRespectNewlines);
+}
+
+NS_IMETHODIMP
+HTMLEditor::InsertAsCitedQuotation(const nsAString& aQuotedText,
+ const nsAString& aCitation,
+ bool aInsertHTML,
+ nsIDOMNode** aNodeInserted)
+{
+ // Don't let anyone insert html into a "plaintext" editor:
+ if (IsPlaintextEditor()) {
+ NS_ASSERTION(!aInsertHTML, "InsertAsCitedQuotation: trying to insert html into plaintext editor");
+ return InsertAsPlaintextQuotation(aQuotedText, true, aNodeInserted);
+ }
+
+ // get selection
+ RefPtr<Selection> selection = GetSelection();
+ NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
+
+ AutoEditBatch beginBatching(this);
+ AutoRules beginRulesSniffing(this, EditAction::insertQuotation,
+ nsIEditor::eNext);
+
+ // give rules a chance to handle or cancel
+ TextRulesInfo ruleInfo(EditAction::insertElement);
+ bool cancel, handled;
+ // Protect the edit rules object from dying
+ nsCOMPtr<nsIEditRules> rules(mRules);
+ nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cancel || handled) {
+ return NS_OK; // rules canceled the operation
+ }
+
+ nsCOMPtr<Element> newNode =
+ DeleteSelectionAndCreateElement(*nsGkAtoms::blockquote);
+ NS_ENSURE_TRUE(newNode, NS_ERROR_NULL_POINTER);
+
+ // Try to set type=cite. Ignore it if this fails.
+ newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::type,
+ NS_LITERAL_STRING("cite"), true);
+
+ if (!aCitation.IsEmpty()) {
+ newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::cite, aCitation, true);
+ }
+
+ // Set the selection inside the blockquote so aQuotedText will go there:
+ selection->Collapse(newNode, 0);
+
+ // Ensure that the inserted <blockquote> has a frame to make it IsEditable.
+ FlushFrames();
+
+ if (aInsertHTML) {
+ rv = LoadHTML(aQuotedText);
+ } else {
+ rv = InsertText(aQuotedText); // XXX ignore charset
+ }
+
+ if (aNodeInserted && NS_SUCCEEDED(rv)) {
+ *aNodeInserted = GetAsDOMNode(newNode);
+ NS_IF_ADDREF(*aNodeInserted);
+ }
+
+ // Set the selection to just after the inserted node:
+ if (NS_SUCCEEDED(rv) && newNode) {
+ nsCOMPtr<nsINode> parent = newNode->GetParentNode();
+ int32_t offset = parent ? parent->IndexOf(newNode) : -1;
+ if (parent) {
+ selection->Collapse(parent, offset + 1);
+ }
+ }
+ return rv;
+}
+
+
+void RemoveBodyAndHead(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);
+ }
+}
+
+/**
+ * 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).
+ */
+nsresult FindTargetNode(nsIDOMNode *aStart, nsCOMPtr<nsIDOMNode> &aResult)
+{
+ NS_ENSURE_TRUE(aStart, NS_OK);
+
+ nsCOMPtr<nsIDOMNode> child, tmp;
+
+ nsresult rv = aStart->GetFirstChild(getter_AddRefs(child));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!child) {
+ // If the current result is nullptr, then aStart is a leaf, and is the
+ // fallback result.
+ if (!aResult) {
+ aResult = aStart;
+ }
+ return NS_OK;
+ }
+
+ do {
+ // Is this child the magical cookie?
+ nsCOMPtr<nsIDOMComment> comment = do_QueryInterface(child);
+ if (comment) {
+ nsAutoString data;
+ rv = comment->GetData(data);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (data.EqualsLiteral(kInsertCookie)) {
+ // Yes it is! Return an error so we bubble out and short-circuit the
+ // search.
+ aResult = aStart;
+
+ // Note: it doesn't matter if this fails.
+ aStart->RemoveChild(child, getter_AddRefs(tmp));
+
+ return NS_SUCCESS_EDITOR_FOUND_TARGET;
+ }
+ }
+
+ rv = FindTargetNode(child, aResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (rv == NS_SUCCESS_EDITOR_FOUND_TARGET) {
+ return NS_SUCCESS_EDITOR_FOUND_TARGET;
+ }
+
+ rv = child->GetNextSibling(getter_AddRefs(tmp));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ child = tmp;
+ } while (child);
+
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::CreateDOMFragmentFromPaste(const nsAString& aInputString,
+ const nsAString& aContextStr,
+ const nsAString& aInfoStr,
+ nsCOMPtr<nsIDOMNode>* outFragNode,
+ nsCOMPtr<nsIDOMNode>* outStartNode,
+ nsCOMPtr<nsIDOMNode>* outEndNode,
+ int32_t* outStartOffset,
+ int32_t* outEndOffset,
+ bool aTrustedInput)
+{
+ NS_ENSURE_TRUE(outFragNode && outStartNode && outEndNode, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDocument> doc = GetDocument();
+ NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+
+ // if we have context info, create a fragment for that
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIDOMNode> contextLeaf;
+ RefPtr<DocumentFragment> contextAsNode;
+ if (!aContextStr.IsEmpty()) {
+ rv = ParseFragment(aContextStr, nullptr, doc, getter_AddRefs(contextAsNode),
+ aTrustedInput);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(contextAsNode, NS_ERROR_FAILURE);
+
+ rv = StripFormattingNodes(*contextAsNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RemoveBodyAndHead(*contextAsNode);
+
+ rv = FindTargetNode(contextAsNode, contextLeaf);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIContent> contextLeafAsContent = do_QueryInterface(contextLeaf);
+
+ // create fragment for pasted html
+ nsIAtom* contextAtom;
+ if (contextLeafAsContent) {
+ contextAtom = contextLeafAsContent->NodeInfo()->NameAtom();
+ if (contextLeafAsContent->IsHTMLElement(nsGkAtoms::html)) {
+ contextAtom = nsGkAtoms::body;
+ }
+ } else {
+ contextAtom = nsGkAtoms::body;
+ }
+ RefPtr<DocumentFragment> fragment;
+ rv = ParseFragment(aInputString,
+ contextAtom,
+ doc,
+ getter_AddRefs(fragment),
+ aTrustedInput);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(fragment, NS_ERROR_FAILURE);
+
+ RemoveBodyAndHead(*fragment);
+
+ if (contextAsNode) {
+ // unite the two trees
+ nsCOMPtr<nsIDOMNode> junk;
+ contextLeaf->AppendChild(fragment, getter_AddRefs(junk));
+ fragment = contextAsNode;
+ }
+
+ rv = StripFormattingNodes(*fragment, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If there was no context, then treat all of the data we did get as the
+ // pasted data.
+ if (contextLeaf) {
+ *outEndNode = *outStartNode = contextLeaf;
+ } else {
+ *outEndNode = *outStartNode = fragment;
+ }
+
+ *outFragNode = fragment.forget();
+ *outStartOffset = 0;
+
+ // get the infoString contents
+ if (!aInfoStr.IsEmpty()) {
+ 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 err;
+ int32_t num = numstr1.ToInteger(&err);
+
+ nsCOMPtr<nsIDOMNode> tmp;
+ while (num--) {
+ (*outStartNode)->GetFirstChild(getter_AddRefs(tmp));
+ NS_ENSURE_TRUE(tmp, NS_ERROR_FAILURE);
+ tmp.swap(*outStartNode);
+ }
+
+ num = numstr2.ToInteger(&err);
+ while (num--) {
+ (*outEndNode)->GetLastChild(getter_AddRefs(tmp));
+ NS_ENSURE_TRUE(tmp, NS_ERROR_FAILURE);
+ tmp.swap(*outEndNode);
+ }
+ }
+
+ nsCOMPtr<nsINode> node = do_QueryInterface(*outEndNode);
+ *outEndOffset = node->Length();
+ return NS_OK;
+}
+
+
+nsresult
+HTMLEditor::ParseFragment(const nsAString& aFragStr,
+ nsIAtom* aContextLocalName,
+ nsIDocument* aTargetDocument,
+ DocumentFragment** aFragment,
+ bool aTrustedInput)
+{
+ nsAutoScriptBlockerSuppressNodeRemoved autoBlocker;
+
+ RefPtr<DocumentFragment> fragment =
+ new DocumentFragment(aTargetDocument->NodeInfoManager());
+ nsresult rv = nsContentUtils::ParseFragmentHTML(aFragStr,
+ fragment,
+ aContextLocalName ?
+ aContextLocalName : nsGkAtoms::body,
+ kNameSpaceID_XHTML,
+ false,
+ true);
+ if (!aTrustedInput) {
+ nsTreeSanitizer sanitizer(aContextLocalName ?
+ nsIParserUtils::SanitizerAllowStyle :
+ nsIParserUtils::SanitizerAllowComments);
+ sanitizer.Sanitize(fragment);
+ }
+ fragment.forget(aFragment);
+ return rv;
+}
+
+void
+HTMLEditor::CreateListOfNodesToPaste(
+ DocumentFragment& aFragment,
+ nsTArray<OwningNonNull<nsINode>>& outNodeList,
+ nsINode* aStartNode,
+ int32_t aStartOffset,
+ nsINode* aEndNode,
+ int32_t aEndOffset)
+{
+ // If no info was provided about the boundary between context and stream,
+ // then assume all is stream.
+ if (!aStartNode) {
+ aStartNode = &aFragment;
+ aStartOffset = 0;
+ aEndNode = &aFragment;
+ aEndOffset = aFragment.Length();
+ }
+
+ RefPtr<nsRange> docFragRange;
+ nsresult rv = nsRange::CreateRange(aStartNode, aStartOffset,
+ aEndNode, aEndOffset,
+ getter_AddRefs(docFragRange));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ NS_ENSURE_SUCCESS(rv, );
+
+ // Now use a subtree iterator over the range to create a list of nodes
+ TrivialFunctor functor;
+ DOMSubtreeIterator iter;
+ rv = iter.Init(*docFragRange);
+ NS_ENSURE_SUCCESS(rv, );
+ iter.AppendList(functor, outNodeList);
+}
+
+void
+HTMLEditor::GetListAndTableParents(StartOrEnd aStartOrEnd,
+ nsTArray<OwningNonNull<nsINode>>& aNodeList,
+ nsTArray<OwningNonNull<Element>>& outArray)
+{
+ MOZ_ASSERT(aNodeList.Length());
+
+ // Build up list of parents of first (or last) node in list that are either
+ // lists, or tables.
+ int32_t idx = aStartOrEnd == StartOrEnd::end ? aNodeList.Length() - 1 : 0;
+
+ for (nsCOMPtr<nsINode> node = aNodeList[idx]; node;
+ node = node->GetParentNode()) {
+ if (HTMLEditUtils::IsList(node) || HTMLEditUtils::IsTable(node)) {
+ outArray.AppendElement(*node->AsElement());
+ }
+ }
+}
+
+int32_t
+HTMLEditor::DiscoverPartialListsAndTables(
+ nsTArray<OwningNonNull<nsINode>>& aPasteNodes,
+ nsTArray<OwningNonNull<Element>>& aListsAndTables)
+{
+ int32_t ret = -1;
+ int32_t listAndTableParents = aListsAndTables.Length();
+
+ // Scan insertion list for table elements (other than table).
+ for (auto& curNode : aPasteNodes) {
+ if (HTMLEditUtils::IsTableElement(curNode) &&
+ !curNode->IsHTMLElement(nsGkAtoms::table)) {
+ nsCOMPtr<Element> table = curNode->GetParentElement();
+ while (table && !table->IsHTMLElement(nsGkAtoms::table)) {
+ table = table->GetParentElement();
+ }
+ if (table) {
+ int32_t idx = aListsAndTables.IndexOf(table);
+ if (idx == -1) {
+ return ret;
+ }
+ ret = idx;
+ if (ret == listAndTableParents - 1) {
+ return ret;
+ }
+ }
+ }
+ if (HTMLEditUtils::IsListItem(curNode)) {
+ nsCOMPtr<Element> list = curNode->GetParentElement();
+ while (list && !HTMLEditUtils::IsList(list)) {
+ list = list->GetParentElement();
+ }
+ if (list) {
+ int32_t idx = aListsAndTables.IndexOf(list);
+ if (idx == -1) {
+ return ret;
+ }
+ ret = idx;
+ if (ret == listAndTableParents - 1) {
+ return ret;
+ }
+ }
+ }
+ }
+ return ret;
+}
+
+nsINode*
+HTMLEditor::ScanForListAndTableStructure(
+ StartOrEnd aStartOrEnd,
+ nsTArray<OwningNonNull<nsINode>>& aNodes,
+ Element& aListOrTable)
+{
+ // Look upward from first/last paste node for a piece of this list/table
+ int32_t idx = aStartOrEnd == StartOrEnd::end ? aNodes.Length() - 1 : 0;
+ bool isList = HTMLEditUtils::IsList(&aListOrTable);
+
+ for (nsCOMPtr<nsINode> node = aNodes[idx]; node;
+ node = node->GetParentNode()) {
+ if ((isList && HTMLEditUtils::IsListItem(node)) ||
+ (!isList && HTMLEditUtils::IsTableElement(node) &&
+ !node->IsHTMLElement(nsGkAtoms::table))) {
+ nsCOMPtr<Element> structureNode = node->GetParentElement();
+ if (isList) {
+ while (structureNode && !HTMLEditUtils::IsList(structureNode)) {
+ structureNode = structureNode->GetParentElement();
+ }
+ } else {
+ while (structureNode &&
+ !structureNode->IsHTMLElement(nsGkAtoms::table)) {
+ structureNode = structureNode->GetParentElement();
+ }
+ }
+ if (structureNode == &aListOrTable) {
+ if (isList) {
+ return structureNode;
+ }
+ return node;
+ }
+ }
+ }
+ return nullptr;
+}
+
+void
+HTMLEditor::ReplaceOrphanedStructure(
+ StartOrEnd aStartOrEnd,
+ nsTArray<OwningNonNull<nsINode>>& aNodeArray,
+ nsTArray<OwningNonNull<Element>>& aListAndTableArray,
+ int32_t aHighWaterMark)
+{
+ OwningNonNull<Element> curNode = aListAndTableArray[aHighWaterMark];
+
+ // Find substructure of list or table that must be included in paste.
+ nsCOMPtr<nsINode> replaceNode =
+ ScanForListAndTableStructure(aStartOrEnd, aNodeArray, curNode);
+
+ if (!replaceNode) {
+ return;
+ }
+
+ // If we found substructure, paste it instead of its descendants.
+ // Only replace with the substructure if all the nodes in the list are
+ // descendants.
+ bool shouldReplaceNodes = true;
+ for (uint32_t i = 0; i < aNodeArray.Length(); i++) {
+ uint32_t idx = aStartOrEnd == StartOrEnd::start ?
+ i : (aNodeArray.Length() - i - 1);
+ OwningNonNull<nsINode> endpoint = aNodeArray[idx];
+ if (!EditorUtils::IsDescendantOf(endpoint, replaceNode)) {
+ shouldReplaceNodes = false;
+ break;
+ }
+ }
+
+ if (shouldReplaceNodes) {
+ // Now replace the removed nodes with the structural parent
+ aNodeArray.Clear();
+ if (aStartOrEnd == StartOrEnd::end) {
+ aNodeArray.AppendElement(*replaceNode);
+ } else {
+ aNodeArray.InsertElementAt(0, *replaceNode);
+ }
+ }
+}
+
+} // namespace mozilla