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,6 +3244,67 @@ nsresult EditorBase::ScrollSelectionFocusIntoView() const {
return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
}
EditorDOMPoint EditorBase::ComputePointToInsertText(
const EditorDOMPoint& aPoint, InsertTextTo aInsertTextTo) const {
if (aInsertTextTo == InsertTextTo::SpecifiedPoint) {
return aPoint;
}
if (IsTextEditor()) {
// 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
// point in the nearest text node if there is.
return AsTextEditor()->FindBetterInsertionPoint(aPoint);
}
auto pointToInsert =
aPoint.GetPointInTextNodeIfPointingAroundTextNode<EditorDOMPoint>();
// 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
// been inserted by us and that's compatible behavior with Chrome.
if (pointToInsert.IsInTextNode() &&
HTMLEditUtils::TextHasOnlyOnePreformattedLinefeed(
*pointToInsert.ContainerAs<Text>())) {
if (pointToInsert.IsStartOfContainer()) {
if (Text* const previousText = Text::FromNodeOrNull(
pointToInsert.ContainerAs<Text>()->GetPreviousSibling())) {
pointToInsert = EditorDOMPoint::AtEndOf(*previousText);
} else {
pointToInsert = pointToInsert.ParentPoint();
}
} else {
MOZ_ASSERT(pointToInsert.IsEndOfContainer());
if (Text* const nextText = Text::FromNodeOrNull(
pointToInsert.ContainerAs<Text>()->GetNextSibling())) {
pointToInsert = EditorDOMPoint(nextText, 0u);
} else {
pointToInsert = pointToInsert.AfterContainer();
}
}
}
if (aInsertTextTo == InsertTextTo::AlwaysCreateNewTextNode) {
NS_WARNING_ASSERTION(!pointToInsert.IsInTextNode() ||
pointToInsert.IsStartOfContainer() ||
pointToInsert.IsEndOfContainer(),
"aPointToInsert is \"AlwaysCreateNewTextNode\", but "
"specified point middle of a `Text`");
if (!pointToInsert.IsInTextNode()) {
return pointToInsert;
}
return pointToInsert.IsStartOfContainer()
? EditorDOMPoint(pointToInsert.ContainerAs<Text>())
: (pointToInsert.IsEndOfContainer()
? EditorDOMPoint::After(
*pointToInsert.ContainerAs<Text>())
: pointToInsert);
}
if (aInsertTextTo == InsertTextTo::ExistingTextNodeIfAvailableAndNotStart) {
return !(pointToInsert.IsInTextNode() && pointToInsert.IsStartOfContainer())
? pointToInsert
: EditorDOMPoint(pointToInsert.ContainerAs<Text>());
}
return pointToInsert;
}
Result<InsertTextResult, nsresult> EditorBase::InsertTextWithTransaction(
const nsAString& aStringToInsert, const EditorDOMPoint& aPointToInsert,
InsertTextTo aInsertTextTo) {
@@ -3260,64 +3321,8 @@ Result<InsertTextResult, nsresult> EditorBase::InsertTextWithTransaction(
return InsertTextResult();
}
// 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
// point in the nearest text node if there is.
EditorDOMPoint pointToInsert = [&]() {
if (IsTextEditor()) {
return AsTextEditor()->FindBetterInsertionPoint(aPointToInsert);
}
auto pointToInsert =
aPointToInsert
.GetPointInTextNodeIfPointingAroundTextNode<EditorDOMPoint>();
// 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
// been inserted by us and that's compatible behavior with Chrome.
if (pointToInsert.IsInTextNode() &&
HTMLEditUtils::TextHasOnlyOnePreformattedLinefeed(
*pointToInsert.ContainerAs<Text>())) {
if (pointToInsert.IsStartOfContainer()) {
if (Text* const previousText = Text::FromNodeOrNull(
pointToInsert.ContainerAs<Text>()->GetPreviousSibling())) {
pointToInsert = EditorDOMPoint::AtEndOf(*previousText);
} else {
pointToInsert = pointToInsert.ParentPoint();
}
} else {
MOZ_ASSERT(pointToInsert.IsEndOfContainer());
if (Text* const nextText = Text::FromNodeOrNull(
pointToInsert.ContainerAs<Text>()->GetNextSibling())) {
pointToInsert = EditorDOMPoint(nextText, 0u);
} else {
pointToInsert = pointToInsert.AfterContainer();
}
}
}
if (aInsertTextTo == InsertTextTo::AlwaysCreateNewTextNode) {
NS_WARNING_ASSERTION(!pointToInsert.IsInTextNode() ||
pointToInsert.IsStartOfContainer() ||
pointToInsert.IsEndOfContainer(),
"aPointToInsert is \"AlwaysCreateNewTextNode\", but "
"specified point middle of a `Text`");
if (!pointToInsert.IsInTextNode()) {
return pointToInsert;
}
return pointToInsert.IsStartOfContainer()
? EditorDOMPoint(pointToInsert.ContainerAs<Text>())
: (pointToInsert.IsEndOfContainer()
? EditorDOMPoint::After(
*pointToInsert.ContainerAs<Text>())
: pointToInsert);
}
if (aInsertTextTo == InsertTextTo::ExistingTextNodeIfAvailableAndNotStart) {
return !(pointToInsert.IsInTextNode() &&
pointToInsert.IsStartOfContainer())
? pointToInsert
: EditorDOMPoint(pointToInsert.ContainerAs<Text>());
}
return pointToInsert;
}();
EditorDOMPoint pointToInsert =
ComputePointToInsertText(aPointToInsert, aInsertTextTo);
if (ShouldHandleIMEComposition()) {
if (!pointToInsert.IsInTextNode()) {
// create a text node

View File

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

View File

@@ -631,6 +631,48 @@ class EditorDOMPointBase final {
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.
*/
@@ -728,6 +770,13 @@ class EditorDOMPointBase final {
result.RewindOffset();
return result;
}
template <typename EditorDOMPointType = SelfType>
EditorDOMPointType PreviousPointOrParentPoint() const {
if (IsStartOfContainer()) {
return ParentPoint<EditorDOMPointType>();
}
return PreviousPoint<EditorDOMPointType>();
}
/**
* Clear() makes the instance not point anywhere.

View File

@@ -35,6 +35,7 @@
#include "mozilla/Maybe.h"
#include "mozilla/OwningNonNull.h"
#include "mozilla/PresShell.h"
#include "mozilla/StaticPrefs_editor.h"
#include "mozilla/TextComposition.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Unused.h"
@@ -552,45 +553,47 @@ nsresult HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() {
// attempt to transform any unneeded nbsp's into spaces after doing various
// operations
switch (GetTopLevelEditSubAction()) {
case EditSubAction::eDeleteSelectedContent:
if (TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces) {
break;
}
[[fallthrough]];
case EditSubAction::eInsertText:
case EditSubAction::eInsertTextComingFromIME:
case EditSubAction::eInsertLineBreak:
case EditSubAction::eInsertParagraphSeparator:
case EditSubAction::ePasteHTMLContent:
case EditSubAction::eInsertHTMLSource: {
// Due to the replacement of white-spaces in
// WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(),
// selection ranges may be changed since DOM ranges track the DOM
// mutation by themselves. However, we want to keep selection as-is.
// Therefore, we should restore `Selection` after replacing
// white-spaces.
AutoSelectionRestorer restoreSelection(this);
// TODO: Temporarily, WhiteSpaceVisibilityKeeper replaces ASCII
// white-spaces with NPSPs and then, we'll replace them with ASCII
// white-spaces here. We should avoid this overwriting things as
// far as possible because replacing characters in text nodes
// causes running mutation event listeners which are really
// expensive.
// Adjust end of composition string if there is composition string.
auto pointToAdjust = GetLastIMESelectionEndPoint<EditorDOMPoint>();
if (!pointToAdjust.IsInContentNode()) {
// Otherwise, adjust current selection start point.
pointToAdjust = GetFirstSelectionStartPoint<EditorDOMPoint>();
if (NS_WARN_IF(!pointToAdjust.IsInContentNode())) {
return NS_ERROR_FAILURE;
}
}
const RefPtr<Element> editingHost =
ComputeEditingHost(LimitInBodyElement::No);
if (MOZ_UNLIKELY(!editingHost)) {
break;
const bool needToNormalizeWhiteSpaces = [&]() {
switch (GetTopLevelEditSubAction()) {
case EditSubAction::eDeleteSelectedContent:
return !TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces;
case EditSubAction::eInsertText:
case EditSubAction::eInsertTextComingFromIME:
return !StaticPrefs::
editor_white_space_normalization_blink_compatible();
case EditSubAction::eInsertLineBreak:
case EditSubAction::eInsertParagraphSeparator:
case EditSubAction::ePasteHTMLContent:
case EditSubAction::eInsertHTMLSource:
return true;
default:
return false;
}
}();
if (needToNormalizeWhiteSpaces) {
// Due to the replacement of white-spaces in
// WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(), selection
// ranges may be changed since DOM ranges track the DOM mutation by
// themselves. However, we want to keep selection as-is. Therefore, we
// should restore `Selection` after replacing white-spaces.
AutoSelectionRestorer restoreSelection(this);
// TODO: Temporarily, WhiteSpaceVisibilityKeeper replaces ASCII
// white-spaces with NPSPs and then, we'll replace them with ASCII
// white-spaces here. We should avoid this overwriting things as
// far as possible because replacing characters in text nodes
// causes running mutation event listeners which are really
// expensive.
// Adjust end of composition string if there is composition string.
auto pointToAdjust = GetLastIMESelectionEndPoint<EditorDOMPoint>();
if (!pointToAdjust.IsInContentNode()) {
// Otherwise, adjust current selection start point.
pointToAdjust = GetFirstSelectionStartPoint<EditorDOMPoint>();
if (NS_WARN_IF(!pointToAdjust.IsInContentNode())) {
return NS_ERROR_FAILURE;
}
}
if (const RefPtr<Element> editingHost =
ComputeEditingHost(LimitInBodyElement::No)) {
if (EditorUtils::IsEditableContent(
*pointToAdjust.ContainerAs<nsIContent>(), EditorType::HTML)) {
AutoTrackDOMPoint trackPointToAdjust(RangeUpdaterRef(),
@@ -654,10 +657,7 @@ nsresult HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() {
"WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() "
"failed, but ignored");
}
break;
}
default:
break;
}
// Adjust selection for insert text, html paste, and delete actions if
@@ -1236,7 +1236,8 @@ Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
*this, aInsertionString,
compositionEndPoint.IsSet()
? EditorDOMRange(pointToInsert, compositionEndPoint)
: EditorDOMRange(pointToInsert));
: EditorDOMRange(pointToInsert),
aPurpose);
if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
NS_WARNING("WhiteSpaceVisibilityKeeper::ReplaceText() failed");
return replaceTextResult.propagateErr();
@@ -2948,13 +2949,86 @@ HTMLEditor::GetInclusiveNextCharPointDataForNormalizingWhiteSpaces(
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
void HTMLEditor::GenerateWhiteSpaceSequence(
nsAString& aResult, uint32_t aLength,
nsString& aResult, uint32_t aLength,
const CharPointData& aPreviousCharPointData,
const CharPointData& aNextCharPointData) {
MOZ_ASSERT(aResult.IsEmpty());
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
// white-space sequence in the text node.
MOZ_ASSERT(aPreviousCharPointData.AcrossTextNodeBoundary() ||
@@ -2971,38 +3045,39 @@ void HTMLEditor::GenerateWhiteSpaceSequence(
// without preformatted style. However, Chrome has same issue too.
if (aPreviousCharPointData.Type() == CharPointType::VisibleChar &&
aNextCharPointData.Type() == CharPointType::VisibleChar) {
aResult.Assign(HTMLEditUtils::kSpace);
aResult.SetCharAt(HTMLEditUtils::kSpace, aOffset);
return;
}
// If it's start or end of text, put an NBSP.
if (aPreviousCharPointData.Type() == CharPointType::TextEnd ||
aNextCharPointData.Type() == CharPointType::TextEnd) {
aResult.Assign(HTMLEditUtils::kNBSP);
aResult.SetCharAt(HTMLEditUtils::kNBSP, aOffset);
return;
}
// If the character is next to a preformatted linefeed, we need to put
// an NBSP for avoiding collapsed into the linefeed.
if (aPreviousCharPointData.Type() == CharPointType::PreformattedLineBreak ||
aNextCharPointData.Type() == CharPointType::PreformattedLineBreak) {
aResult.Assign(HTMLEditUtils::kNBSP);
aResult.SetCharAt(HTMLEditUtils::kNBSP, aOffset);
return;
}
// 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
// not ASCII white-spaces.
aResult.Assign(
aResult.SetCharAt(
aPreviousCharPointData.Type() == CharPointType::ASCIIWhiteSpace ||
aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace
? HTMLEditUtils::kNBSP
: HTMLEditUtils::kSpace);
: HTMLEditUtils::kSpace,
aOffset);
return;
}
// Generate pairs of NBSP and ASCII white-space.
aResult.SetLength(aLength);
bool appendNBSP = true; // Basically, starts with an NBSP.
char16_t* lastChar = aResult.EndWriting() - 1;
for (char16_t* iter = aResult.BeginWriting(); iter != lastChar; iter++) {
char16_t* const lastChar = aResult.BeginWriting() + aOffset + aLength - 1;
for (char16_t* iter = aResult.BeginWriting() + aOffset; iter != lastChar;
iter++) {
*iter = appendNBSP ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace;
appendNBSP = !appendNBSP;
}
@@ -3023,10 +3098,142 @@ void HTMLEditor::GenerateWhiteSpaceSequence(
: 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(
EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete,
nsAString& aNormalizedWhiteSpacesInStartNode,
nsAString& aNormalizedWhiteSpacesInEndNode) const {
nsString& aNormalizedWhiteSpacesInStartNode,
nsString& aNormalizedWhiteSpacesInEndNode) const {
MOZ_ASSERT(aStartToDelete.IsSetAndValid());
MOZ_ASSERT(aEndToDelete.IsSetAndValid());
MOZ_ASSERT(aStartToDelete.EqualsOrIsBefore(aEndToDelete));

View File

@@ -1245,6 +1245,131 @@ Maybe<EditorLineBreakType> HTMLEditUtils::GetFollowingUnnecessaryLineBreak(
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,
const nsINode& aNode,
const EmptyCheckOptions& aOptions /* = {} */,

View File

@@ -2190,12 +2190,16 @@ class HTMLEditUtils final {
* GetInclusiveNextNonCollapsibleCharOffset() returns offset of inclusive next
* character which is not collapsible white-space characters.
*/
template <typename PT, typename CT>
static Maybe<uint32_t> GetInclusiveNextNonCollapsibleCharOffset(
const EditorDOMPointInText& aPoint,
const EditorDOMPointBase<PT, CT>& aPoint,
const WalkTextOptions& aWalkTextOptions = {}) {
static_assert(std::is_same<PT, RefPtr<Text>>::value ||
std::is_same<PT, Text*>::value);
MOZ_ASSERT(aPoint.IsSetAndValid());
return GetInclusiveNextNonCollapsibleCharOffset(
*aPoint.ContainerAs<Text>(), aPoint.Offset(), aWalkTextOptions);
*aPoint.template ContainerAs<Text>(), aPoint.Offset(),
aWalkTextOptions);
}
static Maybe<uint32_t> GetInclusiveNextNonCollapsibleCharOffset(
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
* white-space.
*/
template <typename PT, typename CT>
static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith(
const EditorDOMPointInText& aPoint,
const EditorDOMPointBase<PT, CT>& aPoint,
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.IsEndOfContainer());
MOZ_ASSERT_IF(
@@ -2257,7 +2264,8 @@ class HTMLEditUtils final {
!aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible),
aPoint.IsCharCollapsibleASCIISpace());
return GetFirstWhiteSpaceOffsetCollapsedWith(
*aPoint.ContainerAs<Text>(), aPoint.Offset(), aWalkTextOptions);
*aPoint.template ContainerAs<Text>(), aPoint.Offset(),
aWalkTextOptions);
}
static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith(
const Text& aTextNode, uint32_t aOffset,
@@ -2331,6 +2339,41 @@ class HTMLEditUtils final {
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`
* after handling edit action with aDirectionAndAmount.

View File

@@ -6,6 +6,7 @@
#include "HTMLEditor.h"
#include "HTMLEditHelpers.h"
#include "HTMLEditorInlines.h"
#include "HTMLEditorNestedClasses.h"
#include "AutoClonedRangeArray.h"
#include "AutoSelectionRestorer.h"
@@ -4311,6 +4312,57 @@ Result<InsertTextResult, nsresult> HTMLEditor::ReplaceTextWithTransaction(
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(
const nsAString& aStringToInsert, const EditorDOMPoint& aPointToInsert,
InsertTextTo aInsertTextTo) {

View File

@@ -844,6 +844,18 @@ class HTMLEditor final : public EditorBase,
uint32_t aLength,
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,
* this returns error.
@@ -2204,6 +2216,21 @@ class HTMLEditor final : public EditorBase,
TreatEmptyTextNodes aTreatEmptyTextNodes,
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
* DeleteTextAndNormalizeSurroundingWhiteSpaces(). This expands
@@ -2227,8 +2254,8 @@ class HTMLEditor final : public EditorBase,
*/
void ExtendRangeToDeleteWithNormalizingWhiteSpaces(
EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete,
nsAString& aNormalizedWhiteSpacesInStartNode,
nsAString& aNormalizedWhiteSpacesInEndNode) const;
nsString& aNormalizedWhiteSpacesInStartNode,
nsString& aNormalizedWhiteSpacesInEndNode) const;
/**
* CharPointType let the following helper methods of
@@ -2265,20 +2292,16 @@ class HTMLEditor final : public EditorBase,
*/
class MOZ_STACK_CLASS CharPointData final {
public:
CharPointData() = delete;
static CharPointData InDifferentTextNode(CharPointType aCharPointType) {
CharPointData result;
result.mIsInDifferentTextNode = true;
result.mType = aCharPointType;
return result;
return {aCharPointType, true};
}
static CharPointData InSameTextNode(CharPointType aCharPointType) {
CharPointData result;
// 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
// from point of text node view is another element.
result.mIsInDifferentTextNode = aCharPointType == CharPointType::TextEnd;
result.mType = aCharPointType;
return result;
return {aCharPointType, aCharPointType == CharPointType::TextEnd};
}
bool AcrossTextNodeBoundary() const { return mIsInDifferentTextNode; }
@@ -2289,7 +2312,8 @@ class HTMLEditor final : public EditorBase,
CharPointType Type() const { return mType; }
private:
CharPointData() = default;
CharPointData(CharPointType aType, bool aIsInDifferentTextNode)
: mType(aType), mIsInDifferentTextNode(aIsInDifferentTextNode) {}
CharPointType mType;
bool mIsInDifferentTextNode;
@@ -2306,12 +2330,24 @@ class HTMLEditor final : public EditorBase,
CharPointData GetInclusiveNextCharPointDataForNormalizingWhiteSpaces(
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
* be collapsed.
*
* @param aResult [out] White space sequence which won't be
* collapsed, but wrapable.
* collapsed, but wrappable.
* @param aLength Length of generating white-space sequence.
* Must be 1 or larger.
* @param aPreviousCharPointData
@@ -2323,11 +2359,20 @@ class HTMLEditor final : public EditorBase,
* different text nodes white-space.
* @param aNextCharPointData Specify the next char point where it'll be
* inserted. Same as aPreviousCharPointData,
* this must node indidate white-space in same
* this must node indicate white-space in same
* text node.
*/
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& aNextCharPointData);

View File

@@ -15,6 +15,8 @@
#include "mozilla/Attributes.h"
#include "mozilla/OwningNonNull.h"
#include "mozilla/Result.h"
#include "mozilla/dom/Text.h"
#include "nsTextFragment.h"
namespace mozilla {
@@ -522,6 +524,168 @@ class MOZ_STACK_CLASS HTMLEditor::AutoListElementCreator final {
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
#endif // #ifndef HTMLEditorNestedClasses_h

File diff suppressed because it is too large Load Diff

View File

@@ -225,6 +225,8 @@ class WhiteSpaceVisibilityKeeper final {
InsertLineBreak(LineBreakType aLineBreakType, HTMLEditor& aHTMLEditor,
const EditorDOMPoint& aPointToInsert);
using InsertTextFor = EditorBase::InsertTextFor;
/**
* Insert aStringToInsert to aPointToInsert and makes any needed adjustments
* to white-spaces around the insertion point.
@@ -243,7 +245,7 @@ class WhiteSpaceVisibilityKeeper final {
return WhiteSpaceVisibilityKeeper::
InsertTextOrInsertOrUpdateCompositionString(
aHTMLEditor, aStringToInsert, EditorDOMRange(aPointToInsert),
aInsertTextTo, TextIsCompositionString::No);
aInsertTextTo, InsertTextFor::NormalText);
}
/**
@@ -260,13 +262,14 @@ class WhiteSpaceVisibilityKeeper final {
* collapsed and indicate the insertion point.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<InsertTextResult, nsresult>
InsertOrUpdateCompositionString(
HTMLEditor& aHTMLEditor, const nsAString& aCompositionString,
const EditorDOMRange& aCompositionStringRange) {
InsertOrUpdateCompositionString(HTMLEditor& aHTMLEditor,
const nsAString& aCompositionString,
const EditorDOMRange& aCompositionStringRange,
InsertTextFor aPurpose) {
MOZ_ASSERT(EditorBase::InsertingTextForComposition(aPurpose));
return InsertTextOrInsertOrUpdateCompositionString(
aHTMLEditor, aCompositionString, aCompositionStringRange,
HTMLEditor::InsertTextTo::ExistingTextNodeIfAvailable,
TextIsCompositionString::Yes);
HTMLEditor::InsertTextTo::ExistingTextNodeIfAvailable, aPurpose);
}
/**
@@ -344,7 +347,20 @@ class WhiteSpaceVisibilityKeeper final {
HTMLEditor& aHTMLEditor, const EditorDOMRangeInTexts& aRangeToReplace,
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
@@ -361,12 +377,14 @@ class WhiteSpaceVisibilityKeeper final {
* @param aInsertTextTo Whether forcibly creates a new `Text` node in
* specific condition or use existing `Text` if
* available.
* @param aPurpose Whether it's handling normal text input or
* updating composition.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<InsertTextResult, nsresult>
InsertTextOrInsertOrUpdateCompositionString(
HTMLEditor& aHTMLEditor, const nsAString& aStringToInsert,
const EditorDOMRange& aRangeToBeReplaced, InsertTextTo aInsertTextTo,
TextIsCompositionString aTextIsCompositionString);
InsertTextFor aPurpose);
};
} // namespace mozilla