Chromium considers whether an element is block or inline with `display-outside`. Therefore, they treat `inline-block` is an inline element. For following the behavior, we use `BlockInlineCheck::UseComputedDisplayOutsideStyle` in many places. However, if `WSRunScanner` works for considering a collapsible white-space visibility, it needs to treat `inline-block` as a block only when it's an ancestor. In the reported case, the editing host is `inline-block`. So, `WSRunScanner` reached the ancestor limiter if scanning only editable nodes. To fix this bug, this changes the behavior only when the scanner reached the ancestor limiter. In the future, we need to change the scanner methods of `HTMLEditUtils` use the new inline methods. Differential Revision: https://phabricator.services.mozilla.com/D235530
1061 lines
48 KiB
C++
1061 lines
48 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 "ErrorList.h"
|
|
#include "HTMLEditor.h"
|
|
#include "HTMLEditUtils.h"
|
|
|
|
#include "mozilla/Assertions.h"
|
|
#include "mozilla/Casting.h" // for AssertedCast
|
|
|
|
#include "nsDebug.h"
|
|
#include "nsError.h"
|
|
#include "nsIContent.h"
|
|
#include "nsIContentInlines.h"
|
|
#include "nsRange.h"
|
|
#include "nsTextFragment.h"
|
|
|
|
namespace mozilla {
|
|
|
|
using namespace dom;
|
|
|
|
/******************************************************************************
|
|
* mozilla::WSScanResult
|
|
******************************************************************************/
|
|
|
|
void WSScanResult::AssertIfInvalidData(const WSRunScanner& aScanner) const {
|
|
#ifdef DEBUG
|
|
MOZ_ASSERT(mReason == WSType::UnexpectedError ||
|
|
mReason == WSType::InUncomposedDoc ||
|
|
mReason == WSType::NonCollapsibleCharacters ||
|
|
mReason == WSType::CollapsibleWhiteSpaces ||
|
|
mReason == WSType::BRElement ||
|
|
mReason == WSType::PreformattedLineBreak ||
|
|
mReason == WSType::SpecialContent ||
|
|
mReason == WSType::CurrentBlockBoundary ||
|
|
mReason == WSType::OtherBlockBoundary ||
|
|
mReason == WSType::InlineEditingHostBoundary);
|
|
MOZ_ASSERT_IF(mReason == WSType::UnexpectedError, !mContent);
|
|
MOZ_ASSERT_IF(mReason != WSType::UnexpectedError, mContent);
|
|
MOZ_ASSERT_IF(mReason == WSType::InUncomposedDoc,
|
|
!mContent->IsInComposedDoc());
|
|
MOZ_ASSERT_IF(mContent && !mContent->IsInComposedDoc(),
|
|
mReason == WSType::InUncomposedDoc);
|
|
MOZ_ASSERT_IF(mReason == WSType::NonCollapsibleCharacters ||
|
|
mReason == WSType::CollapsibleWhiteSpaces ||
|
|
mReason == WSType::PreformattedLineBreak,
|
|
mContent->IsText());
|
|
MOZ_ASSERT_IF(mReason == WSType::NonCollapsibleCharacters ||
|
|
mReason == WSType::CollapsibleWhiteSpaces ||
|
|
mReason == WSType::PreformattedLineBreak,
|
|
mOffset.isSome());
|
|
MOZ_ASSERT_IF(mReason == WSType::NonCollapsibleCharacters ||
|
|
mReason == WSType::CollapsibleWhiteSpaces ||
|
|
mReason == WSType::PreformattedLineBreak,
|
|
mContent->AsText()->TextDataLength() > 0);
|
|
MOZ_ASSERT_IF(mDirection == ScanDirection::Backward &&
|
|
(mReason == WSType::NonCollapsibleCharacters ||
|
|
mReason == WSType::CollapsibleWhiteSpaces ||
|
|
mReason == WSType::PreformattedLineBreak),
|
|
*mOffset > 0);
|
|
MOZ_ASSERT_IF(mDirection == ScanDirection::Forward &&
|
|
(mReason == WSType::NonCollapsibleCharacters ||
|
|
mReason == WSType::CollapsibleWhiteSpaces ||
|
|
mReason == WSType::PreformattedLineBreak),
|
|
*mOffset < mContent->AsText()->TextDataLength());
|
|
MOZ_ASSERT_IF(mReason == WSType::BRElement,
|
|
mContent->IsHTMLElement(nsGkAtoms::br));
|
|
MOZ_ASSERT_IF(mReason == WSType::PreformattedLineBreak,
|
|
EditorUtils::IsNewLinePreformatted(*mContent));
|
|
MOZ_ASSERT_IF(mReason == WSType::SpecialContent,
|
|
(mContent->IsText() && !mContent->IsEditable()) ||
|
|
(!mContent->IsHTMLElement(nsGkAtoms::br) &&
|
|
!HTMLEditUtils::IsBlockElement(
|
|
*mContent, aScanner.BlockInlineCheckMode())));
|
|
MOZ_ASSERT_IF(mReason == WSType::OtherBlockBoundary,
|
|
HTMLEditUtils::IsBlockElement(*mContent,
|
|
aScanner.BlockInlineCheckMode()));
|
|
MOZ_ASSERT_IF(mReason == WSType::CurrentBlockBoundary, mContent->IsElement());
|
|
MOZ_ASSERT_IF(mReason == WSType::CurrentBlockBoundary &&
|
|
aScanner.mScanMode == WSRunScanner::Scan::EditableNodes,
|
|
mContent->IsEditable());
|
|
MOZ_ASSERT_IF(mReason == WSType::CurrentBlockBoundary,
|
|
HTMLEditUtils::IsBlockElement(
|
|
*mContent, RespectParentBlockBoundary(
|
|
aScanner.BlockInlineCheckMode())));
|
|
MOZ_ASSERT_IF(mReason == WSType::InlineEditingHostBoundary,
|
|
mContent->IsElement());
|
|
MOZ_ASSERT_IF(mReason == WSType::InlineEditingHostBoundary &&
|
|
aScanner.mScanMode == WSRunScanner::Scan::EditableNodes,
|
|
mContent->IsEditable());
|
|
MOZ_ASSERT_IF(mReason == WSType::InlineEditingHostBoundary,
|
|
!HTMLEditUtils::IsBlockElement(
|
|
*mContent, aScanner.BlockInlineCheckMode()));
|
|
MOZ_ASSERT_IF(mReason == WSType::InlineEditingHostBoundary,
|
|
!mContent->GetParentElement() ||
|
|
!mContent->GetParentElement()->IsEditable());
|
|
#endif // #ifdef DEBUG
|
|
}
|
|
|
|
/******************************************************************************
|
|
* mozilla::WSRunScanner
|
|
******************************************************************************/
|
|
|
|
template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
|
|
const EditorDOMPoint& aPoint) const;
|
|
template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
|
|
const EditorRawDOMPoint& aPoint) const;
|
|
template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
|
|
const EditorDOMPointInText& aPoint) const;
|
|
template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
|
|
const EditorRawDOMPointInText& aPoint) const;
|
|
template WSScanResult
|
|
WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(
|
|
const EditorDOMPoint& aPoint) const;
|
|
template WSScanResult
|
|
WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(
|
|
const EditorRawDOMPoint& aPoint) const;
|
|
template WSScanResult
|
|
WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(
|
|
const EditorDOMPointInText& aPoint) const;
|
|
template WSScanResult
|
|
WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(
|
|
const EditorRawDOMPointInText& aPoint) const;
|
|
template EditorDOMPoint WSRunScanner::GetAfterLastVisiblePoint(
|
|
Scan aScanMode, Text& aTextNode, const Element* aAncestorLimiter);
|
|
template EditorRawDOMPoint WSRunScanner::GetAfterLastVisiblePoint(
|
|
Scan aScanMode, Text& aTextNode, const Element* aAncestorLimiter);
|
|
template EditorDOMPoint WSRunScanner::GetFirstVisiblePoint(
|
|
Scan aScanMode, Text& aTextNode, const Element* aAncestorLimiter);
|
|
template EditorRawDOMPoint WSRunScanner::GetFirstVisiblePoint(
|
|
Scan aScanMode, Text& aTextNode, const Element* aAncestorLimiter);
|
|
|
|
template <typename PT, typename CT>
|
|
WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
|
|
const EditorDOMPointBase<PT, CT>& aPoint) const {
|
|
MOZ_ASSERT(aPoint.IsSet());
|
|
MOZ_ASSERT(aPoint.IsInComposedDoc());
|
|
|
|
if (MOZ_UNLIKELY(!aPoint.IsSet())) {
|
|
return WSScanResult::Error();
|
|
}
|
|
|
|
// We may not be able to check editable state in uncomposed tree as expected.
|
|
// For example, only some descendants in an editing host is temporarily
|
|
// removed from the tree, they are not editable unless nested contenteditable
|
|
// attribute is set to "true".
|
|
if (MOZ_UNLIKELY(!aPoint.IsInComposedDoc())) {
|
|
return WSScanResult(*this, WSScanResult::ScanDirection::Backward,
|
|
*aPoint.template ContainerAs<nsIContent>(),
|
|
WSType::InUncomposedDoc);
|
|
}
|
|
|
|
if (!TextFragmentDataAtStartRef().IsInitialized()) {
|
|
return WSScanResult::Error();
|
|
}
|
|
|
|
// 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 (mScanMode == Scan::EditableNodes && aPoint.GetChild() &&
|
|
!HTMLEditUtils::IsSimplyEditableNode((*aPoint.GetChild()))) {
|
|
return WSScanResult(*this, WSScanResult::ScanDirection::Backward,
|
|
*aPoint.GetChild(), WSType::SpecialContent);
|
|
}
|
|
const auto atPreviousChar =
|
|
GetPreviousCharPoint<EditorRawDOMPointInText>(aPoint);
|
|
// When it's a non-empty text node, return it.
|
|
if (atPreviousChar.IsSet() && !atPreviousChar.IsContainerEmpty()) {
|
|
MOZ_ASSERT(!atPreviousChar.IsEndOfContainer());
|
|
return WSScanResult(*this, WSScanResult::ScanDirection::Backward,
|
|
atPreviousChar.template NextPoint<EditorDOMPoint>(),
|
|
atPreviousChar.IsCharCollapsibleASCIISpaceOrNBSP()
|
|
? WSType::CollapsibleWhiteSpaces
|
|
: atPreviousChar.IsCharPreformattedNewLine()
|
|
? WSType::PreformattedLineBreak
|
|
: WSType::NonCollapsibleCharacters);
|
|
}
|
|
}
|
|
|
|
if (NS_WARN_IF(TextFragmentDataAtStartRef().StartRawReason() ==
|
|
WSType::UnexpectedError)) {
|
|
return WSScanResult::Error();
|
|
}
|
|
|
|
switch (TextFragmentDataAtStartRef().StartRawReason()) {
|
|
case WSType::CollapsibleWhiteSpaces:
|
|
case WSType::NonCollapsibleCharacters:
|
|
case WSType::PreformattedLineBreak:
|
|
MOZ_ASSERT(TextFragmentDataAtStartRef().StartRef().IsSet());
|
|
// XXX: If we find the character at last of a text node and we started
|
|
// scanning from following text node of it, some callers may work with the
|
|
// point in the following text node instead of end of the found text node.
|
|
return WSScanResult(*this, WSScanResult::ScanDirection::Backward,
|
|
TextFragmentDataAtStartRef().StartRef(),
|
|
TextFragmentDataAtStartRef().StartRawReason());
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Otherwise, return the start of the range.
|
|
if (TextFragmentDataAtStartRef().GetStartReasonContent() !=
|
|
TextFragmentDataAtStartRef().StartRef().GetContainer()) {
|
|
if (NS_WARN_IF(!TextFragmentDataAtStartRef().GetStartReasonContent())) {
|
|
return WSScanResult::Error();
|
|
}
|
|
// In this case, TextFragmentDataAtStartRef().StartRef().Offset() is not
|
|
// meaningful.
|
|
return WSScanResult(*this, WSScanResult::ScanDirection::Backward,
|
|
*TextFragmentDataAtStartRef().GetStartReasonContent(),
|
|
TextFragmentDataAtStartRef().StartRawReason());
|
|
}
|
|
if (NS_WARN_IF(!TextFragmentDataAtStartRef().StartRef().IsSet())) {
|
|
return WSScanResult::Error();
|
|
}
|
|
return WSScanResult(*this, WSScanResult::ScanDirection::Backward,
|
|
TextFragmentDataAtStartRef().StartRef(),
|
|
TextFragmentDataAtStartRef().StartRawReason());
|
|
}
|
|
|
|
template <typename PT, typename CT>
|
|
WSScanResult WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(
|
|
const EditorDOMPointBase<PT, CT>& aPoint) const {
|
|
MOZ_ASSERT(aPoint.IsSet());
|
|
MOZ_ASSERT(aPoint.IsInComposedDoc());
|
|
|
|
if (MOZ_UNLIKELY(!aPoint.IsSet())) {
|
|
return WSScanResult::Error();
|
|
}
|
|
|
|
// We may not be able to check editable state in uncomposed tree as expected.
|
|
// For example, only some descendants in an editing host is temporarily
|
|
// removed from the tree, they are not editable unless nested contenteditable
|
|
// attribute is set to "true".
|
|
if (MOZ_UNLIKELY(!aPoint.IsInComposedDoc())) {
|
|
return WSScanResult(*this, WSScanResult::ScanDirection::Forward,
|
|
*aPoint.template ContainerAs<nsIContent>(),
|
|
WSType::InUncomposedDoc);
|
|
}
|
|
|
|
if (!TextFragmentDataAtStartRef().IsInitialized()) {
|
|
return WSScanResult::Error();
|
|
}
|
|
|
|
// 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 (mScanMode == Scan::EditableNodes && aPoint.GetChild() &&
|
|
!HTMLEditUtils::IsSimplyEditableNode(*aPoint.GetChild())) {
|
|
return WSScanResult(*this, WSScanResult::ScanDirection::Forward,
|
|
*aPoint.GetChild(), WSType::SpecialContent);
|
|
}
|
|
const auto atNextChar = GetInclusiveNextCharPoint<EditorDOMPoint>(aPoint);
|
|
// When it's a non-empty text node, return it.
|
|
if (atNextChar.IsSet() && !atNextChar.IsContainerEmpty()) {
|
|
return WSScanResult(*this, WSScanResult::ScanDirection::Forward,
|
|
atNextChar,
|
|
!atNextChar.IsEndOfContainer() &&
|
|
atNextChar.IsCharCollapsibleASCIISpaceOrNBSP()
|
|
? WSType::CollapsibleWhiteSpaces
|
|
: !atNextChar.IsEndOfContainer() &&
|
|
atNextChar.IsCharPreformattedNewLine()
|
|
? WSType::PreformattedLineBreak
|
|
: WSType::NonCollapsibleCharacters);
|
|
}
|
|
}
|
|
|
|
if (NS_WARN_IF(TextFragmentDataAtStartRef().EndRawReason() ==
|
|
WSType::UnexpectedError)) {
|
|
return WSScanResult::Error();
|
|
}
|
|
|
|
switch (TextFragmentDataAtStartRef().EndRawReason()) {
|
|
case WSType::CollapsibleWhiteSpaces:
|
|
case WSType::NonCollapsibleCharacters:
|
|
case WSType::PreformattedLineBreak:
|
|
MOZ_ASSERT(TextFragmentDataAtStartRef().StartRef().IsSet());
|
|
// XXX: If we find the character at start of a text node and we
|
|
// started scanning from preceding text node of it, some callers may want
|
|
// to work with the point at end of the preceding text node instead of
|
|
// start of the found text node.
|
|
return WSScanResult(*this, WSScanResult::ScanDirection::Forward,
|
|
TextFragmentDataAtStartRef().EndRef(),
|
|
TextFragmentDataAtStartRef().EndRawReason());
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Otherwise, return the end of the range.
|
|
if (TextFragmentDataAtStartRef().GetEndReasonContent() !=
|
|
TextFragmentDataAtStartRef().EndRef().GetContainer()) {
|
|
if (NS_WARN_IF(!TextFragmentDataAtStartRef().GetEndReasonContent())) {
|
|
return WSScanResult::Error();
|
|
}
|
|
// In this case, TextFragmentDataAtStartRef().EndRef().Offset() is not
|
|
// meaningful.
|
|
return WSScanResult(*this, WSScanResult::ScanDirection::Forward,
|
|
*TextFragmentDataAtStartRef().GetEndReasonContent(),
|
|
TextFragmentDataAtStartRef().EndRawReason());
|
|
}
|
|
if (NS_WARN_IF(!TextFragmentDataAtStartRef().EndRef().IsSet())) {
|
|
return WSScanResult::Error();
|
|
}
|
|
return WSScanResult(*this, WSScanResult::ScanDirection::Forward,
|
|
TextFragmentDataAtStartRef().EndRef(),
|
|
TextFragmentDataAtStartRef().EndRawReason());
|
|
}
|
|
|
|
// static
|
|
template <typename EditorDOMPointType>
|
|
EditorDOMPointType WSRunScanner::GetAfterLastVisiblePoint(
|
|
Scan aScanMode, Text& aTextNode,
|
|
const Element* aAncestorLimiter /* = nullptr */) {
|
|
EditorDOMPoint atLastCharOfTextNode(
|
|
&aTextNode, AssertedCast<uint32_t>(std::max<int64_t>(
|
|
static_cast<int64_t>(aTextNode.Length()) - 1, 0)));
|
|
if (!atLastCharOfTextNode.IsContainerEmpty() &&
|
|
!atLastCharOfTextNode.IsCharCollapsibleASCIISpace()) {
|
|
return EditorDOMPointType::AtEndOf(aTextNode);
|
|
}
|
|
const TextFragmentData textFragmentData(
|
|
aScanMode, atLastCharOfTextNode,
|
|
BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter);
|
|
if (NS_WARN_IF(!textFragmentData.IsInitialized())) {
|
|
return EditorDOMPointType(); // TODO: Make here return error with Err.
|
|
}
|
|
const EditorDOMRange& invisibleWhiteSpaceRange =
|
|
textFragmentData.InvisibleTrailingWhiteSpaceRangeRef();
|
|
if (!invisibleWhiteSpaceRange.IsPositioned() ||
|
|
invisibleWhiteSpaceRange.Collapsed()) {
|
|
return EditorDOMPointType::AtEndOf(aTextNode);
|
|
}
|
|
return invisibleWhiteSpaceRange.StartRef().To<EditorDOMPointType>();
|
|
}
|
|
|
|
// static
|
|
template <typename EditorDOMPointType>
|
|
EditorDOMPointType WSRunScanner::GetFirstVisiblePoint(
|
|
Scan aScanMode, Text& aTextNode,
|
|
const Element* aAncestorLimiter /* = nullptr */) {
|
|
EditorDOMPoint atStartOfTextNode(&aTextNode, 0);
|
|
if (!atStartOfTextNode.IsContainerEmpty() &&
|
|
atStartOfTextNode.IsCharCollapsibleASCIISpace()) {
|
|
return atStartOfTextNode.To<EditorDOMPointType>();
|
|
}
|
|
const TextFragmentData textFragmentData(
|
|
aScanMode, atStartOfTextNode, BlockInlineCheck::UseComputedDisplayStyle,
|
|
aAncestorLimiter);
|
|
if (NS_WARN_IF(!textFragmentData.IsInitialized())) {
|
|
return EditorDOMPointType(); // TODO: Make here return error with Err.
|
|
}
|
|
const EditorDOMRange& invisibleWhiteSpaceRange =
|
|
textFragmentData.InvisibleLeadingWhiteSpaceRangeRef();
|
|
if (!invisibleWhiteSpaceRange.IsPositioned() ||
|
|
invisibleWhiteSpaceRange.Collapsed()) {
|
|
return atStartOfTextNode.To<EditorDOMPointType>();
|
|
}
|
|
return invisibleWhiteSpaceRange.EndRef().To<EditorDOMPointType>();
|
|
}
|
|
|
|
/*****************************************************************************
|
|
* Implementation for new white-space normalizer
|
|
*****************************************************************************/
|
|
|
|
// static
|
|
EditorDOMRangeInTexts
|
|
WSRunScanner::ComputeRangeInTextNodesContainingInvisibleWhiteSpaces(
|
|
const TextFragmentData& aStart, const TextFragmentData& aEnd) {
|
|
MOZ_ASSERT(aStart.ScanMode() == aEnd.ScanMode());
|
|
|
|
// Corresponding to handling invisible white-spaces part of
|
|
// `TextFragmentData::GetReplaceRangeDataAtEndOfDeletionRange()` and
|
|
// `TextFragmentData::GetReplaceRangeDataAtStartOfDeletionRange()`
|
|
|
|
MOZ_ASSERT(aStart.ScanStartRef().IsSetAndValid());
|
|
MOZ_ASSERT(aEnd.ScanStartRef().IsSetAndValid());
|
|
MOZ_ASSERT(aStart.ScanStartRef().EqualsOrIsBefore(aEnd.ScanStartRef()));
|
|
MOZ_ASSERT(aStart.ScanStartRef().IsInTextNode());
|
|
MOZ_ASSERT(aEnd.ScanStartRef().IsInTextNode());
|
|
|
|
// XXX `GetReplaceRangeDataAtEndOfDeletionRange()` and
|
|
// `GetReplaceRangeDataAtStartOfDeletionRange()` use
|
|
// `GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt()` and
|
|
// `GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt()`.
|
|
// However, they are really odd as mentioned with "XXX" comments
|
|
// in them. For the new white-space normalizer, we need to treat
|
|
// invisible white-spaces stricter because the legacy path handles
|
|
// white-spaces multiple times (e.g., calling `HTMLEditor::
|
|
// DeleteNodeIfInvisibleAndEditableTextNode()` later) and that hides
|
|
// the bug, but in the new path, we should stop doing same things
|
|
// multiple times for both performance and footprint. Therefore,
|
|
// even though the result might be different in some edge cases,
|
|
// we should use clean path for now. Perhaps, we should fix the odd
|
|
// cases before shipping `beforeinput` event in release channel.
|
|
|
|
const EditorDOMRange& invisibleLeadingWhiteSpaceRange =
|
|
aStart.InvisibleLeadingWhiteSpaceRangeRef();
|
|
const EditorDOMRange& invisibleTrailingWhiteSpaceRange =
|
|
aEnd.InvisibleTrailingWhiteSpaceRangeRef();
|
|
const bool hasInvisibleLeadingWhiteSpaces =
|
|
invisibleLeadingWhiteSpaceRange.IsPositioned() &&
|
|
!invisibleLeadingWhiteSpaceRange.Collapsed();
|
|
const bool hasInvisibleTrailingWhiteSpaces =
|
|
invisibleLeadingWhiteSpaceRange != invisibleTrailingWhiteSpaceRange &&
|
|
invisibleTrailingWhiteSpaceRange.IsPositioned() &&
|
|
!invisibleTrailingWhiteSpaceRange.Collapsed();
|
|
|
|
EditorDOMRangeInTexts result(aStart.ScanStartRef().AsInText(),
|
|
aEnd.ScanStartRef().AsInText());
|
|
MOZ_ASSERT(result.IsPositionedAndValid());
|
|
if (!hasInvisibleLeadingWhiteSpaces && !hasInvisibleTrailingWhiteSpaces) {
|
|
return result;
|
|
}
|
|
|
|
MOZ_ASSERT_IF(
|
|
hasInvisibleLeadingWhiteSpaces && hasInvisibleTrailingWhiteSpaces,
|
|
invisibleLeadingWhiteSpaceRange.StartRef().IsBefore(
|
|
invisibleTrailingWhiteSpaceRange.StartRef()));
|
|
const EditorDOMPoint& aroundFirstInvisibleWhiteSpace =
|
|
hasInvisibleLeadingWhiteSpaces
|
|
? invisibleLeadingWhiteSpaceRange.StartRef()
|
|
: invisibleTrailingWhiteSpaceRange.StartRef();
|
|
if (aroundFirstInvisibleWhiteSpace.IsBefore(result.StartRef())) {
|
|
if (aroundFirstInvisibleWhiteSpace.IsInTextNode()) {
|
|
result.SetStart(aroundFirstInvisibleWhiteSpace.AsInText());
|
|
MOZ_ASSERT(result.IsPositionedAndValid());
|
|
} else {
|
|
const auto atFirstInvisibleWhiteSpace =
|
|
hasInvisibleLeadingWhiteSpaces
|
|
? aStart.GetInclusiveNextCharPoint<EditorDOMPointInText>(
|
|
aroundFirstInvisibleWhiteSpace,
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(
|
|
aStart.ScanMode()))
|
|
: aEnd.GetInclusiveNextCharPoint<EditorDOMPointInText>(
|
|
aroundFirstInvisibleWhiteSpace,
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(
|
|
aEnd.ScanMode()));
|
|
MOZ_ASSERT(atFirstInvisibleWhiteSpace.IsSet());
|
|
MOZ_ASSERT(
|
|
atFirstInvisibleWhiteSpace.EqualsOrIsBefore(result.StartRef()));
|
|
result.SetStart(atFirstInvisibleWhiteSpace);
|
|
MOZ_ASSERT(result.IsPositionedAndValid());
|
|
}
|
|
}
|
|
MOZ_ASSERT_IF(
|
|
hasInvisibleLeadingWhiteSpaces && hasInvisibleTrailingWhiteSpaces,
|
|
invisibleLeadingWhiteSpaceRange.EndRef().IsBefore(
|
|
invisibleTrailingWhiteSpaceRange.EndRef()));
|
|
const EditorDOMPoint& afterLastInvisibleWhiteSpace =
|
|
hasInvisibleTrailingWhiteSpaces
|
|
? invisibleTrailingWhiteSpaceRange.EndRef()
|
|
: invisibleLeadingWhiteSpaceRange.EndRef();
|
|
if (afterLastInvisibleWhiteSpace.EqualsOrIsBefore(result.EndRef())) {
|
|
MOZ_ASSERT(result.IsPositionedAndValid());
|
|
return result;
|
|
}
|
|
if (afterLastInvisibleWhiteSpace.IsInTextNode()) {
|
|
result.SetEnd(afterLastInvisibleWhiteSpace.AsInText());
|
|
MOZ_ASSERT(result.IsPositionedAndValid());
|
|
return result;
|
|
}
|
|
const auto atLastInvisibleWhiteSpace =
|
|
hasInvisibleTrailingWhiteSpaces
|
|
? aEnd.GetPreviousCharPoint<EditorDOMPointInText>(
|
|
afterLastInvisibleWhiteSpace,
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(aEnd.ScanMode()))
|
|
: aStart.GetPreviousCharPoint<EditorDOMPointInText>(
|
|
afterLastInvisibleWhiteSpace,
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(
|
|
aStart.ScanMode()));
|
|
MOZ_ASSERT(atLastInvisibleWhiteSpace.IsSet());
|
|
MOZ_ASSERT(atLastInvisibleWhiteSpace.IsContainerEmpty() ||
|
|
atLastInvisibleWhiteSpace.IsAtLastContent());
|
|
MOZ_ASSERT(result.EndRef().EqualsOrIsBefore(atLastInvisibleWhiteSpace));
|
|
result.SetEnd(atLastInvisibleWhiteSpace.IsEndOfContainer()
|
|
? atLastInvisibleWhiteSpace
|
|
: atLastInvisibleWhiteSpace.NextPoint());
|
|
MOZ_ASSERT(result.IsPositionedAndValid());
|
|
return result;
|
|
}
|
|
|
|
// static
|
|
Result<EditorDOMRangeInTexts, nsresult>
|
|
WSRunScanner::GetRangeInTextNodesToBackspaceFrom(
|
|
Scan aScanMode, const EditorDOMPoint& aPoint,
|
|
const Element* aAncestorLimiter /* = nullptr */) {
|
|
// Corresponding to computing delete range part of
|
|
// `WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace()`
|
|
MOZ_ASSERT(aPoint.IsSetAndValid());
|
|
|
|
const TextFragmentData textFragmentDataAtCaret(
|
|
aScanMode, aPoint, BlockInlineCheck::UseComputedDisplayStyle,
|
|
aAncestorLimiter);
|
|
if (NS_WARN_IF(!textFragmentDataAtCaret.IsInitialized())) {
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
auto atPreviousChar =
|
|
textFragmentDataAtCaret.GetPreviousCharPoint<EditorDOMPointInText>(
|
|
aPoint, ShouldIgnoreNonEditableSiblingsOrDescendants(aScanMode));
|
|
if (!atPreviousChar.IsSet()) {
|
|
return EditorDOMRangeInTexts(); // There is no content in the block.
|
|
}
|
|
|
|
// XXX When previous char point is in an empty text node, we do nothing,
|
|
// but this must look odd from point of user view. We should delete
|
|
// something before aPoint.
|
|
if (atPreviousChar.IsEndOfContainer()) {
|
|
return EditorDOMRangeInTexts();
|
|
}
|
|
|
|
// Extend delete range if previous char is a low surrogate following
|
|
// a high surrogate.
|
|
EditorDOMPointInText atNextChar = atPreviousChar.NextPoint();
|
|
if (!atPreviousChar.IsStartOfContainer()) {
|
|
if (atPreviousChar.IsCharLowSurrogateFollowingHighSurrogate()) {
|
|
atPreviousChar = atPreviousChar.PreviousPoint();
|
|
}
|
|
// If caret is in middle of a surrogate pair, delete the surrogate pair
|
|
// (blink-compat).
|
|
else if (atPreviousChar.IsCharHighSurrogateFollowedByLowSurrogate()) {
|
|
atNextChar = atNextChar.NextPoint();
|
|
}
|
|
}
|
|
|
|
// If previous char is an collapsible white-spaces, delete all adjacent
|
|
// white-spaces which are collapsed together.
|
|
EditorDOMRangeInTexts rangeToDelete;
|
|
if (atPreviousChar.IsCharCollapsibleASCIISpace() ||
|
|
atPreviousChar.IsCharPreformattedNewLineCollapsedWithWhiteSpaces()) {
|
|
const auto startToDelete =
|
|
textFragmentDataAtCaret
|
|
.GetFirstASCIIWhiteSpacePointCollapsedTo<EditorDOMPointInText>(
|
|
atPreviousChar, nsIEditor::ePrevious,
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(aScanMode));
|
|
if (!startToDelete.IsSet()) {
|
|
NS_WARNING(
|
|
"WSRunScanner::GetFirstASCIIWhiteSpacePointCollapsedTo() failed");
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
const auto endToDelete =
|
|
textFragmentDataAtCaret
|
|
.GetEndOfCollapsibleASCIIWhiteSpaces<EditorDOMPointInText>(
|
|
atPreviousChar, nsIEditor::ePrevious,
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(aScanMode));
|
|
if (!endToDelete.IsSet()) {
|
|
NS_WARNING("WSRunScanner::GetEndOfCollapsibleASCIIWhiteSpaces() failed");
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
rangeToDelete = EditorDOMRangeInTexts(startToDelete, endToDelete);
|
|
}
|
|
// if previous char is not a collapsible white-space, remove it.
|
|
else {
|
|
rangeToDelete = EditorDOMRangeInTexts(atPreviousChar, atNextChar);
|
|
}
|
|
|
|
// If there is no removable and visible content, we should do nothing.
|
|
if (rangeToDelete.Collapsed()) {
|
|
return EditorDOMRangeInTexts();
|
|
}
|
|
|
|
// And also delete invisible white-spaces if they become visible.
|
|
const TextFragmentData textFragmentDataAtStart =
|
|
rangeToDelete.StartRef() != aPoint
|
|
? TextFragmentData(aScanMode, rangeToDelete.StartRef(),
|
|
BlockInlineCheck::UseComputedDisplayStyle,
|
|
aAncestorLimiter)
|
|
: textFragmentDataAtCaret;
|
|
const TextFragmentData textFragmentDataAtEnd =
|
|
rangeToDelete.EndRef() != aPoint
|
|
? TextFragmentData(aScanMode, rangeToDelete.EndRef(),
|
|
BlockInlineCheck::UseComputedDisplayStyle,
|
|
aAncestorLimiter)
|
|
: textFragmentDataAtCaret;
|
|
if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized()) ||
|
|
NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) {
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
EditorDOMRangeInTexts extendedRangeToDelete =
|
|
WSRunScanner::ComputeRangeInTextNodesContainingInvisibleWhiteSpaces(
|
|
textFragmentDataAtStart, textFragmentDataAtEnd);
|
|
MOZ_ASSERT(extendedRangeToDelete.IsPositionedAndValid());
|
|
return extendedRangeToDelete.IsPositioned() ? extendedRangeToDelete
|
|
: rangeToDelete;
|
|
}
|
|
|
|
// static
|
|
Result<EditorDOMRangeInTexts, nsresult>
|
|
WSRunScanner::GetRangeInTextNodesToForwardDeleteFrom(
|
|
Scan aScanMode, const EditorDOMPoint& aPoint,
|
|
const Element* aAncestorLimiter /* = nullptr */) {
|
|
// Corresponding to computing delete range part of
|
|
// `WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace()`
|
|
MOZ_ASSERT(aPoint.IsSetAndValid());
|
|
|
|
const TextFragmentData textFragmentDataAtCaret(
|
|
aScanMode, aPoint, BlockInlineCheck::UseComputedDisplayStyle,
|
|
aAncestorLimiter);
|
|
if (NS_WARN_IF(!textFragmentDataAtCaret.IsInitialized())) {
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
auto atCaret =
|
|
textFragmentDataAtCaret.GetInclusiveNextCharPoint<EditorDOMPointInText>(
|
|
aPoint, ShouldIgnoreNonEditableSiblingsOrDescendants(aScanMode));
|
|
if (!atCaret.IsSet()) {
|
|
return EditorDOMRangeInTexts(); // There is no content in the block.
|
|
}
|
|
// If caret is in middle of a surrogate pair, we should remove next
|
|
// character (blink-compat).
|
|
if (!atCaret.IsEndOfContainer() &&
|
|
atCaret.IsCharLowSurrogateFollowingHighSurrogate()) {
|
|
atCaret = atCaret.NextPoint();
|
|
}
|
|
|
|
// XXX When next char point is in an empty text node, we do nothing,
|
|
// but this must look odd from point of user view. We should delete
|
|
// something after aPoint.
|
|
if (atCaret.IsEndOfContainer()) {
|
|
return EditorDOMRangeInTexts();
|
|
}
|
|
|
|
// Extend delete range if previous char is a low surrogate following
|
|
// a high surrogate.
|
|
EditorDOMPointInText atNextChar = atCaret.NextPoint();
|
|
if (atCaret.IsCharHighSurrogateFollowedByLowSurrogate()) {
|
|
atNextChar = atNextChar.NextPoint();
|
|
}
|
|
|
|
// If next char is a collapsible white-space, delete all adjacent white-spaces
|
|
// which are collapsed together.
|
|
EditorDOMRangeInTexts rangeToDelete;
|
|
if (atCaret.IsCharCollapsibleASCIISpace() ||
|
|
atCaret.IsCharPreformattedNewLineCollapsedWithWhiteSpaces()) {
|
|
const auto startToDelete =
|
|
textFragmentDataAtCaret
|
|
.GetFirstASCIIWhiteSpacePointCollapsedTo<EditorDOMPointInText>(
|
|
atCaret, nsIEditor::eNext,
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(aScanMode));
|
|
if (!startToDelete.IsSet()) {
|
|
NS_WARNING(
|
|
"WSRunScanner::GetFirstASCIIWhiteSpacePointCollapsedTo() failed");
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
const EditorDOMPointInText endToDelete =
|
|
textFragmentDataAtCaret
|
|
.GetEndOfCollapsibleASCIIWhiteSpaces<EditorDOMPointInText>(
|
|
atCaret, nsIEditor::eNext,
|
|
ShouldIgnoreNonEditableSiblingsOrDescendants(aScanMode));
|
|
if (!endToDelete.IsSet()) {
|
|
NS_WARNING("WSRunScanner::GetEndOfCollapsibleASCIIWhiteSpaces() failed");
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
rangeToDelete = EditorDOMRangeInTexts(startToDelete, endToDelete);
|
|
}
|
|
// if next char is not a collapsible white-space, remove it.
|
|
else {
|
|
rangeToDelete = EditorDOMRangeInTexts(atCaret, atNextChar);
|
|
}
|
|
|
|
// If there is no removable and visible content, we should do nothing.
|
|
if (rangeToDelete.Collapsed()) {
|
|
return EditorDOMRangeInTexts();
|
|
}
|
|
|
|
// And also delete invisible white-spaces if they become visible.
|
|
const TextFragmentData textFragmentDataAtStart =
|
|
rangeToDelete.StartRef() != aPoint
|
|
? TextFragmentData(aScanMode, rangeToDelete.StartRef(),
|
|
BlockInlineCheck::UseComputedDisplayStyle,
|
|
aAncestorLimiter)
|
|
: textFragmentDataAtCaret;
|
|
const TextFragmentData textFragmentDataAtEnd =
|
|
rangeToDelete.EndRef() != aPoint
|
|
? TextFragmentData(aScanMode, rangeToDelete.EndRef(),
|
|
BlockInlineCheck::UseComputedDisplayStyle,
|
|
aAncestorLimiter)
|
|
: textFragmentDataAtCaret;
|
|
if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized()) ||
|
|
NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) {
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
EditorDOMRangeInTexts extendedRangeToDelete =
|
|
WSRunScanner::ComputeRangeInTextNodesContainingInvisibleWhiteSpaces(
|
|
textFragmentDataAtStart, textFragmentDataAtEnd);
|
|
MOZ_ASSERT(extendedRangeToDelete.IsPositionedAndValid());
|
|
return extendedRangeToDelete.IsPositioned() ? extendedRangeToDelete
|
|
: rangeToDelete;
|
|
}
|
|
|
|
// static
|
|
EditorDOMRange WSRunScanner::GetRangesForDeletingAtomicContent(
|
|
Scan aScanMode, const nsIContent& aAtomicContent,
|
|
const Element* aAncestorLimiter /* = nullptr */) {
|
|
if (aAtomicContent.IsHTMLElement(nsGkAtoms::br)) {
|
|
// Preceding white-spaces should be preserved, but the following
|
|
// white-spaces should be invisible around `<br>` element.
|
|
const TextFragmentData textFragmentDataAfterBRElement(
|
|
aScanMode, EditorDOMPoint::After(aAtomicContent),
|
|
BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter);
|
|
if (NS_WARN_IF(!textFragmentDataAfterBRElement.IsInitialized())) {
|
|
return EditorDOMRange(); // TODO: Make here return error with Err.
|
|
}
|
|
const EditorDOMRangeInTexts followingInvisibleWhiteSpaces =
|
|
textFragmentDataAfterBRElement.GetNonCollapsedRangeInTexts(
|
|
textFragmentDataAfterBRElement
|
|
.InvisibleLeadingWhiteSpaceRangeRef());
|
|
return followingInvisibleWhiteSpaces.IsPositioned() &&
|
|
!followingInvisibleWhiteSpaces.Collapsed()
|
|
? EditorDOMRange(
|
|
EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
|
|
followingInvisibleWhiteSpaces.EndRef())
|
|
: EditorDOMRange(
|
|
EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
|
|
EditorDOMPoint::After(aAtomicContent));
|
|
}
|
|
|
|
if (!HTMLEditUtils::IsBlockElement(
|
|
aAtomicContent, BlockInlineCheck::UseComputedDisplayStyle)) {
|
|
// Both preceding and following white-spaces around it should be preserved
|
|
// around inline elements like `<img>`.
|
|
return EditorDOMRange(
|
|
EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
|
|
EditorDOMPoint::After(aAtomicContent));
|
|
}
|
|
|
|
// Both preceding and following white-spaces can be invisible around a
|
|
// block element.
|
|
const TextFragmentData textFragmentDataBeforeAtomicContent(
|
|
aScanMode, EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
|
|
BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter);
|
|
if (NS_WARN_IF(!textFragmentDataBeforeAtomicContent.IsInitialized())) {
|
|
return EditorDOMRange(); // TODO: Make here return error with Err.
|
|
}
|
|
const EditorDOMRangeInTexts precedingInvisibleWhiteSpaces =
|
|
textFragmentDataBeforeAtomicContent.GetNonCollapsedRangeInTexts(
|
|
textFragmentDataBeforeAtomicContent
|
|
.InvisibleTrailingWhiteSpaceRangeRef());
|
|
const TextFragmentData textFragmentDataAfterAtomicContent(
|
|
aScanMode, EditorDOMPoint::After(aAtomicContent),
|
|
BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter);
|
|
if (NS_WARN_IF(!textFragmentDataAfterAtomicContent.IsInitialized())) {
|
|
return EditorDOMRange(); // TODO: Make here return error with Err.
|
|
}
|
|
const EditorDOMRangeInTexts followingInvisibleWhiteSpaces =
|
|
textFragmentDataAfterAtomicContent.GetNonCollapsedRangeInTexts(
|
|
textFragmentDataAfterAtomicContent
|
|
.InvisibleLeadingWhiteSpaceRangeRef());
|
|
if (precedingInvisibleWhiteSpaces.StartRef().IsSet() &&
|
|
followingInvisibleWhiteSpaces.EndRef().IsSet()) {
|
|
return EditorDOMRange(precedingInvisibleWhiteSpaces.StartRef(),
|
|
followingInvisibleWhiteSpaces.EndRef());
|
|
}
|
|
if (precedingInvisibleWhiteSpaces.StartRef().IsSet()) {
|
|
return EditorDOMRange(precedingInvisibleWhiteSpaces.StartRef(),
|
|
EditorDOMPoint::After(aAtomicContent));
|
|
}
|
|
if (followingInvisibleWhiteSpaces.EndRef().IsSet()) {
|
|
return EditorDOMRange(
|
|
EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
|
|
followingInvisibleWhiteSpaces.EndRef());
|
|
}
|
|
return EditorDOMRange(
|
|
EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
|
|
EditorDOMPoint::After(aAtomicContent));
|
|
}
|
|
|
|
// static
|
|
EditorDOMRange WSRunScanner::GetRangeForDeletingBlockElementBoundaries(
|
|
Scan aScanMode, const Element& aLeftBlockElement,
|
|
const Element& aRightBlockElement,
|
|
const EditorDOMPoint& aPointContainingTheOtherBlock,
|
|
const Element* aAncestorLimiter /* = nullptr */) {
|
|
MOZ_ASSERT(&aLeftBlockElement != &aRightBlockElement);
|
|
MOZ_ASSERT_IF(
|
|
aPointContainingTheOtherBlock.IsSet(),
|
|
aPointContainingTheOtherBlock.GetContainer() == &aLeftBlockElement ||
|
|
aPointContainingTheOtherBlock.GetContainer() == &aRightBlockElement);
|
|
MOZ_ASSERT_IF(
|
|
aPointContainingTheOtherBlock.GetContainer() == &aLeftBlockElement,
|
|
aRightBlockElement.IsInclusiveDescendantOf(
|
|
aPointContainingTheOtherBlock.GetChild()));
|
|
MOZ_ASSERT_IF(
|
|
aPointContainingTheOtherBlock.GetContainer() == &aRightBlockElement,
|
|
aLeftBlockElement.IsInclusiveDescendantOf(
|
|
aPointContainingTheOtherBlock.GetChild()));
|
|
MOZ_ASSERT_IF(
|
|
!aPointContainingTheOtherBlock.IsSet(),
|
|
!aRightBlockElement.IsInclusiveDescendantOf(&aLeftBlockElement));
|
|
MOZ_ASSERT_IF(
|
|
!aPointContainingTheOtherBlock.IsSet(),
|
|
!aLeftBlockElement.IsInclusiveDescendantOf(&aRightBlockElement));
|
|
MOZ_ASSERT_IF(!aPointContainingTheOtherBlock.IsSet(),
|
|
EditorRawDOMPoint(const_cast<Element*>(&aLeftBlockElement))
|
|
.IsBefore(EditorRawDOMPoint(
|
|
const_cast<Element*>(&aRightBlockElement))));
|
|
MOZ_ASSERT_IF(aAncestorLimiter,
|
|
aLeftBlockElement.IsInclusiveDescendantOf(aAncestorLimiter));
|
|
MOZ_ASSERT_IF(aAncestorLimiter,
|
|
aRightBlockElement.IsInclusiveDescendantOf(aAncestorLimiter));
|
|
MOZ_ASSERT_IF(aScanMode == Scan::EditableNodes,
|
|
const_cast<Element&>(aLeftBlockElement).GetEditingHost() ==
|
|
const_cast<Element&>(aRightBlockElement).GetEditingHost());
|
|
|
|
EditorDOMRange range;
|
|
// Include trailing invisible white-spaces in aLeftBlockElement.
|
|
const TextFragmentData textFragmentDataAtEndOfLeftBlockElement(
|
|
aScanMode,
|
|
aPointContainingTheOtherBlock.GetContainer() == &aLeftBlockElement
|
|
? aPointContainingTheOtherBlock
|
|
: EditorDOMPoint::AtEndOf(const_cast<Element&>(aLeftBlockElement)),
|
|
BlockInlineCheck::UseComputedDisplayOutsideStyle, aAncestorLimiter);
|
|
if (NS_WARN_IF(!textFragmentDataAtEndOfLeftBlockElement.IsInitialized())) {
|
|
return EditorDOMRange(); // TODO: Make here return error with Err.
|
|
}
|
|
if (textFragmentDataAtEndOfLeftBlockElement.StartsFromInvisibleBRElement()) {
|
|
// If the left block element ends with an invisible `<br>` element,
|
|
// it'll be deleted (and it means there is no invisible trailing
|
|
// white-spaces). Therefore, the range should start from the invisible
|
|
// `<br>` element.
|
|
range.SetStart(EditorDOMPoint(
|
|
textFragmentDataAtEndOfLeftBlockElement.StartReasonBRElementPtr()));
|
|
} else {
|
|
const EditorDOMRange& trailingWhiteSpaceRange =
|
|
textFragmentDataAtEndOfLeftBlockElement
|
|
.InvisibleTrailingWhiteSpaceRangeRef();
|
|
if (trailingWhiteSpaceRange.StartRef().IsSet()) {
|
|
range.SetStart(trailingWhiteSpaceRange.StartRef());
|
|
} else {
|
|
range.SetStart(textFragmentDataAtEndOfLeftBlockElement.ScanStartRef());
|
|
}
|
|
}
|
|
// Include leading invisible white-spaces in aRightBlockElement.
|
|
const TextFragmentData textFragmentDataAtStartOfRightBlockElement(
|
|
aScanMode,
|
|
aPointContainingTheOtherBlock.GetContainer() == &aRightBlockElement &&
|
|
!aPointContainingTheOtherBlock.IsEndOfContainer()
|
|
? aPointContainingTheOtherBlock.NextPoint()
|
|
: EditorDOMPoint(const_cast<Element*>(&aRightBlockElement), 0u),
|
|
BlockInlineCheck::UseComputedDisplayOutsideStyle, aAncestorLimiter);
|
|
if (NS_WARN_IF(!textFragmentDataAtStartOfRightBlockElement.IsInitialized())) {
|
|
return EditorDOMRange(); // TODO: Make here return error with Err.
|
|
}
|
|
const EditorDOMRange& leadingWhiteSpaceRange =
|
|
textFragmentDataAtStartOfRightBlockElement
|
|
.InvisibleLeadingWhiteSpaceRangeRef();
|
|
if (leadingWhiteSpaceRange.EndRef().IsSet()) {
|
|
range.SetEnd(leadingWhiteSpaceRange.EndRef());
|
|
} else {
|
|
range.SetEnd(textFragmentDataAtStartOfRightBlockElement.ScanStartRef());
|
|
}
|
|
return range;
|
|
}
|
|
|
|
// static
|
|
EditorDOMRange
|
|
WSRunScanner::GetRangeContainingInvisibleWhiteSpacesAtRangeBoundaries(
|
|
Scan aScanMode, const EditorDOMRange& aRange,
|
|
const Element* aAncestorLimiter /* = nullptr */) {
|
|
MOZ_ASSERT(aRange.IsPositionedAndValid());
|
|
MOZ_ASSERT(aRange.EndRef().IsSetAndValid());
|
|
MOZ_ASSERT(aRange.StartRef().IsSetAndValid());
|
|
|
|
EditorDOMRange result;
|
|
const TextFragmentData textFragmentDataAtStart(
|
|
aScanMode, aRange.StartRef(), BlockInlineCheck::UseComputedDisplayStyle,
|
|
aAncestorLimiter);
|
|
if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized())) {
|
|
return EditorDOMRange(); // TODO: Make here return error with Err.
|
|
}
|
|
const EditorDOMRangeInTexts invisibleLeadingWhiteSpacesAtStart =
|
|
textFragmentDataAtStart.GetNonCollapsedRangeInTexts(
|
|
textFragmentDataAtStart.InvisibleLeadingWhiteSpaceRangeRef());
|
|
if (invisibleLeadingWhiteSpacesAtStart.IsPositioned() &&
|
|
!invisibleLeadingWhiteSpacesAtStart.Collapsed()) {
|
|
result.SetStart(invisibleLeadingWhiteSpacesAtStart.StartRef());
|
|
} else {
|
|
const EditorDOMRangeInTexts invisibleTrailingWhiteSpacesAtStart =
|
|
textFragmentDataAtStart.GetNonCollapsedRangeInTexts(
|
|
textFragmentDataAtStart.InvisibleTrailingWhiteSpaceRangeRef());
|
|
if (invisibleTrailingWhiteSpacesAtStart.IsPositioned() &&
|
|
!invisibleTrailingWhiteSpacesAtStart.Collapsed()) {
|
|
MOZ_ASSERT(
|
|
invisibleTrailingWhiteSpacesAtStart.StartRef().EqualsOrIsBefore(
|
|
aRange.StartRef()));
|
|
result.SetStart(invisibleTrailingWhiteSpacesAtStart.StartRef());
|
|
}
|
|
// If there is no invisible white-space and the line starts with a
|
|
// text node, shrink the range to start of the text node.
|
|
else if (!aRange.StartRef().IsInTextNode() &&
|
|
(textFragmentDataAtStart.StartsFromBlockBoundary() ||
|
|
textFragmentDataAtStart.StartsFromInlineEditingHostBoundary()) &&
|
|
textFragmentDataAtStart.EndRef().IsInTextNode()) {
|
|
result.SetStart(textFragmentDataAtStart.EndRef());
|
|
}
|
|
}
|
|
if (!result.StartRef().IsSet()) {
|
|
result.SetStart(aRange.StartRef());
|
|
}
|
|
|
|
const TextFragmentData textFragmentDataAtEnd(
|
|
aScanMode, aRange.EndRef(), BlockInlineCheck::UseComputedDisplayStyle,
|
|
aAncestorLimiter);
|
|
if (NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) {
|
|
return EditorDOMRange(); // TODO: Make here return error with Err.
|
|
}
|
|
const EditorDOMRangeInTexts invisibleLeadingWhiteSpacesAtEnd =
|
|
textFragmentDataAtEnd.GetNonCollapsedRangeInTexts(
|
|
textFragmentDataAtEnd.InvisibleTrailingWhiteSpaceRangeRef());
|
|
if (invisibleLeadingWhiteSpacesAtEnd.IsPositioned() &&
|
|
!invisibleLeadingWhiteSpacesAtEnd.Collapsed()) {
|
|
result.SetEnd(invisibleLeadingWhiteSpacesAtEnd.EndRef());
|
|
} else {
|
|
const EditorDOMRangeInTexts invisibleLeadingWhiteSpacesAtEnd =
|
|
textFragmentDataAtEnd.GetNonCollapsedRangeInTexts(
|
|
textFragmentDataAtEnd.InvisibleLeadingWhiteSpaceRangeRef());
|
|
if (invisibleLeadingWhiteSpacesAtEnd.IsPositioned() &&
|
|
!invisibleLeadingWhiteSpacesAtEnd.Collapsed()) {
|
|
MOZ_ASSERT(aRange.EndRef().EqualsOrIsBefore(
|
|
invisibleLeadingWhiteSpacesAtEnd.EndRef()));
|
|
result.SetEnd(invisibleLeadingWhiteSpacesAtEnd.EndRef());
|
|
}
|
|
// If there is no invisible white-space and the line ends with a text
|
|
// node, shrink the range to end of the text node.
|
|
else if (!aRange.EndRef().IsInTextNode() &&
|
|
(textFragmentDataAtEnd.EndsByBlockBoundary() ||
|
|
textFragmentDataAtEnd.EndsByInlineEditingHostBoundary()) &&
|
|
textFragmentDataAtEnd.StartRef().IsInTextNode()) {
|
|
result.SetEnd(EditorDOMPoint::AtEndOf(
|
|
*textFragmentDataAtEnd.StartRef().ContainerAs<Text>()));
|
|
}
|
|
}
|
|
if (!result.EndRef().IsSet()) {
|
|
result.SetEnd(aRange.EndRef());
|
|
}
|
|
MOZ_ASSERT(result.IsPositionedAndValid());
|
|
return result;
|
|
}
|
|
|
|
/******************************************************************************
|
|
* Utilities for other things.
|
|
******************************************************************************/
|
|
|
|
// static
|
|
Result<bool, nsresult>
|
|
WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
|
|
Scan aScanMode, nsRange& aRange,
|
|
const Element* aAncestorLimiter /* = nullptr */) {
|
|
MOZ_ASSERT(aRange.IsPositioned());
|
|
MOZ_ASSERT(!aRange.IsInAnySelection(),
|
|
"Changing range in selection may cause running script");
|
|
|
|
if (NS_WARN_IF(!aRange.GetStartContainer()) ||
|
|
NS_WARN_IF(!aRange.GetEndContainer())) {
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
if (!aRange.GetStartContainer()->IsContent() ||
|
|
!aRange.GetEndContainer()->IsContent()) {
|
|
return false;
|
|
}
|
|
|
|
// If the range crosses a block boundary, we should do nothing for now
|
|
// because it hits a bug of inserting a padding `<br>` element after
|
|
// joining the blocks.
|
|
if (HTMLEditUtils::GetInclusiveAncestorElement(
|
|
*aRange.GetStartContainer()->AsContent(),
|
|
aScanMode == Scan::EditableNodes
|
|
? HTMLEditUtils::ClosestEditableBlockElementExceptHRElement
|
|
: HTMLEditUtils::ClosestBlockElementExceptHRElement,
|
|
BlockInlineCheck::UseComputedDisplayStyle) !=
|
|
HTMLEditUtils::GetInclusiveAncestorElement(
|
|
*aRange.GetEndContainer()->AsContent(),
|
|
aScanMode == Scan::EditableNodes
|
|
? HTMLEditUtils::ClosestEditableBlockElementExceptHRElement
|
|
: HTMLEditUtils::ClosestBlockElementExceptHRElement,
|
|
BlockInlineCheck::UseComputedDisplayStyle)) {
|
|
return false;
|
|
}
|
|
|
|
nsIContent* startContent = nullptr;
|
|
if (aRange.GetStartContainer() && aRange.GetStartContainer()->IsText() &&
|
|
aRange.GetStartContainer()->AsText()->Length() == aRange.StartOffset()) {
|
|
// If next content is a visible `<br>` element, special inline content
|
|
// (e.g., `<img>`, non-editable text node, etc) or a block level void
|
|
// element like `<hr>`, the range should start with it.
|
|
const TextFragmentData textFragmentDataAtStart(
|
|
aScanMode, EditorRawDOMPoint(aRange.StartRef()),
|
|
BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter);
|
|
if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized())) {
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
if (textFragmentDataAtStart.EndsByVisibleBRElement()) {
|
|
startContent = textFragmentDataAtStart.EndReasonBRElementPtr();
|
|
} else if (textFragmentDataAtStart.EndsBySpecialContent() ||
|
|
(textFragmentDataAtStart.EndsByOtherBlockElement() &&
|
|
!HTMLEditUtils::IsContainerNode(
|
|
*textFragmentDataAtStart
|
|
.EndReasonOtherBlockElementPtr()))) {
|
|
startContent = textFragmentDataAtStart.GetEndReasonContent();
|
|
}
|
|
}
|
|
|
|
nsIContent* endContent = nullptr;
|
|
if (aRange.GetEndContainer() && aRange.GetEndContainer()->IsText() &&
|
|
!aRange.EndOffset()) {
|
|
// If previous content is a visible `<br>` element, special inline content
|
|
// (e.g., `<img>`, non-editable text node, etc) or a block level void
|
|
// element like `<hr>`, the range should end after it.
|
|
const TextFragmentData textFragmentDataAtEnd(
|
|
aScanMode, EditorRawDOMPoint(aRange.EndRef()),
|
|
BlockInlineCheck::UseComputedDisplayStyle, aAncestorLimiter);
|
|
if (NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) {
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
if (textFragmentDataAtEnd.StartsFromVisibleBRElement()) {
|
|
endContent = textFragmentDataAtEnd.StartReasonBRElementPtr();
|
|
} else if (textFragmentDataAtEnd.StartsFromSpecialContent() ||
|
|
(textFragmentDataAtEnd.StartsFromOtherBlockElement() &&
|
|
!HTMLEditUtils::IsContainerNode(
|
|
*textFragmentDataAtEnd
|
|
.StartReasonOtherBlockElementPtr()))) {
|
|
endContent = textFragmentDataAtEnd.GetStartReasonContent();
|
|
}
|
|
}
|
|
|
|
if (!startContent && !endContent) {
|
|
return false;
|
|
}
|
|
|
|
nsresult rv = aRange.SetStartAndEnd(
|
|
startContent ? RangeBoundary(
|
|
startContent->GetParentNode(),
|
|
startContent->GetPreviousSibling()) // at startContent
|
|
: aRange.StartRef(),
|
|
endContent ? RangeBoundary(endContent->GetParentNode(),
|
|
endContent) // after endContent
|
|
: aRange.EndRef());
|
|
if (NS_FAILED(rv)) {
|
|
NS_WARNING("nsRange::SetStartAndEnd() failed");
|
|
return Err(rv);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace mozilla
|