Source code

Revision control

Other Tools

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "WSRunObject.h"
#include "HTMLEditUtils.h"
#include "mozilla/Assertions.h"
#include "mozilla/Casting.h"
#include "mozilla/EditorDOMPoint.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/mozalloc.h"
#include "mozilla/OwningNonNull.h"
#include "mozilla/RangeUtils.h"
#include "mozilla/SelectionState.h"
#include "mozilla/StaticPrefs_dom.h" // for StaticPrefs::dom_*
#include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_*
#include "mozilla/InternalMutationEvent.h"
#include "mozilla/dom/AncestorIterator.h"
#include "nsAString.h"
#include "nsCRT.h"
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsIContent.h"
#include "nsIContentInlines.h"
#include "nsISupportsImpl.h"
#include "nsRange.h"
#include "nsString.h"
#include "nsTextFragment.h"
namespace mozilla {
using namespace dom;
using LeafNodeType = HTMLEditUtils::LeafNodeType;
using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
const EditorDOMPoint& aPoint) const;
template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
const EditorRawDOMPoint& aPoint) const;
template WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom(
const EditorDOMPoint& aPoint) const;
template WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom(
const EditorRawDOMPoint& aPoint) const;
template EditorDOMPoint WSRunScanner::GetAfterLastVisiblePoint(
Text& aTextNode, const Element* aAncestorLimiter);
template EditorRawDOMPoint WSRunScanner::GetAfterLastVisiblePoint(
Text& aTextNode, const Element* aAncestorLimiter);
template EditorDOMPoint WSRunScanner::GetFirstVisiblePoint(
Text& aTextNode, const Element* aAncestorLimiter);
template EditorRawDOMPoint WSRunScanner::GetFirstVisiblePoint(
Text& aTextNode, const Element* aAncestorLimiter);
template nsresult WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
HTMLEditor& aHTMLEditor, const EditorDOMPoint& aScanStartPoint);
template nsresult WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
HTMLEditor& aHTMLEditor, const EditorRawDOMPoint& aScanStartPoint);
template nsresult WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
HTMLEditor& aHTMLEditor, const EditorDOMPointInText& aScanStartPoint);
template WSRunScanner::TextFragmentData::TextFragmentData(
const EditorDOMPoint& aPoint, const Element* aEditingHost);
template WSRunScanner::TextFragmentData::TextFragmentData(
const EditorRawDOMPoint& aPoint, const Element* aEditingHost);
template WSRunScanner::TextFragmentData::TextFragmentData(
const EditorDOMPointInText& aPoint, const Element* aEditingHost);
Result<EditorDOMPoint, nsresult>
WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement(
HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToSplit,
const Element& aSplittingBlockElement) {
if (NS_WARN_IF(!aPointToSplit.IsInContentNode()) ||
NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(aSplittingBlockElement)) ||
NS_WARN_IF(!EditorUtils::IsEditableContent(
*aPointToSplit.ContainerAsContent(), EditorType::HTML))) {
return Err(NS_ERROR_FAILURE);
}
// The container of aPointToSplit may be not splittable, e.g., selection
// may be collapsed **in** a `<br>` element or a comment node. So, look
// for splittable point with climbing the tree up.
EditorDOMPoint pointToSplit(aPointToSplit);
for (nsIContent* content : aPointToSplit.ContainerAsContent()
->InclusiveAncestorsOfType<nsIContent>()) {
if (content == &aSplittingBlockElement) {
break;
}
if (HTMLEditUtils::IsSplittableNode(*content)) {
break;
}
pointToSplit.Set(content);
}
{
AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), &pointToSplit);
nsresult rv = WhiteSpaceVisibilityKeeper::
MakeSureToKeepVisibleWhiteSpacesVisibleAfterSplit(aHTMLEditor,
pointToSplit);
if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
return Err(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_FAILED(rv)) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::"
"MakeSureToKeepVisibleWhiteSpacesVisibleAfterSplit() failed");
return Err(rv);
}
}
if (NS_WARN_IF(!pointToSplit.IsInContentNode()) ||
NS_WARN_IF(!pointToSplit.ContainerAsContent()->IsInclusiveDescendantOf(
&aSplittingBlockElement)) ||
NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(aSplittingBlockElement)) ||
NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(
*pointToSplit.ContainerAsContent()))) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
return pointToSplit;
}
// static
EditActionResult WhiteSpaceVisibilityKeeper::
MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement(
HTMLEditor& aHTMLEditor, Element& aLeftBlockElement,
Element& aRightBlockElement, const EditorDOMPoint& aAtRightBlockChild,
const Maybe<nsAtom*>& aListElementTagName,
const HTMLBRElement* aPrecedingInvisibleBRElement) {
MOZ_ASSERT(
EditorUtils::IsDescendantOf(aLeftBlockElement, aRightBlockElement));
MOZ_ASSERT(&aRightBlockElement == aAtRightBlockChild.GetContainer());
// NOTE: This method may extend deletion range:
// - to delete invisible white-spaces at end of aLeftBlockElement
// - to delete invisible white-spaces at start of
// afterRightBlockChild.GetChild()
// - to delete invisible white-spaces before afterRightBlockChild.GetChild()
// - to delete invisible `<br>` element at end of aLeftBlockElement
AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
nsresult rv = WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces(
aHTMLEditor, EditorDOMPoint::AtEndOf(aLeftBlockElement));
if (NS_FAILED(rv)) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces() "
"failed at left block");
return EditActionResult(rv);
}
// Check whether aLeftBlockElement is a descendant of aRightBlockElement.
if (aHTMLEditor.MayHaveMutationEventListeners()) {
EditorDOMPoint leftBlockContainingPointInRightBlockElement;
if (aHTMLEditor.MayHaveMutationEventListeners() &&
!EditorUtils::IsDescendantOf(
aLeftBlockElement, aRightBlockElement,
&leftBlockContainingPointInRightBlockElement)) {
NS_WARNING(
"Deleting invisible whitespace at end of left block element caused "
"moving the left block element outside the right block element");
return EditActionResult(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
if (leftBlockContainingPointInRightBlockElement != aAtRightBlockChild) {
NS_WARNING(
"Deleting invisible whitespace at end of left block element caused "
"changing the left block element in the right block element");
return EditActionResult(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
if (!EditorUtils::IsEditableContent(aRightBlockElement, EditorType::HTML)) {
NS_WARNING(
"Deleting invisible whitespace at end of left block element caused "
"making the right block element non-editable");
return EditActionResult(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
if (!EditorUtils::IsEditableContent(aLeftBlockElement, EditorType::HTML)) {
NS_WARNING(
"Deleting invisible whitespace at end of left block element caused "
"making the left block element non-editable");
return EditActionResult(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
}
OwningNonNull<Element> rightBlockElement = aRightBlockElement;
EditorDOMPoint afterRightBlockChild = aAtRightBlockChild.NextPoint();
{
// We can't just track rightBlockElement because it's an Element.
AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(),
&afterRightBlockChild);
nsresult rv = WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces(
aHTMLEditor, afterRightBlockChild);
if (NS_FAILED(rv)) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces() "
"failed at right block child");
return EditActionResult(rv);
}
// XXX AutoTrackDOMPoint instance, tracker, hasn't been destroyed here.
// Do we really need to do update rightBlockElement here??
// XXX And afterRightBlockChild.GetContainerAsElement() always returns
// an element pointer so that probably here should not use
// accessors of EditorDOMPoint, should use DOM API directly instead.
if (afterRightBlockChild.GetContainerAsElement()) {
rightBlockElement = *afterRightBlockChild.GetContainerAsElement();
} else if (NS_WARN_IF(
!afterRightBlockChild.GetContainerParentAsElement())) {
return EditActionResult(NS_ERROR_UNEXPECTED);
} else {
rightBlockElement = *afterRightBlockChild.GetContainerParentAsElement();
}
}
// Do br adjustment.
RefPtr<HTMLBRElement> invisibleBRElementAtEndOfLeftBlockElement =
WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound(
aHTMLEditor.GetActiveEditingHost(),
EditorDOMPoint::AtEndOf(aLeftBlockElement));
NS_ASSERTION(
aPrecedingInvisibleBRElement == invisibleBRElementAtEndOfLeftBlockElement,
"The preceding invisible BR element computation was different");
EditActionResult ret(NS_OK);
// NOTE: Keep syncing with CanMergeLeftAndRightBlockElements() of
// AutoInclusiveAncestorBlockElementsJoiner.
if (NS_WARN_IF(aListElementTagName.isSome())) {
// Since 2002, here was the following comment:
// > The idea here is to take all children in rightListElement that are
// > past offset, and pull them into leftlistElement.
// However, this has never been performed because we are here only when
// neither left list nor right list is a descendant of the other but
// in such case, getting a list item in the right list node almost
// always failed since a variable for offset of
// rightListElement->GetChildAt() was not initialized. So, it might be
// a bug, but we should keep this traditional behavior for now. If you
// find when we get here, please remove this comment if we don't need to
// do it. Otherwise, please move children of the right list node to the
// end of the left list node.
// XXX Although, we do nothing here, but for keeping traditional
// behavior, we should mark as handled.
ret.MarkAsHandled();
} else {
// XXX Why do we ignore the result of MoveOneHardLineContents()?
NS_ASSERTION(rightBlockElement == afterRightBlockChild.GetContainer(),
"The relation is not guaranteed but assumed");
#ifdef DEBUG
Result<bool, nsresult> firstLineHasContent =
aHTMLEditor.CanMoveOrDeleteSomethingInHardLine(EditorRawDOMPoint(
rightBlockElement, afterRightBlockChild.Offset()));
#endif // #ifdef DEBUG
MoveNodeResult moveNodeResult = aHTMLEditor.MoveOneHardLineContents(
EditorDOMPoint(rightBlockElement, afterRightBlockChild.Offset()),
EditorDOMPoint(&aLeftBlockElement, 0),
HTMLEditor::MoveToEndOfContainer::Yes);
if (NS_WARN_IF(moveNodeResult.EditorDestroyed())) {
return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(moveNodeResult.Succeeded(),
"HTMLEditor::MoveOneHardLineContents("
"MoveToEndOfContainer::Yes) failed, but ignored");
if (moveNodeResult.Succeeded()) {
#ifdef DEBUG
MOZ_ASSERT(!firstLineHasContent.isErr());
if (firstLineHasContent.inspect()) {
NS_ASSERTION(moveNodeResult.Handled(),
"Failed to consider whether moving or not something");
} else {
NS_ASSERTION(moveNodeResult.Ignored(),
"Failed to consider whether moving or not something");
}
#endif // #ifdef DEBUG
ret |= moveNodeResult;
}
// Now, all children of rightBlockElement were moved to leftBlockElement.
// So, afterRightBlockChild is now invalid.
afterRightBlockChild.Clear();
}
if (!invisibleBRElementAtEndOfLeftBlockElement) {
return ret;
}
rv = aHTMLEditor.DeleteNodeWithTransaction(
*invisibleBRElementAtEndOfLeftBlockElement);
if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed, but ignored");
return EditActionResult(rv);
}
return EditActionHandled();
}
// static
EditActionResult WhiteSpaceVisibilityKeeper::
MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement(
HTMLEditor& aHTMLEditor, Element& aLeftBlockElement,
Element& aRightBlockElement, const EditorDOMPoint& aAtLeftBlockChild,
nsIContent& aLeftContentInBlock,
const Maybe<nsAtom*>& aListElementTagName,
const HTMLBRElement* aPrecedingInvisibleBRElement) {
MOZ_ASSERT(
EditorUtils::IsDescendantOf(aRightBlockElement, aLeftBlockElement));
MOZ_ASSERT(
&aLeftBlockElement == &aLeftContentInBlock ||
EditorUtils::IsDescendantOf(aLeftContentInBlock, aLeftBlockElement));
MOZ_ASSERT(&aLeftBlockElement == aAtLeftBlockChild.GetContainer());
// NOTE: This method may extend deletion range:
// - to delete invisible white-spaces at start of aRightBlockElement
// - to delete invisible white-spaces before aRightBlockElement
// - to delete invisible white-spaces at start of aAtLeftBlockChild.GetChild()
// - to delete invisible white-spaces before aAtLeftBlockChild.GetChild()
// - to delete invisible `<br>` element before aAtLeftBlockChild.GetChild()
AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
nsresult rv = WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces(
aHTMLEditor, EditorDOMPoint(&aRightBlockElement, 0));
if (NS_FAILED(rv)) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces() failed "
"at right block");
return EditActionResult(rv);
}
// Check whether aRightBlockElement is a descendant of aLeftBlockElement.
if (aHTMLEditor.MayHaveMutationEventListeners()) {
EditorDOMPoint rightBlockContainingPointInLeftBlockElement;
if (aHTMLEditor.MayHaveMutationEventListeners() &&
!EditorUtils::IsDescendantOf(
aRightBlockElement, aLeftBlockElement,
&rightBlockContainingPointInLeftBlockElement)) {
NS_WARNING(
"Deleting invisible whitespace at start of right block element "
"caused moving the right block element outside the left block "
"element");
return EditActionResult(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
if (rightBlockContainingPointInLeftBlockElement != aAtLeftBlockChild) {
NS_WARNING(
"Deleting invisible whitespace at start of right block element "
"caused changing the right block element position in the left block "
"element");
return EditActionResult(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
if (!EditorUtils::IsEditableContent(aLeftBlockElement, EditorType::HTML)) {
NS_WARNING(
"Deleting invisible whitespace at start of right block element "
"caused making the left block element non-editable");
return EditActionResult(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
if (!EditorUtils::IsEditableContent(aRightBlockElement, EditorType::HTML)) {
NS_WARNING(
"Deleting invisible whitespace at start of right block element "
"caused making the right block element non-editable");
return EditActionResult(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
}
OwningNonNull<Element> originalLeftBlockElement = aLeftBlockElement;
OwningNonNull<Element> leftBlockElement = aLeftBlockElement;
EditorDOMPoint atLeftBlockChild(aAtLeftBlockChild);
{
// We can't just track leftBlockElement because it's an Element, so track
// something else.
AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), &atLeftBlockChild);
nsresult rv = WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces(
aHTMLEditor, EditorDOMPoint(atLeftBlockChild.GetContainer(),
atLeftBlockChild.Offset()));
if (NS_FAILED(rv)) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces() "
"failed at left block child");
return EditActionResult(rv);
}
// XXX AutoTrackDOMPoint instance, tracker, hasn't been destroyed here.
// Do we really need to do update aRightBlockElement here??
// XXX And atLeftBlockChild.GetContainerAsElement() always returns
// an element pointer so that probably here should not use
// accessors of EditorDOMPoint, should use DOM API directly instead.
if (atLeftBlockChild.GetContainerAsElement()) {
leftBlockElement = *atLeftBlockChild.GetContainerAsElement();
} else if (NS_WARN_IF(!atLeftBlockChild.GetContainerParentAsElement())) {
return EditActionResult(NS_ERROR_UNEXPECTED);
} else {
leftBlockElement = *atLeftBlockChild.GetContainerParentAsElement();
}
}
// Do br adjustment.
RefPtr<HTMLBRElement> invisibleBRElementBeforeLeftBlockElement =
WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound(
aHTMLEditor.GetActiveEditingHost(), atLeftBlockChild);
NS_ASSERTION(
aPrecedingInvisibleBRElement == invisibleBRElementBeforeLeftBlockElement,
"The preceding invisible BR element computation was different");
EditActionResult ret(NS_OK);
// NOTE: Keep syncing with CanMergeLeftAndRightBlockElements() of
// AutoInclusiveAncestorBlockElementsJoiner.
if (aListElementTagName.isSome()) {
// XXX Why do we ignore the error from MoveChildrenWithTransaction()?
MOZ_ASSERT(originalLeftBlockElement == atLeftBlockChild.GetContainer(),
"This is not guaranteed, but assumed");
#ifdef DEBUG
Result<bool, nsresult> rightBlockHasContent =
aHTMLEditor.CanMoveChildren(aRightBlockElement, aLeftBlockElement);
#endif // #ifdef DEBUG
MoveNodeResult moveNodeResult = aHTMLEditor.MoveChildrenWithTransaction(
aRightBlockElement, EditorDOMPoint(atLeftBlockChild.GetContainer(),
atLeftBlockChild.Offset()));
if (NS_WARN_IF(moveNodeResult.EditorDestroyed())) {
return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(
moveNodeResult.Succeeded(),
"HTMLEditor::MoveChildrenWithTransaction() failed, but ignored");
if (moveNodeResult.Succeeded()) {
ret |= moveNodeResult;
#ifdef DEBUG
MOZ_ASSERT(!rightBlockHasContent.isErr());
if (rightBlockHasContent.inspect()) {
NS_ASSERTION(moveNodeResult.Handled(),
"Failed to consider whether moving or not children");
} else {
NS_ASSERTION(moveNodeResult.Ignored(),
"Failed to consider whether moving or not children");
}
#endif // #ifdef DEBUG
}
// atLeftBlockChild was moved to rightListElement. So, it's invalid now.
atLeftBlockChild.Clear();
} else {
// Left block is a parent of right block, and the parent of the previous
// visible content. Right block is a child and contains the contents we
// want to move.
EditorDOMPoint atPreviousContent;
if (&aLeftContentInBlock == leftBlockElement) {
// We are working with valid HTML, aLeftContentInBlock is a block node,
// and is therefore allowed to contain aRightBlockElement. This is the
// simple case, we will simply move the content in aRightBlockElement
// out of its block.
atPreviousContent = atLeftBlockChild;
} else {
// We try to work as well as possible with HTML that's already invalid.
// Although "right block" is a block, and a block must not be contained
// in inline elements, reality is that broken documents do exist. The
// DIRECT parent of "left NODE" might be an inline element. Previous
// versions of this code skipped inline parents until the first block
// parent was found (and used "left block" as the destination).
// However, in some situations this strategy moves the content to an
// unexpected position. (see bug 200416) The new idea is to make the
// moving content a sibling, next to the previous visible content.
atPreviousContent.Set(&aLeftContentInBlock);
// We want to move our content just after the previous visible node.
atPreviousContent.AdvanceOffset();
}
MOZ_ASSERT(atPreviousContent.IsSet());
// Because we don't want the moving content to receive the style of the
// previous content, we split the previous content's style.
#ifdef DEBUG
Result<bool, nsresult> firstLineHasContent =
aHTMLEditor.CanMoveOrDeleteSomethingInHardLine(
EditorRawDOMPoint(&aRightBlockElement, 0));
#endif // #ifdef DEBUG
Element* editingHost = aHTMLEditor.GetActiveEditingHost();
// XXX It's odd to continue handling this edit action if there is no
// editing host.
if (!editingHost || &aLeftContentInBlock != editingHost) {
SplitNodeResult splitResult =
aHTMLEditor.SplitAncestorStyledInlineElementsAt(atPreviousContent,
nullptr, nullptr);
if (splitResult.Failed()) {
NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
return EditActionResult(splitResult.Rv());
}
if (splitResult.Handled()) {
if (splitResult.GetNextNode()) {
atPreviousContent.Set(splitResult.GetNextNode());
if (!atPreviousContent.IsSet()) {
NS_WARNING("Next node of split point was orphaned");
return EditActionResult(NS_ERROR_NULL_POINTER);
}
} else {
atPreviousContent = splitResult.SplitPoint();
if (!atPreviousContent.IsSet()) {
NS_WARNING("Split node was orphaned");
return EditActionResult(NS_ERROR_NULL_POINTER);
}
}
}
}
MoveNodeResult moveNodeResult = aHTMLEditor.MoveOneHardLineContents(
EditorDOMPoint(&aRightBlockElement, 0), atPreviousContent);
if (moveNodeResult.Failed()) {
NS_WARNING("HTMLEditor::MoveOneHardLineContents() failed");
return EditActionResult(moveNodeResult.Rv());
}
#ifdef DEBUG
MOZ_ASSERT(!firstLineHasContent.isErr());
if (firstLineHasContent.inspect()) {
NS_ASSERTION(moveNodeResult.Handled(),
"Failed to consider whether moving or not something");
} else {
NS_ASSERTION(moveNodeResult.Ignored(),
"Failed to consider whether moving or not something");
}
#endif // #ifdef DEBUG
ret |= moveNodeResult;
}
if (!invisibleBRElementBeforeLeftBlockElement) {
return ret;
}
rv = aHTMLEditor.DeleteNodeWithTransaction(
*invisibleBRElementBeforeLeftBlockElement);
if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed, but ignored");
return EditActionResult(rv);
}
return EditActionHandled();
}
// static
EditActionResult WhiteSpaceVisibilityKeeper::
MergeFirstLineOfRightBlockElementIntoLeftBlockElement(
HTMLEditor& aHTMLEditor, Element& aLeftBlockElement,
Element& aRightBlockElement, const Maybe<nsAtom*>& aListElementTagName,
const HTMLBRElement* aPrecedingInvisibleBRElement) {
MOZ_ASSERT(
!EditorUtils::IsDescendantOf(aLeftBlockElement, aRightBlockElement));
MOZ_ASSERT(
!EditorUtils::IsDescendantOf(aRightBlockElement, aLeftBlockElement));
// NOTE: This method may extend deletion range:
// - to delete invisible white-spaces at end of aLeftBlockElement
// - to delete invisible white-spaces at start of aRightBlockElement
// - to delete invisible `<br>` element at end of aLeftBlockElement
AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
// Adjust white-space at block boundaries
nsresult rv = WhiteSpaceVisibilityKeeper::
MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange(
aHTMLEditor,
EditorDOMRange(EditorDOMPoint::AtEndOf(aLeftBlockElement),
EditorDOMPoint(&aRightBlockElement, 0)));
if (NS_FAILED(rv)) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::"
"MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange() failed");
return EditActionResult(rv);
}
// Do br adjustment.
RefPtr<HTMLBRElement> invisibleBRElementAtEndOfLeftBlockElement =
WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound(
aHTMLEditor.GetActiveEditingHost(),
EditorDOMPoint::AtEndOf(aLeftBlockElement));
NS_ASSERTION(
aPrecedingInvisibleBRElement == invisibleBRElementAtEndOfLeftBlockElement,
"The preceding invisible BR element computation was different");
EditActionResult ret(NS_OK);
if (aListElementTagName.isSome() ||
aLeftBlockElement.NodeInfo()->NameAtom() ==
aRightBlockElement.NodeInfo()->NameAtom()) {
// Nodes are same type. merge them.
EditorDOMPoint atFirstChildOfRightNode;
nsresult rv = aHTMLEditor.JoinNearestEditableNodesWithTransaction(
aLeftBlockElement, aRightBlockElement, &atFirstChildOfRightNode);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::JoinNearestEditableNodesWithTransaction()"
" failed, but ignored");
if (aListElementTagName.isSome() && atFirstChildOfRightNode.IsSet()) {
CreateElementResult convertListTypeResult =
aHTMLEditor.ChangeListElementType(
aRightBlockElement, MOZ_KnownLive(*aListElementTagName.ref()),
*nsGkAtoms::li);
if (NS_WARN_IF(convertListTypeResult.EditorDestroyed())) {
return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(
convertListTypeResult.Succeeded(),
"HTMLEditor::ChangeListElementType() failed, but ignored");
}
ret.MarkAsHandled();
} else {
#ifdef DEBUG
Result<bool, nsresult> firstLineHasContent =
aHTMLEditor.CanMoveOrDeleteSomethingInHardLine(
EditorRawDOMPoint(&aRightBlockElement, 0));
#endif // #ifdef DEBUG
// Nodes are dissimilar types.
MoveNodeResult moveNodeResult = aHTMLEditor.MoveOneHardLineContents(
EditorDOMPoint(&aRightBlockElement, 0),
EditorDOMPoint(&aLeftBlockElement, 0),
HTMLEditor::MoveToEndOfContainer::Yes);
if (moveNodeResult.Failed()) {
NS_WARNING(
"HTMLEditor::MoveOneHardLineContents(MoveToEndOfContainer::Yes) "
"failed");
return EditActionResult(moveNodeResult.Rv());
}
#ifdef DEBUG
MOZ_ASSERT(!firstLineHasContent.isErr());
if (firstLineHasContent.inspect()) {
NS_ASSERTION(moveNodeResult.Handled(),
"Failed to consider whether moving or not something");
} else {
NS_ASSERTION(moveNodeResult.Ignored(),
"Failed to consider whether moving or not something");
}
#endif // #ifdef DEBUG
ret |= moveNodeResult;
}
if (!invisibleBRElementAtEndOfLeftBlockElement) {
return ret.MarkAsHandled();
}
rv = aHTMLEditor.DeleteNodeWithTransaction(
*invisibleBRElementAtEndOfLeftBlockElement);
if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
return ret.SetResult(NS_ERROR_EDITOR_DESTROYED);
}
// XXX In other top level if blocks, the result of
// DeleteNodeWithTransaction() is ignored. Why does only this result
// is respected?
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
return EditActionResult(rv);
}
return EditActionHandled();
}
// static
Result<RefPtr<Element>, nsresult> WhiteSpaceVisibilityKeeper::InsertBRElement(
HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToInsert) {
if (NS_WARN_IF(!aPointToInsert.IsSet())) {
return Err(NS_ERROR_INVALID_ARG);
}
// MOOSE: for now, we always assume non-PRE formatting. Fix this later.
// meanwhile, the pre case is handled in HandleInsertText() in
// HTMLEditSubActionHandler.cpp
Element* editingHost = aHTMLEditor.GetActiveEditingHost();
TextFragmentData textFragmentDataAtInsertionPoint(aPointToInsert,
editingHost);
if (NS_WARN_IF(!textFragmentDataAtInsertionPoint.IsInitialized())) {
return Err(NS_ERROR_FAILURE);
}
const EditorDOMRange invisibleLeadingWhiteSpaceRangeOfNewLine =
textFragmentDataAtInsertionPoint
.GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt(aPointToInsert);
const EditorDOMRange invisibleTrailingWhiteSpaceRangeOfCurrentLine =
textFragmentDataAtInsertionPoint
.GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt(aPointToInsert);
const Maybe<const VisibleWhiteSpacesData> visibleWhiteSpaces =
!invisibleLeadingWhiteSpaceRangeOfNewLine.IsPositioned() ||
!invisibleTrailingWhiteSpaceRangeOfCurrentLine.IsPositioned()
? Some(textFragmentDataAtInsertionPoint.VisibleWhiteSpacesDataRef())
: Nothing();
const PointPosition pointPositionWithVisibleWhiteSpaces =
visibleWhiteSpaces.isSome() && visibleWhiteSpaces.ref().IsInitialized()
? visibleWhiteSpaces.ref().ComparePoint(aPointToInsert)
: PointPosition::NotInSameDOMTree;
EditorDOMPoint pointToInsert(aPointToInsert);
{
// Some scoping for AutoTrackDOMPoint. This will track our insertion
// point while we tweak any surrounding white-space
AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), &pointToInsert);
if (invisibleTrailingWhiteSpaceRangeOfCurrentLine.IsPositioned()) {
if (!invisibleTrailingWhiteSpaceRangeOfCurrentLine.Collapsed()) {
// XXX Why don't we remove all of the invisible white-spaces?
MOZ_ASSERT(invisibleTrailingWhiteSpaceRangeOfCurrentLine.StartRef() ==
pointToInsert);
nsresult rv = aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
invisibleTrailingWhiteSpaceRangeOfCurrentLine.StartRef(),
invisibleTrailingWhiteSpaceRangeOfCurrentLine.EndRef(),
HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
return Err(rv);
}
}
}
// If new line will start with visible white-spaces, it needs to be start
// with an NBSP.
else if (pointPositionWithVisibleWhiteSpaces ==
PointPosition::StartOfFragment ||
pointPositionWithVisibleWhiteSpaces ==
PointPosition::MiddleOfFragment) {
EditorRawDOMPointInText atNextCharOfInsertionPoint =
textFragmentDataAtInsertionPoint.GetInclusiveNextEditableCharPoint(
pointToInsert);
if (atNextCharOfInsertionPoint.IsSet() &&
!atNextCharOfInsertionPoint.IsEndOfContainer() &&
atNextCharOfInsertionPoint.IsCharCollapsibleASCIISpace()) {
EditorRawDOMPointInText atPreviousCharOfNextCharOfInsertionPoint =
textFragmentDataAtInsertionPoint.GetPreviousEditableCharPoint(
atNextCharOfInsertionPoint);
if (!atPreviousCharOfNextCharOfInsertionPoint.IsSet() ||
atPreviousCharOfNextCharOfInsertionPoint.IsEndOfContainer() ||
!atPreviousCharOfNextCharOfInsertionPoint.IsCharASCIISpace()) {
// We are at start of non-nbsps. Convert to a single nbsp.
EditorRawDOMPointInText endOfCollapsibleASCIIWhiteSpaces =
textFragmentDataAtInsertionPoint
.GetEndOfCollapsibleASCIIWhiteSpaces(
atNextCharOfInsertionPoint, nsIEditor::eNone);
nsresult rv =
WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes(
aHTMLEditor,
EditorDOMRangeInTexts(atNextCharOfInsertionPoint,
endOfCollapsibleASCIIWhiteSpaces),
nsDependentSubstring(&HTMLEditUtils::kNBSP, 1));
if (NS_FAILED(rv)) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::"
"ReplaceTextAndRemoveEmptyTextNodes() failed");
return Err(rv);
}
}
}
}
if (invisibleLeadingWhiteSpaceRangeOfNewLine.IsPositioned()) {
if (!invisibleLeadingWhiteSpaceRangeOfNewLine.Collapsed()) {
// XXX Why don't we remove all of the invisible white-spaces?
MOZ_ASSERT(invisibleLeadingWhiteSpaceRangeOfNewLine.EndRef() ==
pointToInsert);
// XXX If the DOM tree has been changed above,
// invisibleLeadingWhiteSpaceRangeOfNewLine may be invalid now.
// So, we may do something wrong here.
nsresult rv = aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
invisibleLeadingWhiteSpaceRangeOfNewLine.StartRef(),
invisibleLeadingWhiteSpaceRangeOfNewLine.EndRef(),
HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
if (NS_FAILED(rv)) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::"
"DeleteTextAndTextNodesWithTransaction() failed");
return Err(rv);
}
}
}
// If the `<br>` element is put immediately after an NBSP, it should be
// replaced with an ASCII white-space.
else if (pointPositionWithVisibleWhiteSpaces ==
PointPosition::MiddleOfFragment ||
pointPositionWithVisibleWhiteSpaces ==
PointPosition::EndOfFragment) {
// XXX If the DOM tree has been changed above, pointToInsert` and/or
// `visibleWhiteSpaces` may be invalid. So, we may do
// something wrong here.
EditorDOMPointInText atNBSPReplacedWithASCIIWhiteSpace =
textFragmentDataAtInsertionPoint
.GetPreviousNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
pointToInsert);
if (atNBSPReplacedWithASCIIWhiteSpace.IsSet()) {
AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
nsresult rv = aHTMLEditor.ReplaceTextWithTransaction(
MOZ_KnownLive(*atNBSPReplacedWithASCIIWhiteSpace.ContainerAsText()),
atNBSPReplacedWithASCIIWhiteSpace.Offset(), 1, u" "_ns);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed failed");
return Err(rv);
}
}
}
}
Result<RefPtr<Element>, nsresult> resultOfInsertingBRElement =
aHTMLEditor.InsertBRElementWithTransaction(pointToInsert,
nsIEditor::eNone);
NS_WARNING_ASSERTION(
resultOfInsertingBRElement.isOk(),
"HTMLEditor::InsertBRElementWithTransaction(eNone) failed");
MOZ_ASSERT_IF(resultOfInsertingBRElement.isOk(),
resultOfInsertingBRElement.inspect());
return resultOfInsertingBRElement;
}
// static
nsresult WhiteSpaceVisibilityKeeper::ReplaceText(
HTMLEditor& aHTMLEditor, const nsAString& aStringToInsert,
const EditorDOMRange& aRangeToBeReplaced,
EditorRawDOMPoint* aPointAfterInsertedString /* = nullptr */) {
// MOOSE: for now, we always assume non-PRE formatting. Fix this later.
// meanwhile, the pre case is handled in HandleInsertText() in
// HTMLEditSubActionHandler.cpp
// MOOSE: for now, just getting the ws logic straight. This implementation
// is very slow. Will need to replace edit rules impl with a more efficient
// text sink here that does the minimal amount of searching/replacing/copying
if (aStringToInsert.IsEmpty()) {
MOZ_ASSERT(aRangeToBeReplaced.Collapsed());
if (aPointAfterInsertedString) {
*aPointAfterInsertedString = aRangeToBeReplaced.StartRef();
}
return NS_OK;
}
RefPtr<Element> editingHost = aHTMLEditor.GetActiveEditingHost();
TextFragmentData textFragmentDataAtStart(aRangeToBeReplaced.StartRef(),
editingHost);
if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized())) {
return NS_ERROR_FAILURE;
}
const bool isInsertionPointEqualsOrIsBeforeStartOfText =
aRangeToBeReplaced.StartRef().EqualsOrIsBefore(
textFragmentDataAtStart.StartRef());
TextFragmentData textFragmentDataAtEnd =
aRangeToBeReplaced.Collapsed()
? textFragmentDataAtStart
: TextFragmentData(aRangeToBeReplaced.EndRef(), editingHost);
if (NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) {
return NS_ERROR_FAILURE;
}
const bool isInsertionPointEqualsOrAfterEndOfText =
textFragmentDataAtEnd.EndRef().EqualsOrIsBefore(
aRangeToBeReplaced.EndRef());
const EditorDOMRange invisibleLeadingWhiteSpaceRangeAtStart =
textFragmentDataAtStart
.GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt(
aRangeToBeReplaced.StartRef());
const EditorDOMRange invisibleTrailingWhiteSpaceRangeAtEnd =
textFragmentDataAtEnd.GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt(
aRangeToBeReplaced.EndRef());
const Maybe<const VisibleWhiteSpacesData> visibleWhiteSpacesAtStart =
!invisibleLeadingWhiteSpaceRangeAtStart.IsPositioned()
? Some(textFragmentDataAtStart.VisibleWhiteSpacesDataRef())
: Nothing();
const PointPosition pointPositionWithVisibleWhiteSpacesAtStart =
visibleWhiteSpacesAtStart.isSome() &&
visibleWhiteSpacesAtStart.ref().IsInitialized()
? visibleWhiteSpacesAtStart.ref().ComparePoint(
aRangeToBeReplaced.StartRef())
: PointPosition::NotInSameDOMTree;
const Maybe<const VisibleWhiteSpacesData> visibleWhiteSpacesAtEnd =
!invisibleTrailingWhiteSpaceRangeAtEnd.IsPositioned()
? Some(textFragmentDataAtEnd.VisibleWhiteSpacesDataRef())
: Nothing();
const PointPosition pointPositionWithVisibleWhiteSpacesAtEnd =
visibleWhiteSpacesAtEnd.isSome() &&
visibleWhiteSpacesAtEnd.ref().IsInitialized()
? visibleWhiteSpacesAtEnd.ref().ComparePoint(
aRangeToBeReplaced.EndRef())
: PointPosition::NotInSameDOMTree;
EditorDOMPoint pointToInsert(aRangeToBeReplaced.StartRef());
nsAutoString theString(aStringToInsert);
{
// Some scoping for AutoTrackDOMPoint. This will track our insertion
// point while we tweak any surrounding white-space
AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), &pointToInsert);
if (invisibleTrailingWhiteSpaceRangeAtEnd.IsPositioned()) {
if (!invisibleTrailingWhiteSpaceRangeAtEnd.Collapsed()) {
// XXX Why don't we remove all of the invisible white-spaces?
MOZ_ASSERT(invisibleTrailingWhiteSpaceRangeAtEnd.StartRef() ==
pointToInsert);
nsresult rv = aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
invisibleTrailingWhiteSpaceRangeAtEnd.StartRef(),
invisibleTrailingWhiteSpaceRangeAtEnd.EndRef(),
HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
return rv;
}
}
}
// Replace an NBSP at inclusive next character of replacing range to an
// ASCII white-space if inserting into a visible white-space sequence.
// XXX With modifying the inserting string later, this creates a line break
// opportunity after the inserting string, but this causes
// inconsistent result with inserting order. E.g., type white-space
// n times with various order.
else if (pointPositionWithVisibleWhiteSpacesAtEnd ==
PointPosition::StartOfFragment ||
pointPositionWithVisibleWhiteSpacesAtEnd ==
PointPosition::MiddleOfFragment) {
EditorDOMPointInText atNBSPReplacedWithASCIIWhiteSpace =
textFragmentDataAtEnd
.GetInclusiveNextNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
aRangeToBeReplaced.EndRef());
if (atNBSPReplacedWithASCIIWhiteSpace.IsSet()) {
AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
nsresult rv = aHTMLEditor.ReplaceTextWithTransaction(
MOZ_KnownLive(*atNBSPReplacedWithASCIIWhiteSpace.ContainerAsText()),
atNBSPReplacedWithASCIIWhiteSpace.Offset(), 1, u" "_ns);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
return rv;
}
}
}
if (invisibleLeadingWhiteSpaceRangeAtStart.IsPositioned()) {
if (!invisibleLeadingWhiteSpaceRangeAtStart.Collapsed()) {
// XXX Why don't we remove all of the invisible white-spaces?
MOZ_ASSERT(invisibleLeadingWhiteSpaceRangeAtStart.EndRef() ==
pointToInsert);
// XXX If the DOM tree has been changed above,
// invisibleLeadingWhiteSpaceRangeAtStart may be invalid now.
// So, we may do something wrong here.
nsresult rv = aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
invisibleLeadingWhiteSpaceRangeAtStart.StartRef(),
invisibleLeadingWhiteSpaceRangeAtStart.EndRef(),
HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
return rv;
}
}
}
// Replace an NBSP at previous character of insertion point to an ASCII
// white-space if inserting into a visible white-space sequence.
// XXX With modifying the inserting string later, this creates a line break
// opportunity before the inserting string, but this causes
// inconsistent result with inserting order. E.g., type white-space
// n times with various order.
else if (pointPositionWithVisibleWhiteSpacesAtStart ==
PointPosition::MiddleOfFragment ||
pointPositionWithVisibleWhiteSpacesAtStart ==
PointPosition::EndOfFragment) {
// XXX If the DOM tree has been changed above, pointToInsert` and/or
// `visibleWhiteSpaces` may be invalid. So, we may do
// something wrong here.
EditorDOMPointInText atNBSPReplacedWithASCIIWhiteSpace =
textFragmentDataAtStart
.GetPreviousNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
pointToInsert);
if (atNBSPReplacedWithASCIIWhiteSpace.IsSet()) {
AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
nsresult rv = aHTMLEditor.ReplaceTextWithTransaction(
MOZ_KnownLive(*atNBSPReplacedWithASCIIWhiteSpace.ContainerAsText()),
atNBSPReplacedWithASCIIWhiteSpace.Offset(), 1, u" "_ns);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed failed");
return rv;
}
}
}
// After this block, pointToInsert is modified by AutoTrackDOMPoint.
}
// If white-space and/or linefeed characters are collapsible, and inserting
// string starts and/or ends with a collapsible characters, we need to
// replace them with NBSP for making sure the collapsible characters visible.
// FYI: There is no case only linefeeds are collapsible. So, we need to
// do the things only when white-spaces are collapsible.
MOZ_DIAGNOSTIC_ASSERT(!theString.IsEmpty());
if (NS_WARN_IF(!pointToInsert.IsInContentNode()) ||
!EditorUtils::IsWhiteSpacePreformatted(
*pointToInsert.ContainerAsContent())) {
const bool isNewLineCollapsible = !pointToInsert.IsInContentNode() ||
!EditorUtils::IsNewLinePreformatted(
*pointToInsert.ContainerAsContent());
auto isCollapsibleChar = [&isNewLineCollapsible](char16_t aChar) -> bool {
return nsCRT::IsAsciiSpace(aChar) &&
(isNewLineCollapsible || aChar != HTMLEditUtils::kNewLine);
};
if (isCollapsibleChar(theString[0])) {
// If inserting string will follow some invisible leading white-spaces,
// the string needs to start with an NBSP.
if (invisibleLeadingWhiteSpaceRangeAtStart.IsPositioned()) {
theString.SetCharAt(HTMLEditUtils::kNBSP, 0);
}
// If inserting around visible white-spaces, check whether the previous
// character of insertion point is an NBSP or an ASCII white-space.
else if (pointPositionWithVisibleWhiteSpacesAtStart ==
PointPosition::MiddleOfFragment ||
pointPositionWithVisibleWhiteSpacesAtStart ==
PointPosition::EndOfFragment) {
EditorDOMPointInText atPreviousChar =
textFragmentDataAtStart.GetPreviousEditableCharPoint(pointToInsert);
if (atPreviousChar.IsSet() && !atPreviousChar.IsEndOfContainer() &&
atPreviousChar.IsCharASCIISpace()) {
theString.SetCharAt(HTMLEditUtils::kNBSP, 0);
}
}
// If the insertion point is (was) before the start of text and it's
// immediately after a hard line break, the first ASCII white-space should
// be replaced with an NBSP for making it visible.
else if (textFragmentDataAtStart.StartsFromHardLineBreak() &&
isInsertionPointEqualsOrIsBeforeStartOfText) {
theString.SetCharAt(HTMLEditUtils::kNBSP, 0);
}
}
// Then the tail. Note that it may be the first character.
const uint32_t lastCharIndex = theString.Length() - 1;
if (isCollapsibleChar(theString[lastCharIndex])) {
// If inserting string will be followed by some invisible trailing
// white-spaces, the string needs to end with an NBSP.
if (invisibleTrailingWhiteSpaceRangeAtEnd.IsPositioned()) {
theString.SetCharAt(HTMLEditUtils::kNBSP, lastCharIndex);
}
// If inserting around visible white-spaces, check whether the inclusive
// next character of end of replaced range is an NBSP or an ASCII
// white-space.
if (pointPositionWithVisibleWhiteSpacesAtEnd ==
PointPosition::StartOfFragment ||
pointPositionWithVisibleWhiteSpacesAtEnd ==
PointPosition::MiddleOfFragment) {
EditorDOMPointInText atNextChar =
textFragmentDataAtEnd.GetInclusiveNextEditableCharPoint(
pointToInsert);
if (atNextChar.IsSet() && !atNextChar.IsEndOfContainer() &&
atNextChar.IsCharASCIISpace()) {
theString.SetCharAt(HTMLEditUtils::kNBSP, lastCharIndex);
}
}
// If the end of replacing range is (was) after the end of text and it's
// immediately before block boundary, the last ASCII white-space should
// be replaced with an NBSP for making it visible.
else if (textFragmentDataAtEnd.EndsByBlockBoundary() &&
isInsertionPointEqualsOrAfterEndOfText) {
theString.SetCharAt(HTMLEditUtils::kNBSP, lastCharIndex);
}
}
// Next, scan string for adjacent ws and convert to nbsp/space combos
// MOOSE: don't need to convert tabs here since that is done by
// WillInsertText() before we are called. Eventually, all that logic will
// be pushed down into here and made more efficient.
enum class PreviousChar {
NonCollapsibleChar,
CollapsibleChar,
PreformattedNewLine,
};
PreviousChar previousChar = PreviousChar::NonCollapsibleChar;
for (uint32_t i = 0; i <= lastCharIndex; i++) {
if (isCollapsibleChar(theString[i])) {
// If current char is collapsible and 2nd or latter character of
// collapsible characters, we need to make the previous character an
// NBSP for avoiding current character to be collapsed to it.
if (previousChar == PreviousChar::CollapsibleChar) {
MOZ_ASSERT(i > 0);
theString.SetCharAt(HTMLEditUtils::kNBSP, i - 1);
// Keep previousChar as PreviousChar::CollapsibleChar.
continue;
}
// If current character is a collapsbile white-space and the previous
// character is a preformatted linefeed, we need to replace the current
// character with an NBSP for avoiding collapsed with the previous
// linefeed.
if (previousChar == PreviousChar::PreformattedNewLine) {
MOZ_ASSERT(i > 0);
theString.SetCharAt(HTMLEditUtils::kNBSP, i);
previousChar = PreviousChar::NonCollapsibleChar;
continue;
}
previousChar = PreviousChar::CollapsibleChar;
continue;
}
if (theString[i] != HTMLEditUtils::kNewLine) {
previousChar = PreviousChar::NonCollapsibleChar;
continue;
}
// If current character is a preformatted linefeed and the previous
// character is collapbile white-space, the previous character will be
// collapsed into current linefeed. Therefore, we need to replace the
// previous character with an NBSP.
MOZ_ASSERT(!isNewLineCollapsible);
if (previousChar == PreviousChar::CollapsibleChar) {
MOZ_ASSERT(i > 0);
theString.SetCharAt(HTMLEditUtils::kNBSP, i - 1);
}
previousChar = PreviousChar::PreformattedNewLine;
}
}
// XXX If the point is not editable, InsertTextWithTransaction() returns
// error, but we keep handling it. But I think that it wastes the
// runtime cost. So, perhaps, we should return error code which couldn't
// modify it and make each caller of this method decide whether it should
// keep or stop handling the edit action.
if (!aHTMLEditor.GetDocument()) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::ReplaceText() lost proper document");
return NS_ERROR_UNEXPECTED;
}
OwningNonNull<Document> document = *aHTMLEditor.GetDocument();
nsresult rv = aHTMLEditor.InsertTextWithTransaction(
document, theString, pointToInsert, aPointAfterInsertedString);
if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_SUCCEEDED(rv)) {
return NS_OK;
}
NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed, but ignored");
// XXX Temporarily, set new insertion point to the original point.
if (aPointAfterInsertedString) {
*aPointAfterInsertedString = pointToInsert;
}
return NS_OK;
}
// static
nsresult WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace(
HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint) {
Element* editingHost = aHTMLEditor.GetActiveEditingHost();
TextFragmentData textFragmentDataAtDeletion(aPoint, editingHost);
if (NS_WARN_IF(!textFragmentDataAtDeletion.IsInitialized())) {
return NS_ERROR_FAILURE;
}
EditorDOMPointInText atPreviousCharOfStart =
textFragmentDataAtDeletion.GetPreviousEditableCharPoint(aPoint);
if (!atPreviousCharOfStart.IsSet() ||
atPreviousCharOfStart.IsEndOfContainer()) {
return NS_OK;
}
// If the char is a collapsible white-space or a non-collapsible new line
// but it can collapse adjacent white-spaces, we need to extend the range
// to delete all invisible white-spaces.
if (atPreviousCharOfStart.IsCharCollapsibleASCIISpace() ||
atPreviousCharOfStart
.IsCharPreformattedNewLineCollapsedWithWhiteSpaces()) {
EditorDOMPoint startToDelete =
textFragmentDataAtDeletion.GetFirstASCIIWhiteSpacePointCollapsedTo(
atPreviousCharOfStart, nsIEditor::ePrevious);
EditorDOMPoint endToDelete =
textFragmentDataAtDeletion.GetEndOfCollapsibleASCIIWhiteSpaces(
atPreviousCharOfStart, nsIEditor::ePrevious);
nsresult rv =
WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints(
aHTMLEditor, &startToDelete, &endToDelete);
if (NS_FAILED(rv)) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints() "
"failed");
return rv;
}
rv = aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
startToDelete, endToDelete,
HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
return rv;
}
if (atPreviousCharOfStart.IsCharCollapsibleNBSP()) {
EditorDOMPoint startToDelete(atPreviousCharOfStart);
EditorDOMPoint endToDelete(startToDelete.NextPoint());
nsresult rv =
WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints(
aHTMLEditor, &startToDelete, &endToDelete);
if (NS_FAILED(rv)) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints() "
"failed");
return rv;
}
rv = aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
startToDelete, endToDelete,
HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
return rv;
}
nsresult rv = aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
atPreviousCharOfStart, atPreviousCharOfStart.NextPoint(),
HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
return rv;
}
// static
nsresult WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace(
HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint) {
Element* editingHost = aHTMLEditor.GetActiveEditingHost();
TextFragmentData textFragmentDataAtDeletion(aPoint, editingHost);
if (NS_WARN_IF(!textFragmentDataAtDeletion.IsInitialized())) {
return NS_ERROR_FAILURE;
}
EditorDOMPointInText atNextCharOfStart =
textFragmentDataAtDeletion.GetInclusiveNextEditableCharPoint(aPoint);
if (!atNextCharOfStart.IsSet() || atNextCharOfStart.IsEndOfContainer()) {
return NS_OK;
}
// If the char is a collapsible white-space or a non-collapsible new line
// but it can collapse adjacent white-spaces, we need to extend the range
// to delete all invisible white-spaces.
if (atNextCharOfStart.IsCharCollapsibleASCIISpace() ||
atNextCharOfStart.IsCharPreformattedNewLineCollapsedWithWhiteSpaces()) {
EditorDOMPoint startToDelete =
textFragmentDataAtDeletion.GetFirstASCIIWhiteSpacePointCollapsedTo(
atNextCharOfStart, nsIEditor::eNext);
EditorDOMPoint endToDelete =
textFragmentDataAtDeletion.GetEndOfCollapsibleASCIIWhiteSpaces(
atNextCharOfStart, nsIEditor::eNext);
nsresult rv =
WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints(
aHTMLEditor, &startToDelete, &endToDelete);
if (NS_FAILED(rv)) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints() "
"failed");
return rv;
}
rv = aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
startToDelete, endToDelete,
HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
return rv;
}
if (atNextCharOfStart.IsCharCollapsibleNBSP()) {
EditorDOMPoint startToDelete(atNextCharOfStart);
EditorDOMPoint endToDelete(startToDelete.NextPoint());
nsresult rv =
WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints(
aHTMLEditor, &startToDelete, &endToDelete);
if (NS_FAILED(rv)) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints() "
"failed");
return rv;
}
rv = aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
startToDelete, endToDelete,
HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
return rv;
}
nsresult rv = aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
atNextCharOfStart, atNextCharOfStart.NextPoint(),
HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
return rv;
}
// static
nsresult WhiteSpaceVisibilityKeeper::DeleteContentNodeAndJoinTextNodesAroundIt(
HTMLEditor& aHTMLEditor, nsIContent& aContentToDelete,
const EditorDOMPoint& aCaretPoint) {
EditorDOMPoint atContent(&aContentToDelete);
if (!atContent.IsSet()) {
NS_WARNING("Deleting content node was an orphan node");
return NS_ERROR_FAILURE;
}
if (!HTMLEditUtils::IsRemovableNode(aContentToDelete)) {
NS_WARNING("Deleting content node wasn't removable");
return NS_ERROR_FAILURE;
}
nsresult rv = WhiteSpaceVisibilityKeeper::
MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange(
aHTMLEditor, EditorDOMRange(atContent, atContent.NextPoint()));
if (NS_FAILED(rv)) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::"
"MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange() failed");
return rv;
}
nsCOMPtr<nsIContent> previousEditableSibling =
HTMLEditUtils::GetPreviousSibling(
aContentToDelete, {WalkTreeOption::IgnoreNonEditableNode});
// Delete the node, and join like nodes if appropriate
rv = aHTMLEditor.DeleteNodeWithTransaction(aContentToDelete);
if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
return rv;
}
// Are they both text nodes? If so, join them!
// XXX This may cause odd behavior if there is non-editable nodes
// around the atomic content.
if (!aCaretPoint.IsInTextNode() || !previousEditableSibling ||
!previousEditableSibling->IsText()) {
return NS_OK;
}
nsIContent* nextEditableSibling = HTMLEditUtils::GetNextSibling(
*previousEditableSibling, {WalkTreeOption::IgnoreNonEditableNode});
if (aCaretPoint.GetContainer() != nextEditableSibling) {
return NS_OK;
}
EditorDOMPoint atFirstChildOfRightNode;
rv = aHTMLEditor.JoinNearestEditableNodesWithTransaction(
*previousEditableSibling,
MOZ_KnownLive(*aCaretPoint.GetContainerAsText()),
&atFirstChildOfRightNode);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::JoinNearestEditableNodesWithTransaction() failed");
return rv;
}
if (!atFirstChildOfRightNode.IsSet()) {
NS_WARNING(
"HTMLEditor::JoinNearestEditableNodesWithTransaction() didn't return "
"right node position");
return NS_ERROR_FAILURE;
}
// Fix up selection
rv = aHTMLEditor.CollapseSelectionTo(atFirstChildOfRightNode);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::CollapseSelectionTo() failed");
return rv;
}
template <typename PT, typename CT>
WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
const EditorDOMPointBase<PT, CT>& aPoint) const {
MOZ_ASSERT(aPoint.IsSet());
if (!TextFragmentDataAtStartRef().IsInitialized()) {
return WSScanResult(nullptr, WSType::UnexpectedError);
}
// If the range has visible text and start of the visible text is before
// aPoint, return previous character in the text.
const VisibleWhiteSpacesData& visibleWhiteSpaces =
TextFragmentDataAtStartRef().VisibleWhiteSpacesDataRef();
if (visibleWhiteSpaces.IsInitialized() &&
visibleWhiteSpaces.StartRef().IsBefore(aPoint)) {
// If the visible things are not editable, we shouldn't scan "editable"
// things now. Whether keep scanning editable things or not should be
// considered by the caller.
if (aPoint.GetChild() && !aPoint.GetChild()->IsEditable()) {
return WSScanResult(aPoint.GetChild(), WSType::SpecialContent);
}
EditorDOMPointInText atPreviousChar = GetPreviousEditableCharPoint(aPoint);
// When it's a non-empty text node, return it.
if (atPreviousChar.IsSet() && !atPreviousChar.IsContainerEmpty()) {
MOZ_ASSERT(!atPreviousChar.IsEndOfContainer());
return WSScanResult(atPreviousChar.NextPoint(),
atPreviousChar.IsCharCollapsibleASCIISpaceOrNBSP()
? WSType::CollapsibleWhiteSpaces
: WSType::NonCollapsibleCharacters);
}
}
// Otherwise, return the start of the range.
if (TextFragmentDataAtStartRef().GetStartReasonContent() !=
TextFragmentDataAtStartRef().StartRef().GetContainer()) {
// In this case, TextFragmentDataAtStartRef().StartRef().Offset() is not
// meaningful.
return WSScanResult(TextFragmentDataAtStartRef().GetStartReasonContent(),
TextFragmentDataAtStartRef().StartRawReason());
}
return WSScanResult(TextFragmentDataAtStartRef().StartRef(),
TextFragmentDataAtStartRef().StartRawReason());
}
template <typename PT, typename CT>
WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom(
const EditorDOMPointBase<PT, CT>& aPoint) const {
MOZ_ASSERT(aPoint.IsSet());
if (!TextFragmentDataAtStartRef().IsInitialized()) {
return WSScanResult(nullptr, WSType::UnexpectedError);
}
// If the range has visible text and aPoint equals or is before the end of the
// visible text, return inclusive next character in the text.
const VisibleWhiteSpacesData& visibleWhiteSpaces =
TextFragmentDataAtStartRef().VisibleWhiteSpacesDataRef();
if (visibleWhiteSpaces.IsInitialized() &&
aPoint.EqualsOrIsBefore(visibleWhiteSpaces.EndRef())) {
// If the visible things are not editable, we shouldn't scan "editable"
// things now. Whether keep scanning editable things or not should be
// considered by the caller.
if (aPoint.GetChild() && !aPoint.GetChild()->IsEditable()) {
return WSScanResult(aPoint.GetChild(), WSType::SpecialContent);
}
EditorDOMPointInText atNextChar = GetInclusiveNextEditableCharPoint(aPoint);
// When it's a non-empty text node, return it.
if (atNextChar.IsSet() && !atNextChar.IsContainerEmpty()) {
return WSScanResult(atNextChar,
!atNextChar.IsEndOfContainer() &&
atNextChar.IsCharCollapsibleASCIISpaceOrNBSP()
? WSType::CollapsibleWhiteSpaces
: WSType::NonCollapsibleCharacters);
}
}
// Otherwise, return the end of the range.
if (TextFragmentDataAtStartRef().GetEndReasonContent() !=
TextFragmentDataAtStartRef().EndRef().GetContainer()) {
// In this case, TextFragmentDataAtStartRef().EndRef().Offset() is not
// meaningful.
return WSScanResult(TextFragmentDataAtStartRef().GetEndReasonContent(),
TextFragmentDataAtStartRef().EndRawReason());
}
return WSScanResult(TextFragmentDataAtStartRef().EndRef(),
TextFragmentDataAtStartRef().EndRawReason());
}
template <typename EditorDOMPointType>
WSRunScanner::TextFragmentData::TextFragmentData(
const EditorDOMPointType& aPoint, const Element* aEditingHost)
: mEditingHost(aEditingHost) {
if (!aPoint.IsSetAndValid()) {
NS_WARNING("aPoint was invalid");
return;
}
if (!aPoint.IsInContentNode()) {
NS_WARNING("aPoint was in Document or DocumentFragment");
// I.e., we're try to modify outside of root element. We don't need to
// support such odd case because web apps cannot append text nodes as
// direct child of Document node.
return;
}
mScanStartPoint = aPoint;
NS_ASSERTION(EditorUtils::IsEditableContent(
*mScanStartPoint.ContainerAsContent(), EditorType::HTML),
"Given content is not editable");
NS_ASSERTION(
mScanStartPoint.ContainerAsContent()->GetAsElementOrParentElement(),
"Given content is not an element and an orphan node");
if (NS_WARN_IF(!EditorUtils::IsEditableContent(
*mScanStartPoint.ContainerAsContent(), EditorType::HTML))) {
return;
}
const Element* editableBlockElementOrInlineEditingHost =
HTMLEditUtils::GetInclusiveAncestorElement(
*mScanStartPoint.ContainerAsContent(),
HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost);
if (!editableBlockElementOrInlineEditingHost) {
NS_WARNING(
"HTMLEditUtils::GetInclusiveAncestorElement(HTMLEditUtils::"
"ClosestEditableBlockElementOrInlineEditingHost) couldn't find "
"editing host");
return;
}
mStart = BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
mScanStartPoint, *editableBlockElementOrInlineEditingHost, mEditingHost,
&mNBSPData);
MOZ_ASSERT_IF(mStart.IsNonCollapsibleCharacters(),
!mStart.PointRef().IsPreviousCharPreformattedNewLine());
MOZ_ASSERT_IF(mStart.IsPreformattedLineBreak(),
mStart.PointRef().IsPreviousCharPreformattedNewLine());
mEnd = BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
mScanStartPoint, *editableBlockElementOrInlineEditingHost, mEditingHost,
&mNBSPData);
MOZ_ASSERT_IF(mEnd.IsNonCollapsibleCharacters(),
!mEnd.PointRef().IsCharPreformattedNewLine());
MOZ_ASSERT_IF(mEnd.IsPreformattedLineBreak(),
mEnd.PointRef().IsCharPreformattedNewLine());
}
// static
template <typename EditorDOMPointType>
Maybe<WSRunScanner::TextFragmentData::BoundaryData> WSRunScanner::
TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceStartInTextNode(
const EditorDOMPointType& aPoint, NoBreakingSpaceData* aNBSPData) {
MOZ_ASSERT(aPoint.IsSetAndValid());
MOZ_DIAGNOSTIC_ASSERT(aPoint.IsInTextNode());
const bool isWhiteSpaceCollapsible =
!EditorUtils::IsWhiteSpacePreformatted(*aPoint.ContainerAsText());
const bool isNewLineCollapsible =
!EditorUtils::IsNewLinePreformatted(*aPoint.ContainerAsText());
const nsTextFragment& textFragment = aPoint.ContainerAsText()->TextFragment();
for (uint32_t i = std::min(aPoint.Offset(), textFragment.GetLength()); i;
i--) {
WSType wsTypeOfNonCollapsibleChar;
switch (textFragment.CharAt(AssertedCast<int32_t>(i - 1))) {
case HTMLEditUtils::kSpace:
case HTMLEditUtils::kCarridgeReturn:
case HTMLEditUtils::kTab:
if (isWhiteSpaceCollapsible) {
continue; // collapsible white-space or invisible white-space.
}
// preformatted white-space.
wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
break;
case HTMLEditUtils::kNewLine:
if (isNewLineCollapsible) {
continue; // collapsible linefeed.
}
// preformatted linefeed.
wsTypeOfNonCollapsibleChar = WSType::PreformattedLineBreak;
break;
case HTMLEditUtils::kNBSP:
if (isWhiteSpaceCollapsible) {
if (aNBSPData) {
aNBSPData->NotifyNBSP(