/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "HTMLEditor.h"
#include "HTMLEditorInlines.h"
#include "HTMLEditorNestedClasses.h"
#include <utility>
#include "AutoClonedRangeArray.h"
#include "AutoSelectionRestorer.h"
#include "CSSEditUtils.h"
#include "EditAction.h"
#include "EditorDOMPoint.h"
#include "EditorLineBreak.h"
#include "EditorUtils.h"
#include "HTMLEditHelpers.h"
#include "HTMLEditUtils.h"
#include "PendingStyles.h" // for SpecifiedStyle
#include "WhiteSpaceVisibilityKeeper.h"
#include "WSRunScanner.h"
#include "ErrorList.h"
#include "mozilla/Assertions.h"
#include "mozilla/Attributes.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/ContentIterator.h"
#include "mozilla/EditorForwards.h"
#include "mozilla/IntegerRange.h"
#include "mozilla/InternalMutationEvent.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/Maybe.h"
#include "mozilla/OwningNonNull.h"
#include "mozilla/PresShell.h"
#include "mozilla/TextComposition.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Unused.h"
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/ElementInlines.h"
#include "mozilla/dom/HTMLBRElement.h"
#include "mozilla/dom/RangeBinding.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/dom/StaticRange.h"
#include "nsAtom.h"
#include "nsCRT.h"
#include "nsCRTGlue.h"
#include "nsComponentManagerUtils.h"
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsFrameSelection.h"
#include "nsGkAtoms.h"
#include "nsIContent.h"
#include "nsIFrame.h"
#include "nsINode.h"
#include "nsLiteralString.h"
#include "nsPrintfCString.h"
#include "nsRange.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include "nsStringFwd.h"
#include "nsStyledElement.h"
#include "nsTArray.h"
#include "nsTextNode.h"
#include "nsThreadUtils.h"
class nsISupports;
namespace mozilla {
using namespace dom;
using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
using EmptyCheckOptions = HTMLEditUtils::EmptyCheckOptions;
using LeafNodeType = HTMLEditUtils::LeafNodeType;
using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
using WalkTextOption = HTMLEditUtils::WalkTextOption;
using WalkTreeDirection = HTMLEditUtils::WalkTreeDirection;
using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
/********************************************************
* first some helpful functors we will use
********************************************************/
static bool IsPendingStyleCachePreservingSubAction(
EditSubAction aEditSubAction) {
switch (aEditSubAction) {
case EditSubAction::eDeleteSelectedContent:
case EditSubAction::eInsertLineBreak:
case EditSubAction::eInsertParagraphSeparator:
case EditSubAction::eCreateOrChangeList:
case EditSubAction::eIndent:
case EditSubAction::eOutdent:
case EditSubAction::eSetOrClearAlignment:
case EditSubAction::eCreateOrRemoveBlock:
case EditSubAction::eFormatBlockForHTMLCommand:
case EditSubAction::eMergeBlockContents:
case EditSubAction::eRemoveList:
case EditSubAction::eCreateOrChangeDefinitionListItem:
case EditSubAction::eInsertElement:
case EditSubAction::eInsertQuotation:
case EditSubAction::eInsertQuotedText:
return true;
default:
return false;
}
}
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
const EditorDOMRange& aRange);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
const EditorRawDOMRange& aRange);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
const EditorDOMPoint& aStartPoint,
const EditorDOMPoint& aEndPoint);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
const EditorRawDOMPoint& aStartPoint,
const EditorDOMPoint& aEndPoint);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
const EditorDOMPoint& aStartPoint,
const EditorRawDOMPoint& aEndPoint);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
const EditorRawDOMPoint& aStartPoint,
const EditorRawDOMPoint& aEndPoint);
nsresult HTMLEditor::InitEditorContentAndSelection() {
MOZ_ASSERT(IsEditActionDataAvailable());
// We should do nothing with the result of GetRoot() if only a part of the
// document is editable.
if (!EntireDocumentIsEditable()) {
return NS_OK;
}
nsresult rv = MaybeCreatePaddingBRElementForEmptyEditor();
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::MaybeCreatePaddingBRElementForEmptyEditor() failed");
return rv;
}
// If the selection hasn't been set up yet, set it up collapsed to the end of
// our editable content.
// XXX I think that this shouldn't do it in `HTMLEditor` because it maybe
// removed by the web app and if they call `Selection::AddRange()` without
// checking the range count, it may cause multiple selection ranges.
if (!SelectionRef().RangeCount()) {
nsresult rv = CollapseSelectionToEndOfLastLeafNodeOfDocument();
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() "
"failed");
return rv;
}
}
if (IsPlaintextMailComposer()) {
// XXX Should we do this in HTMLEditor? It's odd to guarantee that last
// empty line is visible only when it's in the plain text mode.
nsresult rv = EnsurePaddingBRElementInMultilineEditor();
if (NS_FAILED(rv)) {
NS_WARNING(
"EditorBase::EnsurePaddingBRElementInMultilineEditor() failed");
return rv;
}
}
Element* bodyOrDocumentElement = GetRoot();
if (NS_WARN_IF(!bodyOrDocumentElement && !GetDocument())) {
return NS_ERROR_FAILURE;
}
if (!bodyOrDocumentElement) {
return NS_OK;
}
rv = InsertBRElementToEmptyListItemsAndTableCellsInRange(
RawRangeBoundary(bodyOrDocumentElement, 0u),
RawRangeBoundary(bodyOrDocumentElement,
bodyOrDocumentElement->GetChildCount()));
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange() "
"failed, but ignored");
return NS_OK;
}
void HTMLEditor::OnStartToHandleTopLevelEditSubAction(
EditSubAction aTopLevelEditSubAction,
nsIEditor::EDirection aDirectionOfTopLevelEditSubAction, ErrorResult& aRv) {
MOZ_ASSERT(IsEditActionDataAvailable());
MOZ_ASSERT(!aRv.Failed());
EditorBase::OnStartToHandleTopLevelEditSubAction(
aTopLevelEditSubAction, aDirectionOfTopLevelEditSubAction, aRv);
MOZ_ASSERT(GetTopLevelEditSubAction() == aTopLevelEditSubAction);
MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() ==
aDirectionOfTopLevelEditSubAction);
if (NS_WARN_IF(Destroyed())) {
aRv.
Throw(NS_ERROR_EDITOR_DESTROYED);
return;
}
if (!mInitSucceeded) {
return;
// We should do nothing if we're being initialized.
}
NS_WARNING_ASSERTION(
!aRv.Failed(),
"EditorBase::OnStartToHandleTopLevelEditSubAction() failed");
// Let's work with the latest layout information after (maybe) dispatching
// `beforeinput` event.
RefPtr<Document> document = GetDocument();
if (NS_WARN_IF(!document)) {
aRv.
Throw(NS_ERROR_UNEXPECTED);
return;
}
document->FlushPendingNotifications(FlushType::Frames);
if (NS_WARN_IF(Destroyed())) {
aRv.
Throw(NS_ERROR_EDITOR_DESTROYED);
return;
}
// Remember where our selection was before edit action took place:
const auto atCompositionStart =
GetFirstIMESelectionStartPoint<EditorRawDOMPoint>();
if (atCompositionStart.IsSet()) {
// If there is composition string, let's remember current composition
// range.
TopLevelEditSubActionDataRef().mSelectedRange->StoreRange(
atCompositionStart, GetLastIMESelectionEndPoint<EditorRawDOMPoint>());
}
else {
// Get the selection location
// XXX This may occur so that I think that we shouldn't throw exception
// in this case.
if (NS_WARN_IF(!SelectionRef().RangeCount())) {
aRv.
Throw(NS_ERROR_UNEXPECTED);
return;
}
if (
const nsRange* range = SelectionRef().GetRangeAt(0)) {
TopLevelEditSubActionDataRef().mSelectedRange->StoreRange(*range);
}
}
// Register with range updater to track this as we perturb the doc
RangeUpdaterRef().RegisterRangeItem(
*TopLevelEditSubActionDataRef().mSelectedRange);
// Remember current inline styles for deletion and normal insertion ops
const bool cacheInlineStyles = [&]() {
switch (aTopLevelEditSubAction) {
case EditSubAction::eInsertText:
case EditSubAction::eInsertTextComingFromIME:
case EditSubAction::eDeleteSelectedContent:
return true;
default:
return IsPendingStyleCachePreservingSubAction(aTopLevelEditSubAction);
}
}();
if (cacheInlineStyles) {
const RefPtr<Element> editingHost =
ComputeEditingHost(LimitInBodyElement::No);
if (NS_WARN_IF(!editingHost)) {
aRv.
Throw(NS_ERROR_FAILURE);
return;
}
nsIContent*
const startContainer =
HTMLEditUtils::GetContentToPreserveInlineStyles(
TopLevelEditSubActionDataRef()
.mSelectedRange->StartPoint<EditorRawDOMPoint>(),
*editingHost);
if (NS_WARN_IF(!startContainer)) {
aRv.
Throw(NS_ERROR_FAILURE);
return;
}
if (
const RefPtr<Element> startContainerElement =
startContainer->GetAsElementOrParentElement()) {
nsresult rv = CacheInlineStyles(*startContainerElement);
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::CacheInlineStyles() failed");
aRv.
Throw(rv);
return;
}
}
}
// Stabilize the document against contenteditable count changes
if (document->GetEditingState() == Document::EditingState::eContentEditable) {
document->ChangeContentEditableCount(nullptr, +1);
TopLevelEditSubActionDataRef().mRestoreContentEditableCount =
true;
}
// Check that selection is in subtree defined by body node
nsresult rv = EnsureSelectionInBodyOrDocumentElement();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
aRv.
Throw(NS_ERROR_EDITOR_DESTROYED);
return;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::EnsureSelectionInBodyOrDocumentElement() "
"failed, but ignored");
}
nsresult HTMLEditor::OnEndHandlingTopLevelEditSubAction() {
MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
nsresult rv;
while (
true) {
if (NS_WARN_IF(Destroyed())) {
rv = NS_ERROR_EDITOR_DESTROYED;
break;
}
if (!mInitSucceeded) {
rv = NS_OK;
// We should do nothing if we're being initialized.
break;
}
// Do all the tricky stuff
rv = OnEndHandlingTopLevelEditSubActionInternal();
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() failied");
// Perhaps, we need to do the following jobs even if the editor has been
// destroyed since they adjust some states of HTML document but don't
// modify the DOM tree nor Selection.
// Free up selectionState range item
if (TopLevelEditSubActionDataRef().mSelectedRange) {
RangeUpdaterRef().DropRangeItem(
*TopLevelEditSubActionDataRef().mSelectedRange);
}
// Reset the contenteditable count to its previous value
if (TopLevelEditSubActionDataRef().mRestoreContentEditableCount) {
Document* document = GetDocument();
if (NS_WARN_IF(!document)) {
rv = NS_ERROR_FAILURE;
break;
}
if (document->GetEditingState() ==
Document::EditingState::eContentEditable) {
document->ChangeContentEditableCount(nullptr, -1);
}
}
break;
}
DebugOnly<nsresult> rvIgnored =
EditorBase::OnEndHandlingTopLevelEditSubAction();
NS_WARNING_ASSERTION(
NS_FAILED(rv) || NS_SUCCEEDED(rvIgnored),
"EditorBase::OnEndHandlingTopLevelEditSubAction() failed, but ignored");
MOZ_ASSERT(!GetTopLevelEditSubAction());
MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() == eNone);
return rv;
}
nsresult HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() {
MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
// If we just maintained the DOM tree for consistent behavior even after
// web apps modified the DOM, we should not touch the DOM in this
// post-processor.
if (GetTopLevelEditSubAction() ==
EditSubAction::eMaintainWhiteSpaceVisibility) {
return NS_OK;
}
nsresult rv = EnsureSelectionInBodyOrDocumentElement();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::EnsureSelectionInBodyOrDocumentElement() "
"failed, but ignored");
switch (GetTopLevelEditSubAction()) {
case EditSubAction::eReplaceHeadWithHTMLSource:
case EditSubAction::eCreatePaddingBRElementForEmptyEditor:
return NS_OK;
default:
break;
}
if (TopLevelEditSubActionDataRef().mChangedRange->IsPositioned() &&
GetTopLevelEditSubAction() != EditSubAction::eUndo &&
GetTopLevelEditSubAction() != EditSubAction::eRedo) {
// don't let any txns in here move the selection around behind our back.
// Note that this won't prevent explicit selection setting from working.
AutoTransactionsConserveSelection dontChangeMySelection(*
this);
{
EditorDOMRange changedRange(
*TopLevelEditSubActionDataRef().mChangedRange);
if (changedRange.IsPositioned() &&
changedRange.EnsureNotInNativeAnonymousSubtree()) {
bool isBlockLevelSubAction =
false;
switch (GetTopLevelEditSubAction()) {
case EditSubAction::eInsertText:
case EditSubAction::eInsertTextComingFromIME:
case EditSubAction::eInsertLineBreak:
case EditSubAction::eInsertParagraphSeparator:
case EditSubAction::eDeleteText: {
// XXX We should investigate whether this is really needed because
// it seems that the following code does not handle the
// white-spaces.
RefPtr<nsRange> extendedChangedRange =
CreateRangeIncludingAdjuscentWhiteSpaces(changedRange);
if (extendedChangedRange) {
MOZ_ASSERT(extendedChangedRange->IsPositioned());
// Use extended range temporarily.
TopLevelEditSubActionDataRef().mChangedRange =
std::move(extendedChangedRange);
}
break;
}
case EditSubAction::eCreateOrChangeList:
case EditSubAction::eCreateOrChangeDefinitionListItem:
case EditSubAction::eRemoveList:
case EditSubAction::eFormatBlockForHTMLCommand:
case EditSubAction::eCreateOrRemoveBlock:
case EditSubAction::eIndent:
case EditSubAction::eOutdent:
case EditSubAction::eSetOrClearAlignment:
case EditSubAction::eSetPositionToAbsolute:
case EditSubAction::eSetPositionToStatic:
case EditSubAction::eDecreaseZIndex:
case EditSubAction::eIncreaseZIndex:
isBlockLevelSubAction =
true;
[[fallthrough]];
default: {
Element* editingHost = ComputeEditingHost();
if (MOZ_UNLIKELY(!editingHost)) {
break;
}
RefPtr<nsRange> extendedChangedRange = AutoClonedRangeArray::
CreateRangeWrappingStartAndEndLinesContainingBoundaries(
changedRange, GetTopLevelEditSubAction(),
isBlockLevelSubAction
? BlockInlineCheck::UseHTMLDefaultStyle
: BlockInlineCheck::UseComputedDisplayOutsideStyle,
*editingHost);
if (!extendedChangedRange) {
break;
}
MOZ_ASSERT(extendedChangedRange->IsPositioned());
// Use extended range temporarily.
TopLevelEditSubActionDataRef().mChangedRange =
std::move(extendedChangedRange);
break;
}
}
}
}
// if we did a ranged deletion or handling backspace key, make sure we have
// a place to put caret.
// Note we only want to do this if the overall operation was deletion,
// not if deletion was done along the way for
// EditSubAction::eInsertHTMLSource, EditSubAction::eInsertText, etc.
// That's why this is here rather than DeleteSelectionAsSubAction().
// However, we shouldn't insert <br> elements if we've already removed
// empty block parents because users may want to disappear the line by
// the deletion.
// XXX We should make HandleDeleteSelection() store expected container
// for handling this here since we cannot trust current selection is
// collapsed at deleted point.
if (GetTopLevelEditSubAction() == EditSubAction::eDeleteSelectedContent &&
TopLevelEditSubActionDataRef().mDidDeleteNonCollapsedRange &&
!TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks) {
const auto newCaretPosition =
GetFirstSelectionStartPoint<EditorDOMPoint>();
if (!newCaretPosition.IsSet()) {
NS_WARNING(
"There was no selection range");
return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
}
Result<CreateLineBreakResult, nsresult>
insertPaddingBRElementResultOrError =
InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded(
newCaretPosition);
if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) {
NS_WARNING(
"HTMLEditor::"
"InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded() failed");
return insertPaddingBRElementResultOrError.unwrapErr();
}
nsresult rv =
insertPaddingBRElementResultOrError.unwrap().SuggestCaretPointTo(
*
this, {SuggestCaret::OnlyIfHasSuggestion});
if (NS_FAILED(rv)) {
NS_WARNING(
"CaretPoint::SuggestCaretPointTo() failed");
return rv;
}
NS_WARNING_ASSERTION(
rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
"CaretPoint::SuggestCaretPointTo() failed, but ignored");
}
// add in any needed <br>s, and remove any unneeded ones.
nsresult rv = InsertBRElementToEmptyListItemsAndTableCellsInRange(
TopLevelEditSubActionDataRef().mChangedRange->StartRef().AsRaw(),
TopLevelEditSubActionDataRef().mChangedRange->EndRef().AsRaw());
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange()"
" failed, but ignored");
// merge any adjacent text nodes
switch (GetTopLevelEditSubAction()) {
case EditSubAction::eInsertText:
case EditSubAction::eInsertTextComingFromIME:
break;
default: {
nsresult rv = CollapseAdjacentTextNodes(
MOZ_KnownLive(*TopLevelEditSubActionDataRef().mChangedRange));
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::CollapseAdjacentTextNodes() failed");
return rv;
}
break;
}
}
// Clean up any empty nodes in the changed range unless they are inserted
// intentionally.
if (TopLevelEditSubActionDataRef().mNeedsToCleanUpEmptyElements) {
nsresult rv = RemoveEmptyNodesIn(
EditorDOMRange(*TopLevelEditSubActionDataRef().mChangedRange));
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::RemoveEmptyNodesIn() failed");
return rv;
}
}
// attempt to transform any unneeded nbsp's into spaces after doing various
// operations
switch (GetTopLevelEditSubAction()) {
case EditSubAction::eDeleteSelectedContent:
if (TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces) {
break;
}
[[fallthrough]];
case EditSubAction::eInsertText:
case EditSubAction::eInsertTextComingFromIME:
case EditSubAction::eInsertLineBreak:
case EditSubAction::eInsertParagraphSeparator:
case EditSubAction::ePasteHTMLContent:
case EditSubAction::eInsertHTMLSource: {
// Due to the replacement of white-spaces in
// WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(),
// selection ranges may be changed since DOM ranges track the DOM
// mutation by themselves. However, we want to keep selection as-is.
// Therefore, we should restore `Selection` after replacing
// white-spaces.
AutoSelectionRestorer restoreSelection(
this);
// TODO: Temporarily, WhiteSpaceVisibilityKeeper replaces ASCII
// white-spaces with NPSPs and then, we'll replace them with ASCII
// white-spaces here. We should avoid this overwriting things as
// far as possible because replacing characters in text nodes
// causes running mutation event listeners which are really
// expensive.
// Adjust end of composition string if there is composition string.
auto pointToAdjust = GetLastIMESelectionEndPoint<EditorDOMPoint>();
if (!pointToAdjust.IsInContentNode()) {
// Otherwise, adjust current selection start point.
pointToAdjust = GetFirstSelectionStartPoint<EditorDOMPoint>();
if (NS_WARN_IF(!pointToAdjust.IsInContentNode())) {
return NS_ERROR_FAILURE;
}
}
const RefPtr<Element> editingHost =
ComputeEditingHost(LimitInBodyElement::No);
if (MOZ_UNLIKELY(!editingHost)) {
break;
}
if (EditorUtils::IsEditableContent(
*pointToAdjust.ContainerAs<nsIContent>(), EditorType::HTML)) {
AutoTrackDOMPoint trackPointToAdjust(RangeUpdaterRef(),
&pointToAdjust);
nsresult rv =
WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
*
this, pointToAdjust, *editingHost);
if (NS_FAILED(rv)) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() "
"failed");
return rv;
}
}
// also do this for original selection endpoints.
// XXX Hmm, if `NormalizeVisibleWhiteSpacesAt()` runs mutation event
// listener and that causes changing `mSelectedRange`, what we
// should do?
if (NS_WARN_IF(!TopLevelEditSubActionDataRef()
.mSelectedRange->IsPositioned())) {
return NS_ERROR_FAILURE;
}
EditorDOMPoint atStart =
TopLevelEditSubActionDataRef().mSelectedRange->StartPoint();
if (atStart != pointToAdjust && atStart.IsInContentNode() &&
EditorUtils::IsEditableContent(*atStart.ContainerAs<nsIContent>(),
EditorType::HTML)) {
AutoTrackDOMPoint trackPointToAdjust(RangeUpdaterRef(),
&pointToAdjust);
AutoTrackDOMPoint trackStartPoint(RangeUpdaterRef(), &atStart);
nsresult rv =
WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
*
this, atStart, *editingHost);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() "
"failed, but ignored");
}
// we only need to handle old selection endpoint if it was different
// from start
EditorDOMPoint atEnd =
TopLevelEditSubActionDataRef().mSelectedRange->EndPoint();
if (!TopLevelEditSubActionDataRef().mSelectedRange->Collapsed() &&
atEnd != pointToAdjust && atEnd != atStart &&
atEnd.IsInContentNode() &&
EditorUtils::IsEditableContent(*atEnd.ContainerAs<nsIContent>(),
EditorType::HTML)) {
nsresult rv =
WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
*
this, atEnd, *editingHost);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() "
"failed, but ignored");
}
break;
}
default:
break;
}
// Adjust selection for insert text, html paste, and delete actions if
// we haven't removed new empty blocks. Note that if empty block parents
// are removed, Selection should've been adjusted by the method which
// did it.
if (!TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks &&
SelectionRef().IsCollapsed()) {
switch (GetTopLevelEditSubAction()) {
case EditSubAction::eInsertText:
case EditSubAction::eInsertTextComingFromIME:
case EditSubAction::eInsertLineBreak:
case EditSubAction::eInsertParagraphSeparator:
case EditSubAction::ePasteHTMLContent:
case EditSubAction::eInsertHTMLSource:
// XXX AdjustCaretPositionAndEnsurePaddingBRElement() intentionally
// does not create padding `<br>` element for empty editor.
// Investigate which is better that whether this should does it
// or wait MaybeCreatePaddingBRElementForEmptyEditor().
rv = AdjustCaretPositionAndEnsurePaddingBRElement(
GetDirectionOfTopLevelEditSubAction());
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::AdjustCaretPositionAndEnsurePaddingBRElement() "
"failed");
return rv;
}
break;
default:
break;
}
}
// check for any styles which were removed inappropriately
bool reapplyCachedStyle;
switch (GetTopLevelEditSubAction()) {
case EditSubAction::eInsertText:
case EditSubAction::eInsertTextComingFromIME:
case EditSubAction::eDeleteSelectedContent:
reapplyCachedStyle =
true;
break;
default:
reapplyCachedStyle =
IsPendingStyleCachePreservingSubAction(GetTopLevelEditSubAction());
break;
}
// If the selection is in empty inline HTML elements, we should delete
// them unless it's inserted intentionally.
if (mPlaceholderBatch &&
TopLevelEditSubActionDataRef().mNeedsToCleanUpEmptyElements &&
SelectionRef().IsCollapsed() && SelectionRef().GetFocusNode()) {
RefPtr<Element> mostDistantEmptyInlineAncestor = nullptr;
for (Element* ancestor :
SelectionRef().GetFocusNode()->InclusiveAncestorsOfType<Element>()) {
if (!ancestor->IsHTMLElement() ||
!HTMLEditUtils::IsRemovableFromParentNode(*ancestor) ||
!HTMLEditUtils::IsEmptyInlineContainer(
*ancestor, {EmptyCheckOption::TreatSingleBRElementAsVisible},
BlockInlineCheck::UseComputedDisplayStyle)) {
break;
}
mostDistantEmptyInlineAncestor = ancestor;
}
if (mostDistantEmptyInlineAncestor) {
nsresult rv =
DeleteNodeWithTransaction(*mostDistantEmptyInlineAncestor);
if (NS_FAILED(rv)) {
NS_WARNING(
"EditorBase::DeleteNodeWithTransaction() failed at deleting "
"empty inline ancestors");
return rv;
}
}
}
// But the cached inline styles should be restored from type-in-state later.
if (reapplyCachedStyle) {
DebugOnly<nsresult> rvIgnored =
mPendingStylesToApplyToNewContent->UpdateSelState(*
this);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"PendingStyles::UpdateSelState() failed, but ignored");
rvIgnored = ReapplyCachedStyles();
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"HTMLEditor::ReapplyCachedStyles() failed, but ignored");
TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear();
}
}
rv = HandleInlineSpellCheck(
TopLevelEditSubActionDataRef().mSelectedRange->StartPoint(),
TopLevelEditSubActionDataRef().mChangedRange);
if (NS_FAILED(rv)) {
NS_WARNING(
"EditorBase::HandleInlineSpellCheck() failed");
return rv;
}
// detect empty doc
// XXX Need to investigate when the padding <br> element is removed because
// I don't see the <br> element with testing manually. If it won't be
// used, we can get rid of this cost.
rv = MaybeCreatePaddingBRElementForEmptyEditor();
if (NS_FAILED(rv)) {
NS_WARNING(
"EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() failed");
return rv;
}
// adjust selection HINT if needed
if (!TopLevelEditSubActionDataRef().mDidExplicitlySetInterLine &&
SelectionRef().IsCollapsed()) {
SetSelectionInterlinePosition();
}
return NS_OK;
}
Result<EditActionResult, nsresult> HTMLEditor::CanHandleHTMLEditSubAction(
CheckSelectionInReplacedElement aCheckSelectionInReplacedElement
/* = CheckSelectionInReplacedElement::Yes */) const {
MOZ_ASSERT(IsEditActionDataAvailable());
if (NS_WARN_IF(Destroyed())) {
return Err(NS_ERROR_EDITOR_DESTROYED);
}
// If there is not selection ranges, we should ignore the result.
if (!SelectionRef().RangeCount()) {
return EditActionResult::CanceledResult();
}
const nsRange* range = SelectionRef().GetRangeAt(0);
nsINode* selStartNode = range->GetStartContainer();
if (NS_WARN_IF(!selStartNode) || NS_WARN_IF(!selStartNode->IsContent())) {
return Err(NS_ERROR_FAILURE);
}
if (!HTMLEditUtils::IsSimplyEditableNode(*selStartNode)) {
return EditActionResult::CanceledResult();
}
nsINode* selEndNode = range->GetEndContainer();
if (NS_WARN_IF(!selEndNode) || NS_WARN_IF(!selEndNode->IsContent())) {
return Err(NS_ERROR_FAILURE);
}
if (selStartNode == selEndNode) {
if (aCheckSelectionInReplacedElement ==
CheckSelectionInReplacedElement::Yes &&
HTMLEditUtils::IsNonEditableReplacedContent(
*selStartNode->AsContent())) {
return EditActionResult::CanceledResult();
}
return EditActionResult::IgnoredResult();
}
if (HTMLEditUtils::IsNonEditableReplacedContent(*selStartNode->AsContent()) ||
HTMLEditUtils::IsNonEditableReplacedContent(*selEndNode->AsContent())) {
return EditActionResult::CanceledResult();
}
if (!HTMLEditUtils::IsSimplyEditableNode(*selEndNode)) {
return EditActionResult::CanceledResult();
}
// If anchor node is in an HTML element which has inert attribute, we should
// do nothing.
// XXX HTMLEditor typically uses first range instead of anchor/focus range.
// Therefore, referring first range here is more reasonable than
// anchor/focus range of Selection.
nsIContent*
const selAnchorContent = SelectionRef().GetDirection() == eDirNext
? nsIContent::FromNode(selStartNode)
: nsIContent::FromNode(selEndNode);
if (selAnchorContent &&
HTMLEditUtils::ContentIsInert(*selAnchorContent->AsContent())) {
return EditActionResult::CanceledResult();
}
// XXX What does it mean the common ancestor is editable? I have no idea.
// It should be in same (active) editing host, and even if it's editable,
// there may be non-editable contents in the range.
nsINode* commonAncestor = range->GetClosestCommonInclusiveAncestor();
if (MOZ_UNLIKELY(!commonAncestor)) {
NS_WARNING(
"AbstractRange::GetClosestCommonInclusiveAncestor() returned nullptr");
return Err(NS_ERROR_FAILURE);
}
return HTMLEditUtils::IsSimplyEditableNode(*commonAncestor)
? EditActionResult::IgnoredResult()
: EditActionResult::CanceledResult();
}
MOZ_CAN_RUN_SCRIPT
static nsStaticAtom& MarginPropertyAtomForIndent(
nsIContent& aContent) {
nsAutoString direction;
DebugOnly<nsresult> rvIgnored = CSSEditUtils::GetComputedProperty(
aContent, *nsGkAtoms::direction, direction);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"CSSEditUtils::GetComputedProperty(nsGkAtoms::direction)"
" failed, but ignored");
return direction.EqualsLiteral(
"rtl") ? *nsGkAtoms::marginRight
: *nsGkAtoms::marginLeft;
}
nsresult HTMLEditor::EnsureCaretNotAfterInvisibleBRElement(
const Element& aEditingHost) {
MOZ_ASSERT(IsEditActionDataAvailable());
MOZ_ASSERT(SelectionRef().IsCollapsed());
// If we are after a padding `<br>` element for empty last line in the same
// block, then move selection to be before it
const nsRange* firstRange = SelectionRef().GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
EditorRawDOMPoint atSelectionStart(firstRange->StartRef());
if (NS_WARN_IF(!atSelectionStart.IsSet())) {
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(atSelectionStart.IsSetAndValid());
if (!atSelectionStart.IsInContentNode()) {
return NS_OK;
}
nsIContent* previousBRElement = HTMLEditUtils::GetPreviousContent(
atSelectionStart, {}, BlockInlineCheck::UseComputedDisplayStyle,
&aEditingHost);
if (!previousBRElement || !previousBRElement->IsHTMLElement(nsGkAtoms::br) ||
!previousBRElement->GetParent() ||
!EditorUtils::IsEditableContent(*previousBRElement->GetParent(),
EditorType::HTML) ||
!HTMLEditUtils::IsInvisibleBRElement(*previousBRElement)) {
return NS_OK;
}
const RefPtr<
const Element> blockElementAtSelectionStart =
HTMLEditUtils::GetInclusiveAncestorElement(
*atSelectionStart.ContainerAs<nsIContent>(),
HTMLEditUtils::ClosestBlockElement,
BlockInlineCheck::UseComputedDisplayStyle);
const RefPtr<
const Element> parentBlockElementOfBRElement =
HTMLEditUtils::GetAncestorElement(
*previousBRElement, HTMLEditUtils::ClosestBlockElement,
BlockInlineCheck::UseComputedDisplayStyle);
if (!blockElementAtSelectionStart ||
blockElementAtSelectionStart != parentBlockElementOfBRElement) {
return NS_OK;
}
// If we are here then the selection is right after a padding <br>
// element for empty last line that is in the same block as the
// selection. We need to move the selection start to be before the
// padding <br> element.
EditorRawDOMPoint atInvisibleBRElement(previousBRElement);
nsresult rv = CollapseSelectionTo(atInvisibleBRElement);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::CollapseSelectionTo() failed");
return rv;
}
nsresult HTMLEditor::MaybeCreatePaddingBRElementForEmptyEditor() {
MOZ_ASSERT(IsEditActionDataAvailable());
if (mPaddingBRElementForEmptyEditor) {
return NS_OK;
}
// XXX I think that we should not insert a <br> element if we're for a web
// content. Probably, this is required only by chrome editors such as
// the mail composer of Thunderbird and the composer of SeaMonkey.
const RefPtr<Element> bodyOrDocumentElement = GetRoot();
if (!bodyOrDocumentElement) {
return NS_OK;
}
// Skip adding the padding <br> element for empty editor if body
// is read-only.
if (!HTMLEditUtils::IsSimplyEditableNode(*bodyOrDocumentElement)) {
return NS_OK;
}
// Now we've got the body element. Iterate over the body element's children,
// looking for editable content. If no editable content is found, insert the
// padding <br> element.
EditorType editorType = GetEditorType();
bool isRootEditable =
EditorUtils::IsEditableContent(*bodyOrDocumentElement, editorType);
for (nsIContent* child = bodyOrDocumentElement->GetFirstChild(); child;
child = child->GetNextSibling()) {
if (EditorUtils::IsPaddingBRElementForEmptyEditor(*child) ||
!isRootEditable || EditorUtils::IsEditableContent(*child, editorType) ||
HTMLEditUtils::IsBlockElement(
*child, BlockInlineCheck::UseComputedDisplayStyle)) {
return NS_OK;
}
}
IgnoredErrorResult ignoredError;
AutoEditSubActionNotifier startToHandleEditSubAction(
*
this, EditSubAction::eCreatePaddingBRElementForEmptyEditor,
nsIEditor::eNone, ignoredError);
if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
return ignoredError.StealNSResult();
}
NS_WARNING_ASSERTION(
!ignoredError.Failed(),
"HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
Result<CreateElementResult, nsresult> insertPaddingBRElementResultOrError =
InsertBRElement(WithTransaction::Yes,
BRElementType::PaddingForEmptyEditor,
EditorDOMPoint(bodyOrDocumentElement, 0u));
if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) {
NS_WARNING(
"EditorBase::InsertBRElement(WithTransaction::Yes, "
"BRElementType::PaddingForEmptyEditor) failed");
return insertPaddingBRElementResultOrError.propagateErr();
}
CreateElementResult insertPaddingBRElementResult =
insertPaddingBRElementResultOrError.unwrap();
mPaddingBRElementForEmptyEditor =
HTMLBRElement::FromNode(insertPaddingBRElementResult.GetNewNode());
nsresult rv = insertPaddingBRElementResult.SuggestCaretPointTo(
*
this, {SuggestCaret::AndIgnoreTrivialError});
if (NS_FAILED(rv)) {
NS_WARNING(
"CaretPoint::SuggestCaretPointTo() failed");
return rv;
}
NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
"CaretPoint::SuggestCaretPointTo() failed, but ignored");
return NS_OK;
}
nsresult HTMLEditor::EnsureNoPaddingBRElementForEmptyEditor() {
MOZ_ASSERT(IsEditActionDataAvailable());
if (!mPaddingBRElementForEmptyEditor) {
return NS_OK;
}
// If we're an HTML editor, a mutation event listener may recreate padding
// <br> element for empty editor again during the call of
// DeleteNodeWithTransaction(). So, move it first.
RefPtr<HTMLBRElement> paddingBRElement(
std::move(mPaddingBRElementForEmptyEditor));
nsresult rv = DeleteNodeWithTransaction(*paddingBRElement);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::DeleteNodeWithTransaction() failed");
return rv;
}
nsresult HTMLEditor::ReflectPaddingBRElementForEmptyEditor() {
if (NS_WARN_IF(!mRootElement)) {
NS_WARNING(
"Failed to handle padding BR element due to no root element");
return NS_ERROR_FAILURE;
}
// The idea here is to see if the magic empty node has suddenly reappeared. If
// it has, set our state so we remember it. There is a tradeoff between doing
// here and at redo, or doing it everywhere else that might care. Since undo
// and redo are relatively rare, it makes sense to take the (small)
// performance hit here.
nsIContent* firstLeafChild = HTMLEditUtils::GetFirstLeafContent(
*mRootElement, {LeafNodeType::OnlyLeafNode});
if (firstLeafChild &&
EditorUtils::IsPaddingBRElementForEmptyEditor(*firstLeafChild)) {
mPaddingBRElementForEmptyEditor =
static_cast<HTMLBRElement*>(firstLeafChild);
}
else {
mPaddingBRElementForEmptyEditor = nullptr;
}
return NS_OK;
}
nsresult HTMLEditor::PrepareInlineStylesForCaret() {
MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
MOZ_ASSERT(SelectionRef().IsCollapsed());
// XXX This method works with the top level edit sub-action, but this
// must be wrong if we are handling nested edit action.
if (TopLevelEditSubActionDataRef().mDidDeleteSelection) {
switch (GetTopLevelEditSubAction()) {
case EditSubAction::eInsertText:
case EditSubAction::eInsertTextComingFromIME:
case EditSubAction::eDeleteSelectedContent: {
nsresult rv = ReapplyCachedStyles();
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::ReapplyCachedStyles() failed");
return rv;
}
break;
}
default:
break;
}
}
// For most actions we want to clear the cached styles, but there are
// exceptions
if (!IsPendingStyleCachePreservingSubAction(GetTopLevelEditSubAction())) {
TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear();
}
return NS_OK;
}
Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
EditSubAction aEditSubAction,
const nsAString& aInsertionString,
SelectionHandling aSelectionHandling) {
MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText ||
aEditSubAction == EditSubAction::eInsertTextComingFromIME);
MOZ_ASSERT_IF(aSelectionHandling == SelectionHandling::Ignore,
aEditSubAction == EditSubAction::eInsertTextComingFromIME);
{
Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
if (MOZ_UNLIKELY(result.isErr())) {
NS_WARNING(
"HTMLEditor::CanHandleHTMLEditSubAction() failed");
return result;
}
if (result.inspect().Canceled()) {
return result;
}
}
UndefineCaretBidiLevel();
// If the selection isn't collapsed, delete it. Don't delete existing inline
// tags, because we're hopefully going to insert text (bug 787432).
if (!SelectionRef().IsCollapsed() &&
aSelectionHandling == SelectionHandling::
Delete) {
nsresult rv =
DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip);
if (NS_FAILED(rv)) {
NS_WARNING(
"EditorBase::DeleteSelectionAsSubAction(nsIEditor::eNone, "
"nsIEditor::eNoStrip) failed");
return Err(rv);
}
}
const RefPtr<Element> editingHost =
ComputeEditingHost(LimitInBodyElement::No);
if (NS_WARN_IF(!editingHost)) {
return Err(NS_ERROR_FAILURE);
}
nsresult rv = 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) && SelectionRef().IsCollapsed()) {
nsresult rv = EnsureCaretNotAfterInvisibleBRElement(*editingHost);
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 = 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");
}
}
RefPtr<Document> document = GetDocument();
if (NS_WARN_IF(!document)) {
return Err(NS_ERROR_FAILURE);
}
const bool isHandlingComposition =
aEditSubAction == EditSubAction::eInsertTextComingFromIME;
auto pointToInsert = isHandlingComposition
? GetFirstIMESelectionStartPoint<EditorDOMPoint>()
: GetFirstSelectionStartPoint<EditorDOMPoint>();
if (!pointToInsert.IsSet()) {
if (MOZ_LIKELY(isHandlingComposition)) {
pointToInsert = GetFirstSelectionStartPoint<EditorDOMPoint>();
}
if (NS_WARN_IF(!pointToInsert.IsSet())) {
return Err(NS_ERROR_FAILURE);
}
}
// for every property that is set, insert a new inline style node
// XXX I think that if this is second or later composition update, we should
// not change the style because we won't update composition with keeping
// inline elements in composing range.
Result<EditorDOMPoint, nsresult> setStyleResult =
CreateStyleForInsertText(pointToInsert, *editingHost);
if (MOZ_UNLIKELY(setStyleResult.isErr())) {
NS_WARNING(
"HTMLEditor::CreateStyleForInsertText() failed");
return setStyleResult.propagateErr();
}
if (setStyleResult.inspect().IsSet()) {
pointToInsert = setStyleResult.unwrap();
}
if (NS_WARN_IF(!pointToInsert.IsSetAndValid()) ||
NS_WARN_IF(!pointToInsert.IsInContentNode())) {
return Err(NS_ERROR_FAILURE);
}
MOZ_ASSERT(pointToInsert.IsSetAndValid());
// If the point is not in an element which can contain text nodes, climb up
// the DOM tree.
if (!pointToInsert.IsInTextNode()) {
while (!HTMLEditUtils::CanNodeContain(*pointToInsert.GetContainer(),
*nsGkAtoms::textTagName)) {
if (NS_WARN_IF(pointToInsert.GetContainer() == editingHost) ||
NS_WARN_IF(!pointToInsert.GetContainerParentAs<nsIContent>())) {
NS_WARNING(
"Selection start point couldn't have text nodes");
return Err(NS_ERROR_FAILURE);
}
pointToInsert.Set(pointToInsert.ContainerAs<nsIContent>());
}
}
if (isHandlingComposition) {
if (aInsertionString.IsEmpty()) {
// Right now the WhiteSpaceVisibilityKeeper code bails on empty strings,
// but IME needs the InsertTextWithTransaction() call to still happen
// since empty strings are meaningful there.
Result<InsertTextResult, nsresult> insertEmptyTextResultOrError =
InsertTextWithTransaction(*document, aInsertionString, pointToInsert,
InsertTextTo::ExistingTextNodeIfAvailable);
if (MOZ_UNLIKELY(insertEmptyTextResultOrError.isErr())) {
NS_WARNING(
"HTMLEditor::InsertTextWithTransaction() failed");
return insertEmptyTextResultOrError.propagateErr();
}
const InsertTextResult insertEmptyTextResult =
insertEmptyTextResultOrError.unwrap();
nsresult rv = EnsureNoFollowingUnnecessaryLineBreak(
insertEmptyTextResult.EndOfInsertedTextRef());
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed");
return Err(rv);
}
// If we replaced non-empty composition string with an empty string,
// its preceding character may be a collapsible ASCII white-space.
// Therefore, we may need to insert a padding <br> after the white-space.
Result<CreateLineBreakResult, nsresult>
insertPaddingBRElementResultOrError = InsertPaddingBRElementIfNeeded(
insertEmptyTextResult.EndOfInsertedTextRef(), nsIEditor::eNoStrip,
*editingHost);
if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) {
NS_WARNING(
"HTMLEditor::InsertPaddingBRElementIfNeeded(eNoStrip) failed");
insertEmptyTextResult.IgnoreCaretPointSuggestion();
return insertPaddingBRElementResultOrError.propagateErr();
}
insertPaddingBRElementResultOrError.unwrap().IgnoreCaretPointSuggestion();
rv = insertEmptyTextResult.SuggestCaretPointTo(
*
this, {SuggestCaret::OnlyIfHasSuggestion,
SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
SuggestCaret::AndIgnoreTrivialError});
if (NS_FAILED(rv)) {
NS_WARNING(
"CaretPoint::SuggestCaretPointTo() failed");
return Err(rv);
}
NS_WARNING_ASSERTION(
rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
"CaretPoint::SuggestCaretPointTo() failed, but ignored");
return EditActionResult::HandledResult();
}
auto compositionEndPoint = GetLastIMESelectionEndPoint<EditorDOMPoint>();
if (!compositionEndPoint.IsSet()) {
compositionEndPoint = pointToInsert;
}
Result<InsertTextResult, nsresult> replaceTextResult =
WhiteSpaceVisibilityKeeper::ReplaceText(
*
this, aInsertionString,
EditorDOMRange(pointToInsert, compositionEndPoint),
InsertTextTo::ExistingTextNodeIfAvailable);
if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::ReplaceText() failed");
return replaceTextResult.propagateErr();
}
InsertTextResult unwrappedReplacedTextResult = replaceTextResult.unwrap();
nsresult rv = EnsureNoFollowingUnnecessaryLineBreak(
unwrappedReplacedTextResult.EndOfInsertedTextRef());
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed");
return Err(rv);
}
// CompositionTransaction should've set selection so that we should ignore
// caret suggestion.
unwrappedReplacedTextResult.IgnoreCaretPointSuggestion();
const auto newCompositionStartPoint =
GetFirstIMESelectionStartPoint<EditorDOMPoint>();
const auto newCompositionEndPoint =
GetLastIMESelectionEndPoint<EditorDOMPoint>();
if (NS_WARN_IF(!newCompositionStartPoint.IsSet()) ||
NS_WARN_IF(!newCompositionEndPoint.IsSet())) {
// Mutation event listener has changed the DOM tree...
return EditActionResult::HandledResult();
}
rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd(
newCompositionStartPoint.ToRawRangeBoundary(),
newCompositionEndPoint.ToRawRangeBoundary());
if (NS_FAILED(rv)) {
NS_WARNING(
"nsRange::SetStartAndEnd() failed");
return Err(rv);
}
return EditActionResult::HandledResult();
}
MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText);
// find where we are
EditorDOMPoint currentPoint(pointToInsert);
// is our text going to be PREformatted?
// We remember this so that we know how to handle tabs.
const bool isWhiteSpaceCollapsible = !EditorUtils::IsWhiteSpacePreformatted(
*pointToInsert.ContainerAs<nsIContent>());
const Maybe<LineBreakType> lineBreakType = GetPreferredLineBreakType(
*pointToInsert.ContainerAs<nsIContent>(), *editingHost);
if (NS_WARN_IF(lineBreakType.isNothing())) {
return Err(NS_ERROR_FAILURE);
}
// turn off the edit listener: we know how to
// build the "doc changed range" ourselves, and it's
// must faster to do it once here than to track all
// the changes one at a time.
AutoRestore<
bool> disableListener(
EditSubActionDataRef().mAdjustChangedRangeFromListener);
EditSubActionDataRef().mAdjustChangedRangeFromListener =
false;
// don't change my selection in subtransactions
AutoTransactionsConserveSelection dontChangeMySelection(*
this);
{
AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToInsert);
const auto GetInsertTextTo = [](int32_t aInclusiveNextLinefeedOffset,
uint32_t aLineStartOffset) {
if (aInclusiveNextLinefeedOffset > 0) {
return aLineStartOffset > 0
// If we'll insert a <br> and we're inserting 2nd or later
// line, we should always create new `Text` since it'll be
// between 2 <br> elements.
? InsertTextTo::AlwaysCreateNewTextNode
// If we'll insert a <br> and we're inserting first line,
// we should append text to preceding text node, but
// we don't want to insert it to a a following text node
// because of avoiding to split the `Text`.
: InsertTextTo::ExistingTextNodeIfAvailableAndNotStart;
}
// If we're inserting the last line, the text should be inserted to
// start of the following `Text` if there is or middle of the `Text`
// at insertion position if we're inserting only the line.
return InsertTextTo::ExistingTextNodeIfAvailable;
};
// for efficiency, break out the pre case separately. This is because
// its a lot cheaper to search the input string for only newlines than
// it is to search for both tabs and newlines.
if (!isWhiteSpaceCollapsible || IsPlaintextMailComposer()) {
if (*lineBreakType == LineBreakType::Linefeed) {
// Both Chrome and us inserts a preformatted linefeed with its own
// `Text` node in various cases. However, when inserting multiline
// text, we should insert a `Text` because Chrome does so and the
// comment field in https://discussions.apple.com/ handles the new
// `Text` to split each line into a paragraph. At that time, it's
// not assumed that inserted text is split at every linefeed.
MOZ_ASSERT(*lineBreakType == LineBreakType::Linefeed);
Result<InsertTextResult, nsresult> insertTextResult =
InsertTextWithTransaction(
*document, aInsertionString, currentPoint,
InsertTextTo::ExistingTextNodeIfAvailable);
if (MOZ_UNLIKELY(insertTextResult.isErr())) {
NS_WARNING(
"HTMLEditor::InsertTextWithTransaction() failed");
return insertTextResult.propagateErr();
}
// Ignore the caret suggestion because of `dontChangeMySelection`
// above.
insertTextResult.inspect().IgnoreCaretPointSuggestion();
if (insertTextResult.inspect().Handled()) {
pointToInsert = currentPoint = insertTextResult.unwrap()
.EndOfInsertedTextRef()
.To<EditorDOMPoint>();
}
else {
pointToInsert = currentPoint;
}
}
else {
MOZ_ASSERT(*lineBreakType == LineBreakType::BRElement);
uint32_t nextOffset = 0;
while (nextOffset < aInsertionString.Length()) {
const uint32_t lineStartOffset = nextOffset;
const int32_t inclusiveNextLinefeedOffset =
aInsertionString.FindChar(nsCRT::LF, lineStartOffset);
const uint32_t lineLength =
inclusiveNextLinefeedOffset != -1
?
static_cast<uint32_t>(inclusiveNextLinefeedOffset) -
lineStartOffset
: aInsertionString.Length() - lineStartOffset;
if (lineLength) {
// lineText does not include the preformatted line break.
const nsDependentSubstring lineText(aInsertionString,
lineStartOffset, lineLength);
Result<InsertTextResult, nsresult> insertTextResult =
InsertTextWithTransaction(
*document, lineText, currentPoint,
GetInsertTextTo(inclusiveNextLinefeedOffset,
lineStartOffset));
if (MOZ_UNLIKELY(insertTextResult.isErr())) {
NS_WARNING(
"HTMLEditor::InsertTextWithTransaction() failed");
return insertTextResult.propagateErr();
}
// Ignore the caret suggestion because of `dontChangeMySelection`
// above.
insertTextResult.inspect().IgnoreCaretPointSuggestion();
if (insertTextResult.inspect().Handled()) {
pointToInsert = currentPoint = insertTextResult.unwrap()
.EndOfInsertedTextRef()
.To<EditorDOMPoint>();
}
else {
pointToInsert = currentPoint;
}
if (inclusiveNextLinefeedOffset < 0) {
break;
// We reached the last line
}
}
MOZ_ASSERT(inclusiveNextLinefeedOffset >= 0);
Result<CreateLineBreakResult, nsresult> insertLineBreakResultOrError =
InsertLineBreak(WithTransaction::Yes, *lineBreakType,
currentPoint);
if (MOZ_UNLIKELY(insertLineBreakResultOrError.isErr())) {
NS_WARNING(nsPrintfCString(
"HTMLEditor::InsertLineBreak("
"WithTransaction::Yes, %s) failed",
ToString(*lineBreakType).c_str())
.get());
return insertLineBreakResultOrError.propagateErr();
}
CreateLineBreakResult insertLineBreakResult =
insertLineBreakResultOrError.unwrap();
// We don't want to update selection here because we've blocked
// InsertNodeTransaction updating selection with
// dontChangeMySelection.
insertLineBreakResult.IgnoreCaretPointSuggestion();
MOZ_ASSERT(!AllowsTransactionsToChangeSelection());
nextOffset = inclusiveNextLinefeedOffset + 1;
pointToInsert =
insertLineBreakResult.AfterLineBreak<EditorDOMPoint>();
currentPoint.SetAfter(&insertLineBreakResult.LineBreakContentRef());
if (NS_WARN_IF(!pointToInsert.IsSetAndValidInComposedDoc()) ||
NS_WARN_IF(!currentPoint.IsSetAndValidInComposedDoc())) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
}
}
}
else {
uint32_t nextOffset = 0;
while (nextOffset < aInsertionString.Length()) {
const uint32_t lineStartOffset = nextOffset;
const int32_t inclusiveNextLinefeedOffset =
aInsertionString.FindChar(nsCRT::LF, lineStartOffset);
const uint32_t lineLength =
inclusiveNextLinefeedOffset != -1
?
static_cast<uint32_t>(inclusiveNextLinefeedOffset) -
lineStartOffset
: aInsertionString.Length() - lineStartOffset;
if (lineLength) {
auto insertTextResult =
[&]() MOZ_CAN_RUN_SCRIPT -> Result<InsertTextResult, nsresult> {
// lineText does not include the preformatted line break.
const nsDependentSubstring lineText(aInsertionString,
lineStartOffset, lineLength);
if (!lineText.Contains(u
'\t')) {
return WhiteSpaceVisibilityKeeper::InsertText(
*
this, lineText, currentPoint,
GetInsertTextTo(inclusiveNextLinefeedOffset,
lineStartOffset));
}
nsAutoString formattedLineText(lineText);
formattedLineText.ReplaceSubstring(u
"\t"_ns, u
" "_ns);
return WhiteSpaceVisibilityKeeper::InsertText(
*
this, formattedLineText, currentPoint,
GetInsertTextTo(inclusiveNextLinefeedOffset, lineStartOffset));
}();
if (MOZ_UNLIKELY(insertTextResult.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::InsertText() failed");
return insertTextResult.propagateErr();
}
// Ignore the caret suggestion because of `dontChangeMySelection`
// above.
insertTextResult.inspect().IgnoreCaretPointSuggestion();
if (insertTextResult.inspect().Handled()) {
pointToInsert = currentPoint =
insertTextResult.unwrap().EndOfInsertedTextRef();
}
else {
pointToInsert = currentPoint;
}
if (inclusiveNextLinefeedOffset < 0) {
break;
// We reached the last line
}
}
Result<CreateLineBreakResult, nsresult> insertLineBreakResultOrError =
WhiteSpaceVisibilityKeeper::InsertLineBreak(*lineBreakType, *
this,
currentPoint);
if (MOZ_UNLIKELY(insertLineBreakResultOrError.isErr())) {
NS_WARNING(
nsPrintfCString(
"WhiteSpaceVisibilityKeeper::InsertLineBreak(%s) failed",
ToString(*lineBreakType).c_str())
.get());
return insertLineBreakResultOrError.propagateErr();
}
CreateLineBreakResult insertLineBreakResult =
insertLineBreakResultOrError.unwrap();
// TODO: Some methods called for handling non-preformatted text use
// ComputeEditingHost(). Therefore, they depend on the latest
// selection. So we cannot skip updating selection here.
nsresult rv = insertLineBreakResult.SuggestCaretPointTo(
*
this, {SuggestCaret::OnlyIfHasSuggestion,
SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
SuggestCaret::AndIgnoreTrivialError});
if (NS_FAILED(rv)) {
NS_WARNING(
"CreateElementResult::SuggestCaretPointTo() failed");
return Err(rv);
}
NS_WARNING_ASSERTION(
rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
"CreateElementResult::SuggestCaretPointTo() failed, but ignored");
nextOffset = inclusiveNextLinefeedOffset + 1;
pointToInsert = insertLineBreakResult.AfterLineBreak<EditorDOMPoint>();
currentPoint.SetAfter(&insertLineBreakResult.LineBreakContentRef());
if (NS_WARN_IF(!pointToInsert.IsSetAndValidInComposedDoc()) ||
NS_WARN_IF(!currentPoint.IsSetAndValidInComposedDoc())) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
}
}
// After this block, pointToInsert is updated by AutoTrackDOMPoint.
}
if (currentPoint.IsSet()) {
// If we appended a collapsible white-space to the end of the text node,
// its following content may be removed by the web app. Then, we need to
// keep it visible even if it becomes immediately before a block boundary.
// For referring the node from our mutation observer, we need to store the
// text node temporarily.
if (currentPoint.IsInTextNode() &&
MOZ_LIKELY(!currentPoint.IsStartOfContainer()) &&
currentPoint.IsEndOfContainer() &&
currentPoint.IsPreviousCharCollapsibleASCIISpace()) {
mLastCollapsibleWhiteSpaceAppendedTextNode =
currentPoint.ContainerAs<Text>();
}
nsresult rv = EnsureNoFollowingUnnecessaryLineBreak(currentPoint);
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed");
return Err(rv);
}
currentPoint.SetInterlinePosition(InterlinePosition::EndOfLine);
rv = CollapseSelectionTo(currentPoint);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return Err(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Selection::Collapse() failed, but ignored");
// manually update the doc changed range so that AfterEdit will clean up
// the correct portion of the document.
rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd(
pointToInsert.ToRawRangeBoundary(), currentPoint.ToRawRangeBoundary());
if (NS_FAILED(rv)) {
NS_WARNING(
"nsRange::SetStartAndEnd() failed");
return Err(rv);
}
return EditActionResult::HandledResult();
}
DebugOnly<nsresult> rvIgnored =
SelectionRef().SetInterlinePosition(InterlinePosition::EndOfLine);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Selection::SetInterlinePosition(InterlinePosition::"
"EndOfLine) failed, but ignored");
rv = TopLevelEditSubActionDataRef().mChangedRange->CollapseTo(pointToInsert);
if (NS_FAILED(rv)) {
NS_WARNING(
"nsRange::CollapseTo() failed");
return Err(rv);
}
return EditActionResult::HandledResult();
}
nsresult HTMLEditor::InsertLineBreakAsSubAction() {
MOZ_ASSERT(IsEditActionDataAvailable());
MOZ_ASSERT(!IsSelectionRangeContainerNotContent());
if (NS_WARN_IF(!mInitSucceeded)) {
return NS_ERROR_NOT_INITIALIZED;
}
{
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;
}
}
// XXX This may be called by execCommand() with "insertLineBreak".
// In such case, naming the transaction "TypingTxnName" is odd.
AutoPlaceholderBatch treatAsOneTransaction(*
this, *nsGkAtoms::TypingTxnName,
ScrollSelectionIntoView::Yes,
__FUNCTION__);
// calling it text insertion to trigger moz br treatment by rules
// XXX Why do we use EditSubAction::eInsertText here? Looks like
// EditSubAction::eInsertLineBreak or EditSubAction::eInsertNode
// is better.
IgnoredErrorResult ignoredError;
AutoEditSubActionNotifier startToHandleEditSubAction(
*
this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError);
if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
--> --------------------
--> maximum size reached
--> --------------------