Bug 1940377 - part 6: Make delete handlers use the new white-space normalizer r=m_kato

Unfortunately, this patch becomes big because this includes multiple utility
methods to normalize white-spaces and touching a lot of places.

In some edge cases, this starts failing when the new white-space normalizer is
enabled.

```
FAIL execCommand("forwarddelete", false, ""): "a  [] |   b" - assert_equals: Modified text is wrong expected "a  " but got "a  "
FAIL execCommand("forwarddelete", false, ""): "a   [] |    b" - assert_equals: Modified text is wrong expected "a   " but got "a   "
FAIL execCommand("forwarddelete", false, ""): "a   []b|    c" - assert_equals: Modified text is wrong expected "a   " but got "a   "
FAIL execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;[]&nbsp;<span style=white-space:pre;>   </span>" - assert_equals: expected "a&nbsp;&nbsp;&nbsp;<span style=\"white-space:pre;\">   </span>" but got "a&nbsp; &nbsp;<span style=\"white-space:pre;\">   </span>"
```
In these failures, we normalize the trailing white-spaces of the preceding
`Text` too, but Chrome does not do that.  However, in some cases, they do
in similar cases.  Therefore, it's hard to align all cases due to the handling
order difference.  Additionally, these failure results are not odd.  So, these
new failures are acceptable.

```
FAIL execCommand("forwarddelete", false, ""): "<span>abc[]&nbsp;<span> &nbsp;def</span></span>" - assert_equals: expected "<span>abc<span>&nbsp; def</span></span>" but got "<span>abc<span> &nbsp;def</span></span>"
FAIL execCommand("forwarddelete", false, ""): "<span><span>abc[]&nbsp;</span> &nbsp;def</span>" - assert_equals: expected "<span><span>abc</span>&nbsp; def</span>" but got "<span><span>abc</span> &nbsp;def</span>"
FAIL execCommand("forwarddelete", false, ""): "<span>abc[]&nbsp;</span><span> &nbsp;def</span>" - assert_equals: expected "<span>abc</span><span>&nbsp; def</span>" but got "<span>abc</span><span> &nbsp;def</span>"
FAIL execCommand("forwarddelete", false, ""): "<span>abc[]&nbsp; </span><span>&nbsp;&nbsp;def</span>" - assert_equals: expected "<span>abc&nbsp;</span><span>&nbsp;&nbsp;def</span>" but got "<span>abc </span><span>&nbsp;&nbsp;def</span>"

```
In these failures, we don't normalize the leading white-spaces of the following
`Text`.  However, Chrome does so in some other cases.  So, it's hard to align
the behavior in these cases too.  The new normalizer ensures that the preceding
`Text` ends with a visible char.  Therefore, the following `Text` won't starts
with new invisible white-spaces.  Therefore, these failures are also acceptable.

```
FAIL execCommand("forwarddelete", false, ""): "<span style=white-space:pre;>  [] </span>&nbsp;&nbsp;&nbsp;a" - assert_equals: expected "<span style=\"white-space:pre;\">  </span>&nbsp; &nbsp;a" but got "<span style=\"white-space:pre;\">  </span>&nbsp;&nbsp;&nbsp;a"
```
In this failure, we don't normalize the following `Text` but Chrome does it.
However, I don't think we should do it because this updates the `Text` which
is preformatted.  Therefore, this is also acceptable.

So, I think that we can accept all new failures.

Differential Revision: https://phabricator.services.mozilla.com/D239468
This commit is contained in:
Masayuki Nakano
2025-03-08 22:31:56 +00:00
parent 1dc6c56a97
commit 2e3d9fe6d5
15 changed files with 2324 additions and 609 deletions

View File

@@ -1060,6 +1060,24 @@ Element* AutoClonedRangeArray::GetClosestAncestorAnyListElementOfRange() const {
return nullptr;
}
void AutoClonedRangeArray::RemoveCollapsedRanges() {
for (const auto index : Reversed(IntegerRange(mRanges.Length()))) {
if (mRanges[index]->Collapsed()) {
mRanges.RemoveElementAt(index);
}
}
if (mAnchorFocusRange->Collapsed()) {
MOZ_ASSERT(!mRanges.Contains(mAnchorFocusRange.get()));
if (mRanges.IsEmpty()) {
RemoveAllRanges();
} else {
mAnchorFocusRange = mRanges.LastElement();
}
} else {
MOZ_ASSERT(mRanges.Contains(mAnchorFocusRange.get()));
}
}
/******************************************************************************
* mozilla::AutoClonedSelectionRangeArray
*****************************************************************************/

View File

@@ -320,6 +320,8 @@ class MOZ_STACK_CLASS AutoClonedRangeArray {
mDirection = nsDirection::eDirNext;
}
void RemoveCollapsedRanges();
/**
* If the points are same (i.e., mean a collapsed range) and in an empty block
* element except the padding <br> element, this makes aStartPoint and

View File

@@ -1268,16 +1268,27 @@ class EditorDOMPointBase final {
return range.forget();
}
EditorDOMPointInText GetAsInText() const {
[[nodiscard]] EditorDOMPointInText GetAsInText() const {
return IsInTextNode() ? EditorDOMPointInText(ContainerAs<dom::Text>(),
Offset(), mInterlinePosition)
: EditorDOMPointInText();
}
MOZ_NEVER_INLINE_DEBUG EditorDOMPointInText AsInText() const {
[[nodiscard]] EditorDOMPointInText AsInText() const {
MOZ_ASSERT(IsInTextNode());
return EditorDOMPointInText(ContainerAs<dom::Text>(), Offset(),
mInterlinePosition);
}
[[nodiscard]] EditorRawDOMPointInText GetAsRawInText() const {
return IsInTextNode()
? EditorRawDOMPointInText(ContainerAs<dom::Text>(), Offset(),
mInterlinePosition)
: EditorRawDOMPointInText();
}
[[nodiscard]] EditorRawDOMPointInText AsRawInText() const {
MOZ_ASSERT(IsInTextNode());
return EditorRawDOMPointInText(ContainerAs<dom::Text>(), Offset(),
mInterlinePosition);
}
template <typename A, typename B>
bool IsBefore(const EditorDOMPointBase<A, B>& aOther) const {
@@ -1405,7 +1416,8 @@ class EditorDOMRangeBase final {
}
explicit EditorDOMRangeBase(EditorDOMPointType&& aStart,
EditorDOMPointType&& aEnd)
: mStart(std::move(aStart)), mEnd(std::move(aEnd)) {
: mStart(std::forward<EditorDOMPointType>(aStart)),
mEnd(std::forward<EditorDOMPointType>(aEnd)) {
MOZ_ASSERT_IF(mStart.IsSet(), mStart.IsSetAndValid());
MOZ_ASSERT_IF(mEnd.IsSet(), mEnd.IsSetAndValid());
MOZ_ASSERT_IF(mStart.IsSet() && mEnd.IsSet(),
@@ -1585,7 +1597,7 @@ class EditorDOMRangeBase final {
template <typename OtherRangeType>
bool operator==(const OtherRangeType& aOther) const {
return (!IsPositioned() && !aOther.IsPositioned()) ||
(mStart == aOther.mStart && mEnd == aOther.mEnd);
(mStart == aOther.StartRef() && mEnd == aOther.EndRef());
}
template <typename OtherRangeType>
bool operator!=(const OtherRangeType& aOther) const {
@@ -1640,6 +1652,10 @@ class EditorDOMRangeBase final {
}
return range.forget();
}
nsresult SetToRange(nsRange& aRange) const {
return aRange.SetStartAndEnd(mStart.ToRawRangeBoundary(),
mEnd.ToRawRangeBoundary());
}
friend std::ostream& operator<<(std::ostream& aStream,
const SelfType& aRange) {

View File

@@ -556,7 +556,10 @@ nsresult HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() {
const bool needToNormalizeWhiteSpaces = [&]() {
switch (GetTopLevelEditSubAction()) {
case EditSubAction::eDeleteSelectedContent:
return !TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces;
if (TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces) {
return false;
}
[[fallthrough]];
case EditSubAction::eInsertText:
case EditSubAction::eInsertTextComingFromIME:
case EditSubAction::eInsertLineBreak:
@@ -3251,6 +3254,109 @@ HTMLEditor::NormalizeWhiteSpacesToInsertText(
return result;
}
HTMLEditor::ReplaceWhiteSpacesData HTMLEditor::GetNormalizedStringAt(
const EditorDOMPointInText& aPoint) const {
MOZ_ASSERT(aPoint.IsSet());
// If white-spaces are preformatted, we don't need to normalize white-spaces.
if (EditorUtils::IsWhiteSpacePreformatted(*aPoint.ContainerAs<Text>())) {
return ReplaceWhiteSpacesData();
}
const Text& textNode = *aPoint.ContainerAs<Text>();
const nsTextFragment& textFragment = textNode.TextFragment();
// We don't want to make invisible things visible with this normalization.
// Therefore, we need to know whether there are invisible leading and/or
// trailing white-spaces in the `Text`.
// Then, compute visible white-space length before/after the point.
// Note that these lengths may contain invisible white-spaces.
const uint32_t precedingWhiteSpaceLength = [&]() {
if (aPoint.IsStartOfContainer()) {
return 0u;
}
const auto nonWhiteSpaceOffset =
HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
textNode, aPoint.Offset(),
{HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
const uint32_t firstWhiteSpaceOffset =
nonWhiteSpaceOffset ? *nonWhiteSpaceOffset + 1u : 0u;
return aPoint.Offset() - firstWhiteSpaceOffset;
}();
const uint32_t followingWhiteSpaceLength = [&]() {
if (aPoint.IsEndOfContainer()) {
return 0u;
}
const auto nonWhiteSpaceOffset =
HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
textNode, aPoint.Offset(),
{HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
MOZ_ASSERT(nonWhiteSpaceOffset.valueOr(textFragment.GetLength()) >=
aPoint.Offset());
return nonWhiteSpaceOffset.valueOr(textFragment.GetLength()) -
aPoint.Offset();
}();
if (!precedingWhiteSpaceLength && !followingWhiteSpaceLength) {
return ReplaceWhiteSpacesData();
}
// Now, we can know invisible white-space length in precedingWhiteSpaceLength
// and followingWhiteSpaceLength.
const uint32_t precedingInvisibleWhiteSpaceCount =
HTMLEditUtils::GetInvisibleWhiteSpaceCount(
textNode, aPoint.Offset() - precedingWhiteSpaceLength,
precedingWhiteSpaceLength);
MOZ_ASSERT(precedingWhiteSpaceLength >= precedingInvisibleWhiteSpaceCount);
const uint32_t newPrecedingWhiteSpaceLength =
precedingWhiteSpaceLength - precedingInvisibleWhiteSpaceCount;
const uint32_t followingInvisibleSpaceCount =
HTMLEditUtils::GetInvisibleWhiteSpaceCount(textNode, aPoint.Offset(),
followingWhiteSpaceLength);
MOZ_ASSERT(followingWhiteSpaceLength >= followingInvisibleSpaceCount);
const uint32_t newFollowingWhiteSpaceLength =
followingWhiteSpaceLength - followingInvisibleSpaceCount;
nsAutoString stringToInsertWithSurroundingSpaces;
if (newPrecedingWhiteSpaceLength || newFollowingWhiteSpaceLength) {
stringToInsertWithSurroundingSpaces.SetLength(newPrecedingWhiteSpaceLength +
newFollowingWhiteSpaceLength);
for (auto index : IntegerRange(newPrecedingWhiteSpaceLength +
newFollowingWhiteSpaceLength)) {
stringToInsertWithSurroundingSpaces.SetCharAt(' ', index);
}
}
ReplaceWhiteSpacesData result(
std::move(stringToInsertWithSurroundingSpaces),
aPoint.Offset() - precedingWhiteSpaceLength, // replace start
precedingWhiteSpaceLength + followingWhiteSpaceLength, // replace length
// aPoint.Offset() after replacing the white-spaces
aPoint.Offset() - precedingWhiteSpaceLength +
newPrecedingWhiteSpaceLength);
if (!result.mNormalizedString.IsEmpty()) {
HTMLEditor::NormalizeAllWhiteSpaceSequences(
result.mNormalizedString,
CharPointData::InSameTextNode(
!result.mReplaceStartOffset
? CharPointType::TextEnd
: (textFragment.CharAt(result.mReplaceStartOffset - 1u) ==
HTMLEditUtils::kNewLine
? CharPointType::PreformattedLineBreak
: CharPointType::VisibleChar)),
CharPointData::InSameTextNode(
result.mReplaceEndOffset >= textFragment.GetLength()
? CharPointType::TextEnd
: (textFragment.CharAt(result.mReplaceEndOffset) ==
HTMLEditUtils::kNewLine
? CharPointType::PreformattedLineBreak
: CharPointType::VisibleChar)),
EditorUtils::IsNewLinePreformatted(textNode) ? Linefeed::Collapsible
: Linefeed::Preformatted);
}
return result;
}
HTMLEditor::ReplaceWhiteSpacesData
HTMLEditor::GetFollowingNormalizedStringToSplitAt(
const EditorDOMPointInText& aPointToSplit) const {
@@ -3408,6 +3514,154 @@ HTMLEditor::GetPrecedingNormalizedStringToSplitAt(
return result;
}
HTMLEditor::ReplaceWhiteSpacesData
HTMLEditor::GetSurroundingNormalizedStringToDelete(const Text& aTextNode,
uint32_t aOffset,
uint32_t aLength) const {
MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible());
MOZ_ASSERT(aOffset <= aTextNode.TextDataLength());
MOZ_ASSERT(aOffset + aLength <= aTextNode.TextDataLength());
if (EditorUtils::IsWhiteSpacePreformatted(aTextNode) || !aLength ||
(!aOffset && aLength >= aTextNode.TextDataLength())) {
return ReplaceWhiteSpacesData();
}
const bool isNewLineCollapsible =
!EditorUtils::IsNewLinePreformatted(aTextNode);
const auto IsPreformattedLineBreak = [&](char16_t aChar) {
return !isNewLineCollapsible && aChar == HTMLEditUtils::kNewLine;
};
const auto IsCollapsibleChar = [&](char16_t aChar) {
return !IsPreformattedLineBreak(aChar) && nsCRT::IsAsciiSpace(aChar);
};
const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) {
return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar);
};
const nsTextFragment& textFragment = aTextNode.TextFragment();
const char16_t precedingChar =
aOffset ? textFragment.CharAt(aOffset - 1u) : static_cast<char16_t>(0);
const char16_t followingChar = aOffset + aLength < textFragment.GetLength()
? textFragment.CharAt(aOffset + aLength)
: static_cast<char16_t>(0);
// If there is no surrounding white-spaces, we need to do nothing here.
if (!IsCollapsibleCharOrNBSP(precedingChar) &&
!IsCollapsibleCharOrNBSP(followingChar)) {
return ReplaceWhiteSpacesData();
}
const uint32_t precedingWhiteSpaceLength = [&]() {
if (!IsCollapsibleCharOrNBSP(precedingChar)) {
return 0u;
}
const auto nonWhiteSpaceOffset =
HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
aTextNode, aOffset,
{HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
const uint32_t firstWhiteSpaceOffset =
nonWhiteSpaceOffset ? *nonWhiteSpaceOffset + 1u : 0u;
return aOffset - firstWhiteSpaceOffset;
}();
const uint32_t followingWhiteSpaceLength = [&]() {
if (!IsCollapsibleCharOrNBSP(followingChar)) {
return 0u;
}
const auto nonWhiteSpaceOffset =
HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
aTextNode, aOffset + aLength,
{HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
MOZ_ASSERT(nonWhiteSpaceOffset.valueOr(textFragment.GetLength()) >=
aOffset + aLength);
return nonWhiteSpaceOffset.valueOr(textFragment.GetLength()) -
(aOffset + aLength);
}();
if (NS_WARN_IF(!precedingWhiteSpaceLength && !followingWhiteSpaceLength)) {
return ReplaceWhiteSpacesData();
}
const uint32_t precedingInvisibleWhiteSpaceCount =
HTMLEditUtils::GetInvisibleWhiteSpaceCount(
aTextNode, aOffset - precedingWhiteSpaceLength,
precedingWhiteSpaceLength);
MOZ_ASSERT(precedingWhiteSpaceLength >= precedingInvisibleWhiteSpaceCount);
const uint32_t followingInvisibleSpaceCount =
HTMLEditUtils::GetInvisibleWhiteSpaceCount(aTextNode, aOffset + aLength,
followingWhiteSpaceLength);
MOZ_ASSERT(followingWhiteSpaceLength >= followingInvisibleSpaceCount);
// Let's try to return early if there is only one white-space around the
// deleting range to avoid to run the expensive path.
if (precedingWhiteSpaceLength == 1u && !precedingInvisibleWhiteSpaceCount &&
!followingWhiteSpaceLength) {
// If there is only one ASCII space and it'll be followed by a
// non-collapsible character except preformatted linebreak after deletion,
// we don't need to normalize the preceding white-space.
if (precedingChar == HTMLEditUtils::kSpace && followingChar &&
!IsPreformattedLineBreak(followingChar)) {
return ReplaceWhiteSpacesData();
}
// If there is only one NBSP and it'll be the last character or will be
// followed by a collapsible white-space, we don't need to normalize the
// preceding white-space.
if (precedingChar == HTMLEditUtils::kNBSP &&
(!followingChar || IsPreformattedLineBreak(followingChar))) {
return ReplaceWhiteSpacesData();
}
}
if (followingWhiteSpaceLength == 1u && !followingInvisibleSpaceCount &&
!precedingWhiteSpaceLength) {
// If there is only one ASCII space and it'll follow by a non-collapsible
// character after deletion, we don't need to normalize the following
// white-space.
if (followingChar == HTMLEditUtils::kSpace && precedingChar &&
!IsPreformattedLineBreak(precedingChar)) {
return ReplaceWhiteSpacesData();
}
// If there is only one NBSP and it'll be the first character or will
// follow a preformatted line break, we don't need to normalize the
// following white-space.
if (followingChar == HTMLEditUtils::kNBSP &&
(!precedingChar || IsPreformattedLineBreak(precedingChar))) {
return ReplaceWhiteSpacesData();
}
}
const uint32_t newPrecedingWhiteSpaceLength =
precedingWhiteSpaceLength - precedingInvisibleWhiteSpaceCount;
const uint32_t newFollowingWhiteSpaceLength =
followingWhiteSpaceLength - followingInvisibleSpaceCount;
nsAutoString surroundingWhiteSpaces;
if (newPrecedingWhiteSpaceLength || newFollowingWhiteSpaceLength) {
surroundingWhiteSpaces.SetLength(newPrecedingWhiteSpaceLength +
newFollowingWhiteSpaceLength);
for (const auto offset : IntegerRange(newPrecedingWhiteSpaceLength +
newFollowingWhiteSpaceLength)) {
surroundingWhiteSpaces.SetCharAt(' ', offset);
}
}
ReplaceWhiteSpacesData result(
std::move(surroundingWhiteSpaces), aOffset - precedingWhiteSpaceLength,
precedingWhiteSpaceLength + aLength + followingWhiteSpaceLength,
aOffset - precedingInvisibleWhiteSpaceCount);
if (!result.mNormalizedString.IsEmpty()) {
HTMLEditor::NormalizeAllWhiteSpaceSequences(
result.mNormalizedString,
CharPointData::InSameTextNode(
!result.mReplaceStartOffset
? CharPointType::TextEnd
: (textFragment.CharAt(result.mReplaceStartOffset - 1u) ==
HTMLEditUtils::kNewLine
? CharPointType::PreformattedLineBreak
: CharPointType::VisibleChar)),
CharPointData::InSameTextNode(
result.mReplaceEndOffset >= textFragment.GetLength()
? CharPointType::TextEnd
: (textFragment.CharAt(result.mReplaceEndOffset) ==
HTMLEditUtils::kNewLine
? CharPointType::PreformattedLineBreak
: CharPointType::VisibleChar)),
isNewLineCollapsible ? Linefeed::Collapsible : Linefeed::Preformatted);
}
return result;
}
void HTMLEditor::ExtendRangeToDeleteWithNormalizingWhiteSpaces(
EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete,
nsString& aNormalizedWhiteSpacesInStartNode,
@@ -3757,7 +4011,9 @@ HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces(
{LeafNodeType::LeafNodeOrNonEditableNode},
BlockInlineCheck::UseComputedDisplayStyle,
editableBlockElementOrInlineEditingHost)) {
if (HTMLEditUtils::IsSimplyEditableNode(*nextContent)) {
if (HTMLEditUtils::IsSimplyEditableNode(*nextContent) &&
!HTMLEditUtils::IsBlockElement(
*nextContent, BlockInlineCheck::UseComputedDisplayStyle)) {
newCaretPosition =
nextContent->IsText() ||
HTMLEditUtils::IsContainerNode(*nextContent)
@@ -3815,14 +4071,115 @@ HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces(
} else {
insertPaddingBRElementOrError.unwrap().IgnoreCaretPointSuggestion();
}
}
if (!newCaretPosition.IsSetAndValid()) {
NS_WARNING("Inserting <br> element caused unexpected DOM tree");
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
if (!newCaretPosition.IsSetAndValid()) {
NS_WARNING("Inserting <br> element caused unexpected DOM tree");
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
}
return CaretPoint(std::move(newCaretPosition));
}
Result<JoinNodesResult, nsresult>
HTMLEditor::JoinTextNodesWithNormalizeWhiteSpaces(Text& aLeftText,
Text& aRightText) {
MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible());
if (EditorUtils::IsWhiteSpacePreformatted(aLeftText)) {
Result<JoinNodesResult, nsresult> joinResultOrError =
JoinNodesWithTransaction(aLeftText, aRightText);
NS_WARNING_ASSERTION(joinResultOrError.isOk(),
"HTMLEditor::JoinNodesWithTransaction() failed");
return joinResultOrError;
}
const bool isNewLinePreformatted =
EditorUtils::IsNewLinePreformatted(aLeftText);
const auto IsCollapsibleChar = [&](char16_t aChar) {
return (aChar == HTMLEditUtils::kNewLine && !isNewLinePreformatted) ||
nsCRT::IsAsciiSpace(aChar);
};
const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) {
return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar);
};
const char16_t lastLeftChar = aLeftText.TextFragment().SafeLastChar();
char16_t firstRightChar = aRightText.TextFragment().SafeFirstChar();
const char16_t secondRightChar = aRightText.TextFragment().GetLength() >= 2
? aRightText.TextFragment().CharAt(1u)
: static_cast<char16_t>(0);
if (IsCollapsibleCharOrNBSP(firstRightChar)) {
// If the right Text starts only with a collapsible white-space and it'll
// follow a non-collapsible char, we should make it an ASCII white-space.
if (secondRightChar && !IsCollapsibleCharOrNBSP(secondRightChar) &&
lastLeftChar && !IsCollapsibleChar(lastLeftChar)) {
if (firstRightChar != HTMLEditUtils::kSpace) {
Result<InsertTextResult, nsresult> replaceWhiteSpaceResultOrError =
ReplaceTextWithTransaction(aRightText, 0u, 1u, u" "_ns);
if (MOZ_UNLIKELY(replaceWhiteSpaceResultOrError.isErr())) {
NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
return replaceWhiteSpaceResultOrError.propagateErr();
}
replaceWhiteSpaceResultOrError.unwrap().IgnoreCaretPointSuggestion();
if (NS_WARN_IF(aLeftText.GetNextSibling() != &aRightText)) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
firstRightChar = HTMLEditUtils::kSpace;
}
}
// Otherwise, normalize the white-spaces before join, i.e., it will start
// with an NBSP.
else {
Result<EditorDOMPoint, nsresult> atFirstVisibleThingOrError =
WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter(
*this, EditorDOMPoint(&aRightText, 0u), {});
if (MOZ_UNLIKELY(atFirstVisibleThingOrError.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter() failed");
return atFirstVisibleThingOrError.propagateErr();
}
if (!aRightText.GetParentNode()) {
return JoinNodesResult(EditorDOMPoint::AtEndOf(aLeftText), aRightText);
}
}
} else if (IsCollapsibleCharOrNBSP(lastLeftChar) &&
lastLeftChar != HTMLEditUtils::kSpace &&
aLeftText.TextFragment().GetLength() >= 2u) {
// If the last char of the left `Text` is a single white-space but not an
// ASCII space, let's replace it with an ASCII space.
const char16_t secondLastChar = aLeftText.TextFragment().CharAt(
aLeftText.TextFragment().GetLength() - 2u);
if (!IsCollapsibleCharOrNBSP(secondLastChar) &&
!IsCollapsibleCharOrNBSP(firstRightChar)) {
Result<InsertTextResult, nsresult> replaceWhiteSpaceResultOrError =
ReplaceTextWithTransaction(aLeftText,
aLeftText.TextFragment().GetLength() - 1u,
1u, u" "_ns);
if (MOZ_UNLIKELY(replaceWhiteSpaceResultOrError.isErr())) {
NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
return replaceWhiteSpaceResultOrError.propagateErr();
}
replaceWhiteSpaceResultOrError.unwrap().IgnoreCaretPointSuggestion();
if (NS_WARN_IF(aLeftText.GetNextSibling() != &aRightText)) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
}
}
Result<JoinNodesResult, nsresult> joinResultOrError =
JoinNodesWithTransaction(aLeftText, aRightText);
if (MOZ_UNLIKELY(joinResultOrError.isErr())) {
NS_WARNING("HTMLEditor::JoinNodesWithTransaction() failed");
return joinResultOrError;
}
JoinNodesResult joinResult = joinResultOrError.unwrap();
const EditorDOMPointInText startOfRightTextData =
joinResult.AtJoinedPoint<EditorRawDOMPoint>().GetAsInText();
if (NS_WARN_IF(!startOfRightTextData.IsSet()) ||
(firstRightChar &&
(NS_WARN_IF(startOfRightTextData.IsEndOfContainer()) ||
NS_WARN_IF(firstRightChar != startOfRightTextData.Char())))) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
return std::move(joinResult);
}
// static
bool HTMLEditor::CanInsertLineBreak(LineBreakType aLineBreakType,
const nsIContent& aContent) {

View File

@@ -406,10 +406,13 @@ bool HTMLEditUtils::IsVisibleElementEvenIfLeafNode(const nsIContent& aContent) {
aContent, BlockInlineCheck::UseComputedDisplayStyle)) {
return true;
}
if (aContent.IsAnyOfHTMLElements(nsGkAtoms::applet, nsGkAtoms::iframe,
nsGkAtoms::img, nsGkAtoms::meter,
nsGkAtoms::progress, nsGkAtoms::select,
nsGkAtoms::textarea)) {
// <br> element may not have a frame, but it always affects surrounding
// content. Therefore, it should be treated as visible. The others which are
// checked here are replace elements which provide something visible content.
if (aContent.IsAnyOfHTMLElements(nsGkAtoms::applet, nsGkAtoms::br,
nsGkAtoms::iframe, nsGkAtoms::img,
nsGkAtoms::meter, nsGkAtoms::progress,
nsGkAtoms::select, nsGkAtoms::textarea)) {
return true;
}
if (const HTMLInputElement* inputElement =
@@ -920,12 +923,6 @@ Element* HTMLEditUtils::GetElementOfImmediateBlockBoundary(
return nextContent->AsElement();
}
// If there is a visible content which generates something visible,
// stop scanning.
if (HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*nextContent)) {
return nullptr;
}
if (nextContent->IsHTMLElement(nsGkAtoms::br)) {
// If aContent is a <br> element, another <br> element prevents the
// block boundary special handling.
@@ -945,6 +942,12 @@ Element* HTMLEditUtils::GetElementOfImmediateBlockBoundary(
return nextContent->AsElement();
}
// If there is a visible content which generates something visible,
// stop scanning.
if (HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*nextContent)) {
return nullptr;
}
continue;
}

View File

@@ -4229,9 +4229,19 @@ Result<CaretPoint, nsresult> HTMLEditor::DeleteTextWithTransaction(
Result<InsertTextResult, nsresult> HTMLEditor::ReplaceTextWithTransaction(
dom::Text& aTextNode, const ReplaceWhiteSpacesData& aData) {
return ReplaceTextWithTransaction(aTextNode, aData.mReplaceStartOffset,
aData.ReplaceLength(),
aData.mNormalizedString);
Result<InsertTextResult, nsresult> insertTextResultOrError =
ReplaceTextWithTransaction(aTextNode, aData.mReplaceStartOffset,
aData.ReplaceLength(),
aData.mNormalizedString);
if (MOZ_UNLIKELY(insertTextResultOrError.isErr()) ||
aData.mNewOffsetAfterReplace > aTextNode.TextDataLength()) {
return insertTextResultOrError;
}
InsertTextResult insertTextResult = insertTextResultOrError.unwrap();
insertTextResult.IgnoreCaretPointSuggestion();
EditorDOMPoint pointToPutCaret(&aTextNode, aData.mNewOffsetAfterReplace);
return InsertTextResult(std::move(insertTextResult),
std::move(pointToPutCaret));
}
Result<InsertTextResult, nsresult> HTMLEditor::ReplaceTextWithTransaction(
@@ -5413,10 +5423,16 @@ nsresult HTMLEditor::CollapseAdjacentTextNodes(nsRange& aRange) {
continue;
}
Result<JoinNodesResult, nsresult> joinNodesResultOrError =
JoinNodesWithTransaction(MOZ_KnownLive(leftTextNode),
MOZ_KnownLive(rightTextNode));
StaticPrefs::editor_white_space_normalization_blink_compatible()
? JoinTextNodesWithNormalizeWhiteSpaces(
MOZ_KnownLive(leftTextNode), MOZ_KnownLive(rightTextNode))
: JoinNodesWithTransaction(MOZ_KnownLive(leftTextNode),
MOZ_KnownLive(rightTextNode));
if (MOZ_UNLIKELY(joinNodesResultOrError.isErr())) {
NS_WARNING("HTMLEditor::JoinNodesWithTransaction() failed");
NS_WARNING(
StaticPrefs::editor_white_space_normalization_blink_compatible()
? "HTMLEditor::JoinTextNodesWithNormalizeWhiteSpaces() failed"
: "HTMLEditor::JoinNodesWithTransaction() failed");
return joinNodesResultOrError.unwrapErr();
}
}

View File

@@ -2240,6 +2240,14 @@ class HTMLEditor final : public EditorBase,
const EditorDOMPoint& aPointToInsert, const nsAString& aStringToInsert,
NormalizeSurroundingWhiteSpaces aNormalizeSurroundingWhiteSpaces) const;
/**
* Return normalized white-spaces of a white-space sequence which contains
* aPoint. This returns new offset of aPoint.Offset() after replacing the
* white-space sequence with normalized white-spaces.
*/
ReplaceWhiteSpacesData GetNormalizedStringAt(
const EditorDOMPointInText& aPoint) const;
/**
* Return normalized white-spaces after aPointToSplit if there are some
* collapsible white-spaces after the point.
@@ -2254,6 +2262,13 @@ class HTMLEditor final : public EditorBase,
ReplaceWhiteSpacesData GetPrecedingNormalizedStringToSplitAt(
const EditorDOMPointInText& aPointToSplit) const;
/**
* Return normalized surrounding white-spaces of the given range in aTextNode
* if there are some collapsible white-spaces.
*/
ReplaceWhiteSpacesData GetSurroundingNormalizedStringToDelete(
const Text& aTextNode, uint32_t aOffset, uint32_t aLength) const;
/**
* ExtendRangeToDeleteWithNormalizingWhiteSpaces() is a helper method of
* DeleteTextAndNormalizeSurroundingWhiteSpaces(). This expands
@@ -3622,6 +3637,13 @@ class HTMLEditor final : public EditorBase,
MaybeCollapseSelectionAtFirstEditableNode(
bool aIgnoreIfSelectionInEditingHost) const;
/**
* Join aLeftText and aRightText with normalizing white-spaces at the joining
* point if it's required. aRightText must be the next sibling of aLeftText.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<JoinNodesResult, nsresult>
JoinTextNodesWithNormalizeWhiteSpaces(Text& aLeftText, Text& aRightText);
class BlobReader final {
using AutoEditActionDataSetter = EditorBase::AutoEditActionDataSetter;

View File

@@ -397,6 +397,46 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final {
AutoClonedSelectionRangeArray& aRangesToDelete) const {
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
MOZ_ASSERT(CanFallbackToDeleteRangesWithTransaction(aRangesToDelete));
if (StaticPrefs::editor_white_space_normalization_blink_compatible()) {
AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
&aRangesToDelete.FirstRangeRef());
for (OwningNonNull<nsRange>& range : Reversed(aRangesToDelete.Ranges())) {
if (MOZ_UNLIKELY(!range->IsPositioned() || range->Collapsed())) {
continue;
}
Maybe<AutoTrackDOMRange> trackRange;
if (range != aRangesToDelete.FirstRangeRef()) {
trackRange.emplace(aHTMLEditor.RangeUpdaterRef(), &range);
}
Result<EditorDOMRange, nsresult> rangeToDeleteOrError =
WhiteSpaceVisibilityKeeper::NormalizeSurroundingWhiteSpacesToJoin(
aHTMLEditor, EditorDOMRange(range));
if (MOZ_UNLIKELY(rangeToDeleteOrError.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::"
"NormalizeSurroundingWhiteSpacesToJoin() failed");
return rangeToDeleteOrError.propagateErr();
}
trackRange.reset();
EditorDOMRange rangeToDelete = rangeToDeleteOrError.unwrap();
if (MOZ_LIKELY(rangeToDelete.IsPositionedAndValidInComposedDoc())) {
nsresult rv = range->SetStartAndEnd(
rangeToDelete.StartRef().ToRawRangeBoundary(),
rangeToDelete.EndRef().ToRawRangeBoundary());
if (NS_FAILED(rv)) {
NS_WARNING("nsRange::SetStartAndEnd() failed");
return Err(rv);
}
}
}
aRangesToDelete.RemoveCollapsedRanges();
if (MOZ_UNLIKELY(aRangesToDelete.IsCollapsed())) {
return CaretPoint(
EditorDOMPoint(aRangesToDelete.FirstRangeRef()->StartRef()));
}
}
Result<CaretPoint, nsresult> caretPointOrError =
aHTMLEditor.DeleteRangesWithTransaction(mOriginalDirectionAndAmount,
mOriginalStripWrappers,
@@ -429,6 +469,30 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final {
return NS_ERROR_FAILURE;
}
if (StaticPrefs::editor_white_space_normalization_blink_compatible()) {
for (OwningNonNull<nsRange>& range : Reversed(aRangesToDelete.Ranges())) {
if (MOZ_UNLIKELY(!range->IsPositioned() || range->Collapsed())) {
continue;
}
const EditorDOMRange extendedRange = WSRunScanner::
GetRangeContainingInvisibleWhiteSpacesAtRangeBoundaries(
WSRunScanner::Scan::EditableNodes, EditorDOMRange(range));
if (EditorRawDOMRange(range) != extendedRange) {
nsresult rv = range->SetStartAndEnd(
extendedRange.StartRef().ToRawRangeBoundary(),
extendedRange.EndRef().ToRawRangeBoundary());
if (NS_FAILED(rv)) {
NS_WARNING("nsRange::SetStartAndEnd() failed");
return NS_ERROR_FAILURE;
}
}
}
aRangesToDelete.RemoveCollapsedRanges();
if (MOZ_UNLIKELY(aRangesToDelete.IsCollapsed())) {
return NS_OK;
}
}
for (const OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) {
if (range->Collapsed()) {
continue;
@@ -1405,6 +1469,22 @@ Result<CaretPoint, nsresult> HTMLEditor::DeleteRangesWithTransaction(
}
if (isDeleteSelection) {
{
AutoTrackDOMPoint trackPointToInsertLineBreak(
RangeUpdaterRef(), &pointToInsertLineBreak);
nsresult rv =
EnsureNoFollowingUnnecessaryLineBreak(pointToInsertLineBreak);
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed");
return Err(rv);
}
trackPointToInsertLineBreak.FlushAndStopTracking();
if (NS_WARN_IF(!pointToInsertLineBreak
.IsInContentNodeAndValidInComposedDoc())) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
}
Result<CreateLineBreakResult, nsresult> insertPaddingBRElementOrError =
InsertPaddingBRElementIfNeeded(
pointToInsertLineBreak,
@@ -2307,6 +2387,7 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteTextAroundCollapsedRanges(
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
AutoClonedSelectionRangeArray& aRangesToDelete,
const Element& aEditingHost) {
MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible());
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
MOZ_ASSERT(aDirectionAndAmount == nsIEditor::eNext ||
aDirectionAndAmount == nsIEditor::ePrevious);
@@ -2331,19 +2412,71 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteTextAroundCollapsedRanges(
return Err(NS_ERROR_FAILURE);
}
// If deleting some characters makes the last line before a block boundary
// empty, we need to put a line break.
const bool becomesEmptyLine = [&]() {
if (!rangeToDelete.StartRef().IsStartOfContainer() ||
!rangeToDelete.EndRef().IsEndOfContainer()) {
return false;
}
const WSScanResult previousThing =
WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::All, rangeToDelete.StartRef(),
BlockInlineCheck::UseComputedDisplayOutsideStyle);
if (!previousThing.ReachedLineBoundary() ||
previousThing.ReachedBlockBoundary()) {
return false;
}
WSScanResult nextThing =
WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::All, rangeToDelete.EndRef(),
BlockInlineCheck::UseComputedDisplayOutsideStyle);
if (nextThing.ReachedBRElement() ||
nextThing.ReachedPreformattedLineBreak()) {
nextThing = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::All,
nextThing.PointAfterReachedContent<EditorRawDOMPoint>(),
BlockInlineCheck::UseComputedDisplayOutsideStyle);
}
return nextThing.ReachedBlockBoundary();
}();
Result<CaretPoint, nsresult> caretPointOrError =
aHTMLEditor.DeleteTextAndNormalizeSurroundingWhiteSpaces(
rangeToDelete.StartRef().AsInText(),
rangeToDelete.EndRef().AsInText(),
TreatEmptyTextNodes::RemoveAllEmptyInlineAncestors,
!aEditingHost.IsContentEditablePlainTextOnly()
? TreatEmptyTextNodes::RemoveAllEmptyInlineAncestors
: TreatEmptyTextNodes::Remove,
aDirectionAndAmount == nsIEditor::eNext ? DeleteDirection::Forward
: DeleteDirection::Backward,
aEditingHost);
aHTMLEditor.TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces = true;
NS_WARNING_ASSERTION(
caretPointOrError.isOk(),
"HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces() failed");
return caretPointOrError;
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
NS_WARNING(
"HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces() failed");
return caretPointOrError;
}
if (!becomesEmptyLine) {
return caretPointOrError;
}
const EditorDOMPoint pointToPutLineBreak =
caretPointOrError.unwrap().UnwrapCaretPoint();
const Maybe<LineBreakType> lineBreakType =
aHTMLEditor.GetPreferredLineBreakType(
*pointToPutLineBreak.ContainerAs<nsIContent>(), aEditingHost);
if (NS_WARN_IF(lineBreakType.isNothing())) {
return Err(NS_ERROR_FAILURE);
}
Result<CreateLineBreakResult, nsresult> lineBreakOrError =
aHTMLEditor.InsertLineBreak(WithTransaction::Yes, *lineBreakType,
pointToPutLineBreak, nsIEditor::ePrevious);
if (MOZ_UNLIKELY(lineBreakOrError.isErr())) {
NS_WARNING(
"HTMLEditor::InsertLineBreak(WithTransaction::Yes, "
"nsIEditor::ePrevious) failed");
return lineBreakOrError.propagateErr();
}
return CaretPoint(lineBreakOrError.unwrap().UnwrapCaretPoint());
}
Result<CaretPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler::
@@ -3154,6 +3287,29 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
return Err(NS_ERROR_FAILURE);
}
if (StaticPrefs::editor_white_space_normalization_blink_compatible() &&
(mMode == Mode::DeletePrecedingBRElementOfBlock ||
mMode == Mode::DeletePrecedingPreformattedLineBreak) &&
pointToPutCaret.IsSetAndValidInComposedDoc()) {
// If we're deleting only the preceding lines of a block, we should
// normalize the white-spaces at start of the block for compatibility
// with the other browsers.
AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
&pointToPutCaret);
Result<EditorDOMPoint, nsresult> atFirstVisibleThingOrError =
WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter(
aHTMLEditor, pointToPutCaret, {});
if (MOZ_UNLIKELY(atFirstVisibleThingOrError.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter() failed");
return atFirstVisibleThingOrError.propagateErr();
}
trackPointToPutCaret.FlushAndStopTracking();
if (NS_WARN_IF(!pointToPutCaret.IsSetAndValidInComposedDoc())) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
}
if (pointToPutCaret.IsSet()) {
nsresult rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
@@ -3184,10 +3340,43 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
EditorRawDOMPoint newCaretPosition =
HTMLEditUtils::GetGoodCaretPointFor<EditorRawDOMPoint>(
*mLeafContentInOtherBlock, aDirectionAndAmount);
if (MOZ_UNLIKELY(!newCaretPosition.IsSet())) {
if (MOZ_UNLIKELY(!newCaretPosition.IsInContentNode())) {
NS_WARNING("HTMLEditUtils::GetGoodCaretPointFor() failed");
return Err(NS_ERROR_FAILURE);
}
if (StaticPrefs::editor_white_space_normalization_blink_compatible()) {
// If we're deleting only a line break and move caret to left block, we
// want to normalize the white-spaces at end of the left block for the
// compatibility with the other browsers.
WSScanResult nextThingOfCaretPoint =
WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::All, newCaretPosition,
BlockInlineCheck::UseComputedDisplayOutsideStyle);
if (nextThingOfCaretPoint.ReachedBRElement() ||
nextThingOfCaretPoint.ReachedPreformattedLineBreak()) {
nextThingOfCaretPoint =
WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::All,
nextThingOfCaretPoint
.PointAfterReachedContent<EditorRawDOMPoint>(),
BlockInlineCheck::UseComputedDisplayOutsideStyle);
}
if (nextThingOfCaretPoint.ReachedBlockBoundary()) {
const EditorDOMPoint atBlockBoundary =
nextThingOfCaretPoint.ReachedCurrentBlockBoundary()
? EditorDOMPoint::AtEndOf(*nextThingOfCaretPoint.ElementPtr())
: EditorDOMPoint(nextThingOfCaretPoint.ElementPtr());
Result<EditorDOMPoint, nsresult> afterLastVisibleThingOrError =
WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesBefore(
aHTMLEditor, atBlockBoundary, {});
if (MOZ_UNLIKELY(afterLastVisibleThingOrError.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesBefore() "
"failed");
return afterLastVisibleThingOrError.propagateErr();
}
}
}
rv = aHTMLEditor.CollapseSelectionTo(newCaretPosition);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return Err(NS_ERROR_EDITOR_DESTROYED);
@@ -3871,6 +4060,7 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
return EditActionResult::HandledResult();
}
unwrappedMoveFirstLineResult.IgnoreCaretPointSuggestion();
tracker.FlushAndStopTracking();
nsresult rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return Err(NS_ERROR_EDITOR_DESTROYED);
@@ -4045,7 +4235,7 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteNonCollapsedRanges(
// Figure out if the endpoints are in nodes that can be merged. Adjust
// surrounding white-space in preparation to delete selection.
if (!aHTMLEditor.IsPlaintextMailComposer()) {
{
if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) {
AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
&aRangesToDelete.FirstRangeRef());
Result<CaretPoint, nsresult> caretPointOrError =
@@ -4059,6 +4249,44 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteNonCollapsedRanges(
// Ignore caret point suggestion because there was
// AutoTransactionsConserveSelection.
caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
} else {
MOZ_ASSERT(
StaticPrefs::editor_white_space_normalization_blink_compatible());
AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
&aRangesToDelete.FirstRangeRef());
for (OwningNonNull<nsRange>& range : Reversed(aRangesToDelete.Ranges())) {
if (MOZ_UNLIKELY(!range->IsPositioned() || range->Collapsed())) {
continue;
}
Maybe<AutoTrackDOMRange> trackRange;
if (range != aRangesToDelete.FirstRangeRef()) {
trackRange.emplace(aHTMLEditor.RangeUpdaterRef(), &range);
}
Result<EditorDOMRange, nsresult> rangeToDeleteOrError =
WhiteSpaceVisibilityKeeper::NormalizeSurroundingWhiteSpacesToJoin(
aHTMLEditor, EditorDOMRange(range));
if (MOZ_UNLIKELY(rangeToDeleteOrError.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::"
"NormalizeSurroundingWhiteSpacesToJoin() failed");
return rangeToDeleteOrError.propagateErr();
}
trackRange.reset();
EditorDOMRange rangeToDelete = rangeToDeleteOrError.unwrap();
if (MOZ_LIKELY(rangeToDelete.IsPositionedAndValidInComposedDoc())) {
nsresult rv = range->SetStartAndEnd(
rangeToDelete.StartRef().ToRawRangeBoundary(),
rangeToDelete.EndRef().ToRawRangeBoundary());
if (NS_FAILED(rv)) {
NS_WARNING("nsRange::SetStartAndEnd() failed");
return Err(rv);
}
}
}
aRangesToDelete.RemoveCollapsedRanges();
if (MOZ_UNLIKELY(aRangesToDelete.IsCollapsed())) {
return EditActionResult::HandledResult();
}
}
if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned()) ||
(aHTMLEditor.MayHaveMutationEventListeners() &&
@@ -4422,8 +4650,24 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
*aRangeToDelete.GetStartContainer()->AsContent()->GetEditingHost(),
BlockInlineCheck::UseComputedDisplayOutsideStyle));
RefPtr<nsRange> rangeToDelete(&aRangeToDelete);
{
const OwningNonNull<nsRange> rangeToDelete(aRangeToDelete);
if (StaticPrefs::editor_white_space_normalization_blink_compatible()) {
Result<EditorDOMRange, nsresult> rangeToDeleteOrError =
WhiteSpaceVisibilityKeeper::NormalizeSurroundingWhiteSpacesToJoin(
aHTMLEditor, EditorDOMRange(rangeToDelete));
if (MOZ_UNLIKELY(rangeToDeleteOrError.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::NormalizeSurroundingWhiteSpacesToJoin() "
"failed");
return rangeToDeleteOrError.propagateErr();
}
nsresult rv = rangeToDeleteOrError.unwrap().SetToRange(rangeToDelete);
if (NS_FAILED(rv)) {
NS_WARNING("EditorDOMRange::SetToRange() failed");
return Err(rv);
}
}
if (!rangeToDelete->Collapsed()) {
AutoClonedSelectionRangeArray rangesToDelete(*rangeToDelete,
aLimitersAndCaretData);
AutoTrackDOMRange trackRangeToDelete(aHTMLEditor.RangeUpdaterRef(),
@@ -4534,26 +4778,46 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
aSelectionWasCollapsed == SelectionWasCollapsed::Yes &&
nsIEditor::DirectionIsBackspace(aDirectionAndAmount);
AutoClonedSelectionRangeArray rangesToDelete(aRangeToDelete,
aLimitersAndCaretData);
Result<CaretPoint, nsresult> caretPointOrError =
aHTMLEditor.DeleteRangesWithTransaction(aDirectionAndAmount,
aStripWrappers, rangesToDelete);
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
NS_WARNING("HTMLEditor::DeleteRangesWithTransaction() failed");
return caretPointOrError.propagateErr();
const OwningNonNull<nsRange> rangeToDelete(aRangeToDelete);
if (StaticPrefs::editor_white_space_normalization_blink_compatible()) {
Result<EditorDOMRange, nsresult> rangeToDeleteOrError =
WhiteSpaceVisibilityKeeper::NormalizeSurroundingWhiteSpacesToJoin(
aHTMLEditor, EditorDOMRange(*rangeToDelete));
if (MOZ_UNLIKELY(rangeToDeleteOrError.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::NormalizeSurroundingWhiteSpacesToJoin() "
"failed");
return rangeToDeleteOrError.propagateErr();
}
nsresult rv = rangeToDeleteOrError.unwrap().SetToRange(rangeToDelete);
if (NS_FAILED(rv)) {
NS_WARNING("EditorDOMRange::SetToRange() failed");
return Err(rv);
}
}
if (!rangeToDelete->Collapsed()) {
const AutoClonedSelectionRangeArray rangesToDelete(rangeToDelete,
aLimitersAndCaretData);
Result<CaretPoint, nsresult> caretPointOrError =
aHTMLEditor.DeleteRangesWithTransaction(aDirectionAndAmount,
aStripWrappers, rangesToDelete);
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
NS_WARNING("HTMLEditor::DeleteRangesWithTransaction() failed");
return caretPointOrError.propagateErr();
}
nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
SuggestCaret::AndIgnoreTrivialError});
if (NS_FAILED(rv)) {
NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
return Err(rv);
nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
SuggestCaret::AndIgnoreTrivialError});
if (NS_FAILED(rv)) {
NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
return Err(rv);
}
NS_WARNING_ASSERTION(
rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
"CaretPoint::SuggestCaretPointTo() failed, but ignored");
}
NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
"CaretPoint::SuggestCaretPointTo() failed, but ignored");
if (NS_WARN_IF(!mLeftContent->GetParentNode()) ||
NS_WARN_IF(!mRightContent->GetParentNode()) ||
@@ -4617,7 +4881,7 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
}
// Otherwise, we should put caret at start of the right content.
rv = aHTMLEditor.CollapseSelectionTo(
nsresult rv = aHTMLEditor.CollapseSelectionTo(
atFirstChildOfTheLastRightNodeOrError.inspect());
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::CollapseSelectionTo() failed");
@@ -7919,9 +8183,14 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::
EditorRawDOMPoint startPoint =
HTMLEditUtils::GetPreviousEditablePoint<EditorRawDOMPoint>(
*mEmptyInclusiveAncestorBlockElement, &aEditingHost,
// In this case, we don't join block elements so that we won't
// delete invisible trailing whitespaces in the previous element.
InvisibleWhiteSpaces::Preserve,
!StaticPrefs::editor_white_space_normalization_blink_compatible()
// In this case, we don't join block elements so that we won't
// delete invisible trailing whitespaces in the previous
// element.
? InvisibleWhiteSpaces::Preserve
// With the new normalizer, we'ill delete invisible
// white-spaces.
: InvisibleWhiteSpaces::Ignore,
// In this case, we won't join table cells so that we should
// get a range which is in a table cell even if it's in a
// table.
@@ -7945,9 +8214,13 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::
EditorRawDOMPoint endPoint =
HTMLEditUtils::GetNextEditablePoint<EditorRawDOMPoint>(
*mEmptyInclusiveAncestorBlockElement, &aEditingHost,
// In this case, we don't join block elements so that we won't
// delete invisible trailing whitespaces in the next element.
InvisibleWhiteSpaces::Preserve,
!StaticPrefs::editor_white_space_normalization_blink_compatible()
// In this case, we don't join block elements so that we won't
// delete invisible trailing whitespaces in the next element.
? InvisibleWhiteSpaces::Preserve
// With the new normalizer, we'ill delete invisible
// white-spaces.
: InvisibleWhiteSpaces::Ignore,
// In this case, we won't join table cells so that we should
// get a range which is in a table cell even if it's in a
// table.
@@ -8166,27 +8439,45 @@ HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::Run(
!HTMLEditUtils::IsListItem(mEmptyInclusiveAncestorBlockElement) &&
pointToPutCaret.GetContainer() ==
mEmptyInclusiveAncestorBlockElement->GetParentNode();
nsCOMPtr<nsINode> parentNode =
mEmptyInclusiveAncestorBlockElement->GetParentNode();
nsCOMPtr<nsIContent> nextSibling =
mEmptyInclusiveAncestorBlockElement->GetNextSibling();
EditorDOMPoint atEmptyInclusiveAncestorBlockElement(
mEmptyInclusiveAncestorBlockElement);
{
AutoTrackDOMPoint trackEmptyBlockPoint(
aHTMLEditor.RangeUpdaterRef(), &atEmptyInclusiveAncestorBlockElement);
AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
&pointToPutCaret);
nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
MOZ_KnownLive(*mEmptyInclusiveAncestorBlockElement));
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
return Err(rv);
if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) {
nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
MOZ_KnownLive(*mEmptyInclusiveAncestorBlockElement));
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
return Err(rv);
}
} else {
Result<CaretPoint, nsresult> caretPointOrError =
WhiteSpaceVisibilityKeeper::DeleteContentNodeAndJoinTextNodesAroundIt(
aHTMLEditor, MOZ_KnownLive(*mEmptyInclusiveAncestorBlockElement),
pointToPutCaret, aEditingHost);
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::"
"DeleteContentNodeAndJoinTextNodesAroundIt() failed");
return caretPointOrError.propagateErr();
}
caretPointOrError.unwrap().MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
}
if (NS_WARN_IF(!parentNode->IsInComposedDoc()) ||
NS_WARN_IF(nextSibling && nextSibling->GetParentNode() != parentNode)) {
trackEmptyBlockPoint.FlushAndStopTracking();
trackPointToPutCaret.FlushAndStopTracking();
if (NS_WARN_IF(!atEmptyInclusiveAncestorBlockElement
.IsInContentNodeAndValidInComposedDoc()) ||
NS_WARN_IF(pointToPutCaret.IsSet() &&
!pointToPutCaret.IsInContentNodeAndValidInComposedDoc())) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
}
auto pointToInsertLineBreak = nextSibling
? EditorDOMPoint(nextSibling)
: EditorDOMPoint::AtEndOf(*parentNode);
EditorDOMPoint pointToInsertLineBreak =
std::move(atEmptyInclusiveAncestorBlockElement);
DeleteRangeResult deleteNodeResult(pointToInsertLineBreak,
std::move(pointToPutCaret));
if ((aHTMLEditor.IsMailEditor() || aHTMLEditor.IsPlaintextMailComposer()) &&

View File

@@ -693,19 +693,53 @@ struct MOZ_STACK_CLASS HTMLEditor::NormalizedStringToInsertText final {
struct MOZ_STACK_CLASS HTMLEditor::ReplaceWhiteSpacesData final {
ReplaceWhiteSpacesData() = default;
/**
* @param aWhiteSpaces The new white-spaces which we will replace the
* range with.
* @param aStartOffset Replace start offset in the text node.
* @param aReplaceLength Replace length in the text node.
* @param aOffsetAfterReplacing
* [optional] If the caller may want to put caret
* middle of the white-spaces, the offset may be
* changed by deleting some invisible white-spaces.
* Therefore, this may be set for the purpose.
*/
ReplaceWhiteSpacesData(const nsAString& aWhiteSpaces, uint32_t aStartOffset,
uint32_t aReplaceLength)
uint32_t aReplaceLength,
uint32_t aOffsetAfterReplacing = UINT32_MAX)
: mNormalizedString(aWhiteSpaces),
mReplaceStartOffset(aStartOffset),
mReplaceEndOffset(aStartOffset + aReplaceLength) {
mReplaceEndOffset(aStartOffset + aReplaceLength),
mNewOffsetAfterReplace(aOffsetAfterReplacing) {
MOZ_ASSERT(ReplaceLength() >= mNormalizedString.Length());
MOZ_ASSERT_IF(mNewOffsetAfterReplace != UINT32_MAX,
mNewOffsetAfterReplace <=
mReplaceStartOffset + mNormalizedString.Length());
}
/**
* @param aWhiteSpaces The new white-spaces which we will replace the
* range with.
* @param aStartOffset Replace start offset in the text node.
* @param aReplaceLength Replace length in the text node.
* @param aOffsetAfterReplacing
* [optional] If the caller may want to put caret
* middle of the white-spaces, the offset may be
* changed by deleting some invisible white-spaces.
* Therefore, this may be set for the purpose.
*/
ReplaceWhiteSpacesData(nsAutoString&& aWhiteSpaces, uint32_t aStartOffset,
uint32_t aReplaceLength)
uint32_t aReplaceLength,
uint32_t aOffsetAfterReplacing = UINT32_MAX)
: mNormalizedString(std::forward<nsAutoString>(aWhiteSpaces)),
mReplaceStartOffset(aStartOffset),
mReplaceEndOffset(aStartOffset + aReplaceLength) {
mReplaceEndOffset(aStartOffset + aReplaceLength),
mNewOffsetAfterReplace(aOffsetAfterReplacing) {
MOZ_ASSERT(ReplaceLength() >= mNormalizedString.Length());
MOZ_ASSERT_IF(mNewOffsetAfterReplace != UINT32_MAX,
mNewOffsetAfterReplace <=
mReplaceStartOffset + mNormalizedString.Length());
}
ReplaceWhiteSpacesData GetMinimizedData(const Text& aText) const {
@@ -764,7 +798,63 @@ struct MOZ_STACK_CLASS HTMLEditor::ReplaceWhiteSpacesData final {
Substring(mNormalizedString, precedingUnnecessaryLength,
mNormalizedString.Length() - (precedingUnnecessaryLength +
followingUnnecessaryLength)),
minimizedReplaceStart, minimizedReplaceEnd - minimizedReplaceStart);
minimizedReplaceStart, minimizedReplaceEnd - minimizedReplaceStart,
mNewOffsetAfterReplace);
}
/**
* Return the normalized string before mNewOffsetAfterReplace. So,
* mNewOffsetAfterReplace must not be UINT32_MAX and in the replaced range
* when this is called.
*
* @param aReplaceEndOffset Specify the offset in the Text node of
* mNewOffsetAfterReplace before replacing with the
* data.
* @return The substring before mNewOffsetAfterReplace which is typically set
* for new caret position in the Text node or collapsed deleting range
* surrounded by the white-spaces.
*/
[[nodiscard]] ReplaceWhiteSpacesData PreviousDataOfNewOffset(
uint32_t aReplaceEndOffset) const {
MOZ_ASSERT(mNewOffsetAfterReplace != UINT32_MAX);
MOZ_ASSERT(mReplaceStartOffset <= mNewOffsetAfterReplace);
MOZ_ASSERT(mReplaceEndOffset >= mNewOffsetAfterReplace);
MOZ_ASSERT(mReplaceStartOffset <= aReplaceEndOffset);
MOZ_ASSERT(mReplaceEndOffset >= aReplaceEndOffset);
if (!ReplaceLength() || aReplaceEndOffset == mReplaceStartOffset) {
return ReplaceWhiteSpacesData();
}
return ReplaceWhiteSpacesData(
Substring(mNormalizedString, 0u,
mNewOffsetAfterReplace - mReplaceStartOffset),
mReplaceStartOffset, aReplaceEndOffset - mReplaceStartOffset);
}
/**
* Return the normalized string after mNewOffsetAfterReplace. So,
* mNewOffsetAfterReplace must not be UINT32_MAX and in the replaced range
* when this is called.
*
* @param aReplaceStartOffset Specify the replace start offset with the
* normalized white-spaces.
* @return The substring after mNewOffsetAfterReplace which is typically set
* for new caret position in the Text node or collapsed deleting range
* surrounded by the white-spaces.
*/
[[nodiscard]] ReplaceWhiteSpacesData NextDataOfNewOffset(
uint32_t aReplaceStartOffset) const {
MOZ_ASSERT(mNewOffsetAfterReplace != UINT32_MAX);
MOZ_ASSERT(mReplaceStartOffset <= mNewOffsetAfterReplace);
MOZ_ASSERT(mReplaceEndOffset >= mNewOffsetAfterReplace);
MOZ_ASSERT(mReplaceStartOffset <= aReplaceStartOffset);
MOZ_ASSERT(mReplaceEndOffset >= aReplaceStartOffset);
if (!ReplaceLength() || aReplaceStartOffset == mReplaceEndOffset) {
return ReplaceWhiteSpacesData();
}
return ReplaceWhiteSpacesData(
Substring(mNormalizedString,
mNewOffsetAfterReplace - mReplaceStartOffset),
aReplaceStartOffset, mReplaceEndOffset - aReplaceStartOffset);
}
[[nodiscard]] uint32_t ReplaceLength() const {
@@ -783,14 +873,25 @@ struct MOZ_STACK_CLASS HTMLEditor::ReplaceWhiteSpacesData final {
return *this;
}
MOZ_ASSERT(mReplaceEndOffset == aOther.mReplaceStartOffset);
MOZ_ASSERT_IF(
aOther.mNewOffsetAfterReplace != UINT32_MAX,
aOther.mNewOffsetAfterReplace >= DeletingInvisibleWhiteSpaces());
return ReplaceWhiteSpacesData(
nsAutoString(mNormalizedString + aOther.mNormalizedString),
mReplaceStartOffset, aOther.mReplaceEndOffset);
mReplaceStartOffset, aOther.mReplaceEndOffset,
aOther.mNewOffsetAfterReplace != UINT32_MAX
? aOther.mNewOffsetAfterReplace - DeletingInvisibleWhiteSpaces()
: mNewOffsetAfterReplace);
}
nsAutoString mNormalizedString;
const uint32_t mReplaceStartOffset = 0u;
const uint32_t mReplaceEndOffset = 0u;
// If the caller specifies a point in a white-space sequence, some invisible
// white-spaces will be deleted with replacing them with normalized string.
// Then, they may want to keep the position for putting caret or something.
// So, this may store a specific offset in the text node after replacing.
const uint32_t mNewOffsetAfterReplace = UINT32_MAX;
};
} // namespace mozilla

View File

@@ -408,7 +408,7 @@ class MOZ_STACK_CLASS AutoTrackDOMRange final {
mEndPointTracker.emplace(aRangeUpdater,
const_cast<EditorDOMPoint*>(&aRange->EndRef()));
}
AutoTrackDOMRange(RangeUpdater& aRangeUpdater, RefPtr<nsRange>* aRange)
AutoTrackDOMRange(RangeUpdater& aRangeUpdater, const RefPtr<nsRange>* aRange)
: mStartPoint((*aRange)->StartRef()),
mEndPoint((*aRange)->EndRef()),
mRangeRefPtr(aRange),
@@ -416,7 +416,8 @@ class MOZ_STACK_CLASS AutoTrackDOMRange final {
mStartPointTracker.emplace(aRangeUpdater, &mStartPoint);
mEndPointTracker.emplace(aRangeUpdater, &mEndPoint);
}
AutoTrackDOMRange(RangeUpdater& aRangeUpdater, OwningNonNull<nsRange>* aRange)
AutoTrackDOMRange(RangeUpdater& aRangeUpdater,
const OwningNonNull<nsRange>* aRange)
: mStartPoint((*aRange)->StartRef()),
mEndPoint((*aRange)->EndRef()),
mRangeRefPtr(nullptr),
@@ -499,8 +500,8 @@ class MOZ_STACK_CLASS AutoTrackDOMRange final {
Maybe<AutoTrackDOMPoint> mEndPointTracker;
EditorDOMPoint mStartPoint;
EditorDOMPoint mEndPoint;
RefPtr<nsRange>* mRangeRefPtr;
OwningNonNull<nsRange>* mRangeOwningNonNull;
const RefPtr<nsRange>* mRangeRefPtr;
const OwningNonNull<nsRange>* mRangeOwningNonNull;
};
class MOZ_STACK_CLASS AutoTrackDOMMoveNodeResult final {

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,7 @@
#include "mozilla/Assertions.h"
#include "mozilla/Maybe.h"
#include "mozilla/Result.h"
#include "mozilla/StaticPrefs_editor.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/HTMLBRElement.h"
#include "mozilla/dom/Text.h"
@@ -74,6 +75,8 @@ class WhiteSpaceVisibilityKeeper final {
EditorDOMPoint* aStartPoint,
EditorDOMPoint* aEndPoint,
const Element& aEditingHost) {
MOZ_ASSERT(
!StaticPrefs::editor_white_space_normalization_blink_compatible());
MOZ_ASSERT(aStartPoint->IsSetAndValid());
MOZ_ASSERT(aEndPoint->IsSetAndValid());
AutoTrackDOMPoint trackerStart(aHTMLEditor.RangeUpdaterRef(), aStartPoint);
@@ -88,23 +91,10 @@ class WhiteSpaceVisibilityKeeper final {
return caretPointOrError;
}
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<CaretPoint, nsresult>
PrepareToDeleteRange(HTMLEditor& aHTMLEditor,
const EditorDOMPoint& aStartPoint,
const EditorDOMPoint& aEndPoint,
const Element& aEditingHost) {
MOZ_ASSERT(aStartPoint.IsSetAndValid());
MOZ_ASSERT(aEndPoint.IsSetAndValid());
Result<CaretPoint, nsresult> caretPointOrError =
WhiteSpaceVisibilityKeeper::PrepareToDeleteRange(
aHTMLEditor, EditorDOMRange(aStartPoint, aEndPoint), aEditingHost);
NS_WARNING_ASSERTION(
caretPointOrError.isOk(),
"WhiteSpaceVisibilityKeeper::PrepareToDeleteRange() failed");
return caretPointOrError;
}
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<CaretPoint, nsresult>
PrepareToDeleteRange(HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
const Element& aEditingHost) {
MOZ_ASSERT(
!StaticPrefs::editor_white_space_normalization_blink_compatible());
MOZ_ASSERT(aRange.IsPositionedAndValid());
Result<CaretPoint, nsresult> caretPointOrError =
WhiteSpaceVisibilityKeeper::
@@ -131,6 +121,10 @@ class WhiteSpaceVisibilityKeeper final {
const Element& aSplittingBlockElement);
enum class NormalizeOption {
// If set, don't normalize white-spaces before the point.
HandleOnlyFollowingWhiteSpaces,
// If set, don't normalize white-spaces after the point.
HandleOnlyPrecedingWhiteSpaces,
// If set, don't normalize following white-spaces if starts with an NBSP.
StopIfFollowingWhiteSpacesStartsWithNBSP,
// If set, don't normalize preceding white-spaces if ends with an NBSP.
@@ -138,6 +132,34 @@ class WhiteSpaceVisibilityKeeper final {
};
using NormalizeOptions = EnumSet<NormalizeOption>;
/**
* Normalize preceding white-spaces of aPoint. aPoint should not be middle of
* a Text node.
*
* @return If this updates some characters of the last `Text` node, this
* returns the end of the `Text`. Otherwise, this returns the position
* of the found `Text` which ends with a visible character or aPoint.
* Note that returning aPoint does not mean nothing is changed.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<EditorDOMPoint, nsresult>
NormalizeWhiteSpacesBefore(HTMLEditor& aHTMLEditor,
const EditorDOMPoint& aPoint,
NormalizeOptions aOptions);
/**
* Normalize following white-spaces of aPoint. aPoint should not be middle of
* a Text node.
*
* @return If this updates some characters of the first `Text` node, this
* returns the start of the `Text`. Otherwise, this returns the position
* of the found `Text` which starts with a visible character or aPoint.
* Note that returning aPoint does not mean nothing is changed.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<EditorDOMPoint, nsresult>
NormalizeWhiteSpacesAfter(HTMLEditor& aHTMLEditor,
const EditorDOMPoint& aPoint,
NormalizeOptions aOptions);
/**
* Normalize surrounding white-spaces of aPointToSplit. This may normalize
* 2 `Text` nodes if the point is surrounded by them.
@@ -148,6 +170,14 @@ class WhiteSpaceVisibilityKeeper final {
const EditorDOMPoint& aPointToSplit,
NormalizeOptions aOptions);
/**
* Normalize surrounding white-spaces of both boundaries of aRangeToDelete.
* This returns the range which should be deleted later.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<EditorDOMRange, nsresult>
NormalizeSurroundingWhiteSpacesToJoin(HTMLEditor& aHTMLEditor,
const EditorDOMRange& aRangeToDelete);
/**
* MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement() merges
* first line in aRightBlockElement into end of aLeftBlockElement which
@@ -377,6 +407,18 @@ class WhiteSpaceVisibilityKeeper final {
HTMLEditor& aHTMLEditor, const EditorDOMPointInText& aPointToSplit,
NormalizeOptions aOptions);
/**
* Normalize surrounding white-spaces of the range between aOffset - aOffset +
* aLength.
*
* @return The delete range after normalized.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<EditorDOMRange, nsresult>
NormalizeSurroundingWhiteSpacesToDeleteCharacters(HTMLEditor& aHTMLEditor,
dom::Text& aTextNode,
uint32_t aOffset,
uint32_t aLength);
/**
* Delete leading or trailing invisible white-spaces around block boundaries
* or collapsed white-spaces in a white-space sequence if aPoint is around
@@ -392,6 +434,30 @@ class WhiteSpaceVisibilityKeeper final {
EnsureNoInvisibleWhiteSpaces(HTMLEditor& aHTMLEditor,
const EditorDOMPoint& aPoint);
/**
* Delete preceding invisible white-spaces before aPoint.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult
EnsureNoInvisibleWhiteSpacesBefore(HTMLEditor& aHTMLEditor,
const EditorDOMPoint& aPoint);
/**
* Delete following invisible white-spaces after aPoint.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult
EnsureNoInvisibleWhiteSpacesAfter(HTMLEditor& aHTMLEditor,
const EditorDOMPoint& aPoint);
/**
* If aPoint points a collapsible white-space, normalize entire the
* white-space sequence.
*
* @return Equivalent point of aPoint after normalizing the white-spaces.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<EditorDOMPoint, nsresult>
NormalizeWhiteSpacesAt(HTMLEditor& aHTMLEditor,
const EditorDOMPointInText& aPoint);
/**
* Insert aStringToInsert to aRangeToBeReplaced.StartRef() with normalizing
* white-spaces around there.

View File

@@ -1,6 +0,0 @@
[delete-img.tentative.html]
[document.execCommand("delete") when "a&nbsp;&nbsp;&nbsp;<img src="${src}">[\]&nbsp;&nbsp;&nbsp;b"]
expected: FAIL
[document.execCommand("delete") when "a&nbsp;&nbsp;&nbsp;<img src="${src}">[\]b"]
expected: FAIL

View File

@@ -1,4 +1,16 @@
[forwarddelete-to-join-blocks.tentative.html]
[document.execCommand("forwarddelete") when "<div>&nbsp;&nbsp;&nbsp;[\]</div><div><br></div>"]
expected: FAIL
[document.execCommand("forwarddelete") when "<div>a&nbsp;&nbsp;&nbsp;[\]</div><div><br></div>"]
expected: FAIL
[document.execCommand("forwarddelete") when "<div>&nbsp;&nbsp;&nbsp;[\]<div><br></div></div>"]
expected: FAIL
[document.execCommand("forwarddelete") when "<div>a&nbsp;&nbsp;&nbsp;[\]<div><br></div></div>"]
expected: FAIL
[document.execCommand("forwarddelete") when "<div>a[\]<div>&nbsp;&nbsp;&nbsp;b</div></div>"]
expected: FAIL

View File

@@ -1,175 +1,28 @@
[forwarddelete.tentative.html]
[execCommand("forwarddelete", false, ""): "&nbsp; [\]&nbsp; &nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;|&nbsp;|[\]&nbsp;&nbsp;&nbsp;&nbsp;c"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc[\]&nbsp; </span>&nbsp; def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;| [\]|&nbsp;&nbsp;&nbsp;&nbsp;c"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp;&nbsp; &nbsp; &nbsp; [\]&nbsp; &nbsp; b" (length of whitespace sequence: 11)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp;&nbsp; &nbsp; &nbsp; &nbsp;[\] &nbsp; b" (length of whitespace sequence: 11)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a &nbsp; &nbsp;[\] &nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 11)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc &nbsp;<span> [\]&nbsp;def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc&nbsp; </span><span>&nbsp;[\] def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp; &nbsp; &nbsp; &nbsp; [\]&nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc &nbsp;</span>[\]&nbsp; def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a[\]&nbsp;&nbsp;&nbsp;&nbsp; b" (length of whitespace sequence: 5)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;[\]b&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;c"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp; [\]&nbsp;|&nbsp; &nbsp;b"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp;&nbsp; [\]&nbsp; &nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 11)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\]&nbsp;<span>&nbsp;def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp;&nbsp; &nbsp;[\] &nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 11)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a &nbsp; [\]|&nbsp; &nbsp;b"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc&nbsp;&nbsp;[\]<span> &nbsp;def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a &nbsp; &nbsp; &nbsp; &nbsp; [\]&nbsp;b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc &nbsp;<span>[\]&nbsp; def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a &nbsp;[\] &nbsp; &nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 11)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;[\]b&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;c"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;|[\] |&nbsp;&nbsp;&nbsp;&nbsp;c"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp; &nbsp;[\] &nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp; [\]&nbsp;| &nbsp; b"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc &nbsp;</span><span> [\]&nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp;&nbsp;[\] &nbsp; &nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 11)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp;&nbsp; [\]&nbsp;b" (length of whitespace sequence: 4)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc&nbsp;&nbsp;<span>[\] &nbsp;def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp;[\]&nbsp;&nbsp; b" (length of whitespace sequence: 4)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;[\]&nbsp; b" (length of whitespace sequence: 5)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp; [\]&nbsp; &nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp; &nbsp; [\]&nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a &nbsp; |[\]&nbsp; &nbsp;b"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc&nbsp; </span>&nbsp;[\] def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;[\] &nbsp; &nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\] &nbsp;</span><span>&nbsp;&nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "[\]&nbsp; &nbsp; b" (length of whitespace sequence: 4)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc[\] &nbsp;</span> &nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a[\]&nbsp; &nbsp; b" (length of whitespace sequence: 4)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\] &nbsp;</span><span> &nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc &nbsp;</span>&nbsp;[\] def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp;&nbsp;&nbsp; [\]&nbsp;b" (length of whitespace sequence: 5)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a &nbsp; [\]&nbsp;b" (length of whitespace sequence: 4)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc &nbsp;</span><span>&nbsp;[\] def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;[\]&nbsp;&nbsp; b" (length of whitespace sequence: 5)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp; &nbsp;[\] &nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc&nbsp;&nbsp;[\]</span><span> &nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\] <span>&nbsp;def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc&nbsp;&nbsp;</span><span>[\] &nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "[\]&nbsp;&nbsp;&nbsp; b" (length of whitespace sequence: 4)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc &nbsp;[\]<span>&nbsp; def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp;[\] &nbsp; &nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp; [\]&nbsp;b" (length of whitespace sequence: 4)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp;&nbsp; &nbsp; [\]&nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 11)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp; &nbsp; &nbsp; [\]&nbsp; &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\]&nbsp;<span> def</span></span>"]
expected: FAIL
@@ -179,212 +32,68 @@
[execCommand("forwarddelete", false, ""): "<span><span>abc[\]&nbsp;</span> def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp; [\]&nbsp; b" (length of whitespace sequence: 4)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc[\]&nbsp; </span>&nbsp;&nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp; [\]&nbsp;b" (length of whitespace sequence: 5)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\]&nbsp;&nbsp;<span>&nbsp;&nbsp;def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a &nbsp; [\]&nbsp; b" (length of whitespace sequence: 5)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; [\]&nbsp;b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp; &nbsp;|[\] &nbsp; b"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc&nbsp; <span>&nbsp;[\] def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\]&nbsp;&nbsp;</span><span>&nbsp;&nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;&nbsp;||[\]&nbsp;&nbsp;&nbsp;&nbsp;c"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp; &nbsp; &nbsp;[\] &nbsp; &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;[\]&nbsp;&nbsp;&nbsp; b" (length of whitespace sequence: 5)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "[\]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\] &nbsp;<span> &nbsp;def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc&nbsp; [\]</span>&nbsp; def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a &nbsp; &nbsp; &nbsp; &nbsp;[\] &nbsp; b" (length of whitespace sequence: 11)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; [\]&nbsp; b" (length of whitespace sequence: 11)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a &nbsp; &nbsp; &nbsp; [\]&nbsp; &nbsp; b" (length of whitespace sequence: 11)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp; &nbsp; &nbsp; &nbsp;[\] &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc&nbsp;&nbsp;</span>[\] &nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;&nbsp;|[\]|&nbsp;&nbsp;&nbsp;&nbsp;c"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\] </span><span>&nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;[\]&nbsp;| |&nbsp;&nbsp;&nbsp;c"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;[\] &nbsp; b" (length of whitespace sequence: 4)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;[\]&nbsp;&nbsp;| |&nbsp;&nbsp;&nbsp;c"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc[\]&nbsp;&nbsp;</span>&nbsp;&nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;[\]&nbsp;&nbsp;|&nbsp;|&nbsp;&nbsp;&nbsp;c"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a[\]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a &nbsp; &nbsp; [\]&nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 11)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp;[\] &nbsp; b" (length of whitespace sequence: 4)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a &nbsp; &nbsp; &nbsp; &nbsp; [\]&nbsp; b" (length of whitespace sequence: 11)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a &nbsp; &nbsp; &nbsp;[\] &nbsp; &nbsp; b" (length of whitespace sequence: 11)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc&nbsp; [\]</span><span>&nbsp; def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp; &nbsp;|[\]&nbsp; &nbsp;b"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc&nbsp; <span>[\]&nbsp; def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc[\] </span>&nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp; &nbsp; &nbsp;[\] &nbsp; &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;[\]&nbsp;&nbsp;||&nbsp;&nbsp;&nbsp;&nbsp;c"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc&nbsp; </span><span>[\]&nbsp; def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\]&nbsp; <span>&nbsp;&nbsp;def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\] &nbsp;<span>&nbsp;&nbsp;def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;| |[\]&nbsp;&nbsp;&nbsp;&nbsp;c"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc &nbsp;</span> [\]&nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc &nbsp;<span>&nbsp;[\] def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc&nbsp; </span>[\]&nbsp; def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\]&nbsp;</span><span> def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp; [\]&nbsp; b" (length of whitespace sequence: 4)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc &nbsp;[\]</span><span>&nbsp; def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp; &nbsp; &nbsp; [\]&nbsp; &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp;&nbsp; &nbsp; &nbsp;[\] &nbsp; &nbsp; b" (length of whitespace sequence: 11)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc[\] &nbsp;</span>&nbsp;&nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\]&nbsp; </span><span>&nbsp; def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp; &nbsp; &nbsp; &nbsp; [\]&nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;[\]&nbsp;||&nbsp;&nbsp;&nbsp;&nbsp;c"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc &nbsp;</span><span>[\]&nbsp; def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc&nbsp; [\]<span>&nbsp; def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;|[\]b&nbsp;&nbsp;&nbsp;&nbsp;c"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp; &nbsp; &nbsp; &nbsp;[\] &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc[\]&nbsp;</span>&nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc &nbsp;[\]</span>&nbsp; def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc&nbsp;&nbsp;[\]</span> &nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "&nbsp;&nbsp;[\]&nbsp; b" (length of whitespace sequence: 4)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a &nbsp;[\] &nbsp; b" (length of whitespace sequence: 5)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp; &nbsp; [\]&nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 10)]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\]&nbsp;</span><span>&nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\]&nbsp; </span><span>&nbsp;&nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;[\]&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;b"]
[execCommand("forwarddelete", false, ""): "a &nbsp;[\] |&nbsp; &nbsp;b"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a &nbsp; [\]&nbsp; &nbsp; &nbsp; &nbsp; b" (length of whitespace sequence: 11)]
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;[\]&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;b"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;[\]&nbsp;&nbsp;<span style=white-space:pre;> </span>"]
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;[\]b|&nbsp;&nbsp;&nbsp;&nbsp;c"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;[\]&nbsp;&nbsp;<span style=white-space:pre;>b </span>"]
[execCommand("forwarddelete", false, ""): "<span>abc[\]&nbsp;<span> &nbsp;def</span></span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span><span>abc[\]&nbsp;</span> &nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span>abc[\]&nbsp;</span><span> &nbsp;def</span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "a&nbsp;&nbsp;&nbsp;[\]&nbsp;<span style=white-space:pre;> </span>"]
expected: FAIL
[execCommand("forwarddelete", false, ""): "<span style=white-space:pre;> [\] </span>&nbsp;&nbsp;&nbsp;a"]
expected: FAIL