Bug 1940377 - part 1: Make WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString support blink compatible white-space sequence r=m_kato

First, it makes it deletes invisible white-spaces at the insertion point.
This makes the further processing simpler.

Second, it extends the range to update in `Text` which will contain the
insertion string when it's not used for inserting nor updating composition
string, as containing all surrounding white-spaces at the insertion point.

Next, it normalizes the inserting string with the surrounding white-spaces with
the same rules as the other browsers especially as Chrome.
1. a collapsible white-space at start or end of a `Text` is always an NBSP.
2. a collapsible white-space immediately before or after a preformatted line
break is always an NBSP.
3. a collapsible white-space surrounded by non-collapsible chars is always an
ASCII white-space.
4. collapsible white-spaces are pairs of an NBSP and an ASCII white-space.

Then, we can make `HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal()`
stop normalizing white-spaces when inserting text.

Differential Revision: https://phabricator.services.mozilla.com/D239463
This commit is contained in:
Masayuki Nakano
2025-03-08 22:31:54 +00:00
parent 589bee8c53
commit c0723c81ee
13 changed files with 1504 additions and 670 deletions

View File

@@ -3244,32 +3244,20 @@ nsresult EditorBase::ScrollSelectionFocusIntoView() const {
return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK; return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
} }
Result<InsertTextResult, nsresult> EditorBase::InsertTextWithTransaction( EditorDOMPoint EditorBase::ComputePointToInsertText(
const nsAString& aStringToInsert, const EditorDOMPoint& aPointToInsert, const EditorDOMPoint& aPoint, InsertTextTo aInsertTextTo) const {
InsertTextTo aInsertTextTo) { if (aInsertTextTo == InsertTextTo::SpecifiedPoint) {
MOZ_ASSERT_IF(IsTextEditor(), return aPoint;
aInsertTextTo == InsertTextTo::ExistingTextNodeIfAvailable);
if (NS_WARN_IF(!aPointToInsert.IsSet())) {
return Err(NS_ERROR_INVALID_ARG);
}
MOZ_ASSERT(aPointToInsert.IsSetAndValid());
if (!ShouldHandleIMEComposition() && aStringToInsert.IsEmpty()) {
return InsertTextResult();
} }
if (IsTextEditor()) {
// In some cases, the node may be the anonymous div element or a padding // In some cases, the node may be the anonymous div element or a padding
// <br> element for empty last line. Let's try to look for better insertion // <br> element for empty last line. Let's try to look for better insertion
// point in the nearest text node if there is. // point in the nearest text node if there is.
EditorDOMPoint pointToInsert = [&]() { return AsTextEditor()->FindBetterInsertionPoint(aPoint);
if (IsTextEditor()) {
return AsTextEditor()->FindBetterInsertionPoint(aPointToInsert);
} }
auto pointToInsert = auto pointToInsert =
aPointToInsert aPoint.GetPointInTextNodeIfPointingAroundTextNode<EditorDOMPoint>();
.GetPointInTextNodeIfPointingAroundTextNode<EditorDOMPoint>();
// If the candidate point is in a Text node which has only a preformatted // If the candidate point is in a Text node which has only a preformatted
// linefeed, we should not insert text into the node because it may have // linefeed, we should not insert text into the node because it may have
// been inserted by us and that's compatible behavior with Chrome. // been inserted by us and that's compatible behavior with Chrome.
@@ -3310,14 +3298,31 @@ Result<InsertTextResult, nsresult> EditorBase::InsertTextWithTransaction(
: pointToInsert); : pointToInsert);
} }
if (aInsertTextTo == InsertTextTo::ExistingTextNodeIfAvailableAndNotStart) { if (aInsertTextTo == InsertTextTo::ExistingTextNodeIfAvailableAndNotStart) {
return !(pointToInsert.IsInTextNode() && return !(pointToInsert.IsInTextNode() && pointToInsert.IsStartOfContainer())
pointToInsert.IsStartOfContainer())
? pointToInsert ? pointToInsert
: EditorDOMPoint(pointToInsert.ContainerAs<Text>()); : EditorDOMPoint(pointToInsert.ContainerAs<Text>());
} }
return pointToInsert; return pointToInsert;
}(); }
Result<InsertTextResult, nsresult> EditorBase::InsertTextWithTransaction(
const nsAString& aStringToInsert, const EditorDOMPoint& aPointToInsert,
InsertTextTo aInsertTextTo) {
MOZ_ASSERT_IF(IsTextEditor(),
aInsertTextTo == InsertTextTo::ExistingTextNodeIfAvailable);
if (NS_WARN_IF(!aPointToInsert.IsSet())) {
return Err(NS_ERROR_INVALID_ARG);
}
MOZ_ASSERT(aPointToInsert.IsSetAndValid());
if (!ShouldHandleIMEComposition() && aStringToInsert.IsEmpty()) {
return InsertTextResult();
}
EditorDOMPoint pointToInsert =
ComputePointToInsertText(aPointToInsert, aInsertTextTo);
if (ShouldHandleIMEComposition()) { if (ShouldHandleIMEComposition()) {
if (!pointToInsert.IsInTextNode()) { if (!pointToInsert.IsInTextNode()) {
// create a text node // create a text node

View File

@@ -1775,6 +1775,7 @@ class EditorBase : public nsIEditor,
* available. * available.
*/ */
enum class InsertTextTo { enum class InsertTextTo {
SpecifiedPoint,
ExistingTextNodeIfAvailable, ExistingTextNodeIfAvailable,
ExistingTextNodeIfAvailableAndNotStart, ExistingTextNodeIfAvailableAndNotStart,
AlwaysCreateNewTextNode AlwaysCreateNewTextNode
@@ -1784,6 +1785,12 @@ class EditorBase : public nsIEditor,
const EditorDOMPoint& aPointToInsert, const EditorDOMPoint& aPointToInsert,
InsertTextTo aInsertTextTo); InsertTextTo aInsertTextTo);
/**
* Compute insertion point from aPoint and aInsertTextTo.
*/
[[nodiscard]] EditorDOMPoint ComputePointToInsertText(
const EditorDOMPoint& aPoint, InsertTextTo aInsertTextTo) const;
/** /**
* Insert aStringToInsert to aPointToInsert. * Insert aStringToInsert to aPointToInsert.
*/ */
@@ -3055,7 +3062,9 @@ class EditorBase : public nsIEditor,
// CollapseSelectionTo, DoReplaceText, // CollapseSelectionTo, DoReplaceText,
// RangeUpdaterRef // RangeUpdaterRef
friend class SplitNodeTransaction; // ToGenericNSResult friend class SplitNodeTransaction; // ToGenericNSResult
friend class WhiteSpaceVisibilityKeeper; // AutoTransactionsConserveSelection friend class
WhiteSpaceVisibilityKeeper; // AutoTransactionsConserveSelection,
// ComputePointToInsertText
friend class nsIEditor; // mIsHTMLEditorClass friend class nsIEditor; // mIsHTMLEditorClass
}; };

View File

@@ -631,6 +631,48 @@ class EditorDOMPointBase final {
return AtEndOf(*aContainer.get(), aInterlinePosition); return AtEndOf(*aContainer.get(), aInterlinePosition);
} }
/**
* SetToLastContentOf() sets this to the last child of aContainer or the last
* character of aContainer.
*/
template <typename ContainerType>
void SetToLastContentOf(const ContainerType* aContainer) {
MOZ_ASSERT(aContainer);
mParent = const_cast<ContainerType*>(aContainer);
if (aContainer->IsContainerNode()) {
MOZ_ASSERT(aContainer->GetChildCount());
mChild = aContainer->GetLastChild();
mOffset = mozilla::Some(aContainer->GetChildCount() - 1u);
} else {
MOZ_ASSERT(aContainer->Length());
mChild = nullptr;
mOffset = mozilla::Some(aContainer->Length() - 1u);
}
mIsChildInitialized = true;
mInterlinePosition = InterlinePosition::Undefined;
}
template <typename ContainerType, template <typename> typename StrongPtr>
MOZ_NEVER_INLINE_DEBUG void SetToLastContentOf(
const StrongPtr<ContainerType>& aContainer) {
SetToLastContentOf(aContainer.get());
}
template <typename ContainerType>
MOZ_NEVER_INLINE_DEBUG static SelfType AtLastContentOf(
const ContainerType& aContainer,
InterlinePosition aInterlinePosition = InterlinePosition::Undefined) {
SelfType point;
point.SetToLastContentOf(&aContainer);
point.mInterlinePosition = aInterlinePosition;
return point;
}
template <typename ContainerType, template <typename> typename StrongPtr>
MOZ_NEVER_INLINE_DEBUG static SelfType AtLastContentOf(
const StrongPtr<ContainerType>& aContainer,
InterlinePosition aInterlinePosition = InterlinePosition::Undefined) {
MOZ_ASSERT(aContainer.get());
return AtLastContentOf(*aContainer.get(), aInterlinePosition);
}
/** /**
* SetAfter() sets mChild to next sibling of aChild. * SetAfter() sets mChild to next sibling of aChild.
*/ */
@@ -728,6 +770,13 @@ class EditorDOMPointBase final {
result.RewindOffset(); result.RewindOffset();
return result; return result;
} }
template <typename EditorDOMPointType = SelfType>
EditorDOMPointType PreviousPointOrParentPoint() const {
if (IsStartOfContainer()) {
return ParentPoint<EditorDOMPointType>();
}
return PreviousPoint<EditorDOMPointType>();
}
/** /**
* Clear() makes the instance not point anywhere. * Clear() makes the instance not point anywhere.

View File

@@ -35,6 +35,7 @@
#include "mozilla/Maybe.h" #include "mozilla/Maybe.h"
#include "mozilla/OwningNonNull.h" #include "mozilla/OwningNonNull.h"
#include "mozilla/PresShell.h" #include "mozilla/PresShell.h"
#include "mozilla/StaticPrefs_editor.h"
#include "mozilla/TextComposition.h" #include "mozilla/TextComposition.h"
#include "mozilla/UniquePtr.h" #include "mozilla/UniquePtr.h"
#include "mozilla/Unused.h" #include "mozilla/Unused.h"
@@ -552,24 +553,29 @@ nsresult HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() {
// attempt to transform any unneeded nbsp's into spaces after doing various // attempt to transform any unneeded nbsp's into spaces after doing various
// operations // operations
const bool needToNormalizeWhiteSpaces = [&]() {
switch (GetTopLevelEditSubAction()) { switch (GetTopLevelEditSubAction()) {
case EditSubAction::eDeleteSelectedContent: case EditSubAction::eDeleteSelectedContent:
if (TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces) { return !TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces;
break;
}
[[fallthrough]];
case EditSubAction::eInsertText: case EditSubAction::eInsertText:
case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eInsertTextComingFromIME:
return !StaticPrefs::
editor_white_space_normalization_blink_compatible();
case EditSubAction::eInsertLineBreak: case EditSubAction::eInsertLineBreak:
case EditSubAction::eInsertParagraphSeparator: case EditSubAction::eInsertParagraphSeparator:
case EditSubAction::ePasteHTMLContent: case EditSubAction::ePasteHTMLContent:
case EditSubAction::eInsertHTMLSource: { case EditSubAction::eInsertHTMLSource:
return true;
default:
return false;
}
}();
if (needToNormalizeWhiteSpaces) {
// Due to the replacement of white-spaces in // Due to the replacement of white-spaces in
// WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(), // WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(), selection
// selection ranges may be changed since DOM ranges track the DOM // ranges may be changed since DOM ranges track the DOM mutation by
// mutation by themselves. However, we want to keep selection as-is. // themselves. However, we want to keep selection as-is. Therefore, we
// Therefore, we should restore `Selection` after replacing // should restore `Selection` after replacing white-spaces.
// white-spaces.
AutoSelectionRestorer restoreSelection(this); AutoSelectionRestorer restoreSelection(this);
// TODO: Temporarily, WhiteSpaceVisibilityKeeper replaces ASCII // TODO: Temporarily, WhiteSpaceVisibilityKeeper replaces ASCII
// white-spaces with NPSPs and then, we'll replace them with ASCII // white-spaces with NPSPs and then, we'll replace them with ASCII
@@ -586,11 +592,8 @@ nsresult HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() {
return NS_ERROR_FAILURE; return NS_ERROR_FAILURE;
} }
} }
const RefPtr<Element> editingHost = if (const RefPtr<Element> editingHost =
ComputeEditingHost(LimitInBodyElement::No); ComputeEditingHost(LimitInBodyElement::No)) {
if (MOZ_UNLIKELY(!editingHost)) {
break;
}
if (EditorUtils::IsEditableContent( if (EditorUtils::IsEditableContent(
*pointToAdjust.ContainerAs<nsIContent>(), EditorType::HTML)) { *pointToAdjust.ContainerAs<nsIContent>(), EditorType::HTML)) {
AutoTrackDOMPoint trackPointToAdjust(RangeUpdaterRef(), AutoTrackDOMPoint trackPointToAdjust(RangeUpdaterRef(),
@@ -654,10 +657,7 @@ nsresult HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() {
"WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() " "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() "
"failed, but ignored"); "failed, but ignored");
} }
break;
} }
default:
break;
} }
// Adjust selection for insert text, html paste, and delete actions if // Adjust selection for insert text, html paste, and delete actions if
@@ -1236,7 +1236,8 @@ Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
*this, aInsertionString, *this, aInsertionString,
compositionEndPoint.IsSet() compositionEndPoint.IsSet()
? EditorDOMRange(pointToInsert, compositionEndPoint) ? EditorDOMRange(pointToInsert, compositionEndPoint)
: EditorDOMRange(pointToInsert)); : EditorDOMRange(pointToInsert),
aPurpose);
if (MOZ_UNLIKELY(replaceTextResult.isErr())) { if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
NS_WARNING("WhiteSpaceVisibilityKeeper::ReplaceText() failed"); NS_WARNING("WhiteSpaceVisibilityKeeper::ReplaceText() failed");
return replaceTextResult.propagateErr(); return replaceTextResult.propagateErr();
@@ -2948,13 +2949,86 @@ HTMLEditor::GetInclusiveNextCharPointDataForNormalizingWhiteSpaces(
HTMLEditor::GetCharPointType(nextCharPoint)); HTMLEditor::GetCharPointType(nextCharPoint));
} }
// static
void HTMLEditor::NormalizeAllWhiteSpaceSequences(
nsString& aResult, const CharPointData& aPreviousCharPointData,
const CharPointData& aNextCharPointData, Linefeed aLinefeed) {
MOZ_ASSERT(!aResult.IsEmpty());
const auto IsCollapsibleChar = [&](char16_t aChar) {
if (aChar == HTMLEditUtils::kNewLine) {
return aLinefeed == Linefeed::Preformatted;
}
return nsCRT::IsAsciiSpace(aChar);
};
const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) {
return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar);
};
const uint32_t length = aResult.Length();
for (uint32_t offset = 0; offset < length; offset++) {
const char16_t ch = aResult[offset];
if (!IsCollapsibleCharOrNBSP(ch)) {
continue;
}
const CharPointData previousCharData = [&]() {
if (offset) {
const char16_t prevChar = aResult[offset - 1u];
return CharPointData::InSameTextNode(
prevChar == HTMLEditUtils::kNewLine
? CharPointType::PreformattedLineBreak
: CharPointType::VisibleChar);
}
return aPreviousCharPointData;
}();
const uint32_t endOffset = [&]() {
for (const uint32_t i : IntegerRange(offset, length)) {
if (IsCollapsibleCharOrNBSP(aResult[i])) {
continue;
}
return i;
}
return length;
}();
const CharPointData nextCharData = [&]() {
if (endOffset < length) {
const char16_t nextChar = aResult[endOffset];
return CharPointData::InSameTextNode(
nextChar == HTMLEditUtils::kNewLine
? CharPointType::PreformattedLineBreak
: CharPointType::VisibleChar);
}
return aNextCharPointData;
}();
HTMLEditor::ReplaceStringWithNormalizedWhiteSpaceSequence(
aResult, offset, endOffset - offset, previousCharData, nextCharData);
offset = endOffset;
}
}
// static // static
void HTMLEditor::GenerateWhiteSpaceSequence( void HTMLEditor::GenerateWhiteSpaceSequence(
nsAString& aResult, uint32_t aLength, nsString& aResult, uint32_t aLength,
const CharPointData& aPreviousCharPointData, const CharPointData& aPreviousCharPointData,
const CharPointData& aNextCharPointData) { const CharPointData& aNextCharPointData) {
MOZ_ASSERT(aResult.IsEmpty()); MOZ_ASSERT(aResult.IsEmpty());
MOZ_ASSERT(aLength); MOZ_ASSERT(aLength);
aResult.SetLength(aLength);
HTMLEditor::ReplaceStringWithNormalizedWhiteSpaceSequence(
aResult, 0u, aLength, aPreviousCharPointData, aNextCharPointData);
}
// static
void HTMLEditor::ReplaceStringWithNormalizedWhiteSpaceSequence(
nsString& aResult, uint32_t aOffset, uint32_t aLength,
const CharPointData& aPreviousCharPointData,
const CharPointData& aNextCharPointData) {
MOZ_ASSERT(!aResult.IsEmpty());
MOZ_ASSERT(aLength);
MOZ_ASSERT(aOffset < aResult.Length());
MOZ_ASSERT(aOffset + aLength <= aResult.Length());
// For now, this method does not assume that result will be append to // For now, this method does not assume that result will be append to
// white-space sequence in the text node. // white-space sequence in the text node.
MOZ_ASSERT(aPreviousCharPointData.AcrossTextNodeBoundary() || MOZ_ASSERT(aPreviousCharPointData.AcrossTextNodeBoundary() ||
@@ -2971,38 +3045,39 @@ void HTMLEditor::GenerateWhiteSpaceSequence(
// without preformatted style. However, Chrome has same issue too. // without preformatted style. However, Chrome has same issue too.
if (aPreviousCharPointData.Type() == CharPointType::VisibleChar && if (aPreviousCharPointData.Type() == CharPointType::VisibleChar &&
aNextCharPointData.Type() == CharPointType::VisibleChar) { aNextCharPointData.Type() == CharPointType::VisibleChar) {
aResult.Assign(HTMLEditUtils::kSpace); aResult.SetCharAt(HTMLEditUtils::kSpace, aOffset);
return; return;
} }
// If it's start or end of text, put an NBSP. // If it's start or end of text, put an NBSP.
if (aPreviousCharPointData.Type() == CharPointType::TextEnd || if (aPreviousCharPointData.Type() == CharPointType::TextEnd ||
aNextCharPointData.Type() == CharPointType::TextEnd) { aNextCharPointData.Type() == CharPointType::TextEnd) {
aResult.Assign(HTMLEditUtils::kNBSP); aResult.SetCharAt(HTMLEditUtils::kNBSP, aOffset);
return; return;
} }
// If the character is next to a preformatted linefeed, we need to put // If the character is next to a preformatted linefeed, we need to put
// an NBSP for avoiding collapsed into the linefeed. // an NBSP for avoiding collapsed into the linefeed.
if (aPreviousCharPointData.Type() == CharPointType::PreformattedLineBreak || if (aPreviousCharPointData.Type() == CharPointType::PreformattedLineBreak ||
aNextCharPointData.Type() == CharPointType::PreformattedLineBreak) { aNextCharPointData.Type() == CharPointType::PreformattedLineBreak) {
aResult.Assign(HTMLEditUtils::kNBSP); aResult.SetCharAt(HTMLEditUtils::kNBSP, aOffset);
return; return;
} }
// Now, the white-space will be inserted to a white-space sequence, but not // Now, the white-space will be inserted to a white-space sequence, but not
// end of text. We can put an ASCII white-space only when both sides are // end of text. We can put an ASCII white-space only when both sides are
// not ASCII white-spaces. // not ASCII white-spaces.
aResult.Assign( aResult.SetCharAt(
aPreviousCharPointData.Type() == CharPointType::ASCIIWhiteSpace || aPreviousCharPointData.Type() == CharPointType::ASCIIWhiteSpace ||
aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace
? HTMLEditUtils::kNBSP ? HTMLEditUtils::kNBSP
: HTMLEditUtils::kSpace); : HTMLEditUtils::kSpace,
aOffset);
return; return;
} }
// Generate pairs of NBSP and ASCII white-space. // Generate pairs of NBSP and ASCII white-space.
aResult.SetLength(aLength);
bool appendNBSP = true; // Basically, starts with an NBSP. bool appendNBSP = true; // Basically, starts with an NBSP.
char16_t* lastChar = aResult.EndWriting() - 1; char16_t* const lastChar = aResult.BeginWriting() + aOffset + aLength - 1;
for (char16_t* iter = aResult.BeginWriting(); iter != lastChar; iter++) { for (char16_t* iter = aResult.BeginWriting() + aOffset; iter != lastChar;
iter++) {
*iter = appendNBSP ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace; *iter = appendNBSP ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace;
appendNBSP = !appendNBSP; appendNBSP = !appendNBSP;
} }
@@ -3023,10 +3098,142 @@ void HTMLEditor::GenerateWhiteSpaceSequence(
: HTMLEditUtils::kSpace; : HTMLEditUtils::kSpace;
} }
HTMLEditor::NormalizedStringToInsertText
HTMLEditor::NormalizeWhiteSpacesToInsertText(
const EditorDOMPoint& aPointToInsert, const nsAString& aStringToInsert,
NormalizeSurroundingWhiteSpaces aNormalizeSurroundingWhiteSpaces) const {
MOZ_ASSERT(aPointToInsert.IsSet());
// If white-spaces are preformatted, we don't need to normalize white-spaces.
if (EditorUtils::IsWhiteSpacePreformatted(
*aPointToInsert.ContainerAs<nsIContent>())) {
return NormalizedStringToInsertText(aStringToInsert, aPointToInsert);
}
Text* const textNode = aPointToInsert.GetContainerAs<Text>();
const nsTextFragment* const textFragment =
textNode ? &textNode->TextFragment() : nullptr;
const bool isNewLineCollapsible = !EditorUtils::IsNewLinePreformatted(
*aPointToInsert.ContainerAs<nsIContent>());
// 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 insertion point.
// Note that these lengths may contain invisible white-spaces.
const uint32_t precedingWhiteSpaceLength = [&]() {
if (!textNode || !aNormalizeSurroundingWhiteSpaces ||
aPointToInsert.IsStartOfContainer()) {
return 0u;
}
const auto nonWhiteSpaceOffset =
HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
*textNode, aPointToInsert.Offset(),
{HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
const uint32_t firstWhiteSpaceOffset =
nonWhiteSpaceOffset ? *nonWhiteSpaceOffset + 1u : 0u;
return aPointToInsert.Offset() - firstWhiteSpaceOffset;
}();
const uint32_t followingWhiteSpaceLength = [&]() {
if (!textNode || !aNormalizeSurroundingWhiteSpaces ||
aPointToInsert.IsEndOfContainer()) {
return 0u;
}
MOZ_ASSERT(textFragment);
const auto nonWhiteSpaceOffset =
HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
*textNode, aPointToInsert.Offset(),
{HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
MOZ_ASSERT(nonWhiteSpaceOffset.valueOr(textFragment->GetLength()) >=
aPointToInsert.Offset());
return nonWhiteSpaceOffset.valueOr(textFragment->GetLength()) -
aPointToInsert.Offset();
}();
// Now, we can know invisible white-space length in precedingWhiteSpaceLength
// and followingWhiteSpaceLength.
const uint32_t precedingInvisibleWhiteSpaceCount =
textNode
? HTMLEditUtils::GetInvisibleWhiteSpaceCount(
*textNode, aPointToInsert.Offset() - precedingWhiteSpaceLength,
precedingWhiteSpaceLength)
: 0u;
MOZ_ASSERT(precedingWhiteSpaceLength >= precedingInvisibleWhiteSpaceCount);
const uint32_t newPrecedingWhiteSpaceLength =
precedingWhiteSpaceLength - precedingInvisibleWhiteSpaceCount;
const uint32_t followingInvisibleSpaceCount =
textNode
? HTMLEditUtils::GetInvisibleWhiteSpaceCount(
*textNode, aPointToInsert.Offset(), followingWhiteSpaceLength)
: 0u;
MOZ_ASSERT(followingWhiteSpaceLength >= followingInvisibleSpaceCount);
const uint32_t newFollowingWhiteSpaceLength =
followingWhiteSpaceLength - followingInvisibleSpaceCount;
const nsAutoString stringToInsertWithSurroundingSpaces =
[&]() -> nsAutoString {
if (!newPrecedingWhiteSpaceLength && !newFollowingWhiteSpaceLength) {
return nsAutoString(aStringToInsert);
}
nsAutoString str;
str.SetCapacity(aStringToInsert.Length() + newPrecedingWhiteSpaceLength +
newFollowingWhiteSpaceLength);
for ([[maybe_unused]] auto unused :
IntegerRange(newPrecedingWhiteSpaceLength)) {
str.Append(' ');
}
str.Append(aStringToInsert);
for ([[maybe_unused]] auto unused :
IntegerRange(newFollowingWhiteSpaceLength)) {
str.Append(' ');
}
return str;
}();
const uint32_t insertionOffsetInTextNode =
aPointToInsert.IsInTextNode() ? aPointToInsert.Offset() : 0u;
NormalizedStringToInsertText result(
stringToInsertWithSurroundingSpaces, insertionOffsetInTextNode,
insertionOffsetInTextNode - precedingWhiteSpaceLength, // replace start
precedingWhiteSpaceLength + followingWhiteSpaceLength, // replace length
newPrecedingWhiteSpaceLength, newFollowingWhiteSpaceLength);
// Now, normalize the inserting string.
// Note that if the caller does not want to normalize the following
// white-spaces, we always need to guarantee that neither the first character
// nor the last character of the insertion string is not collapsible, i.e., if
// each one is a collapsible white-space, we need to replace them an NBSP to
// keep the visibility of the collapsible white-spaces. Therefore, if
// aNormalizeSurroundingWhiteSpaces is "No", we need to treat the insertion
// string is the only characters in the `Text`.
HTMLEditor::NormalizeAllWhiteSpaceSequences(
result.mNormalizedString,
CharPointData::InSameTextNode(
!textFragment || !result.mReplaceStartOffset ||
!aNormalizeSurroundingWhiteSpaces
? CharPointType::TextEnd
: (textFragment->CharAt(result.mReplaceStartOffset - 1u) ==
HTMLEditUtils::kNewLine
? CharPointType::PreformattedLineBreak
: CharPointType::VisibleChar)),
CharPointData::InSameTextNode(
!textFragment ||
result.mReplaceEndOffset >= textFragment->GetLength() ||
!aNormalizeSurroundingWhiteSpaces
? CharPointType::TextEnd
: (textFragment->CharAt(result.mReplaceEndOffset) ==
HTMLEditUtils::kNewLine
? CharPointType::PreformattedLineBreak
: CharPointType::VisibleChar)),
isNewLineCollapsible ? Linefeed::Collapsible : Linefeed::Preformatted);
return result;
}
void HTMLEditor::ExtendRangeToDeleteWithNormalizingWhiteSpaces( void HTMLEditor::ExtendRangeToDeleteWithNormalizingWhiteSpaces(
EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete, EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete,
nsAString& aNormalizedWhiteSpacesInStartNode, nsString& aNormalizedWhiteSpacesInStartNode,
nsAString& aNormalizedWhiteSpacesInEndNode) const { nsString& aNormalizedWhiteSpacesInEndNode) const {
MOZ_ASSERT(aStartToDelete.IsSetAndValid()); MOZ_ASSERT(aStartToDelete.IsSetAndValid());
MOZ_ASSERT(aEndToDelete.IsSetAndValid()); MOZ_ASSERT(aEndToDelete.IsSetAndValid());
MOZ_ASSERT(aStartToDelete.EqualsOrIsBefore(aEndToDelete)); MOZ_ASSERT(aStartToDelete.EqualsOrIsBefore(aEndToDelete));

View File

@@ -1245,6 +1245,131 @@ Maybe<EditorLineBreakType> HTMLEditUtils::GetFollowingUnnecessaryLineBreak(
return unnecessaryLineBreak; return unnecessaryLineBreak;
} }
uint32_t HTMLEditUtils::GetFirstVisibleCharOffset(const Text& aText) {
const nsTextFragment& textFragment = aText.TextFragment();
if (!textFragment.GetLength() || !EditorRawDOMPointInText(&aText, 0u)
.IsCharCollapsibleASCIISpaceOrNBSP()) {
return 0u;
}
const WSScanResult previousThingOfText =
WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::All, EditorRawDOMPoint(&aText),
BlockInlineCheck::UseComputedDisplayStyle);
if (!previousThingOfText.ReachedLineBoundary()) {
return 0u;
}
return HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(aText, 0u)
.valueOr(textFragment.GetLength());
}
uint32_t HTMLEditUtils::GetOffsetAfterLastVisibleChar(const Text& aText) {
const nsTextFragment& textFragment = aText.TextFragment();
if (!textFragment.GetLength()) {
return 0u;
}
if (!EditorRawDOMPointInText::AtLastContentOf(aText)
.IsCharCollapsibleASCIISpaceOrNBSP()) {
return textFragment.GetLength();
}
const WSScanResult nextThingOfText =
WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::All, EditorRawDOMPoint::After(aText),
BlockInlineCheck::UseComputedDisplayStyle);
if (!nextThingOfText.ReachedLineBoundary()) {
return textFragment.GetLength();
}
const Maybe<uint32_t> lastNonCollapsibleCharOffset =
HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
aText, textFragment.GetLength());
if (lastNonCollapsibleCharOffset.isNothing()) {
return 0u;
}
if (*lastNonCollapsibleCharOffset == textFragment.GetLength() - 1u) {
return textFragment.GetLength();
}
const uint32_t firstTrailingWhiteSpaceOffset =
*lastNonCollapsibleCharOffset + 1u;
MOZ_ASSERT(firstTrailingWhiteSpaceOffset < textFragment.GetLength());
if (nextThingOfText.ReachedBlockBoundary()) {
return firstTrailingWhiteSpaceOffset;
}
// If followed by <br> or preformatted line break, one white-space is
// rendered.
return firstTrailingWhiteSpaceOffset + 1u;
}
uint32_t HTMLEditUtils::GetInvisibleWhiteSpaceCount(
const Text& aText, uint32_t aOffset /* = 0u */,
uint32_t aLength /* = UINT32_MAX */) {
const nsTextFragment& textFragment = aText.TextFragment();
if (!aLength || textFragment.GetLength() <= aOffset) {
return 0u;
}
const uint32_t endOffset = static_cast<uint32_t>(
std::min(static_cast<uint64_t>(aOffset) + aLength,
static_cast<uint64_t>(textFragment.GetLength())));
const auto firstVisibleOffset = [&]() -> uint32_t {
// If the white-space sequence follows a preformatted linebreak, ASCII
// spaces at start are invisible.
if (aOffset &&
textFragment.CharAt(aOffset - 1u) == HTMLEditUtils::kNewLine) {
MOZ_ASSERT(EditorUtils::IsNewLinePreformatted(aText));
for (const uint32_t offset : IntegerRange(aOffset, endOffset)) {
if (textFragment.CharAt(offset) == HTMLEditUtils::kNBSP) {
return offset;
}
}
return endOffset; // all white-spaces are invisible.
}
if (aOffset) {
return aOffset - 1u;
}
return HTMLEditUtils::GetFirstVisibleCharOffset(aText);
}();
if (firstVisibleOffset >= endOffset) {
return endOffset - aOffset; // All white-spaces are invisible.
}
const auto afterLastVisibleOffset = [&]() -> uint32_t {
// If the white-spaces are followed by a preformatted line break, ASCII
// spaces at end are invisible.
if (endOffset < textFragment.GetLength() &&
textFragment.CharAt(endOffset) == HTMLEditUtils::kNewLine) {
MOZ_ASSERT(EditorUtils::IsNewLinePreformatted(aText));
for (const uint32_t offset : Reversed(IntegerRange(aOffset, endOffset))) {
if (textFragment.CharAt(offset) == HTMLEditUtils::kNBSP) {
return offset + 1u;
}
}
return aOffset; // all white-spaces are invisible.
}
if (endOffset < textFragment.GetLength() - 1u) {
return endOffset;
}
return HTMLEditUtils::GetOffsetAfterLastVisibleChar(aText);
}();
if (aOffset >= afterLastVisibleOffset) {
return endOffset - aOffset; // All white-spaces are invisible.
}
enum class PrevChar { NotChar, Space, NBSP };
PrevChar prevChar = PrevChar::NotChar;
uint32_t invisibleChars = 0u;
for (const uint32_t offset : IntegerRange(aOffset, endOffset)) {
if (textFragment.CharAt(offset) == HTMLEditUtils::kNBSP) {
prevChar = PrevChar::NBSP;
continue;
}
MOZ_ASSERT(
EditorRawDOMPointInText(&aText, offset).IsCharCollapsibleASCIISpace());
if (offset < firstVisibleOffset || offset >= afterLastVisibleOffset ||
// white-space after another white-space is invisible
prevChar == PrevChar::Space) {
invisibleChars++;
}
prevChar = PrevChar::Space;
}
return invisibleChars;
}
bool HTMLEditUtils::IsEmptyNode(nsPresContext* aPresContext, bool HTMLEditUtils::IsEmptyNode(nsPresContext* aPresContext,
const nsINode& aNode, const nsINode& aNode,
const EmptyCheckOptions& aOptions /* = {} */, const EmptyCheckOptions& aOptions /* = {} */,

View File

@@ -2190,12 +2190,16 @@ class HTMLEditUtils final {
* GetInclusiveNextNonCollapsibleCharOffset() returns offset of inclusive next * GetInclusiveNextNonCollapsibleCharOffset() returns offset of inclusive next
* character which is not collapsible white-space characters. * character which is not collapsible white-space characters.
*/ */
template <typename PT, typename CT>
static Maybe<uint32_t> GetInclusiveNextNonCollapsibleCharOffset( static Maybe<uint32_t> GetInclusiveNextNonCollapsibleCharOffset(
const EditorDOMPointInText& aPoint, const EditorDOMPointBase<PT, CT>& aPoint,
const WalkTextOptions& aWalkTextOptions = {}) { const WalkTextOptions& aWalkTextOptions = {}) {
static_assert(std::is_same<PT, RefPtr<Text>>::value ||
std::is_same<PT, Text*>::value);
MOZ_ASSERT(aPoint.IsSetAndValid()); MOZ_ASSERT(aPoint.IsSetAndValid());
return GetInclusiveNextNonCollapsibleCharOffset( return GetInclusiveNextNonCollapsibleCharOffset(
*aPoint.ContainerAs<Text>(), aPoint.Offset(), aWalkTextOptions); *aPoint.template ContainerAs<Text>(), aPoint.Offset(),
aWalkTextOptions);
} }
static Maybe<uint32_t> GetInclusiveNextNonCollapsibleCharOffset( static Maybe<uint32_t> GetInclusiveNextNonCollapsibleCharOffset(
const Text& aTextNode, uint32_t aOffset, const Text& aTextNode, uint32_t aOffset,
@@ -2245,9 +2249,12 @@ class HTMLEditUtils final {
* position. I.e., the character at the position must be a collapsible * position. I.e., the character at the position must be a collapsible
* white-space. * white-space.
*/ */
template <typename PT, typename CT>
static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith( static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith(
const EditorDOMPointInText& aPoint, const EditorDOMPointBase<PT, CT>& aPoint,
const WalkTextOptions& aWalkTextOptions = {}) { const WalkTextOptions& aWalkTextOptions = {}) {
static_assert(std::is_same<PT, RefPtr<Text>>::value ||
std::is_same<PT, Text*>::value);
MOZ_ASSERT(aPoint.IsSetAndValid()); MOZ_ASSERT(aPoint.IsSetAndValid());
MOZ_ASSERT(!aPoint.IsEndOfContainer()); MOZ_ASSERT(!aPoint.IsEndOfContainer());
MOZ_ASSERT_IF( MOZ_ASSERT_IF(
@@ -2257,7 +2264,8 @@ class HTMLEditUtils final {
!aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible), !aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible),
aPoint.IsCharCollapsibleASCIISpace()); aPoint.IsCharCollapsibleASCIISpace());
return GetFirstWhiteSpaceOffsetCollapsedWith( return GetFirstWhiteSpaceOffsetCollapsedWith(
*aPoint.ContainerAs<Text>(), aPoint.Offset(), aWalkTextOptions); *aPoint.template ContainerAs<Text>(), aPoint.Offset(),
aWalkTextOptions);
} }
static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith( static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith(
const Text& aTextNode, uint32_t aOffset, const Text& aTextNode, uint32_t aOffset,
@@ -2331,6 +2339,41 @@ class HTMLEditUtils final {
return EditorDOMPointType(); return EditorDOMPointType();
} }
/**
* Get the first visible char offset in aText. I.e., this returns invisible
* white-space length at start of aText. If there is no visible char in
* aText, this returns the text data length.
* Note that WSRunScanner::GetFirstVisiblePoint() may return different `Text`
* node point, but this does not scan following `Text` nodes even if aText
* is completely invisible.
*/
[[nodiscard]] static uint32_t GetFirstVisibleCharOffset(const Text& aText);
/**
* Get next offset of the last visible char in aText. I.e., this returns
* the first offset of invisible trailing white-spaces. If there is no
* invisible trailing white-spaces in aText, this returns 0.
* Note that WSRunScanner::GetAfterLastVisiblePoint() may return different
* `Text` node point, but this does not scan preceding `Text` nodes even if
* aText is completely invisible.
*/
[[nodiscard]] static uint32_t GetOffsetAfterLastVisibleChar(
const Text& aText);
/**
* Get the number of invisible white-spaces in the white-space sequence. Note
* that some invisible white-spaces may be after the first visible character.
* E.g., "SP SP NBSP SP SP NBSP". If this Text follows a block boundary, the
* first SPs are the leading invisible white-spaces, and the first NBSP is the
* first visible character. However, following 2 SPs are collapsed to one.
* Therefore, one of them is counted as an invisible white-space.
*
* Note that this assumes that all white-spaces starting from aOffset and
* ending by aOffset + aLength are collapsible white-spaces including NBSPs.
*/
[[nodiscard]] static uint32_t GetInvisibleWhiteSpaceCount(
const Text& aText, uint32_t aOffset = 0u, uint32_t aLength = UINT32_MAX);
/** /**
* GetGoodCaretPointFor() returns a good point to collapse `Selection` * GetGoodCaretPointFor() returns a good point to collapse `Selection`
* after handling edit action with aDirectionAndAmount. * after handling edit action with aDirectionAndAmount.

View File

@@ -6,6 +6,7 @@
#include "HTMLEditor.h" #include "HTMLEditor.h"
#include "HTMLEditHelpers.h" #include "HTMLEditHelpers.h"
#include "HTMLEditorInlines.h" #include "HTMLEditorInlines.h"
#include "HTMLEditorNestedClasses.h"
#include "AutoClonedRangeArray.h" #include "AutoClonedRangeArray.h"
#include "AutoSelectionRestorer.h" #include "AutoSelectionRestorer.h"
@@ -4311,6 +4312,57 @@ Result<InsertTextResult, nsresult> HTMLEditor::ReplaceTextWithTransaction(
transaction->SuggestPointToPutCaret<EditorDOMPoint>()); transaction->SuggestPointToPutCaret<EditorDOMPoint>());
} }
Result<InsertTextResult, nsresult>
HTMLEditor::InsertOrReplaceTextWithTransaction(
const EditorDOMPoint& aPointToInsert,
const NormalizedStringToInsertText& aData) {
MOZ_ASSERT(aPointToInsert.IsInContentNodeAndValid());
MOZ_ASSERT_IF(aData.ReplaceLength(), aPointToInsert.IsInTextNode());
Result<InsertTextResult, nsresult> insertTextResultOrError =
!aData.ReplaceLength()
? InsertTextWithTransaction(aData.mNormalizedString, aPointToInsert,
InsertTextTo::SpecifiedPoint)
: ReplaceTextWithTransaction(
MOZ_KnownLive(*aPointToInsert.ContainerAs<Text>()),
aData.mReplaceStartOffset, aData.ReplaceLength(),
aData.mNormalizedString);
if (MOZ_UNLIKELY(insertTextResultOrError.isErr())) {
NS_WARNING(!aData.ReplaceLength()
? "HTMLEditor::InsertTextWithTransaction() failed"
: "HTMLEditor::ReplaceTextWithTransaction() failed");
return insertTextResultOrError;
}
InsertTextResult insertTextResult = insertTextResultOrError.unwrap();
if (!aData.ReplaceLength()) {
auto pointToPutCaret = [&]() -> EditorDOMPoint {
return insertTextResult.HasCaretPointSuggestion()
? insertTextResult.UnwrapCaretPoint()
: insertTextResult.EndOfInsertedTextRef();
}();
return InsertTextResult(std::move(insertTextResult),
std::move(pointToPutCaret));
}
insertTextResult.IgnoreCaretPointSuggestion();
Text* const insertedTextNode =
insertTextResult.EndOfInsertedTextRef().GetContainerAs<Text>();
if (NS_WARN_IF(!insertedTextNode) ||
NS_WARN_IF(!insertedTextNode->IsInComposedDoc()) ||
NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(*insertedTextNode))) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
const uint32_t expectedEndOffset = aData.EndOffsetOfInsertedText();
if (NS_WARN_IF(expectedEndOffset > insertedTextNode->TextDataLength())) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
// We need to return end point of the insertion string instead of end of
// replaced following white-spaces.
EditorDOMPoint endOfNewString(insertedTextNode, expectedEndOffset);
EditorDOMPoint pointToPutCaret = endOfNewString;
return InsertTextResult(std::move(endOfNewString),
CaretPoint(std::move(pointToPutCaret)));
}
Result<InsertTextResult, nsresult> HTMLEditor::InsertTextWithTransaction( Result<InsertTextResult, nsresult> HTMLEditor::InsertTextWithTransaction(
const nsAString& aStringToInsert, const EditorDOMPoint& aPointToInsert, const nsAString& aStringToInsert, const EditorDOMPoint& aPointToInsert,
InsertTextTo aInsertTextTo) { InsertTextTo aInsertTextTo) {

View File

@@ -844,6 +844,18 @@ class HTMLEditor final : public EditorBase,
uint32_t aLength, uint32_t aLength,
const nsAString& aStringToInsert); const nsAString& aStringToInsert);
struct NormalizedStringToInsertText;
/**
* Insert text to aPointToInsert or replace text in the range stored by aData
* in the text node specified by aPointToInsert with the normalized string
* stored by aData. So, aPointToInsert must be in a `Text` node if
* aData.ReplaceLength() is not 0.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<InsertTextResult, nsresult>
InsertOrReplaceTextWithTransaction(const EditorDOMPoint& aPointToInsert,
const NormalizedStringToInsertText& aData);
/** /**
* Insert aStringToInsert to aPointToInsert. If the point is not editable, * Insert aStringToInsert to aPointToInsert. If the point is not editable,
* this returns error. * this returns error.
@@ -2204,6 +2216,21 @@ class HTMLEditor final : public EditorBase,
TreatEmptyTextNodes aTreatEmptyTextNodes, TreatEmptyTextNodes aTreatEmptyTextNodes,
DeleteDirection aDeleteDirection, const Element& aEditingHost); DeleteDirection aDeleteDirection, const Element& aEditingHost);
enum class NormalizeSurroundingWhiteSpaces : bool { No, Yes };
friend constexpr bool operator!(NormalizeSurroundingWhiteSpaces aValue) {
return !static_cast<bool>(aValue);
}
/**
* Return a normalized string. If aPointToInsert is in a `Text` node,
* this returns a range in the `Text` to replace surrounding white-spaces at
* aPointToInsert with the normalized string if white-spaces are collapsible
* and aNormalizeSurroundingWhiteSpaces is "Yes".
*/
NormalizedStringToInsertText NormalizeWhiteSpacesToInsertText(
const EditorDOMPoint& aPointToInsert, const nsAString& aStringToInsert,
NormalizeSurroundingWhiteSpaces aNormalizeSurroundingWhiteSpaces) const;
/** /**
* ExtendRangeToDeleteWithNormalizingWhiteSpaces() is a helper method of * ExtendRangeToDeleteWithNormalizingWhiteSpaces() is a helper method of
* DeleteTextAndNormalizeSurroundingWhiteSpaces(). This expands * DeleteTextAndNormalizeSurroundingWhiteSpaces(). This expands
@@ -2227,8 +2254,8 @@ class HTMLEditor final : public EditorBase,
*/ */
void ExtendRangeToDeleteWithNormalizingWhiteSpaces( void ExtendRangeToDeleteWithNormalizingWhiteSpaces(
EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete, EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete,
nsAString& aNormalizedWhiteSpacesInStartNode, nsString& aNormalizedWhiteSpacesInStartNode,
nsAString& aNormalizedWhiteSpacesInEndNode) const; nsString& aNormalizedWhiteSpacesInEndNode) const;
/** /**
* CharPointType let the following helper methods of * CharPointType let the following helper methods of
@@ -2265,20 +2292,16 @@ class HTMLEditor final : public EditorBase,
*/ */
class MOZ_STACK_CLASS CharPointData final { class MOZ_STACK_CLASS CharPointData final {
public: public:
CharPointData() = delete;
static CharPointData InDifferentTextNode(CharPointType aCharPointType) { static CharPointData InDifferentTextNode(CharPointType aCharPointType) {
CharPointData result; return {aCharPointType, true};
result.mIsInDifferentTextNode = true;
result.mType = aCharPointType;
return result;
} }
static CharPointData InSameTextNode(CharPointType aCharPointType) { static CharPointData InSameTextNode(CharPointType aCharPointType) {
CharPointData result;
// Let's mark this as in different text node if given one indicates // Let's mark this as in different text node if given one indicates
// that there is end of text because it means that adjacent content // that there is end of text because it means that adjacent content
// from point of text node view is another element. // from point of text node view is another element.
result.mIsInDifferentTextNode = aCharPointType == CharPointType::TextEnd; return {aCharPointType, aCharPointType == CharPointType::TextEnd};
result.mType = aCharPointType;
return result;
} }
bool AcrossTextNodeBoundary() const { return mIsInDifferentTextNode; } bool AcrossTextNodeBoundary() const { return mIsInDifferentTextNode; }
@@ -2289,7 +2312,8 @@ class HTMLEditor final : public EditorBase,
CharPointType Type() const { return mType; } CharPointType Type() const { return mType; }
private: private:
CharPointData() = default; CharPointData(CharPointType aType, bool aIsInDifferentTextNode)
: mType(aType), mIsInDifferentTextNode(aIsInDifferentTextNode) {}
CharPointType mType; CharPointType mType;
bool mIsInDifferentTextNode; bool mIsInDifferentTextNode;
@@ -2306,12 +2330,24 @@ class HTMLEditor final : public EditorBase,
CharPointData GetInclusiveNextCharPointDataForNormalizingWhiteSpaces( CharPointData GetInclusiveNextCharPointDataForNormalizingWhiteSpaces(
const EditorDOMPointInText& aPoint) const; const EditorDOMPointInText& aPoint) const;
enum class Linefeed : bool { Collapsible, Preformatted };
/**
* Normalize all white-spaces in aResult. aPreviousCharPointData is used only
* when the first character of aResult is an ASCII space or an NBSP.
* aNextCharPointData is used only when the last character of aResult is an
* ASCII space or an NBSP.
*/
static void NormalizeAllWhiteSpaceSequences(
nsString& aResult, const CharPointData& aPreviousCharPointData,
const CharPointData& aNextCharPointData, Linefeed aLinefeed);
/** /**
* GenerateWhiteSpaceSequence() generates white-space sequence which won't * GenerateWhiteSpaceSequence() generates white-space sequence which won't
* be collapsed. * be collapsed.
* *
* @param aResult [out] White space sequence which won't be * @param aResult [out] White space sequence which won't be
* collapsed, but wrapable. * collapsed, but wrappable.
* @param aLength Length of generating white-space sequence. * @param aLength Length of generating white-space sequence.
* Must be 1 or larger. * Must be 1 or larger.
* @param aPreviousCharPointData * @param aPreviousCharPointData
@@ -2323,11 +2359,20 @@ class HTMLEditor final : public EditorBase,
* different text nodes white-space. * different text nodes white-space.
* @param aNextCharPointData Specify the next char point where it'll be * @param aNextCharPointData Specify the next char point where it'll be
* inserted. Same as aPreviousCharPointData, * inserted. Same as aPreviousCharPointData,
* this must node indidate white-space in same * this must node indicate white-space in same
* text node. * text node.
*/ */
static void GenerateWhiteSpaceSequence( static void GenerateWhiteSpaceSequence(
nsAString& aResult, uint32_t aLength, nsString& aResult, uint32_t aLength,
const CharPointData& aPreviousCharPointData,
const CharPointData& aNextCharPointData);
/**
* Replace characters starting from aOffset in aResult with normalized
* white-space sequence.
*/
static void ReplaceStringWithNormalizedWhiteSpaceSequence(
nsString& aResult, uint32_t aOffset, uint32_t aLength,
const CharPointData& aPreviousCharPointData, const CharPointData& aPreviousCharPointData,
const CharPointData& aNextCharPointData); const CharPointData& aNextCharPointData);

View File

@@ -15,6 +15,8 @@
#include "mozilla/Attributes.h" #include "mozilla/Attributes.h"
#include "mozilla/OwningNonNull.h" #include "mozilla/OwningNonNull.h"
#include "mozilla/Result.h" #include "mozilla/Result.h"
#include "mozilla/dom/Text.h"
#include "nsTextFragment.h"
namespace mozilla { namespace mozilla {
@@ -522,6 +524,168 @@ class MOZ_STACK_CLASS HTMLEditor::AutoListElementCreator final {
const nsAutoString mBulletType; const nsAutoString mBulletType;
}; };
/******************************************************************************
* NormalizedStringToInsertText stores normalized insertion string with
* normalized surrounding white-spaces if the insertion point is surrounded by
* collapsible white-spaces. For deleting invisible (collapsed) white-spaces,
* this also stores the replace range and new white-space length before and
* after the inserting text.
******************************************************************************/
struct MOZ_STACK_CLASS HTMLEditor::NormalizedStringToInsertText final {
NormalizedStringToInsertText(
const nsAString& aStringToInsertWithoutSurroundingWhiteSpaces,
const EditorDOMPoint& aPointToInsert)
: mNormalizedString(aStringToInsertWithoutSurroundingWhiteSpaces),
mReplaceStartOffset(
aPointToInsert.IsInTextNode() ? aPointToInsert.Offset() : 0u),
mReplaceEndOffset(mReplaceStartOffset) {
MOZ_ASSERT(aStringToInsertWithoutSurroundingWhiteSpaces.Length() ==
InsertingTextLength());
}
NormalizedStringToInsertText(
const nsAString& aStringToInsertWithSurroundingWhiteSpaces,
uint32_t aInsertOffset, uint32_t aReplaceStartOffset,
uint32_t aReplaceLength,
uint32_t aNewPrecedingWhiteSpaceLengthBeforeInsertionString,
uint32_t aNewFollowingWhiteSpaceLengthAfterInsertionString)
: mNormalizedString(aStringToInsertWithSurroundingWhiteSpaces),
mReplaceStartOffset(aReplaceStartOffset),
mReplaceEndOffset(mReplaceStartOffset + aReplaceLength),
mReplaceLengthBefore(aInsertOffset - mReplaceStartOffset),
mReplaceLengthAfter(aReplaceLength - mReplaceLengthBefore),
mNewLengthBefore(aNewPrecedingWhiteSpaceLengthBeforeInsertionString),
mNewLengthAfter(aNewFollowingWhiteSpaceLengthAfterInsertionString) {
MOZ_ASSERT(aReplaceStartOffset <= aInsertOffset);
MOZ_ASSERT(aReplaceStartOffset + aReplaceLength >= aInsertOffset);
MOZ_ASSERT(aNewPrecedingWhiteSpaceLengthBeforeInsertionString +
aNewFollowingWhiteSpaceLengthAfterInsertionString <
mNormalizedString.Length());
MOZ_ASSERT(mReplaceLengthBefore + mReplaceLengthAfter == ReplaceLength());
MOZ_ASSERT(mReplaceLengthBefore >= mNewLengthBefore);
MOZ_ASSERT(mReplaceLengthAfter >= mNewLengthAfter);
}
NormalizedStringToInsertText GetMinimizedData(const Text& aText) const {
if (mNormalizedString.IsEmpty() || !ReplaceLength()) {
return *this;
}
const nsTextFragment& textFragment = aText.TextFragment();
const uint32_t minimizedReplaceStart = [&]() {
const auto firstDiffCharOffset =
mNewLengthBefore ? textFragment.FindFirstDifferentCharOffset(
PrecedingWhiteSpaces(), mReplaceStartOffset)
: nsTextFragment::kNotFound;
if (firstDiffCharOffset == nsTextFragment::kNotFound) {
return
// We don't need to insert new normalized white-spaces before the
// inserting string,
(mReplaceStartOffset + mReplaceLengthBefore)
// but keep extending the replacing range for deleting invisible
// white-spaces.
- DeletingPrecedingInvisibleWhiteSpaces();
}
return firstDiffCharOffset;
}();
const uint32_t minimizedReplaceEnd = [&]() {
const auto lastDiffCharOffset =
mNewLengthAfter ? textFragment.RFindFirstDifferentCharOffset(
FollowingWhiteSpaces(), mReplaceEndOffset)
: nsTextFragment::kNotFound;
if (lastDiffCharOffset == nsTextFragment::kNotFound) {
return
// We don't need to insert new normalized white-spaces after the
// inserting string,
(mReplaceEndOffset - mReplaceLengthAfter)
// but keep extending the replacing range for deleting invisible
// white-spaces.
+ DeletingFollowingInvisibleWhiteSpaces();
}
return lastDiffCharOffset + 1u;
}();
if (minimizedReplaceStart == mReplaceStartOffset &&
minimizedReplaceEnd == mReplaceEndOffset) {
return *this;
}
const uint32_t newPrecedingWhiteSpaceLength =
mNewLengthBefore - (minimizedReplaceStart - mReplaceStartOffset);
const uint32_t newFollowingWhiteSpaceLength =
mNewLengthAfter - (mReplaceEndOffset - minimizedReplaceEnd);
return NormalizedStringToInsertText(
Substring(mNormalizedString,
mNewLengthBefore - newPrecedingWhiteSpaceLength,
mNormalizedString.Length() -
(mNewLengthBefore - newPrecedingWhiteSpaceLength) -
(mNewLengthAfter - newFollowingWhiteSpaceLength)),
OffsetToInsertText(), minimizedReplaceStart,
minimizedReplaceEnd - minimizedReplaceStart,
newPrecedingWhiteSpaceLength, newFollowingWhiteSpaceLength);
}
/**
* Return offset to insert the given text.
*/
[[nodiscard]] uint32_t OffsetToInsertText() const {
return mReplaceStartOffset + mReplaceLengthBefore;
}
/**
* Return inserting text length not containing the surrounding white-spaces.
*/
[[nodiscard]] uint32_t InsertingTextLength() const {
return mNormalizedString.Length() - mNewLengthBefore - mNewLengthAfter;
}
/**
* Return end offset of inserted string after replacing the text with
* mNormalizedString.
*/
[[nodiscard]] uint32_t EndOffsetOfInsertedText() const {
return OffsetToInsertText() + InsertingTextLength();
}
/**
* Return the length to replace with mNormalizedString. The result means that
* it's the length of surrounding white-spaces at the insertion point.
*/
[[nodiscard]] uint32_t ReplaceLength() const {
return mReplaceEndOffset - mReplaceStartOffset;
}
[[nodiscard]] uint32_t DeletingPrecedingInvisibleWhiteSpaces() const {
return mReplaceLengthBefore - mNewLengthBefore;
}
[[nodiscard]] uint32_t DeletingFollowingInvisibleWhiteSpaces() const {
return mReplaceLengthAfter - mNewLengthAfter;
}
[[nodiscard]] nsDependentSubstring PrecedingWhiteSpaces() const {
return Substring(mNormalizedString, 0u, mNewLengthBefore);
}
[[nodiscard]] nsDependentSubstring FollowingWhiteSpaces() const {
return Substring(mNormalizedString,
mNormalizedString.Length() - mNewLengthAfter);
}
// Normalizes string which should be inserted.
nsAutoString mNormalizedString;
// Start offset in the `Text` to replace.
const uint32_t mReplaceStartOffset;
// End offset in the `Text` to replace.
const uint32_t mReplaceEndOffset;
// If it needs to replace preceding and/or following white-spaces, these
// members store the length of white-spaces which should be replaced
// before/after the insertion point.
const uint32_t mReplaceLengthBefore = 0u;
const uint32_t mReplaceLengthAfter = 0u;
// If it needs to replace preceding and/or following white-spaces, these
// members store the new length of white-spaces before/after the insertion
// string.
const uint32_t mNewLengthBefore = 0u;
const uint32_t mNewLengthAfter = 0u;
};
} // namespace mozilla } // namespace mozilla
#endif // #ifndef HTMLEditorNestedClasses_h #endif // #ifndef HTMLEditorNestedClasses_h

View File

@@ -1033,28 +1033,221 @@ WhiteSpaceVisibilityKeeper::InsertLineBreak(
return insertBRElementResultOrError; return insertBRElementResultOrError;
} }
Result<EditorDOMPoint, nsresult>
WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces(
HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint) {
if (EditorUtils::IsWhiteSpacePreformatted(
*aPoint.ContainerAs<nsIContent>())) {
return EditorDOMPoint();
}
if (aPoint.IsInTextNode() &&
// If there is a previous char and it's not a collapsible ASCII
// white-space, the point is not in the leading white-spaces.
(!aPoint.IsStartOfContainer() && !aPoint.IsPreviousCharASCIISpace()) &&
// If it does not points a collapsible ASCII white-space, the point is not
// in the trailing white-spaces.
(!aPoint.IsEndOfContainer() && !aPoint.IsCharCollapsibleASCIISpace())) {
return EditorDOMPoint();
}
const Element* const closestBlockElement =
HTMLEditUtils::GetInclusiveAncestorElement(
*aPoint.ContainerAs<nsIContent>(), HTMLEditUtils::ClosestBlockElement,
BlockInlineCheck::UseComputedDisplayStyle);
if (MOZ_UNLIKELY(!closestBlockElement)) {
return EditorDOMPoint(); // aPoint is not in a block.
}
const TextFragmentData textFragmentDataForLeadingWhiteSpaces(
Scan::EditableNodes,
aPoint.IsStartOfContainer() &&
aPoint.GetContainer() == closestBlockElement
? aPoint
: aPoint.PreviousPointOrParentPoint<EditorDOMPoint>(),
BlockInlineCheck::UseComputedDisplayStyle, closestBlockElement);
if (NS_WARN_IF(!textFragmentDataForLeadingWhiteSpaces.IsInitialized())) {
return Err(NS_ERROR_FAILURE);
}
{
const EditorDOMRange& leadingWhiteSpaceRange =
textFragmentDataForLeadingWhiteSpaces
.InvisibleLeadingWhiteSpaceRangeRef();
if (leadingWhiteSpaceRange.IsPositioned() &&
!leadingWhiteSpaceRange.Collapsed()) {
EditorDOMPoint endOfLeadingWhiteSpaces(leadingWhiteSpaceRange.EndRef());
AutoTrackDOMPoint trackEndOfLeadingWhiteSpaces(
aHTMLEditor.RangeUpdaterRef(), &endOfLeadingWhiteSpaces);
Result<CaretPoint, nsresult> caretPointOrError =
aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
leadingWhiteSpaceRange.StartRef(),
leadingWhiteSpaceRange.EndRef(),
HTMLEditor::TreatEmptyTextNodes::
KeepIfContainerOfRangeBoundaries);
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
NS_WARNING(
"HTMLEditor::DeleteTextAndTextNodesWithTransaction("
"TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries) failed");
return caretPointOrError.propagateErr();
}
caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
// If the leading white-spaces were split into multiple text node, we need
// only the last `Text` node.
if (!leadingWhiteSpaceRange.InSameContainer() &&
leadingWhiteSpaceRange.StartRef().IsInTextNode() &&
leadingWhiteSpaceRange.StartRef()
.ContainerAs<Text>()
->IsInComposedDoc() &&
leadingWhiteSpaceRange.EndRef().IsInTextNode() &&
leadingWhiteSpaceRange.EndRef()
.ContainerAs<Text>()
->IsInComposedDoc() &&
!leadingWhiteSpaceRange.StartRef()
.ContainerAs<Text>()
->TextDataLength()) {
nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(
*leadingWhiteSpaceRange.StartRef().ContainerAs<Text>()));
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
return Err(rv);
}
}
trackEndOfLeadingWhiteSpaces.FlushAndStopTracking();
if (NS_WARN_IF(!endOfLeadingWhiteSpaces.IsSetAndValidInComposedDoc())) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
return endOfLeadingWhiteSpaces;
}
}
const TextFragmentData textFragmentData =
textFragmentDataForLeadingWhiteSpaces.ScanStartRef() == aPoint
? textFragmentDataForLeadingWhiteSpaces
: TextFragmentData(Scan::EditableNodes, aPoint,
BlockInlineCheck::UseComputedDisplayStyle,
closestBlockElement);
const EditorDOMRange& trailingWhiteSpaceRange =
textFragmentData.InvisibleTrailingWhiteSpaceRangeRef();
if (trailingWhiteSpaceRange.IsPositioned() &&
!trailingWhiteSpaceRange.Collapsed()) {
EditorDOMPoint startOfTrailingWhiteSpaces(
trailingWhiteSpaceRange.StartRef());
AutoTrackDOMPoint trackStartOfTrailingWhiteSpaces(
aHTMLEditor.RangeUpdaterRef(), &startOfTrailingWhiteSpaces);
Result<CaretPoint, nsresult> caretPointOrError =
aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
trailingWhiteSpaceRange.StartRef(),
trailingWhiteSpaceRange.EndRef(),
HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
NS_WARNING(
"HTMLEditor::DeleteTextAndTextNodesWithTransaction("
"TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries) failed");
return caretPointOrError.propagateErr();
}
caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
// If the leading white-spaces were split into multiple text node, we need
// only the last `Text` node.
if (!trailingWhiteSpaceRange.InSameContainer() &&
trailingWhiteSpaceRange.StartRef().IsInTextNode() &&
trailingWhiteSpaceRange.StartRef()
.ContainerAs<Text>()
->IsInComposedDoc() &&
trailingWhiteSpaceRange.EndRef().IsInTextNode() &&
trailingWhiteSpaceRange.EndRef()
.ContainerAs<Text>()
->IsInComposedDoc() &&
!trailingWhiteSpaceRange.EndRef()
.ContainerAs<Text>()
->TextDataLength()) {
nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
MOZ_KnownLive(*trailingWhiteSpaceRange.EndRef().ContainerAs<Text>()));
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
return Err(rv);
}
}
trackStartOfTrailingWhiteSpaces.FlushAndStopTracking();
if (NS_WARN_IF(!startOfTrailingWhiteSpaces.IsSetAndValidInComposedDoc())) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
return startOfTrailingWhiteSpaces;
}
const auto atCollapsibleASCIISpace = [&]() -> EditorDOMPointInText {
const auto point =
textFragmentData.GetInclusiveNextCharPoint<EditorDOMPointInText>(
textFragmentData.ScanStartRef(), IgnoreNonEditableNodes::Yes);
if (point.IsSet() &&
// XXX Perhaps, we should ignore empty `Text` nodes and keep scanning.
!point.IsEndOfContainer() && point.IsCharCollapsibleASCIISpace()) {
return point;
}
const auto prevPoint =
textFragmentData.GetPreviousCharPoint<EditorDOMPointInText>(
textFragmentData.ScanStartRef(), IgnoreNonEditableNodes::Yes);
return prevPoint.IsSet() &&
// XXX Perhaps, we should ignore empty `Text` nodes and keep
// scanning.
!prevPoint.IsEndOfContainer() &&
prevPoint.IsCharCollapsibleASCIISpace()
? prevPoint
: EditorDOMPointInText();
}();
if (!atCollapsibleASCIISpace.IsSet()) {
return EditorDOMPoint();
}
const auto firstCollapsibleASCIISpacePoint =
textFragmentData
.GetFirstASCIIWhiteSpacePointCollapsedTo<EditorDOMPointInText>(
atCollapsibleASCIISpace, nsIEditor::eNone,
IgnoreNonEditableNodes::No);
const auto endOfCollapsibleASCIISpacePoint =
textFragmentData
.GetEndOfCollapsibleASCIIWhiteSpaces<EditorDOMPointInText>(
atCollapsibleASCIISpace, nsIEditor::eNone,
IgnoreNonEditableNodes::No);
if (firstCollapsibleASCIISpacePoint.NextPoint() ==
endOfCollapsibleASCIISpacePoint) {
// Only one white-space, so that nothing to do.
return EditorDOMPoint();
}
// Okay, there are some collapsed white-spaces. We should delete them with
// keeping first one.
Result<CaretPoint, nsresult> deleteTextResultOrError =
aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
firstCollapsibleASCIISpacePoint.NextPoint(),
endOfCollapsibleASCIISpacePoint,
HTMLEditor::TreatEmptyTextNodes::Remove);
if (MOZ_UNLIKELY(deleteTextResultOrError.isErr())) {
NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
return deleteTextResultOrError.propagateErr();
}
return deleteTextResultOrError.unwrap().UnwrapCaretPoint();
}
// static // static
Result<InsertTextResult, nsresult> Result<InsertTextResult, nsresult>
WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString( WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString(
HTMLEditor& aHTMLEditor, const nsAString& aStringToInsert, HTMLEditor& aHTMLEditor, const nsAString& aStringToInsert,
const EditorDOMRange& aRangeToBeReplaced, InsertTextTo aInsertTextTo, const EditorDOMRange& aRangeToBeReplaced, InsertTextTo aInsertTextTo,
TextIsCompositionString aTextIsCompositionString) { InsertTextFor aPurpose) {
MOZ_ASSERT(aRangeToBeReplaced.StartRef().IsInContentNode()); MOZ_ASSERT(aRangeToBeReplaced.StartRef().IsInContentNode());
MOZ_ASSERT_IF(aTextIsCompositionString == TextIsCompositionString::No, MOZ_ASSERT_IF(!EditorBase::InsertingTextForExtantComposition(aPurpose),
aRangeToBeReplaced.Collapsed()); aRangeToBeReplaced.Collapsed());
if (aStringToInsert.IsEmpty()) {
MOZ_ASSERT(aRangeToBeReplaced.Collapsed());
return InsertTextResult();
}
// TODO: Delete this block once we ship the new normalizer.
if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) {
// MOOSE: for now, we always assume non-PRE formatting. Fix this later. // MOOSE: for now, we always assume non-PRE formatting. Fix this later.
// meanwhile, the pre case is handled in HandleInsertText() in // meanwhile, the pre case is handled in HandleInsertText() in
// HTMLEditSubActionHandler.cpp // HTMLEditSubActionHandler.cpp
// MOOSE: for now, just getting the ws logic straight. This implementation // MOOSE: for now, just getting the ws logic straight. This implementation
// is very slow. Will need to replace edit rules impl with a more efficient // is very slow. Will need to replace edit rules impl with a more efficient
// text sink here that does the minimal amount of searching/replacing/copying // text sink here that does the minimal amount of
// searching/replacing/copying
if (aStringToInsert.IsEmpty()) {
MOZ_ASSERT(aRangeToBeReplaced.Collapsed());
return InsertTextResult();
}
const TextFragmentData textFragmentDataAtStart( const TextFragmentData textFragmentDataAtStart(
Scan::EditableNodes, aRangeToBeReplaced.StartRef(), Scan::EditableNodes, aRangeToBeReplaced.StartRef(),
@@ -1084,7 +1277,8 @@ WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString(
const bool isInvisibleLeadingWhiteSpaceRangeAtStartPositioned = const bool isInvisibleLeadingWhiteSpaceRangeAtStartPositioned =
invisibleLeadingWhiteSpaceRangeAtStart.IsPositioned(); invisibleLeadingWhiteSpaceRangeAtStart.IsPositioned();
EditorDOMRange invisibleTrailingWhiteSpaceRangeAtEnd = EditorDOMRange invisibleTrailingWhiteSpaceRangeAtEnd =
textFragmentDataAtEnd.GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt( textFragmentDataAtEnd
.GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt(
aRangeToBeReplaced.EndRef()); aRangeToBeReplaced.EndRef());
const bool isInvisibleTrailingWhiteSpaceRangeAtEndPositioned = const bool isInvisibleTrailingWhiteSpaceRangeAtEndPositioned =
invisibleTrailingWhiteSpaceRangeAtEnd.IsPositioned(); invisibleTrailingWhiteSpaceRangeAtEnd.IsPositioned();
@@ -1157,8 +1351,8 @@ WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString(
} }
// Replace an NBSP at inclusive next character of replacing range to an // Replace an NBSP at inclusive next character of replacing range to an
// ASCII white-space if inserting into a visible white-space sequence. // ASCII white-space if inserting into a visible white-space sequence.
// XXX With modifying the inserting string later, this creates a line break // XXX With modifying the inserting string later, this creates a line
// opportunity after the inserting string, but this causes // break opportunity after the inserting string, but this causes
// inconsistent result with inserting order. E.g., type white-space // inconsistent result with inserting order. E.g., type white-space
// n times with various order. // n times with various order.
else if (pointPositionWithVisibleWhiteSpacesAtEnd == else if (pointPositionWithVisibleWhiteSpacesAtEnd ==
@@ -1231,7 +1425,8 @@ WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString(
} }
// Replace an NBSP at previous character of insertion point to an ASCII // Replace an NBSP at previous character of insertion point to an ASCII
// white-space if inserting into a visible white-space sequence. // white-space if inserting into a visible white-space sequence.
// XXX With modifying the inserting string later, this creates a line break // XXX With modifying the inserting string later, this creates a line
// break
// opportunity before the inserting string, but this causes // opportunity before the inserting string, but this causes
// inconsistent result with inserting order. E.g., type white-space // inconsistent result with inserting order. E.g., type white-space
// n times with various order. // n times with various order.
@@ -1253,7 +1448,8 @@ WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString(
*atNBSPReplacedWithASCIIWhiteSpace.ContainerAs<Text>()), *atNBSPReplacedWithASCIIWhiteSpace.ContainerAs<Text>()),
atNBSPReplacedWithASCIIWhiteSpace.Offset(), 1, u" "_ns); atNBSPReplacedWithASCIIWhiteSpace.Offset(), 1, u" "_ns);
if (MOZ_UNLIKELY(replaceTextResult.isErr())) { if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed failed"); NS_WARNING(
"HTMLEditor::ReplaceTextWithTransaction() failed failed");
return replaceTextResult.propagateErr(); return replaceTextResult.propagateErr();
} }
// Ignore caret suggestion because there was // Ignore caret suggestion because there was
@@ -1269,8 +1465,9 @@ WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString(
// If white-space and/or linefeed characters are collapsible, and inserting // If white-space and/or linefeed characters are collapsible, and inserting
// string starts and/or ends with a collapsible characters, we need to // string starts and/or ends with a collapsible characters, we need to
// replace them with NBSP for making sure the collapsible characters visible. // replace them with NBSP for making sure the collapsible characters
// FYI: There is no case only linefeeds are collapsible. So, we need to // visible. FYI: There is no case only linefeeds are collapsible. So, we
// need to
// do the things only when white-spaces are collapsible. // do the things only when white-spaces are collapsible.
MOZ_DIAGNOSTIC_ASSERT(!theString.IsEmpty()); MOZ_DIAGNOSTIC_ASSERT(!theString.IsEmpty());
if (NS_WARN_IF(!pointToInsert.IsInContentNode()) || if (NS_WARN_IF(!pointToInsert.IsInContentNode()) ||
@@ -1306,8 +1503,8 @@ WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString(
} }
} }
// If the insertion point is (was) before the start of text and it's // If the insertion point is (was) before the start of text and it's
// immediately after a hard line break, the first ASCII white-space should // immediately after a hard line break, the first ASCII white-space
// be replaced with an NBSP for making it visible. // should be replaced with an NBSP for making it visible.
else if ((textFragmentDataAtStart.StartsFromHardLineBreak() || else if ((textFragmentDataAtStart.StartsFromHardLineBreak() ||
textFragmentDataAtStart textFragmentDataAtStart
.StartsFromInlineEditingHostBoundary()) && .StartsFromInlineEditingHostBoundary()) &&
@@ -1373,9 +1570,9 @@ WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString(
} }
// If current character is a collapsbile white-space and the previous // If current character is a collapsbile white-space and the previous
// character is a preformatted linefeed, we need to replace the current // character is a preformatted linefeed, we need to replace the
// character with an NBSP for avoiding collapsed with the previous // current character with an NBSP for avoiding collapsed with the
// linefeed. // previous linefeed.
if (previousChar == PreviousChar::PreformattedNewLine) { if (previousChar == PreviousChar::PreformattedNewLine) {
MOZ_ASSERT(i > 0); MOZ_ASSERT(i > 0);
theString.SetCharAt(HTMLEditUtils::kNBSP, i); theString.SetCharAt(HTMLEditUtils::kNBSP, i);
@@ -1407,9 +1604,9 @@ WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString(
// XXX If the point is not editable, InsertTextWithTransaction() returns // XXX If the point is not editable, InsertTextWithTransaction() returns
// error, but we keep handling it. But I think that it wastes the // error, but we keep handling it. But I think that it wastes the
// runtime cost. So, perhaps, we should return error code which couldn't // runtime cost. So, perhaps, we should return error code which
// modify it and make each caller of this method decide whether it should // couldn't modify it and make each caller of this method decide whether
// keep or stop handling the edit action. // it should keep or stop handling the edit action.
AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(), AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
&pointToPutCaret); &pointToPutCaret);
Result<InsertTextResult, nsresult> insertTextResult = Result<InsertTextResult, nsresult> insertTextResult =
@@ -1427,6 +1624,85 @@ WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString(
std::move(pointToPutCaret)); std::move(pointToPutCaret));
} }
if (NS_WARN_IF(!aRangeToBeReplaced.StartRef().IsInContentNode())) {
return Err(NS_ERROR_FAILURE); // Cannot insert text
}
EditorDOMPoint pointToInsert = aHTMLEditor.ComputePointToInsertText(
aRangeToBeReplaced.StartRef(), aInsertTextTo);
MOZ_ASSERT(pointToInsert.IsInContentNode());
const bool isWhiteSpaceCollapsible = !EditorUtils::IsWhiteSpacePreformatted(
*aRangeToBeReplaced.StartRef().ContainerAs<nsIContent>());
// First, delete invisible leading white-spaces and trailing white-spaces if
// they are there around the replacing range boundaries. However, don't do
// that if we're updating existing composition string to avoid the composition
// transaction is broken by the text change around composition string.
if (!EditorBase::InsertingTextForExtantComposition(aPurpose) &&
isWhiteSpaceCollapsible && pointToInsert.IsInContentNode()) {
AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
&pointToInsert);
Result<EditorDOMPoint, nsresult>
deletePointOfInvisibleWhiteSpacesAtStartOrError =
WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces(
aHTMLEditor, pointToInsert);
if (MOZ_UNLIKELY(deletePointOfInvisibleWhiteSpacesAtStartOrError.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces() failed");
return deletePointOfInvisibleWhiteSpacesAtStartOrError.propagateErr();
}
trackPointToInsert.FlushAndStopTracking();
const EditorDOMPoint deletePointOfInvisibleWhiteSpacesAtStart =
deletePointOfInvisibleWhiteSpacesAtStartOrError.unwrap();
if (NS_WARN_IF(deletePointOfInvisibleWhiteSpacesAtStart.IsSet() &&
!pointToInsert.IsSetAndValidInComposedDoc())) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
}
if (NS_WARN_IF(!pointToInsert.IsInContentNode())) {
return Err(NS_ERROR_FAILURE);
}
const HTMLEditor::NormalizedStringToInsertText insertTextData =
[&]() MOZ_NEVER_INLINE_DEBUG {
if (!isWhiteSpaceCollapsible) {
return HTMLEditor::NormalizedStringToInsertText(aStringToInsert,
pointToInsert);
}
if (pointToInsert.IsInTextNode() &&
!EditorBase::InsertingTextForComposition(aPurpose)) {
// If normalizing the surrounding white-spaces in the `Text`, we
// should minimize the replacing range to avoid to unnecessary
// replacement.
return aHTMLEditor
.NormalizeWhiteSpacesToInsertText(
pointToInsert, aStringToInsert,
HTMLEditor::NormalizeSurroundingWhiteSpaces::Yes)
.GetMinimizedData(*pointToInsert.ContainerAs<Text>());
}
return aHTMLEditor.NormalizeWhiteSpacesToInsertText(
pointToInsert, aStringToInsert,
// If we're handling composition string, we should not replace
// surrounding white-spaces to avoid to make
// CompositionTransaction confused.
EditorBase::InsertingTextForComposition(aPurpose)
? HTMLEditor::NormalizeSurroundingWhiteSpaces::No
: HTMLEditor::NormalizeSurroundingWhiteSpaces::Yes);
}();
MOZ_ASSERT_IF(insertTextData.ReplaceLength(), pointToInsert.IsInTextNode());
Result<InsertTextResult, nsresult> insertOrReplaceTextResultOrError =
aHTMLEditor.InsertOrReplaceTextWithTransaction(pointToInsert,
insertTextData);
NS_WARNING_ASSERTION(insertOrReplaceTextResultOrError.isOk(),
"HTMLEditor::ReplaceTextWithTransaction() failed");
// TODO: We need to normalize surrounding white-spaces if this insertion ends
// a composition. However, it requires more utility methods. Therefore,
// it'll be implemented in a following patch.
return insertOrReplaceTextResultOrError;
}
// static // static
Result<CaretPoint, nsresult> Result<CaretPoint, nsresult>
WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace( WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace(

View File

@@ -225,6 +225,8 @@ class WhiteSpaceVisibilityKeeper final {
InsertLineBreak(LineBreakType aLineBreakType, HTMLEditor& aHTMLEditor, InsertLineBreak(LineBreakType aLineBreakType, HTMLEditor& aHTMLEditor,
const EditorDOMPoint& aPointToInsert); const EditorDOMPoint& aPointToInsert);
using InsertTextFor = EditorBase::InsertTextFor;
/** /**
* Insert aStringToInsert to aPointToInsert and makes any needed adjustments * Insert aStringToInsert to aPointToInsert and makes any needed adjustments
* to white-spaces around the insertion point. * to white-spaces around the insertion point.
@@ -243,7 +245,7 @@ class WhiteSpaceVisibilityKeeper final {
return WhiteSpaceVisibilityKeeper:: return WhiteSpaceVisibilityKeeper::
InsertTextOrInsertOrUpdateCompositionString( InsertTextOrInsertOrUpdateCompositionString(
aHTMLEditor, aStringToInsert, EditorDOMRange(aPointToInsert), aHTMLEditor, aStringToInsert, EditorDOMRange(aPointToInsert),
aInsertTextTo, TextIsCompositionString::No); aInsertTextTo, InsertTextFor::NormalText);
} }
/** /**
@@ -260,13 +262,14 @@ class WhiteSpaceVisibilityKeeper final {
* collapsed and indicate the insertion point. * collapsed and indicate the insertion point.
*/ */
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<InsertTextResult, nsresult> [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<InsertTextResult, nsresult>
InsertOrUpdateCompositionString( InsertOrUpdateCompositionString(HTMLEditor& aHTMLEditor,
HTMLEditor& aHTMLEditor, const nsAString& aCompositionString, const nsAString& aCompositionString,
const EditorDOMRange& aCompositionStringRange) { const EditorDOMRange& aCompositionStringRange,
InsertTextFor aPurpose) {
MOZ_ASSERT(EditorBase::InsertingTextForComposition(aPurpose));
return InsertTextOrInsertOrUpdateCompositionString( return InsertTextOrInsertOrUpdateCompositionString(
aHTMLEditor, aCompositionString, aCompositionStringRange, aHTMLEditor, aCompositionString, aCompositionStringRange,
HTMLEditor::InsertTextTo::ExistingTextNodeIfAvailable, HTMLEditor::InsertTextTo::ExistingTextNodeIfAvailable, aPurpose);
TextIsCompositionString::Yes);
} }
/** /**
@@ -344,7 +347,20 @@ class WhiteSpaceVisibilityKeeper final {
HTMLEditor& aHTMLEditor, const EditorDOMRangeInTexts& aRangeToReplace, HTMLEditor& aHTMLEditor, const EditorDOMRangeInTexts& aRangeToReplace,
const nsAString& aReplaceString); const nsAString& aReplaceString);
enum class TextIsCompositionString : bool { No, Yes }; /**
* Delete leading or trailing invisible white-spaces around block boundaries
* or collapsed white-spaces in a white-space sequence if aPoint is around
* them.
*
* @param aHTMLEditor The HTMLEditor.
* @param aPoint Point must be in an editable content node.
* @return If deleted some invisible white-spaces, returns the
* removed point.
* If this does nothing, returns unset point.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<EditorDOMPoint, nsresult>
EnsureNoInvisibleWhiteSpaces(HTMLEditor& aHTMLEditor,
const EditorDOMPoint& aPoint);
/** /**
* Insert aStringToInsert to aRangeToBeReplaced.StartRef() with normalizing * Insert aStringToInsert to aRangeToBeReplaced.StartRef() with normalizing
@@ -361,12 +377,14 @@ class WhiteSpaceVisibilityKeeper final {
* @param aInsertTextTo Whether forcibly creates a new `Text` node in * @param aInsertTextTo Whether forcibly creates a new `Text` node in
* specific condition or use existing `Text` if * specific condition or use existing `Text` if
* available. * available.
* @param aPurpose Whether it's handling normal text input or
* updating composition.
*/ */
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<InsertTextResult, nsresult> [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<InsertTextResult, nsresult>
InsertTextOrInsertOrUpdateCompositionString( InsertTextOrInsertOrUpdateCompositionString(
HTMLEditor& aHTMLEditor, const nsAString& aStringToInsert, HTMLEditor& aHTMLEditor, const nsAString& aStringToInsert,
const EditorDOMRange& aRangeToBeReplaced, InsertTextTo aInsertTextTo, const EditorDOMRange& aRangeToBeReplaced, InsertTextTo aInsertTextTo,
TextIsCompositionString aTextIsCompositionString); InsertTextFor aPurpose);
}; };
} // namespace mozilla } // namespace mozilla

View File

@@ -0,0 +1,24 @@
[insertlinebreak.tentative.html]
[execCommand("insertlinebreak", false, "") at "a[\]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b"]
expected: FAIL
[execCommand("insertlinebreak", false, "") at "a&nbsp;[\]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b"]
expected: FAIL
[execCommand("insertlinebreak", false, "") at "a&nbsp;&nbsp;[\]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b"]
expected: FAIL
[execCommand("insertlinebreak", false, "") at "a&nbsp;&nbsp;&nbsp;[\]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b"]
expected: FAIL
[execCommand("insertlinebreak", false, "") at "a&nbsp;&nbsp;&nbsp;&nbsp;[\]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b"]
expected: FAIL
[execCommand("insertlinebreak", false, "") at "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[\]&nbsp;&nbsp;&nbsp;&nbsp;b"]
expected: FAIL
[execCommand("insertlinebreak", false, "") at "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[\]&nbsp;&nbsp;&nbsp;b"]
expected: FAIL
[execCommand("insertlinebreak", false, "") at "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[\]&nbsp;&nbsp;b"]
expected: FAIL

View File

@@ -1,184 +1 @@
[inserttext.tentative.html] [inserttext.tentative.html]
[execCommand("inserttext", false, " ") at "a<span><span>b&nbsp;&nbsp;[\]</span></span><span>&nbsp;c</span>"]
expected: FAIL
[execCommand("inserttext", false, ""): "a|&nbsp;[\]b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a&nbsp; &nbsp;[\]<span style=white-space:pre>b</span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp; &nbsp;[\]</span>c"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span><span>b[\]</span></span><span>c</span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a[\]b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp;[\]</span><span><span>&nbsp;c</span></span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a[\]b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;[\] b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a[\]b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[\]&nbsp;b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp;[\]</span><span>&nbsp;c</span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a&nbsp; &nbsp; &nbsp;[\] b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a[\]<span style=white-space:pre>&nbsp;</span>"]
expected: FAIL
[execCommand("inserttext", false, "b") at "a&nbsp;&nbsp;&nbsp;&nbsp;[\]c"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>&nbsp; &nbsp;[\]</span>b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b[\]</span><span>c</span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp; &nbsp;[\]</span><span><span>&nbsp;c</span></span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>&nbsp; &nbsp; &nbsp;[\]</span>b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp; &nbsp;&nbsp;[\]</span>&nbsp;c"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp; &nbsp;&nbsp;[\]</span><span>&nbsp;c</span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp;&nbsp;[\]</span><span>&nbsp;c</span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp;[\]</span><span> c</span>"]
expected: FAIL
[execCommand("inserttext", false, ""): "a[\]&nbsp;|&nbsp;b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span><span>b&nbsp; &nbsp;[\]</span></span><span>&nbsp;c</span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b[\]</span><span>&nbsp;c</span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp; &nbsp;[\]</span><span>c</span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a&nbsp; &nbsp; &nbsp; &nbsp;[\] b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp;[\]</span><span><span> c</span></span>"]
expected: FAIL
[execCommand("inserttext", false, "b") at "a&nbsp;&nbsp;&nbsp;[\]&nbsp;c"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b[\]</span><span><span>c</span></span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp;&nbsp;[\]</span><span><span>&nbsp;c</span></span>"]
expected: FAIL
[execCommand("inserttext", false, ""): "a&nbsp;|&nbsp;[\]b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b[\]</span><span><span>&nbsp;c</span></span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a[\]<span style=white-space:pre>b</span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp; &nbsp;[\]</span><span>&nbsp;c</span>"]
expected: FAIL
[execCommand("inserttext", false, ""): "a&nbsp;|[\]&nbsp;b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a&nbsp;&nbsp;&nbsp;&nbsp;[\]&nbsp;b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a[\]b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span><span>b&nbsp; &nbsp;[\]</span></span><span>c</span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span><span>b[\]</span></span><span>&nbsp;c</span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;[\] b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp; &nbsp;&nbsp;[\]</span><span><span>&nbsp;c</span></span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp;[\]</span>&nbsp;c"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span><span>b&nbsp; &nbsp;&nbsp;[\]</span></span><span>&nbsp;c</span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a&nbsp;[\] b"]
expected: FAIL
[execCommand("inserttext", false, ""): "a&nbsp;[\]|&nbsp;b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span><span>b&nbsp;[\]</span></span><span> c</span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp; &nbsp;[\]</span><span><span>c</span></span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b[\]</span>&nbsp;c"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span><span>b&nbsp;[\]</span></span><span>&nbsp;c</span>"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a&nbsp; &nbsp;[\] b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp;[\]</span> c"]
expected: FAIL
[execCommand("inserttext", false, ""): "a|[\]&nbsp;b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a[\]b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp;&nbsp;[\]</span>&nbsp;c"]
expected: FAIL
[execCommand("inserttext", false, ""): "a[\]|&nbsp;b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b&nbsp; &nbsp;[\]</span>&nbsp;c"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a&nbsp;[\]&nbsp;b"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a<span>b[\]</span>c"]
expected: FAIL
[execCommand("inserttext", false, " ") at "a&nbsp; &nbsp;[\]<span style=white-space:pre>&nbsp;</span>"]
expected: FAIL