Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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 "ErrorList.h"
#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 "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 "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().IsSet()) {
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
}
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
}
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
}
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())) {
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
}
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"
if (startHTML == -1) {
startHTML = aCfhtml.Find("<!--StartFragment-->");
if (startHTML == -1) {
return NS_OK;
}