1416 lines
61 KiB
C++
1416 lines
61 KiB
C++
/* -*- 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 "WSRunScanner.h"
|
|
|
|
#include "EditorDOMPoint.h"
|
|
#include "EditorUtils.h"
|
|
#include "HTMLEditUtils.h"
|
|
|
|
#include "mozilla/Assertions.h"
|
|
#include "mozilla/dom/AncestorIterator.h"
|
|
|
|
#include "nsCRT.h"
|
|
#include "nsDebug.h"
|
|
#include "nsIContent.h"
|
|
#include "nsIContentInlines.h"
|
|
#include "nsTextFragment.h"
|
|
|
|
namespace mozilla {
|
|
|
|
using namespace dom;
|
|
|
|
using AncestorType = HTMLEditUtils::AncestorType;
|
|
using AncestorTypes = HTMLEditUtils::AncestorTypes;
|
|
using LeafNodeType = HTMLEditUtils::LeafNodeType;
|
|
using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
|
|
|
|
template WSRunScanner::TextFragmentData::TextFragmentData(
|
|
Scan aScanMode, const EditorDOMPoint& aPoint,
|
|
BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter);
|
|
template WSRunScanner::TextFragmentData::TextFragmentData(
|
|
Scan aScanMode, const EditorRawDOMPoint& aPoint,
|
|
BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter);
|
|
template WSRunScanner::TextFragmentData::TextFragmentData(
|
|
Scan aScanMode, const EditorDOMPointInText& aPoint,
|
|
BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter);
|
|
template WSRunScanner::TextFragmentData::TextFragmentData(
|
|
Scan aScanMode, const EditorRawDOMPointInText& aPoint,
|
|
BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter);
|
|
|
|
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
|
|
WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint,
|
|
const EditorDOMPoint& aPoint, BlockInlineCheck aBlockInlineCheck,
|
|
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
|
|
const nsIContent* aFollowingLimiterContent);
|
|
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
|
|
WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint,
|
|
const EditorRawDOMPoint& aPoint, BlockInlineCheck aBlockInlineCheck,
|
|
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
|
|
const nsIContent* aFollowingLimiterContent);
|
|
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
|
|
WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint,
|
|
const EditorDOMPointInText& aPoint, BlockInlineCheck aBlockInlineCheck,
|
|
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
|
|
const nsIContent* aFollowingLimiterContent);
|
|
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
|
|
WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint,
|
|
const EditorRawDOMPointInText& aPoint, BlockInlineCheck aBlockInlineCheck,
|
|
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
|
|
const nsIContent* aFollowingLimiterContent);
|
|
|
|
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
|
|
WSRunScanner::TextFragmentData::GetPreviousCharPoint,
|
|
const EditorDOMPoint& aPoint, BlockInlineCheck aBlockInlineCheck,
|
|
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
|
|
const nsIContent* aPrecedingLimiterContent);
|
|
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
|
|
WSRunScanner::TextFragmentData::GetPreviousCharPoint,
|
|
const EditorRawDOMPoint& aPoint, BlockInlineCheck aBlockInlineCheck,
|
|
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
|
|
const nsIContent* aPrecedingLimiterContent);
|
|
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
|
|
WSRunScanner::TextFragmentData::GetPreviousCharPoint,
|
|
const EditorDOMPointInText& aPoint, BlockInlineCheck aBlockInlineCheck,
|
|
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
|
|
const nsIContent* aPrecedingLimiterContent);
|
|
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
|
|
WSRunScanner::TextFragmentData::GetPreviousCharPoint,
|
|
const EditorRawDOMPointInText& aPoint, BlockInlineCheck aBlockInlineCheck,
|
|
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
|
|
const nsIContent* aPrecedingLimiterContent);
|
|
|
|
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
|
|
WSRunScanner::TextFragmentData::GetEndOfCollapsibleASCIIWhiteSpaces,
|
|
const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
|
|
nsIEditor::EDirection aDirectionToDelete,
|
|
BlockInlineCheck aBlockInlineCheck,
|
|
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
|
|
const nsIContent* aFollowingLimiterContent);
|
|
|
|
NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
|
|
WSRunScanner::TextFragmentData::GetFirstASCIIWhiteSpacePointCollapsedTo,
|
|
const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
|
|
nsIEditor::EDirection aDirectionToDelete,
|
|
BlockInlineCheck aBlockInlineCheck,
|
|
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
|
|
const nsIContent* aPrecedingLimiterContent);
|
|
|
|
constexpr static const AncestorTypes kScanAnyRootAncestorTypes = {
|
|
// If the point is in a block, we need to scan only in the block
|
|
AncestorType::ClosestBlockElement,
|
|
// So, we want a root element of the (shadow) tree root element of the
|
|
// point
|
|
// if there is no parent block
|
|
AncestorType::AllowRootOrAncestorLimiterElement,
|
|
// Basically, given point shouldn't be a void element, so, ignore
|
|
// ancestor
|
|
// void elements
|
|
AncestorType::IgnoreHRElement};
|
|
constexpr static const AncestorTypes kScanEditableRootAncestorTypes = {
|
|
// Only editable elements
|
|
AncestorType::EditableElement,
|
|
// And the others are same as kScanAnyRootAncestorTypes
|
|
AncestorType::ClosestBlockElement,
|
|
AncestorType::AllowRootOrAncestorLimiterElement,
|
|
AncestorType::IgnoreHRElement};
|
|
|
|
template <typename EditorDOMPointType>
|
|
WSRunScanner::TextFragmentData::TextFragmentData(
|
|
Scan aScanMode, const EditorDOMPointType& aPoint,
|
|
BlockInlineCheck aBlockInlineCheck,
|
|
const Element* aAncestorLimiter /* = nullptr */)
|
|
: mBlockInlineCheck(aBlockInlineCheck), mScanMode(aScanMode) {
|
|
if (NS_WARN_IF(!aPoint.IsInContentNodeAndValidInComposedDoc()) ||
|
|
NS_WARN_IF(!aPoint.GetContainerOrContainerParentElement())) {
|
|
// We don't need to support composing in uncomposed tree.
|
|
return;
|
|
}
|
|
|
|
MOZ_ASSERT_IF(
|
|
aAncestorLimiter,
|
|
aPoint.template ContainerAs<nsIContent>()->IsInclusiveDescendantOf(
|
|
aAncestorLimiter));
|
|
|
|
mScanStartPoint = aPoint.template To<EditorDOMPoint>();
|
|
const Element* const
|
|
editableBlockElementOrInlineEditingHostOrNonEditableRootElement =
|
|
HTMLEditUtils::GetInclusiveAncestorElement(
|
|
*mScanStartPoint.ContainerAs<nsIContent>(),
|
|
aScanMode == Scan::EditableNodes ? kScanEditableRootAncestorTypes
|
|
: kScanAnyRootAncestorTypes,
|
|
aBlockInlineCheck, aAncestorLimiter);
|
|
if (NS_WARN_IF(
|
|
!editableBlockElementOrInlineEditingHostOrNonEditableRootElement)) {
|
|
return;
|
|
}
|
|
mStart = BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
|
|
aScanMode, mScanStartPoint, &mNBSPData, aBlockInlineCheck,
|
|
ShouldStopAtNonEditableNode(aScanMode),
|
|
*editableBlockElementOrInlineEditingHostOrNonEditableRootElement);
|
|
MOZ_ASSERT_IF(mStart.IsNonCollapsibleCharacters(),
|
|
!mStart.PointRef().IsPreviousCharPreformattedNewLine());
|
|
MOZ_ASSERT_IF(mStart.IsPreformattedLineBreak(),
|
|
mStart.PointRef().IsPreviousCharPreformattedNewLine());
|
|
mEnd = BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
|
|
aScanMode, mScanStartPoint, &mNBSPData, aBlockInlineCheck,
|
|
ShouldStopAtNonEditableNode(aScanMode),
|
|
*editableBlockElementOrInlineEditingHostOrNonEditableRootElement);
|
|
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,
|
|
BlockInlineCheck aBlockInlineCheck) {
|
|
MOZ_ASSERT(aPoint.IsSetAndValid());
|
|
MOZ_DIAGNOSTIC_ASSERT(aPoint.IsInTextNode());
|
|
|
|
const bool isWhiteSpaceCollapsible = !EditorUtils::IsWhiteSpacePreformatted(
|
|
*aPoint.template ContainerAs<Text>());
|
|
const bool isNewLineCollapsible =
|
|
!EditorUtils::IsNewLinePreformatted(*aPoint.template ContainerAs<Text>());
|
|
const nsTextFragment& textFragment =
|
|
aPoint.template ContainerAs<Text>()->TextFragment();
|
|
for (uint32_t i = std::min(aPoint.Offset(), textFragment.GetLength()); i;
|
|
i--) {
|
|
WSType wsTypeOfNonCollapsibleChar;
|
|
switch (textFragment.CharAt(i - 1)) {
|
|
case HTMLEditUtils::kSpace:
|
|
case HTMLEditUtils::kCarriageReturn:
|
|
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(
|
|
EditorDOMPointInText(aPoint.template ContainerAs<Text>(),
|
|
i - 1),
|
|
NoBreakingSpaceData::Scanning::Backward);
|
|
}
|
|
continue;
|
|
}
|
|
// NBSP is never converted from collapsible white-space/linefeed.
|
|
wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
|
|
break;
|
|
default:
|
|
MOZ_ASSERT(!nsCRT::IsAsciiSpace(textFragment.CharAt(i - 1)));
|
|
wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
|
|
break;
|
|
}
|
|
|
|
return Some(BoundaryData(
|
|
EditorDOMPoint(aPoint.template ContainerAs<Text>(), i),
|
|
*aPoint.template ContainerAs<Text>(), wsTypeOfNonCollapsibleChar));
|
|
}
|
|
|
|
return Nothing();
|
|
}
|
|
|
|
// static
|
|
template <typename EditorDOMPointType>
|
|
WSRunScanner::TextFragmentData::BoundaryData WSRunScanner::TextFragmentData::
|
|
BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
|
|
Scan aScanMode, const EditorDOMPointType& aPoint,
|
|
NoBreakingSpaceData* aNBSPData, BlockInlineCheck aBlockInlineCheck,
|
|
StopAtNonEditableNode aStopAtNonEditableNode,
|
|
const Element& aAncestorLimiter) {
|
|
MOZ_ASSERT(aPoint.IsSetAndValid());
|
|
MOZ_ASSERT_IF(aScanMode == Scan::EditableNodes,
|
|
// FIXME: Both values should be true here.
|
|
HTMLEditUtils::IsSimplyEditableNode(*aPoint.GetContainer()) ==
|
|
HTMLEditUtils::IsSimplyEditableNode(aAncestorLimiter));
|
|
|
|
if (aPoint.IsInTextNode() && !aPoint.IsStartOfContainer()) {
|
|
Maybe<BoundaryData> startInTextNode =
|
|
BoundaryData::ScanCollapsibleWhiteSpaceStartInTextNode(
|
|
aPoint, aNBSPData, aBlockInlineCheck);
|
|
if (startInTextNode.isSome()) {
|
|
return startInTextNode.ref();
|
|
}
|
|
// The text node does not have visible character, let's keep scanning
|
|
// preceding nodes.
|
|
return BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
|
|
aScanMode, EditorDOMPoint(aPoint.template ContainerAs<Text>(), 0),
|
|
aNBSPData, aBlockInlineCheck, aStopAtNonEditableNode, aAncestorLimiter);
|
|
}
|
|
|
|
// Then, we need to check previous leaf node.
|
|
const auto leafNodeTypes =
|
|
aStopAtNonEditableNode == StopAtNonEditableNode::Yes
|
|
? LeafNodeTypes{LeafNodeType::LeafNodeOrNonEditableNode}
|
|
: LeafNodeTypes{LeafNodeType::OnlyLeafNode};
|
|
nsIContent* previousLeafContentOrBlock =
|
|
HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
|
|
aPoint, leafNodeTypes, aBlockInlineCheck, &aAncestorLimiter);
|
|
if (!previousLeafContentOrBlock) {
|
|
// No previous content means that we reached the aAncestorLimiter boundary.
|
|
return BoundaryData(
|
|
aPoint, const_cast<Element&>(aAncestorLimiter),
|
|
HTMLEditUtils::IsBlockElement(
|
|
aAncestorLimiter, RespectParentBlockBoundary(aBlockInlineCheck))
|
|
? WSType::CurrentBlockBoundary
|
|
: WSType::InlineEditingHostBoundary);
|
|
}
|
|
|
|
if (HTMLEditUtils::IsBlockElement(*previousLeafContentOrBlock,
|
|
aBlockInlineCheck)) {
|
|
return BoundaryData(aPoint, *previousLeafContentOrBlock,
|
|
WSType::OtherBlockBoundary);
|
|
}
|
|
|
|
if (!previousLeafContentOrBlock->IsText() ||
|
|
(aStopAtNonEditableNode == StopAtNonEditableNode::Yes &&
|
|
HTMLEditUtils::IsSimplyEditableNode(*previousLeafContentOrBlock) !=
|
|
HTMLEditUtils::IsSimplyEditableNode(aAncestorLimiter))) {
|
|
// it's a break or a special node, like <img>, that is not a block and
|
|
// not a break but still serves as a terminator to ws runs.
|
|
return BoundaryData(aPoint, *previousLeafContentOrBlock,
|
|
previousLeafContentOrBlock->IsHTMLElement(nsGkAtoms::br)
|
|
? WSType::BRElement
|
|
: WSType::SpecialContent);
|
|
}
|
|
|
|
if (!previousLeafContentOrBlock->AsText()->TextLength()) {
|
|
// If it's an empty text node, keep looking for its previous leaf content.
|
|
// Note that even if the empty text node is preformatted, we should keep
|
|
// looking for the previous one.
|
|
return BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
|
|
aScanMode,
|
|
EditorDOMPointInText(previousLeafContentOrBlock->AsText(), 0),
|
|
aNBSPData, aBlockInlineCheck, aStopAtNonEditableNode, aAncestorLimiter);
|
|
}
|
|
|
|
Maybe<BoundaryData> startInTextNode =
|
|
BoundaryData::ScanCollapsibleWhiteSpaceStartInTextNode(
|
|
EditorDOMPointInText::AtEndOf(*previousLeafContentOrBlock->AsText()),
|
|
aNBSPData, aBlockInlineCheck);
|
|
if (startInTextNode.isSome()) {
|
|
return startInTextNode.ref();
|
|
}
|
|
|
|
// The text node does not have visible character, let's keep scanning
|
|
// preceding nodes.
|
|
return BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
|
|
aScanMode, EditorDOMPointInText(previousLeafContentOrBlock->AsText(), 0),
|
|
aNBSPData, aBlockInlineCheck, aStopAtNonEditableNode, aAncestorLimiter);
|
|
}
|
|
|
|
// static
|
|
template <typename EditorDOMPointType>
|
|
Maybe<WSRunScanner::TextFragmentData::BoundaryData> WSRunScanner::
|
|
TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceEndInTextNode(
|
|
const EditorDOMPointType& aPoint, NoBreakingSpaceData* aNBSPData,
|
|
BlockInlineCheck aBlockInlineCheck) {
|
|
MOZ_ASSERT(aPoint.IsSetAndValid());
|
|
MOZ_DIAGNOSTIC_ASSERT(aPoint.IsInTextNode());
|
|
|
|
const bool isWhiteSpaceCollapsible = !EditorUtils::IsWhiteSpacePreformatted(
|
|
*aPoint.template ContainerAs<Text>());
|
|
const bool isNewLineCollapsible =
|
|
!EditorUtils::IsNewLinePreformatted(*aPoint.template ContainerAs<Text>());
|
|
const nsTextFragment& textFragment =
|
|
aPoint.template ContainerAs<Text>()->TextFragment();
|
|
for (uint32_t i = aPoint.Offset(); i < textFragment.GetLength(); i++) {
|
|
WSType wsTypeOfNonCollapsibleChar;
|
|
switch (textFragment.CharAt(i)) {
|
|
case HTMLEditUtils::kSpace:
|
|
case HTMLEditUtils::kCarriageReturn:
|
|
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(
|
|
EditorDOMPointInText(aPoint.template ContainerAs<Text>(), i),
|
|
NoBreakingSpaceData::Scanning::Forward);
|
|
}
|
|
continue;
|
|
}
|
|
// NBSP is never converted from collapsible white-space/linefeed.
|
|
wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
|
|
break;
|
|
default:
|
|
MOZ_ASSERT(!nsCRT::IsAsciiSpace(textFragment.CharAt(i)));
|
|
wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
|
|
break;
|
|
}
|
|
|
|
return Some(BoundaryData(
|
|
EditorDOMPoint(aPoint.template ContainerAs<Text>(), i),
|
|
*aPoint.template ContainerAs<Text>(), wsTypeOfNonCollapsibleChar));
|
|
}
|
|
|
|
return Nothing();
|
|
}
|
|
|
|
// static
|
|
template <typename EditorDOMPointType>
|
|
WSRunScanner::TextFragmentData::BoundaryData
|
|
WSRunScanner::TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
|
|
Scan aScanMode, const EditorDOMPointType& aPoint,
|
|
NoBreakingSpaceData* aNBSPData, BlockInlineCheck aBlockInlineCheck,
|
|
StopAtNonEditableNode aStopAtNonEditableNode,
|
|
const Element& aAncestorLimiter) {
|
|
MOZ_ASSERT(aPoint.IsSetAndValid());
|
|
MOZ_ASSERT_IF(aScanMode == Scan::EditableNodes,
|
|
// FIXME: Both values should be true here.
|
|
HTMLEditUtils::IsSimplyEditableNode(*aPoint.GetContainer()) ==
|
|
HTMLEditUtils::IsSimplyEditableNode(aAncestorLimiter));
|
|
|
|
if (aPoint.IsInTextNode() && !aPoint.IsEndOfContainer()) {
|
|
Maybe<BoundaryData> endInTextNode =
|
|
BoundaryData::ScanCollapsibleWhiteSpaceEndInTextNode(aPoint, aNBSPData,
|
|
aBlockInlineCheck);
|
|
if (endInTextNode.isSome()) {
|
|
return endInTextNode.ref();
|
|
}
|
|
// The text node does not have visible character, let's keep scanning
|
|
// following nodes.
|
|
return BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
|
|
aScanMode,
|
|
EditorDOMPointInText::AtEndOf(*aPoint.template ContainerAs<Text>()),
|
|
aNBSPData, aBlockInlineCheck, aStopAtNonEditableNode, aAncestorLimiter);
|
|
}
|
|
|
|
// Then, we need to check next leaf node.
|
|
const auto leafNodeTypes =
|
|
aStopAtNonEditableNode == StopAtNonEditableNode::Yes
|
|
? LeafNodeTypes{LeafNodeType::LeafNodeOrNonEditableNode}
|
|
: LeafNodeTypes{LeafNodeType::OnlyLeafNode};
|
|
nsIContent* nextLeafContentOrBlock =
|
|
HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
|
|
aPoint, leafNodeTypes, aBlockInlineCheck, &aAncestorLimiter);
|
|
if (!nextLeafContentOrBlock) {
|
|
// No next content means that we reached aAncestorLimiter boundary.
|
|
return BoundaryData(
|
|
aPoint.template To<EditorDOMPoint>(),
|
|
const_cast<Element&>(aAncestorLimiter),
|
|
HTMLEditUtils::IsBlockElement(
|
|
aAncestorLimiter, RespectParentBlockBoundary(aBlockInlineCheck))
|
|
? WSType::CurrentBlockBoundary
|
|
: WSType::InlineEditingHostBoundary);
|
|
}
|
|
|
|
if (HTMLEditUtils::IsBlockElement(*nextLeafContentOrBlock,
|
|
aBlockInlineCheck)) {
|
|
// we encountered a new block. therefore no more ws.
|
|
return BoundaryData(aPoint, *nextLeafContentOrBlock,
|
|
WSType::OtherBlockBoundary);
|
|
}
|
|
|
|
if (!nextLeafContentOrBlock->IsText() ||
|
|
(aStopAtNonEditableNode == StopAtNonEditableNode::Yes &&
|
|
HTMLEditUtils::IsSimplyEditableNode(*nextLeafContentOrBlock) !=
|
|
HTMLEditUtils::IsSimplyEditableNode(aAncestorLimiter))) {
|
|
// we encountered a break or a special node, like <img>,
|
|
// that is not a block and not a break but still
|
|
// serves as a terminator to ws runs.
|
|
return BoundaryData(aPoint, *nextLeafContentOrBlock,
|
|
nextLeafContentOrBlock->IsHTMLElement(nsGkAtoms::br)
|
|
? WSType::BRElement
|
|
: WSType::SpecialContent);
|
|
}
|
|
|
|
if (!nextLeafContentOrBlock->AsText()->TextFragment().GetLength()) {
|
|
// If it's an empty text node, keep looking for its next leaf content.
|
|
// Note that even if the empty text node is preformatted, we should keep
|
|
// looking for the next one.
|
|
return BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
|
|
aScanMode, EditorDOMPointInText(nextLeafContentOrBlock->AsText(), 0),
|
|
aNBSPData, aBlockInlineCheck, aStopAtNonEditableNode, aAncestorLimiter);
|
|
}
|
|
|
|
Maybe<BoundaryData> endInTextNode =
|
|
BoundaryData::ScanCollapsibleWhiteSpaceEndInTextNode(
|
|
EditorDOMPointInText(nextLeafContentOrBlock->AsText(), 0), aNBSPData,
|
|
aBlockInlineCheck);
|
|
if (endInTextNode.isSome()) {
|
|
return endInTextNode.ref();
|
|
}
|
|
|
|
// The text node does not have visible character, let's keep scanning
|
|
// following nodes.
|
|
return BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
|
|
aScanMode,
|
|
EditorDOMPointInText::AtEndOf(*nextLeafContentOrBlock->AsText()),
|
|
aNBSPData, aBlockInlineCheck, aStopAtNonEditableNode, aAncestorLimiter);
|
|
}
|
|
|
|
const EditorDOMRange&
|
|
WSRunScanner::TextFragmentData::InvisibleLeadingWhiteSpaceRangeRef() const {
|
|
if (mLeadingWhiteSpaceRange.isSome()) {
|
|
return mLeadingWhiteSpaceRange.ref();
|
|
}
|
|
|
|
// If it's start of line, there is no invisible leading white-spaces.
|
|
if (!StartsFromHardLineBreak() && !StartsFromInlineEditingHostBoundary()) {
|
|
mLeadingWhiteSpaceRange.emplace();
|
|
return mLeadingWhiteSpaceRange.ref();
|
|
}
|
|
|
|
// If there is no NBSP, all of the given range is leading white-spaces.
|
|
// Note that this result may be collapsed if there is no leading white-spaces.
|
|
if (!mNBSPData.FoundNBSP()) {
|
|
MOZ_ASSERT(mStart.PointRef().IsSet() || mEnd.PointRef().IsSet());
|
|
mLeadingWhiteSpaceRange.emplace(mStart.PointRef(), mEnd.PointRef());
|
|
return mLeadingWhiteSpaceRange.ref();
|
|
}
|
|
|
|
MOZ_ASSERT(mNBSPData.LastPointRef().IsSetAndValid());
|
|
|
|
// Even if the first NBSP is the start, i.e., there is no invisible leading
|
|
// white-space, return collapsed range.
|
|
mLeadingWhiteSpaceRange.emplace(mStart.PointRef(), mNBSPData.FirstPointRef());
|
|
return mLeadingWhiteSpaceRange.ref();
|
|
}
|
|
|
|
const EditorDOMRange&
|
|
WSRunScanner::TextFragmentData::InvisibleTrailingWhiteSpaceRangeRef() const {
|
|
if (mTrailingWhiteSpaceRange.isSome()) {
|
|
return mTrailingWhiteSpaceRange.ref();
|
|
}
|
|
|
|
// If it's not immediately before a block boundary, there is no invisible
|
|
// trailing white-spaces. Note that a collapsible white-space before a <br>
|
|
// element or a preformatted linefeed is visible.
|
|
if (!EndsByBlockBoundary() && !EndsByInlineEditingHostBoundary()) {
|
|
mTrailingWhiteSpaceRange.emplace();
|
|
return mTrailingWhiteSpaceRange.ref();
|
|
}
|
|
|
|
// If there is no NBSP, all of the given range is trailing white-spaces.
|
|
// Note that this result may be collapsed if there is no trailing white-
|
|
// spaces.
|
|
if (!mNBSPData.FoundNBSP()) {
|
|
MOZ_ASSERT(mStart.PointRef().IsSet() || mEnd.PointRef().IsSet());
|
|
mTrailingWhiteSpaceRange.emplace(mStart.PointRef(), mEnd.PointRef());
|
|
return mTrailingWhiteSpaceRange.ref();
|
|
}
|
|
|
|
MOZ_ASSERT(mNBSPData.LastPointRef().IsSetAndValid());
|
|
|
|
// If last NBSP is immediately before the end, there is no trailing white-
|
|
// spaces.
|
|
if (mEnd.PointRef().IsSet() &&
|
|
mNBSPData.LastPointRef().GetContainer() ==
|
|
mEnd.PointRef().GetContainer() &&
|
|
mNBSPData.LastPointRef().Offset() == mEnd.PointRef().Offset() - 1) {
|
|
mTrailingWhiteSpaceRange.emplace();
|
|
return mTrailingWhiteSpaceRange.ref();
|
|
}
|
|
|
|
// Otherwise, the may be some trailing white-spaces.
|
|
MOZ_ASSERT(!mNBSPData.LastPointRef().IsEndOfContainer());
|
|
mTrailingWhiteSpaceRange.emplace(mNBSPData.LastPointRef().NextPoint(),
|
|
mEnd.PointRef());
|
|
return mTrailingWhiteSpaceRange.ref();
|
|
}
|
|
|
|
EditorDOMRangeInTexts
|
|
WSRunScanner::TextFragmentData::GetNonCollapsedRangeInTexts(
|
|
const EditorDOMRange& aRange) const {
|
|
if (!aRange.IsPositioned()) {
|
|
return EditorDOMRangeInTexts();
|
|
}
|
|
if (aRange.Collapsed()) {
|
|
// If collapsed, we can do nothing.
|
|
return EditorDOMRangeInTexts();
|
|
}
|
|
if (aRange.IsInTextNodes()) {
|
|
// Note that this may return a range which don't include any invisible
|
|
// white-spaces due to empty text nodes.
|
|
return aRange.GetAsInTexts();
|
|
}
|
|
|
|
const auto firstPoint =
|
|
aRange.StartRef().IsInTextNode()
|
|
? aRange.StartRef().AsInText()
|
|
: GetInclusiveNextCharPoint<EditorDOMPointInText>(
|
|
aRange.StartRef(),
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(mScanMode));
|
|
if (!firstPoint.IsSet()) {
|
|
return EditorDOMRangeInTexts();
|
|
}
|
|
EditorDOMPointInText endPoint;
|
|
if (aRange.EndRef().IsInTextNode()) {
|
|
endPoint = aRange.EndRef().AsInText();
|
|
} else {
|
|
// FYI: GetPreviousCharPoint() returns last character's point of preceding
|
|
// text node if it's not empty, but we need end of the text node here.
|
|
endPoint = GetPreviousCharPoint<EditorDOMPointInText>(
|
|
aRange.EndRef(),
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(mScanMode));
|
|
if (endPoint.IsSet() && endPoint.IsAtLastContent()) {
|
|
MOZ_ALWAYS_TRUE(endPoint.AdvanceOffset());
|
|
}
|
|
}
|
|
if (!endPoint.IsSet() || firstPoint == endPoint) {
|
|
return EditorDOMRangeInTexts();
|
|
}
|
|
return EditorDOMRangeInTexts(firstPoint, endPoint);
|
|
}
|
|
|
|
const WSRunScanner::VisibleWhiteSpacesData&
|
|
WSRunScanner::TextFragmentData::VisibleWhiteSpacesDataRef() const {
|
|
if (mVisibleWhiteSpacesData.isSome()) {
|
|
return mVisibleWhiteSpacesData.ref();
|
|
}
|
|
|
|
{
|
|
// If all things are obviously visible, we can return range for all of the
|
|
// things quickly.
|
|
const bool mayHaveInvisibleLeadingSpace =
|
|
!StartsFromNonCollapsibleCharacters() && !StartsFromSpecialContent();
|
|
const bool mayHaveInvisibleTrailingWhiteSpace =
|
|
!EndsByNonCollapsibleCharacters() && !EndsBySpecialContent() &&
|
|
!EndsByBRElement() && !EndsByInvisiblePreformattedLineBreak();
|
|
|
|
if (!mayHaveInvisibleLeadingSpace && !mayHaveInvisibleTrailingWhiteSpace) {
|
|
VisibleWhiteSpacesData visibleWhiteSpaces;
|
|
if (mStart.PointRef().IsSet()) {
|
|
visibleWhiteSpaces.SetStartPoint(mStart.PointRef());
|
|
}
|
|
visibleWhiteSpaces.SetStartFrom(mStart.RawReason());
|
|
if (mEnd.PointRef().IsSet()) {
|
|
visibleWhiteSpaces.SetEndPoint(mEnd.PointRef());
|
|
}
|
|
visibleWhiteSpaces.SetEndBy(mEnd.RawReason());
|
|
mVisibleWhiteSpacesData.emplace(visibleWhiteSpaces);
|
|
return mVisibleWhiteSpacesData.ref();
|
|
}
|
|
}
|
|
|
|
// If all of the range is invisible leading or trailing white-spaces,
|
|
// there is no visible content.
|
|
const EditorDOMRange& leadingWhiteSpaceRange =
|
|
InvisibleLeadingWhiteSpaceRangeRef();
|
|
const bool maybeHaveLeadingWhiteSpaces =
|
|
leadingWhiteSpaceRange.StartRef().IsSet() ||
|
|
leadingWhiteSpaceRange.EndRef().IsSet();
|
|
if (maybeHaveLeadingWhiteSpaces &&
|
|
leadingWhiteSpaceRange.StartRef() == mStart.PointRef() &&
|
|
leadingWhiteSpaceRange.EndRef() == mEnd.PointRef()) {
|
|
mVisibleWhiteSpacesData.emplace(VisibleWhiteSpacesData());
|
|
return mVisibleWhiteSpacesData.ref();
|
|
}
|
|
const EditorDOMRange& trailingWhiteSpaceRange =
|
|
InvisibleTrailingWhiteSpaceRangeRef();
|
|
const bool maybeHaveTrailingWhiteSpaces =
|
|
trailingWhiteSpaceRange.StartRef().IsSet() ||
|
|
trailingWhiteSpaceRange.EndRef().IsSet();
|
|
if (maybeHaveTrailingWhiteSpaces &&
|
|
trailingWhiteSpaceRange.StartRef() == mStart.PointRef() &&
|
|
trailingWhiteSpaceRange.EndRef() == mEnd.PointRef()) {
|
|
mVisibleWhiteSpacesData.emplace(VisibleWhiteSpacesData());
|
|
return mVisibleWhiteSpacesData.ref();
|
|
}
|
|
|
|
if (!StartsFromHardLineBreak() && !StartsFromInlineEditingHostBoundary()) {
|
|
VisibleWhiteSpacesData visibleWhiteSpaces;
|
|
if (mStart.PointRef().IsSet()) {
|
|
visibleWhiteSpaces.SetStartPoint(mStart.PointRef());
|
|
}
|
|
visibleWhiteSpaces.SetStartFrom(mStart.RawReason());
|
|
if (!maybeHaveTrailingWhiteSpaces) {
|
|
visibleWhiteSpaces.SetEndPoint(mEnd.PointRef());
|
|
visibleWhiteSpaces.SetEndBy(mEnd.RawReason());
|
|
mVisibleWhiteSpacesData = Some(visibleWhiteSpaces);
|
|
return mVisibleWhiteSpacesData.ref();
|
|
}
|
|
if (trailingWhiteSpaceRange.StartRef().IsSet()) {
|
|
visibleWhiteSpaces.SetEndPoint(trailingWhiteSpaceRange.StartRef());
|
|
}
|
|
visibleWhiteSpaces.SetEndByTrailingWhiteSpaces();
|
|
mVisibleWhiteSpacesData.emplace(visibleWhiteSpaces);
|
|
return mVisibleWhiteSpacesData.ref();
|
|
}
|
|
|
|
MOZ_ASSERT(StartsFromHardLineBreak() ||
|
|
StartsFromInlineEditingHostBoundary());
|
|
MOZ_ASSERT(maybeHaveLeadingWhiteSpaces);
|
|
|
|
VisibleWhiteSpacesData visibleWhiteSpaces;
|
|
if (leadingWhiteSpaceRange.EndRef().IsSet()) {
|
|
visibleWhiteSpaces.SetStartPoint(leadingWhiteSpaceRange.EndRef());
|
|
}
|
|
visibleWhiteSpaces.SetStartFromLeadingWhiteSpaces();
|
|
if (!EndsByBlockBoundary() && !EndsByInlineEditingHostBoundary()) {
|
|
// then no trailing ws. this normal run ends the overall ws run.
|
|
if (mEnd.PointRef().IsSet()) {
|
|
visibleWhiteSpaces.SetEndPoint(mEnd.PointRef());
|
|
}
|
|
visibleWhiteSpaces.SetEndBy(mEnd.RawReason());
|
|
mVisibleWhiteSpacesData.emplace(visibleWhiteSpaces);
|
|
return mVisibleWhiteSpacesData.ref();
|
|
}
|
|
|
|
MOZ_ASSERT(EndsByBlockBoundary() || EndsByInlineEditingHostBoundary());
|
|
|
|
if (!maybeHaveTrailingWhiteSpaces) {
|
|
// normal ws runs right up to adjacent block (nbsp next to block)
|
|
visibleWhiteSpaces.SetEndPoint(mEnd.PointRef());
|
|
visibleWhiteSpaces.SetEndBy(mEnd.RawReason());
|
|
mVisibleWhiteSpacesData.emplace(visibleWhiteSpaces);
|
|
return mVisibleWhiteSpacesData.ref();
|
|
}
|
|
|
|
if (trailingWhiteSpaceRange.StartRef().IsSet()) {
|
|
visibleWhiteSpaces.SetEndPoint(trailingWhiteSpaceRange.StartRef());
|
|
}
|
|
visibleWhiteSpaces.SetEndByTrailingWhiteSpaces();
|
|
mVisibleWhiteSpacesData.emplace(visibleWhiteSpaces);
|
|
return mVisibleWhiteSpacesData.ref();
|
|
}
|
|
|
|
ReplaceRangeData
|
|
WSRunScanner::TextFragmentData::GetReplaceRangeDataAtEndOfDeletionRange(
|
|
const TextFragmentData& aTextFragmentDataAtStartToDelete) const {
|
|
const EditorDOMPoint& startToDelete =
|
|
aTextFragmentDataAtStartToDelete.ScanStartRef();
|
|
const EditorDOMPoint& endToDelete = mScanStartPoint;
|
|
|
|
MOZ_ASSERT(startToDelete.IsSetAndValid());
|
|
MOZ_ASSERT(endToDelete.IsSetAndValid());
|
|
MOZ_ASSERT(startToDelete.EqualsOrIsBefore(endToDelete));
|
|
|
|
if (EndRef().EqualsOrIsBefore(endToDelete)) {
|
|
return ReplaceRangeData();
|
|
}
|
|
|
|
// If deleting range is followed by invisible trailing white-spaces, we need
|
|
// to remove it for making them not visible.
|
|
const EditorDOMRange invisibleTrailingWhiteSpaceRangeAtEnd =
|
|
GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt(endToDelete);
|
|
if (invisibleTrailingWhiteSpaceRangeAtEnd.IsPositioned()) {
|
|
if (invisibleTrailingWhiteSpaceRangeAtEnd.Collapsed()) {
|
|
return ReplaceRangeData();
|
|
}
|
|
// XXX Why don't we remove all invisible white-spaces?
|
|
MOZ_ASSERT(invisibleTrailingWhiteSpaceRangeAtEnd.StartRef() == endToDelete);
|
|
return ReplaceRangeData(invisibleTrailingWhiteSpaceRangeAtEnd, u""_ns);
|
|
}
|
|
|
|
// If end of the deleting range is followed by visible white-spaces which
|
|
// is not preformatted, we might need to replace the following ASCII
|
|
// white-spaces with an NBSP.
|
|
const VisibleWhiteSpacesData& nonPreformattedVisibleWhiteSpacesAtEnd =
|
|
VisibleWhiteSpacesDataRef();
|
|
if (!nonPreformattedVisibleWhiteSpacesAtEnd.IsInitialized()) {
|
|
return ReplaceRangeData();
|
|
}
|
|
const PointPosition pointPositionWithNonPreformattedVisibleWhiteSpacesAtEnd =
|
|
nonPreformattedVisibleWhiteSpacesAtEnd.ComparePoint(endToDelete);
|
|
if (pointPositionWithNonPreformattedVisibleWhiteSpacesAtEnd !=
|
|
PointPosition::StartOfFragment &&
|
|
pointPositionWithNonPreformattedVisibleWhiteSpacesAtEnd !=
|
|
PointPosition::MiddleOfFragment) {
|
|
return ReplaceRangeData();
|
|
}
|
|
// If start of deleting range follows white-spaces or end of delete
|
|
// will be start of a line, the following text cannot start with an
|
|
// ASCII white-space for keeping it visible.
|
|
if (!aTextFragmentDataAtStartToDelete
|
|
.FollowingContentMayBecomeFirstVisibleContent(startToDelete)) {
|
|
return ReplaceRangeData();
|
|
}
|
|
auto nextCharOfStartOfEnd = GetInclusiveNextCharPoint<EditorDOMPointInText>(
|
|
endToDelete, ShouldIgnoreNonEditableSiblingsOrDescendants(mScanMode));
|
|
if (!nextCharOfStartOfEnd.IsSet() ||
|
|
nextCharOfStartOfEnd.IsEndOfContainer() ||
|
|
!nextCharOfStartOfEnd.IsCharCollapsibleASCIISpace()) {
|
|
return ReplaceRangeData();
|
|
}
|
|
if (nextCharOfStartOfEnd.IsStartOfContainer() ||
|
|
nextCharOfStartOfEnd.IsPreviousCharCollapsibleASCIISpace()) {
|
|
nextCharOfStartOfEnd =
|
|
aTextFragmentDataAtStartToDelete
|
|
.GetFirstASCIIWhiteSpacePointCollapsedTo<EditorDOMPointInText>(
|
|
nextCharOfStartOfEnd, nsIEditor::eNone,
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(mScanMode));
|
|
}
|
|
const auto endOfCollapsibleASCIIWhiteSpaces =
|
|
aTextFragmentDataAtStartToDelete
|
|
.GetEndOfCollapsibleASCIIWhiteSpaces<EditorDOMPointInText>(
|
|
nextCharOfStartOfEnd, nsIEditor::eNone,
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(mScanMode));
|
|
return ReplaceRangeData(nextCharOfStartOfEnd,
|
|
endOfCollapsibleASCIIWhiteSpaces,
|
|
nsDependentSubstring(&HTMLEditUtils::kNBSP, 1));
|
|
}
|
|
|
|
ReplaceRangeData
|
|
WSRunScanner::TextFragmentData::GetReplaceRangeDataAtStartOfDeletionRange(
|
|
const TextFragmentData& aTextFragmentDataAtEndToDelete) const {
|
|
const EditorDOMPoint& startToDelete = mScanStartPoint;
|
|
const EditorDOMPoint& endToDelete =
|
|
aTextFragmentDataAtEndToDelete.ScanStartRef();
|
|
|
|
MOZ_ASSERT(startToDelete.IsSetAndValid());
|
|
MOZ_ASSERT(endToDelete.IsSetAndValid());
|
|
MOZ_ASSERT(startToDelete.EqualsOrIsBefore(endToDelete));
|
|
|
|
if (startToDelete.EqualsOrIsBefore(StartRef())) {
|
|
return ReplaceRangeData();
|
|
}
|
|
|
|
const EditorDOMRange invisibleLeadingWhiteSpaceRangeAtStart =
|
|
GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt(startToDelete);
|
|
|
|
// If deleting range follows invisible leading white-spaces, we need to
|
|
// remove them for making them not visible.
|
|
if (invisibleLeadingWhiteSpaceRangeAtStart.IsPositioned()) {
|
|
if (invisibleLeadingWhiteSpaceRangeAtStart.Collapsed()) {
|
|
return ReplaceRangeData();
|
|
}
|
|
|
|
// XXX Why don't we remove all leading white-spaces?
|
|
return ReplaceRangeData(invisibleLeadingWhiteSpaceRangeAtStart, u""_ns);
|
|
}
|
|
|
|
// If start of the deleting range follows visible white-spaces which is not
|
|
// preformatted, we might need to replace previous ASCII white-spaces with
|
|
// an NBSP.
|
|
const VisibleWhiteSpacesData& nonPreformattedVisibleWhiteSpacesAtStart =
|
|
VisibleWhiteSpacesDataRef();
|
|
if (!nonPreformattedVisibleWhiteSpacesAtStart.IsInitialized()) {
|
|
return ReplaceRangeData();
|
|
}
|
|
const PointPosition
|
|
pointPositionWithNonPreformattedVisibleWhiteSpacesAtStart =
|
|
nonPreformattedVisibleWhiteSpacesAtStart.ComparePoint(startToDelete);
|
|
if (pointPositionWithNonPreformattedVisibleWhiteSpacesAtStart !=
|
|
PointPosition::MiddleOfFragment &&
|
|
pointPositionWithNonPreformattedVisibleWhiteSpacesAtStart !=
|
|
PointPosition::EndOfFragment) {
|
|
return ReplaceRangeData();
|
|
}
|
|
// If end of the deleting range is (was) followed by white-spaces or
|
|
// previous character of start of deleting range will be immediately
|
|
// before a block boundary, the text cannot ends with an ASCII white-space
|
|
// for keeping it visible.
|
|
if (!aTextFragmentDataAtEndToDelete.PrecedingContentMayBecomeInvisible(
|
|
endToDelete)) {
|
|
return ReplaceRangeData();
|
|
}
|
|
auto atPreviousCharOfStart = GetPreviousCharPoint<EditorDOMPointInText>(
|
|
startToDelete, ShouldIgnoreNonEditableSiblingsOrDescendants(mScanMode));
|
|
if (!atPreviousCharOfStart.IsSet() ||
|
|
atPreviousCharOfStart.IsEndOfContainer() ||
|
|
!atPreviousCharOfStart.IsCharCollapsibleASCIISpace()) {
|
|
return ReplaceRangeData();
|
|
}
|
|
if (atPreviousCharOfStart.IsStartOfContainer() ||
|
|
atPreviousCharOfStart.IsPreviousCharASCIISpace()) {
|
|
atPreviousCharOfStart =
|
|
GetFirstASCIIWhiteSpacePointCollapsedTo<EditorDOMPointInText>(
|
|
atPreviousCharOfStart, nsIEditor::eNone,
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(mScanMode));
|
|
}
|
|
const auto endOfCollapsibleASCIIWhiteSpaces =
|
|
GetEndOfCollapsibleASCIIWhiteSpaces<EditorDOMPointInText>(
|
|
atPreviousCharOfStart, nsIEditor::eNone,
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(mScanMode));
|
|
return ReplaceRangeData(atPreviousCharOfStart,
|
|
endOfCollapsibleASCIIWhiteSpaces,
|
|
nsDependentSubstring(&HTMLEditUtils::kNBSP, 1));
|
|
}
|
|
|
|
// static
|
|
template <typename EditorDOMPointType, typename PT, typename CT>
|
|
EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint(
|
|
const EditorDOMPointBase<PT, CT>& aPoint,
|
|
BlockInlineCheck aBlockInlineCheck,
|
|
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
|
|
const nsIContent* aFollowingLimiterContent /* = nullptr */) {
|
|
MOZ_ASSERT(aPoint.IsSetAndValid());
|
|
|
|
if (NS_WARN_IF(!aPoint.IsInContentNode())) {
|
|
return EditorDOMPointType();
|
|
}
|
|
|
|
const EditorRawDOMPoint point = [&]() MOZ_NEVER_INLINE_DEBUG {
|
|
nsIContent* const child =
|
|
aPoint.CanContainerHaveChildren() ? aPoint.GetChild() : nullptr;
|
|
if (!child ||
|
|
HTMLEditUtils::IsBlockElement(
|
|
*child, IgnoreInsideBlockBoundary(aBlockInlineCheck)) ||
|
|
HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*child)) {
|
|
return aPoint.template To<EditorRawDOMPoint>();
|
|
}
|
|
if (!child->HasChildNodes()) {
|
|
return EditorRawDOMPoint(child, 0);
|
|
}
|
|
// FIXME: This may skip aFollowingLimiterContent, so, this utility should
|
|
// take a stopper param.
|
|
// FIXME: I think we should stop looking for a leaf node if there is a child
|
|
// block because end reason content should not be the other side of the
|
|
// following block boundary.
|
|
nsIContent* const leafContent = HTMLEditUtils::GetFirstLeafContent(
|
|
*child, {LeafNodeType::LeafNodeOrChildBlock},
|
|
IgnoreInsideBlockBoundary(aBlockInlineCheck));
|
|
if (NS_WARN_IF(!leafContent) ||
|
|
HTMLEditUtils::IsBlockElement(
|
|
*leafContent, IgnoreInsideBlockBoundary(aBlockInlineCheck)) ||
|
|
HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*leafContent)) {
|
|
return EditorRawDOMPoint();
|
|
}
|
|
return EditorRawDOMPoint(leafContent, 0);
|
|
}();
|
|
if (!point.IsSet()) {
|
|
return EditorDOMPointType();
|
|
}
|
|
|
|
// If it points a character in a text node, return it.
|
|
// XXX For the performance, this does not check whether the container
|
|
// is outside of our range.
|
|
if (point.IsInTextNode() &&
|
|
(aIgnoreNonEditableNodes == IgnoreNonEditableNodes::No ||
|
|
HTMLEditUtils::IsSimplyEditableNode(*point.GetContainer())) &&
|
|
!point.IsEndOfContainer()) {
|
|
return EditorDOMPointType(point.ContainerAs<Text>(), point.Offset());
|
|
}
|
|
|
|
if (point.GetContainer() == aFollowingLimiterContent) {
|
|
return EditorDOMPointType();
|
|
}
|
|
|
|
const Element* const
|
|
editableBlockElementOrInlineEditingHostOrNonEditableRootElement =
|
|
HTMLEditUtils::GetInclusiveAncestorElement(
|
|
*aPoint.template ContainerAs<nsIContent>(),
|
|
HTMLEditUtils::IsSimplyEditableNode(
|
|
*aPoint.template ContainerAs<nsIContent>())
|
|
? kScanEditableRootAncestorTypes
|
|
: kScanAnyRootAncestorTypes,
|
|
aBlockInlineCheck);
|
|
if (NS_WARN_IF(
|
|
!editableBlockElementOrInlineEditingHostOrNonEditableRootElement)) {
|
|
return EditorDOMPointType();
|
|
}
|
|
|
|
const auto leafNodeTypes =
|
|
aIgnoreNonEditableNodes == IgnoreNonEditableNodes::Yes
|
|
? LeafNodeTypes(LeafNodeType::LeafNodeOrNonEditableNode,
|
|
LeafNodeType::LeafNodeOrChildBlock)
|
|
: LeafNodeTypes(LeafNodeType::LeafNodeOrChildBlock);
|
|
for (nsIContent* nextContent =
|
|
HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
|
|
*point.ContainerAs<nsIContent>(), leafNodeTypes,
|
|
IgnoreInsideBlockBoundary(aBlockInlineCheck),
|
|
editableBlockElementOrInlineEditingHostOrNonEditableRootElement);
|
|
nextContent;
|
|
nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
|
|
*nextContent, leafNodeTypes,
|
|
IgnoreInsideBlockBoundary(aBlockInlineCheck),
|
|
editableBlockElementOrInlineEditingHostOrNonEditableRootElement)) {
|
|
if (!nextContent->IsText() ||
|
|
(aIgnoreNonEditableNodes == IgnoreNonEditableNodes::Yes &&
|
|
!HTMLEditUtils::IsSimplyEditableNode(*nextContent))) {
|
|
if (nextContent == aFollowingLimiterContent ||
|
|
HTMLEditUtils::IsBlockElement(
|
|
*nextContent, IgnoreInsideBlockBoundary(aBlockInlineCheck)) ||
|
|
HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*nextContent)) {
|
|
break; // Reached end of current runs.
|
|
}
|
|
continue;
|
|
}
|
|
return EditorDOMPointType(nextContent->AsText(), 0);
|
|
}
|
|
return EditorDOMPointType();
|
|
}
|
|
|
|
// static
|
|
template <typename EditorDOMPointType, typename PT, typename CT>
|
|
EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint(
|
|
const EditorDOMPointBase<PT, CT>& aPoint,
|
|
BlockInlineCheck aBlockInlineCheck,
|
|
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
|
|
const nsIContent* aPrecedingLimiterContent /* = nullptr */) {
|
|
MOZ_ASSERT(aPoint.IsSetAndValid());
|
|
|
|
if (NS_WARN_IF(!aPoint.IsInContentNode())) {
|
|
return EditorDOMPointType();
|
|
}
|
|
|
|
const EditorRawDOMPoint point = [&]() MOZ_NEVER_INLINE_DEBUG {
|
|
nsIContent* const previousChild = aPoint.CanContainerHaveChildren()
|
|
? aPoint.GetPreviousSiblingOfChild()
|
|
: nullptr;
|
|
if (!previousChild ||
|
|
HTMLEditUtils::IsBlockElement(
|
|
*previousChild, IgnoreInsideBlockBoundary(aBlockInlineCheck)) ||
|
|
HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*previousChild)) {
|
|
return aPoint.template To<EditorRawDOMPoint>();
|
|
}
|
|
if (!previousChild->HasChildren()) {
|
|
return EditorRawDOMPoint::AtEndOf(*previousChild);
|
|
}
|
|
// FIXME: This may skip aPrecedingLimiterContent, so, this utility should
|
|
// take a stopper param.
|
|
// FIXME: I think we should stop looking for a leaf node if there is a child
|
|
// block because end reason content should not be the other side of the
|
|
// following block boundary.
|
|
nsIContent* const leafContent = HTMLEditUtils::GetLastLeafContent(
|
|
*previousChild, {LeafNodeType::LeafNodeOrChildBlock},
|
|
IgnoreInsideBlockBoundary(aBlockInlineCheck));
|
|
if (NS_WARN_IF(!leafContent) ||
|
|
HTMLEditUtils::IsBlockElement(
|
|
*leafContent, IgnoreInsideBlockBoundary(aBlockInlineCheck)) ||
|
|
HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*leafContent)) {
|
|
return EditorRawDOMPoint();
|
|
}
|
|
return EditorRawDOMPoint::AtEndOf(*leafContent);
|
|
}();
|
|
if (!point.IsSet()) {
|
|
return EditorDOMPointType();
|
|
}
|
|
|
|
// If it points a character in a text node and it's not first character
|
|
// in it, return its previous point.
|
|
// XXX For the performance, this does not check whether the container
|
|
// is outside of our range.
|
|
if (point.IsInTextNode() &&
|
|
(aIgnoreNonEditableNodes == IgnoreNonEditableNodes::No ||
|
|
HTMLEditUtils::IsSimplyEditableNode(*point.GetContainer())) &&
|
|
!point.IsStartOfContainer()) {
|
|
return EditorDOMPointType(point.ContainerAs<Text>(), point.Offset() - 1);
|
|
}
|
|
|
|
if (point.GetContainer() == aPrecedingLimiterContent) {
|
|
return EditorDOMPointType();
|
|
}
|
|
|
|
const Element* const
|
|
editableBlockElementOrInlineEditingHostOrNonEditableRootElement =
|
|
HTMLEditUtils::GetInclusiveAncestorElement(
|
|
*aPoint.template ContainerAs<nsIContent>(),
|
|
HTMLEditUtils::IsSimplyEditableNode(
|
|
*aPoint.template ContainerAs<nsIContent>())
|
|
? kScanEditableRootAncestorTypes
|
|
: kScanAnyRootAncestorTypes,
|
|
aBlockInlineCheck);
|
|
if (NS_WARN_IF(
|
|
!editableBlockElementOrInlineEditingHostOrNonEditableRootElement)) {
|
|
return EditorDOMPointType();
|
|
}
|
|
|
|
const auto leafNodeTypes =
|
|
aIgnoreNonEditableNodes == IgnoreNonEditableNodes::Yes
|
|
? LeafNodeTypes(LeafNodeType::LeafNodeOrNonEditableNode,
|
|
LeafNodeType::LeafNodeOrChildBlock)
|
|
: LeafNodeTypes(LeafNodeType::LeafNodeOrChildBlock);
|
|
for (
|
|
nsIContent* previousContent =
|
|
HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
|
|
*point.ContainerAs<nsIContent>(), leafNodeTypes,
|
|
IgnoreInsideBlockBoundary(aBlockInlineCheck),
|
|
editableBlockElementOrInlineEditingHostOrNonEditableRootElement);
|
|
previousContent;
|
|
previousContent =
|
|
HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
|
|
*previousContent, leafNodeTypes,
|
|
IgnoreInsideBlockBoundary(aBlockInlineCheck),
|
|
editableBlockElementOrInlineEditingHostOrNonEditableRootElement)) {
|
|
if (!previousContent->IsText() ||
|
|
(aIgnoreNonEditableNodes == IgnoreNonEditableNodes::Yes &&
|
|
!HTMLEditUtils::IsSimplyEditableNode(*previousContent))) {
|
|
if (previousContent == aPrecedingLimiterContent ||
|
|
HTMLEditUtils::IsBlockElement(
|
|
*previousContent, IgnoreInsideBlockBoundary(aBlockInlineCheck)) ||
|
|
HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*previousContent)) {
|
|
break; // Reached start of current runs.
|
|
}
|
|
continue;
|
|
}
|
|
return EditorDOMPointType(previousContent->AsText(),
|
|
previousContent->AsText()->TextLength()
|
|
? previousContent->AsText()->TextLength() - 1
|
|
: 0);
|
|
}
|
|
return EditorDOMPointType();
|
|
}
|
|
|
|
// static
|
|
template <typename EditorDOMPointType>
|
|
EditorDOMPointType
|
|
WSRunScanner::TextFragmentData::GetEndOfCollapsibleASCIIWhiteSpaces(
|
|
const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
|
|
nsIEditor::EDirection aDirectionToDelete,
|
|
BlockInlineCheck aBlockInlineCheck,
|
|
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
|
|
const nsIContent* aFollowingLimiterContent /* = nullptr */) {
|
|
MOZ_ASSERT(aDirectionToDelete == nsIEditor::eNone ||
|
|
aDirectionToDelete == nsIEditor::eNext ||
|
|
aDirectionToDelete == nsIEditor::ePrevious);
|
|
MOZ_ASSERT(aPointAtASCIIWhiteSpace.IsSet());
|
|
MOZ_ASSERT(!aPointAtASCIIWhiteSpace.IsEndOfContainer());
|
|
MOZ_ASSERT_IF(!EditorUtils::IsNewLinePreformatted(
|
|
*aPointAtASCIIWhiteSpace.ContainerAs<Text>()),
|
|
aPointAtASCIIWhiteSpace.IsCharCollapsibleASCIISpace());
|
|
MOZ_ASSERT_IF(EditorUtils::IsNewLinePreformatted(
|
|
*aPointAtASCIIWhiteSpace.ContainerAs<Text>()),
|
|
aPointAtASCIIWhiteSpace.IsCharASCIISpace());
|
|
|
|
// If we're deleting text forward and the next visible character is first
|
|
// preformatted new line but white-spaces can be collapsed, we need to
|
|
// delete its following collapsible white-spaces too.
|
|
bool hasSeenPreformattedNewLine =
|
|
aPointAtASCIIWhiteSpace.IsCharPreformattedNewLine();
|
|
auto NeedToScanFollowingWhiteSpaces =
|
|
[&hasSeenPreformattedNewLine,
|
|
&aDirectionToDelete](const EditorDOMPointInText& aAtNextVisibleCharacter)
|
|
MOZ_NEVER_INLINE_DEBUG -> bool {
|
|
MOZ_ASSERT(!aAtNextVisibleCharacter.IsEndOfContainer());
|
|
return !hasSeenPreformattedNewLine &&
|
|
aDirectionToDelete == nsIEditor::eNext &&
|
|
aAtNextVisibleCharacter
|
|
.IsCharPreformattedNewLineCollapsedWithWhiteSpaces();
|
|
};
|
|
auto ScanNextNonCollapsibleChar =
|
|
[&hasSeenPreformattedNewLine,
|
|
&NeedToScanFollowingWhiteSpaces](const EditorDOMPointInText& aPoint)
|
|
MOZ_NEVER_INLINE_DEBUG -> EditorDOMPointInText {
|
|
Maybe<uint32_t> nextVisibleCharOffset =
|
|
HTMLEditUtils::GetNextNonCollapsibleCharOffset(aPoint);
|
|
if (!nextVisibleCharOffset.isSome()) {
|
|
return EditorDOMPointInText(); // Keep scanning following text nodes
|
|
}
|
|
EditorDOMPointInText atNextVisibleChar(aPoint.ContainerAs<Text>(),
|
|
nextVisibleCharOffset.value());
|
|
if (!NeedToScanFollowingWhiteSpaces(atNextVisibleChar)) {
|
|
return atNextVisibleChar;
|
|
}
|
|
hasSeenPreformattedNewLine |= atNextVisibleChar.IsCharPreformattedNewLine();
|
|
nextVisibleCharOffset =
|
|
HTMLEditUtils::GetNextNonCollapsibleCharOffset(atNextVisibleChar);
|
|
if (nextVisibleCharOffset.isSome()) {
|
|
MOZ_ASSERT(aPoint.ContainerAs<Text>() ==
|
|
atNextVisibleChar.ContainerAs<Text>());
|
|
return EditorDOMPointInText(atNextVisibleChar.ContainerAs<Text>(),
|
|
nextVisibleCharOffset.value());
|
|
}
|
|
return EditorDOMPointInText(); // Keep scanning following text nodes
|
|
};
|
|
|
|
// If it's not the last character in the text node, let's scan following
|
|
// characters in it.
|
|
if (!aPointAtASCIIWhiteSpace.IsAtLastContent()) {
|
|
const EditorDOMPointInText atNextVisibleChar(
|
|
ScanNextNonCollapsibleChar(aPointAtASCIIWhiteSpace));
|
|
if (atNextVisibleChar.IsSet()) {
|
|
return atNextVisibleChar.To<EditorDOMPointType>();
|
|
}
|
|
}
|
|
|
|
// Otherwise, i.e., the text node ends with ASCII white-space, keep scanning
|
|
// the following text nodes.
|
|
// XXX Perhaps, we should stop scanning if there is non-editable and visible
|
|
// content.
|
|
EditorDOMPointInText afterLastWhiteSpace = EditorDOMPointInText::AtEndOf(
|
|
*aPointAtASCIIWhiteSpace.ContainerAs<Text>());
|
|
for (EditorDOMPointInText atEndOfPreviousTextNode = afterLastWhiteSpace;;) {
|
|
const auto atStartOfNextTextNode =
|
|
TextFragmentData::GetInclusiveNextCharPoint<EditorDOMPointInText>(
|
|
atEndOfPreviousTextNode, aBlockInlineCheck, aIgnoreNonEditableNodes,
|
|
aFollowingLimiterContent);
|
|
if (!atStartOfNextTextNode.IsSet()) {
|
|
// There is no more text nodes. Return end of the previous text node.
|
|
return afterLastWhiteSpace.To<EditorDOMPointType>();
|
|
}
|
|
|
|
// We can ignore empty text nodes (even if it's preformatted).
|
|
if (atStartOfNextTextNode.IsContainerEmpty()) {
|
|
atEndOfPreviousTextNode = atStartOfNextTextNode;
|
|
continue;
|
|
}
|
|
|
|
// If next node starts with non-white-space character or next node is
|
|
// preformatted, return end of previous text node. However, if it
|
|
// starts with a preformatted linefeed but white-spaces are collapsible,
|
|
// we need to scan following collapsible white-spaces when we're deleting
|
|
// text forward.
|
|
if (!atStartOfNextTextNode.IsCharCollapsibleASCIISpace() &&
|
|
!NeedToScanFollowingWhiteSpaces(atStartOfNextTextNode)) {
|
|
return afterLastWhiteSpace.To<EditorDOMPointType>();
|
|
}
|
|
|
|
// Otherwise, scan the text node.
|
|
const EditorDOMPointInText atNextVisibleChar(
|
|
ScanNextNonCollapsibleChar(atStartOfNextTextNode));
|
|
if (atNextVisibleChar.IsSet()) {
|
|
return atNextVisibleChar.To<EditorDOMPointType>();
|
|
}
|
|
|
|
// The next text nodes ends with white-space too. Try next one.
|
|
afterLastWhiteSpace = atEndOfPreviousTextNode =
|
|
EditorDOMPointInText::AtEndOf(
|
|
*atStartOfNextTextNode.ContainerAs<Text>());
|
|
}
|
|
}
|
|
|
|
// static
|
|
template <typename EditorDOMPointType>
|
|
EditorDOMPointType
|
|
WSRunScanner::TextFragmentData::GetFirstASCIIWhiteSpacePointCollapsedTo(
|
|
const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
|
|
nsIEditor::EDirection aDirectionToDelete,
|
|
BlockInlineCheck aBlockInlineCheck,
|
|
IgnoreNonEditableNodes aIgnoreNonEditableNodes,
|
|
const nsIContent* aPrecedingLimiterContent) {
|
|
MOZ_ASSERT(aDirectionToDelete == nsIEditor::eNone ||
|
|
aDirectionToDelete == nsIEditor::eNext ||
|
|
aDirectionToDelete == nsIEditor::ePrevious);
|
|
MOZ_ASSERT(aPointAtASCIIWhiteSpace.IsSet());
|
|
MOZ_ASSERT(!aPointAtASCIIWhiteSpace.IsEndOfContainer());
|
|
MOZ_ASSERT_IF(!EditorUtils::IsNewLinePreformatted(
|
|
*aPointAtASCIIWhiteSpace.ContainerAs<Text>()),
|
|
aPointAtASCIIWhiteSpace.IsCharCollapsibleASCIISpace());
|
|
MOZ_ASSERT_IF(EditorUtils::IsNewLinePreformatted(
|
|
*aPointAtASCIIWhiteSpace.ContainerAs<Text>()),
|
|
aPointAtASCIIWhiteSpace.IsCharASCIISpace());
|
|
|
|
// If we're deleting text backward and the previous visible character is first
|
|
// preformatted new line but white-spaces can be collapsed, we need to delete
|
|
// its preceding collapsible white-spaces too.
|
|
bool hasSeenPreformattedNewLine =
|
|
aPointAtASCIIWhiteSpace.IsCharPreformattedNewLine();
|
|
auto NeedToScanPrecedingWhiteSpaces =
|
|
[&hasSeenPreformattedNewLine, &aDirectionToDelete](
|
|
const EditorDOMPointInText& aAtPreviousVisibleCharacter)
|
|
MOZ_NEVER_INLINE_DEBUG -> bool {
|
|
MOZ_ASSERT(!aAtPreviousVisibleCharacter.IsEndOfContainer());
|
|
return !hasSeenPreformattedNewLine &&
|
|
aDirectionToDelete == nsIEditor::ePrevious &&
|
|
aAtPreviousVisibleCharacter
|
|
.IsCharPreformattedNewLineCollapsedWithWhiteSpaces();
|
|
};
|
|
auto ScanPreviousNonCollapsibleChar =
|
|
[&hasSeenPreformattedNewLine,
|
|
&NeedToScanPrecedingWhiteSpaces](const EditorDOMPointInText& aPoint)
|
|
MOZ_NEVER_INLINE_DEBUG -> EditorDOMPointInText {
|
|
Maybe<uint32_t> previousVisibleCharOffset =
|
|
HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(aPoint);
|
|
if (previousVisibleCharOffset.isNothing()) {
|
|
return EditorDOMPointInText(); // Keep scanning preceding text nodes
|
|
}
|
|
EditorDOMPointInText atPreviousVisibleCharacter(
|
|
aPoint.ContainerAs<Text>(), previousVisibleCharOffset.value());
|
|
if (!NeedToScanPrecedingWhiteSpaces(atPreviousVisibleCharacter)) {
|
|
return atPreviousVisibleCharacter.NextPoint();
|
|
}
|
|
hasSeenPreformattedNewLine |=
|
|
atPreviousVisibleCharacter.IsCharPreformattedNewLine();
|
|
previousVisibleCharOffset =
|
|
HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
|
|
atPreviousVisibleCharacter);
|
|
if (previousVisibleCharOffset.isSome()) {
|
|
MOZ_ASSERT(aPoint.ContainerAs<Text>() ==
|
|
atPreviousVisibleCharacter.ContainerAs<Text>());
|
|
return EditorDOMPointInText(
|
|
atPreviousVisibleCharacter.ContainerAs<Text>(),
|
|
previousVisibleCharOffset.value() + 1);
|
|
}
|
|
return EditorDOMPointInText(); // Keep scanning preceding text nodes
|
|
};
|
|
|
|
// If there is some characters before it, scan it in the text node first.
|
|
if (!aPointAtASCIIWhiteSpace.IsStartOfContainer()) {
|
|
EditorDOMPointInText atFirstASCIIWhiteSpace(
|
|
ScanPreviousNonCollapsibleChar(aPointAtASCIIWhiteSpace));
|
|
if (atFirstASCIIWhiteSpace.IsSet()) {
|
|
return atFirstASCIIWhiteSpace.To<EditorDOMPointType>();
|
|
}
|
|
}
|
|
|
|
// Otherwise, i.e., the text node starts with ASCII white-space, keep scanning
|
|
// the preceding text nodes.
|
|
// XXX Perhaps, we should stop scanning if there is non-editable and visible
|
|
// content.
|
|
EditorDOMPointInText atLastWhiteSpace =
|
|
EditorDOMPointInText(aPointAtASCIIWhiteSpace.ContainerAs<Text>(), 0u);
|
|
for (EditorDOMPointInText atStartOfPreviousTextNode = atLastWhiteSpace;;) {
|
|
const auto atLastCharOfPreviousTextNode =
|
|
TextFragmentData::GetPreviousCharPoint<EditorDOMPointInText>(
|
|
atStartOfPreviousTextNode, aBlockInlineCheck,
|
|
aIgnoreNonEditableNodes, aPrecedingLimiterContent);
|
|
if (!atLastCharOfPreviousTextNode.IsSet()) {
|
|
// There is no more text nodes. Return end of last text node.
|
|
return atLastWhiteSpace.To<EditorDOMPointType>();
|
|
}
|
|
|
|
// We can ignore empty text nodes (even if it's preformatted).
|
|
if (atLastCharOfPreviousTextNode.IsContainerEmpty()) {
|
|
atStartOfPreviousTextNode = atLastCharOfPreviousTextNode;
|
|
continue;
|
|
}
|
|
|
|
// If next node ends with non-white-space character or next node is
|
|
// preformatted, return start of previous text node.
|
|
if (!atLastCharOfPreviousTextNode.IsCharCollapsibleASCIISpace() &&
|
|
!NeedToScanPrecedingWhiteSpaces(atLastCharOfPreviousTextNode)) {
|
|
return atLastWhiteSpace.To<EditorDOMPointType>();
|
|
}
|
|
|
|
// Otherwise, scan the text node.
|
|
const EditorDOMPointInText atFirstASCIIWhiteSpace(
|
|
ScanPreviousNonCollapsibleChar(atLastCharOfPreviousTextNode));
|
|
if (atFirstASCIIWhiteSpace.IsSet()) {
|
|
return atFirstASCIIWhiteSpace.To<EditorDOMPointType>();
|
|
}
|
|
|
|
// The next text nodes starts with white-space too. Try next one.
|
|
atLastWhiteSpace = atStartOfPreviousTextNode = EditorDOMPointInText(
|
|
atLastCharOfPreviousTextNode.ContainerAs<Text>(), 0u);
|
|
}
|
|
}
|
|
|
|
EditorDOMPointInText WSRunScanner::TextFragmentData::
|
|
GetPreviousNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
|
|
const EditorDOMPoint& aPointToInsert) const {
|
|
MOZ_ASSERT(aPointToInsert.IsSetAndValid());
|
|
MOZ_ASSERT(VisibleWhiteSpacesDataRef().IsInitialized());
|
|
NS_ASSERTION(VisibleWhiteSpacesDataRef().ComparePoint(aPointToInsert) ==
|
|
PointPosition::MiddleOfFragment ||
|
|
VisibleWhiteSpacesDataRef().ComparePoint(aPointToInsert) ==
|
|
PointPosition::EndOfFragment,
|
|
"Previous char of aPoint should be in the visible white-spaces");
|
|
|
|
// Try to change an NBSP to a space, if possible, just to prevent NBSP
|
|
// proliferation. This routine is called when we are about to make this
|
|
// point in the ws abut an inserted break or text, so we don't have to worry
|
|
// about what is after it. What is after it now will end up after the
|
|
// inserted object.
|
|
const auto atPreviousChar = GetPreviousCharPoint<EditorDOMPointInText>(
|
|
aPointToInsert, ShouldIgnoreNonEditableSiblingsOrDescendants(mScanMode));
|
|
if (!atPreviousChar.IsSet() || atPreviousChar.IsEndOfContainer() ||
|
|
!atPreviousChar.IsCharNBSP() ||
|
|
EditorUtils::IsWhiteSpacePreformatted(
|
|
*atPreviousChar.ContainerAs<Text>())) {
|
|
return EditorDOMPointInText();
|
|
}
|
|
|
|
const auto atPreviousCharOfPreviousChar =
|
|
GetPreviousCharPoint<EditorDOMPointInText>(
|
|
atPreviousChar,
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(mScanMode));
|
|
if (atPreviousCharOfPreviousChar.IsSet()) {
|
|
// If the previous char is in different text node and it's preformatted,
|
|
// we shouldn't touch it.
|
|
if (atPreviousChar.ContainerAs<Text>() !=
|
|
atPreviousCharOfPreviousChar.ContainerAs<Text>() &&
|
|
EditorUtils::IsWhiteSpacePreformatted(
|
|
*atPreviousCharOfPreviousChar.ContainerAs<Text>())) {
|
|
return EditorDOMPointInText();
|
|
}
|
|
// If the previous char of the NBSP at previous position of aPointToInsert
|
|
// is an ASCII white-space, we don't need to replace it with same character.
|
|
if (!atPreviousCharOfPreviousChar.IsEndOfContainer() &&
|
|
atPreviousCharOfPreviousChar.IsCharASCIISpace()) {
|
|
return EditorDOMPointInText();
|
|
}
|
|
return atPreviousChar;
|
|
}
|
|
|
|
// If previous content of the NBSP is block boundary, we cannot replace the
|
|
// NBSP with an ASCII white-space to keep it rendered.
|
|
const VisibleWhiteSpacesData& visibleWhiteSpaces =
|
|
VisibleWhiteSpacesDataRef();
|
|
if (!visibleWhiteSpaces.StartsFromNonCollapsibleCharacters() &&
|
|
!visibleWhiteSpaces.StartsFromSpecialContent()) {
|
|
return EditorDOMPointInText();
|
|
}
|
|
return atPreviousChar;
|
|
}
|
|
|
|
EditorDOMPointInText WSRunScanner::TextFragmentData::
|
|
GetInclusiveNextNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
|
|
const EditorDOMPoint& aPointToInsert) const {
|
|
MOZ_ASSERT(aPointToInsert.IsSetAndValid());
|
|
MOZ_ASSERT(VisibleWhiteSpacesDataRef().IsInitialized());
|
|
NS_ASSERTION(VisibleWhiteSpacesDataRef().ComparePoint(aPointToInsert) ==
|
|
PointPosition::StartOfFragment ||
|
|
VisibleWhiteSpacesDataRef().ComparePoint(aPointToInsert) ==
|
|
PointPosition::MiddleOfFragment,
|
|
"Inclusive next char of aPointToInsert should be in the visible "
|
|
"white-spaces");
|
|
|
|
// Try to change an nbsp to a space, if possible, just to prevent nbsp
|
|
// proliferation This routine is called when we are about to make this point
|
|
// in the ws abut an inserted text, so we don't have to worry about what is
|
|
// before it. What is before it now will end up before the inserted text.
|
|
const auto atNextChar = GetInclusiveNextCharPoint<EditorDOMPointInText>(
|
|
aPointToInsert, ShouldIgnoreNonEditableSiblingsOrDescendants(mScanMode));
|
|
if (!atNextChar.IsSet() || NS_WARN_IF(atNextChar.IsEndOfContainer()) ||
|
|
!atNextChar.IsCharNBSP() ||
|
|
EditorUtils::IsWhiteSpacePreformatted(*atNextChar.ContainerAs<Text>())) {
|
|
return EditorDOMPointInText();
|
|
}
|
|
|
|
const auto atNextCharOfNextCharOfNBSP =
|
|
GetInclusiveNextCharPoint<EditorDOMPointInText>(
|
|
atNextChar.NextPoint<EditorRawDOMPointInText>(),
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(mScanMode));
|
|
if (atNextCharOfNextCharOfNBSP.IsSet()) {
|
|
// If the next char is in different text node and it's preformatted,
|
|
// we shouldn't touch it.
|
|
if (atNextChar.ContainerAs<Text>() !=
|
|
atNextCharOfNextCharOfNBSP.ContainerAs<Text>() &&
|
|
EditorUtils::IsWhiteSpacePreformatted(
|
|
*atNextCharOfNextCharOfNBSP.ContainerAs<Text>())) {
|
|
return EditorDOMPointInText();
|
|
}
|
|
// If following character of an NBSP is an ASCII white-space, we don't
|
|
// need to replace it with same character.
|
|
if (!atNextCharOfNextCharOfNBSP.IsEndOfContainer() &&
|
|
atNextCharOfNextCharOfNBSP.IsCharASCIISpace()) {
|
|
return EditorDOMPointInText();
|
|
}
|
|
return atNextChar;
|
|
}
|
|
|
|
// If the NBSP is last character in the hard line, we don't need to
|
|
// replace it because it's required to render multiple white-spaces.
|
|
const VisibleWhiteSpacesData& visibleWhiteSpaces =
|
|
VisibleWhiteSpacesDataRef();
|
|
if (!visibleWhiteSpaces.EndsByNonCollapsibleCharacters() &&
|
|
!visibleWhiteSpaces.EndsBySpecialContent() &&
|
|
!visibleWhiteSpaces.EndsByBRElement()) {
|
|
return EditorDOMPointInText();
|
|
}
|
|
|
|
return atNextChar;
|
|
}
|
|
|
|
} // namespace mozilla
|