Bug 1940377 - part 2: Make HandleInsertLineBreak use the new normalizer if it's available r=m_kato

When inserting a `<br>` element or a preformatted line break at middle of a
`Text`, we need to split it first.  At this time, we should normalize the
surrounding white-spaces before split.  Then, we can use only one
`ReplaceTextTransaction` instance for the normalization in the most cases.

Differential Revision: https://phabricator.services.mozilla.com/D239464
This commit is contained in:
Masayuki Nakano
2025-03-08 00:23:15 +00:00
parent f7f5dd6a64
commit cb5f775b5c
8 changed files with 825 additions and 205 deletions

View File

@@ -958,7 +958,7 @@ class EditorDOMPointBase final {
return !!maybeTextControl;
}
bool IsStartOfContainer() const {
[[nodiscard]] bool IsStartOfContainer() const {
// If we're referring the first point in the container:
// If mParent is not a container like a text node, mOffset is 0.
// If mChild is initialized and it's first child of mParent.
@@ -984,7 +984,29 @@ class EditorDOMPointBase final {
return !mOffset.value();
}
bool IsEndOfContainer() const {
[[nodiscard]] bool IsMiddleOfContainer() const {
if (NS_WARN_IF(!mParent)) {
return false;
}
if (mParent->IsText()) {
return *mOffset && *mOffset < mParent->Length();
}
if (!mParent->HasChildren()) {
return false;
}
if (mIsChildInitialized) {
NS_WARNING_ASSERTION(
mOffset.isNothing() ||
(!mChild && *mOffset == mParent->GetChildCount()) ||
(mChild && mOffset == mParent->ComputeIndexOf(mChild)),
"mOffset does not match with current offset of mChild");
return mChild && mChild != mParent->GetFirstChild();
}
MOZ_ASSERT(mOffset.isSome());
return *mOffset && *mOffset < mParent->Length();
}
[[nodiscard]] bool IsEndOfContainer() const {
// If we're referring after the last point of the container:
// If mParent is not a container like text node, mOffset is same as the
// length of the container.

View File

@@ -559,9 +559,9 @@ nsresult HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() {
return !TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces;
case EditSubAction::eInsertText:
case EditSubAction::eInsertTextComingFromIME:
case EditSubAction::eInsertLineBreak:
return !StaticPrefs::
editor_white_space_normalization_blink_compatible();
case EditSubAction::eInsertLineBreak:
case EditSubAction::eInsertParagraphSeparator:
case EditSubAction::ePasteHTMLContent:
case EditSubAction::eInsertHTMLSource:
@@ -3230,6 +3230,163 @@ HTMLEditor::NormalizeWhiteSpacesToInsertText(
return result;
}
HTMLEditor::ReplaceWhiteSpacesData
HTMLEditor::GetFollowingNormalizedStringToSplitAt(
const EditorDOMPointInText& aPointToSplit) const {
MOZ_ASSERT(aPointToSplit.IsSet());
if (EditorUtils::IsWhiteSpacePreformatted(
*aPointToSplit.ContainerAs<Text>()) ||
aPointToSplit.IsEndOfContainer()) {
return ReplaceWhiteSpacesData();
}
const bool isNewLineCollapsible =
!EditorUtils::IsNewLinePreformatted(*aPointToSplit.ContainerAs<Text>());
const auto IsPreformattedLineBreak = [&](char16_t aChar) {
return !isNewLineCollapsible && aChar == HTMLEditUtils::kNewLine;
};
const auto IsCollapsibleChar = [&](char16_t aChar) {
return !IsPreformattedLineBreak(aChar) && nsCRT::IsAsciiSpace(aChar);
};
const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) {
return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar);
};
const char16_t followingChar = aPointToSplit.Char();
if (!IsCollapsibleCharOrNBSP(followingChar)) {
return ReplaceWhiteSpacesData();
}
const uint32_t followingWhiteSpaceLength = [&]() {
const auto nonWhiteSpaceOffset =
HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
*aPointToSplit.ContainerAs<Text>(), aPointToSplit.Offset(),
{HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
MOZ_ASSERT(nonWhiteSpaceOffset.valueOr(
aPointToSplit.ContainerAs<Text>()->TextDataLength()) >=
aPointToSplit.Offset());
return nonWhiteSpaceOffset.valueOr(
aPointToSplit.ContainerAs<Text>()->TextDataLength()) -
aPointToSplit.Offset();
}();
MOZ_ASSERT(followingWhiteSpaceLength);
if (NS_WARN_IF(!followingWhiteSpaceLength) ||
(followingWhiteSpaceLength == 1u &&
followingChar == HTMLEditUtils::kNBSP)) {
return ReplaceWhiteSpacesData();
}
const uint32_t followingInvisibleSpaceCount =
HTMLEditUtils::GetInvisibleWhiteSpaceCount(
*aPointToSplit.ContainerAs<Text>(), aPointToSplit.Offset(),
followingWhiteSpaceLength);
MOZ_ASSERT(followingWhiteSpaceLength >= followingInvisibleSpaceCount);
const uint32_t newFollowingWhiteSpaceLength =
followingWhiteSpaceLength - followingInvisibleSpaceCount;
nsAutoString followingWhiteSpaces;
if (newFollowingWhiteSpaceLength) {
followingWhiteSpaces.SetLength(newFollowingWhiteSpaceLength);
for (const auto offset : IntegerRange(newFollowingWhiteSpaceLength)) {
followingWhiteSpaces.SetCharAt(' ', offset);
}
}
ReplaceWhiteSpacesData result(std::move(followingWhiteSpaces),
aPointToSplit.Offset(),
followingWhiteSpaceLength);
if (!result.mNormalizedString.IsEmpty()) {
const nsTextFragment& textFragment =
aPointToSplit.ContainerAs<Text>()->TextFragment();
HTMLEditor::NormalizeAllWhiteSpaceSequences(
result.mNormalizedString,
CharPointData::InSameTextNode(CharPointType::TextEnd),
CharPointData::InSameTextNode(
result.mReplaceEndOffset >= textFragment.GetLength()
? CharPointType::TextEnd
: (textFragment.CharAt(result.mReplaceEndOffset) ==
HTMLEditUtils::kNewLine
? CharPointType::PreformattedLineBreak
: CharPointType::VisibleChar)),
isNewLineCollapsible ? Linefeed::Collapsible : Linefeed::Preformatted);
}
return result;
}
HTMLEditor::ReplaceWhiteSpacesData
HTMLEditor::GetPrecedingNormalizedStringToSplitAt(
const EditorDOMPointInText& aPointToSplit) const {
MOZ_ASSERT(aPointToSplit.IsSet());
if (EditorUtils::IsWhiteSpacePreformatted(
*aPointToSplit.ContainerAs<Text>()) ||
aPointToSplit.IsStartOfContainer()) {
return ReplaceWhiteSpacesData();
}
const bool isNewLineCollapsible =
!EditorUtils::IsNewLinePreformatted(*aPointToSplit.ContainerAs<Text>());
const auto IsPreformattedLineBreak = [&](char16_t aChar) {
return !isNewLineCollapsible && aChar == HTMLEditUtils::kNewLine;
};
const auto IsCollapsibleChar = [&](char16_t aChar) {
return !IsPreformattedLineBreak(aChar) && nsCRT::IsAsciiSpace(aChar);
};
const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) {
return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar);
};
const char16_t precedingChar = aPointToSplit.PreviousChar();
if (!IsCollapsibleCharOrNBSP(precedingChar)) {
return ReplaceWhiteSpacesData();
}
const uint32_t precedingWhiteSpaceLength = [&]() {
const auto nonWhiteSpaceOffset =
HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
*aPointToSplit.ContainerAs<Text>(), aPointToSplit.Offset(),
{HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
const uint32_t firstWhiteSpaceOffset =
nonWhiteSpaceOffset ? *nonWhiteSpaceOffset + 1u : 0u;
return aPointToSplit.Offset() - firstWhiteSpaceOffset;
}();
MOZ_ASSERT(precedingWhiteSpaceLength);
if (NS_WARN_IF(!precedingWhiteSpaceLength) ||
(precedingWhiteSpaceLength == 1u &&
precedingChar == HTMLEditUtils::kNBSP)) {
return ReplaceWhiteSpacesData();
}
const uint32_t precedingInvisibleWhiteSpaceCount =
HTMLEditUtils::GetInvisibleWhiteSpaceCount(
*aPointToSplit.ContainerAs<Text>(),
aPointToSplit.Offset() - precedingWhiteSpaceLength,
precedingWhiteSpaceLength);
MOZ_ASSERT(precedingWhiteSpaceLength >= precedingInvisibleWhiteSpaceCount);
const uint32_t newPrecedingWhiteSpaceLength =
precedingWhiteSpaceLength - precedingInvisibleWhiteSpaceCount;
nsAutoString precedingWhiteSpaces;
if (newPrecedingWhiteSpaceLength) {
precedingWhiteSpaces.SetLength(newPrecedingWhiteSpaceLength);
for (const auto offset : IntegerRange(newPrecedingWhiteSpaceLength)) {
precedingWhiteSpaces.SetCharAt(' ', offset);
}
}
ReplaceWhiteSpacesData result(
std::move(precedingWhiteSpaces),
aPointToSplit.Offset() - precedingWhiteSpaceLength,
precedingWhiteSpaceLength);
if (!result.mNormalizedString.IsEmpty()) {
const nsTextFragment& textFragment =
aPointToSplit.ContainerAs<Text>()->TextFragment();
HTMLEditor::NormalizeAllWhiteSpaceSequences(
result.mNormalizedString,
CharPointData::InSameTextNode(
!result.mReplaceStartOffset
? CharPointType::TextEnd
: (textFragment.CharAt(result.mReplaceStartOffset - 1u) ==
HTMLEditUtils::kNewLine
? CharPointType::PreformattedLineBreak
: CharPointType::VisibleChar)),
CharPointData::InSameTextNode(CharPointType::TextEnd),
isNewLineCollapsible ? Linefeed::Collapsible : Linefeed::Preformatted);
}
return result;
}
void HTMLEditor::ExtendRangeToDeleteWithNormalizingWhiteSpaces(
EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete,
nsString& aNormalizedWhiteSpacesInStartNode,

View File

@@ -4191,6 +4191,13 @@ Result<CaretPoint, nsresult> HTMLEditor::DeleteTextWithTransaction(
return caretPointOrError;
}
Result<InsertTextResult, nsresult> HTMLEditor::ReplaceTextWithTransaction(
dom::Text& aTextNode, const ReplaceWhiteSpacesData& aData) {
return ReplaceTextWithTransaction(aTextNode, aData.mReplaceStartOffset,
aData.ReplaceLength(),
aData.mNormalizedString);
}
Result<InsertTextResult, nsresult> HTMLEditor::ReplaceTextWithTransaction(
Text& aTextNode, uint32_t aOffset, uint32_t aLength,
const nsAString& aStringToInsert) {
@@ -4382,8 +4389,17 @@ Result<EditorDOMPoint, nsresult> HTMLEditor::PrepareToInsertLineBreak(
!CanInsertLineBreak(*aPointToInsert.ContainerAs<nsIContent>()))) {
return Err(NS_ERROR_FAILURE);
}
if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) {
return aPointToInsert;
}
Result<EditorDOMPoint, nsresult> pointToInsertOrError =
WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt(
*this, aPointToInsert);
if (NS_WARN_IF(pointToInsertOrError.isErr())) {
return pointToInsertOrError.propagateErr();
}
return pointToInsertOrError.unwrap();
}
// If the text node is not in an element node, we cannot insert a line break
// around the text node.
@@ -4394,21 +4410,34 @@ Result<EditorDOMPoint, nsresult> HTMLEditor::PrepareToInsertLineBreak(
return Err(NS_ERROR_FAILURE);
}
if (aPointToInsert.IsStartOfContainer()) {
Result<EditorDOMPoint, nsresult> pointToInsertOrError =
StaticPrefs::editor_white_space_normalization_blink_compatible()
? WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt(
*this, aPointToInsert)
: aPointToInsert;
if (NS_WARN_IF(pointToInsertOrError.isErr())) {
return pointToInsertOrError.propagateErr();
}
const EditorDOMPoint pointToInsert = pointToInsertOrError.unwrap();
if (!pointToInsert.IsInTextNode()) {
return pointToInsert.ParentPoint();
}
if (pointToInsert.IsStartOfContainer()) {
// Insert before the text node.
return aPointToInsert.ParentPoint();
return pointToInsert.ParentPoint();
}
if (aPointToInsert.IsEndOfContainer()) {
if (pointToInsert.IsEndOfContainer()) {
// Insert after the text node.
return EditorDOMPoint::After(*aPointToInsert.ContainerAs<Text>());
return EditorDOMPoint::After(*pointToInsert.ContainerAs<Text>());
}
MOZ_DIAGNOSTIC_ASSERT(aPointToInsert.IsSetAndValid());
MOZ_DIAGNOSTIC_ASSERT(pointToInsert.IsSetAndValid());
// Unfortunately, we need to split the text node at the offset.
Result<SplitNodeResult, nsresult> splitTextNodeResult =
SplitNodeWithTransaction(aPointToInsert);
SplitNodeWithTransaction(pointToInsert);
if (MOZ_UNLIKELY(splitTextNodeResult.isErr())) {
NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
return splitTextNodeResult.propagateErr();

View File

@@ -856,6 +856,15 @@ class HTMLEditor final : public EditorBase,
InsertOrReplaceTextWithTransaction(const EditorDOMPoint& aPointToInsert,
const NormalizedStringToInsertText& aData);
struct ReplaceWhiteSpacesData;
/**
* Replace or insert white-spaces of aData to aTextNode.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<InsertTextResult, nsresult>
ReplaceTextWithTransaction(dom::Text& aTextNode,
const ReplaceWhiteSpacesData& aData);
/**
* Insert aStringToInsert to aPointToInsert. If the point is not editable,
* this returns error.
@@ -2231,6 +2240,20 @@ class HTMLEditor final : public EditorBase,
const EditorDOMPoint& aPointToInsert, const nsAString& aStringToInsert,
NormalizeSurroundingWhiteSpaces aNormalizeSurroundingWhiteSpaces) const;
/**
* Return normalized white-spaces after aPointToSplit if there are some
* collapsible white-spaces after the point.
*/
ReplaceWhiteSpacesData GetFollowingNormalizedStringToSplitAt(
const EditorDOMPointInText& aPointToSplit) const;
/**
* Return normalized white-spaces before aPointToSplit if there are some
* collapsible white-spaces before the point.
*/
ReplaceWhiteSpacesData GetPrecedingNormalizedStringToSplitAt(
const EditorDOMPointInText& aPointToSplit) const;
/**
* ExtendRangeToDeleteWithNormalizingWhiteSpaces() is a helper method of
* DeleteTextAndNormalizeSurroundingWhiteSpaces(). This expands

View File

@@ -686,6 +686,113 @@ struct MOZ_STACK_CLASS HTMLEditor::NormalizedStringToInsertText final {
const uint32_t mNewLengthAfter = 0u;
};
/******************************************************************************
* ReplaceWhiteSpacesData stores normalized string to replace white-spaces in
* a `Text`. If ReplaceLength() returns 0, this user needs to do nothing.
******************************************************************************/
struct MOZ_STACK_CLASS HTMLEditor::ReplaceWhiteSpacesData final {
ReplaceWhiteSpacesData() = default;
ReplaceWhiteSpacesData(const nsAString& aWhiteSpaces, uint32_t aStartOffset,
uint32_t aReplaceLength)
: mNormalizedString(aWhiteSpaces),
mReplaceStartOffset(aStartOffset),
mReplaceEndOffset(aStartOffset + aReplaceLength) {
MOZ_ASSERT(ReplaceLength() >= mNormalizedString.Length());
}
ReplaceWhiteSpacesData(nsAutoString&& aWhiteSpaces, uint32_t aStartOffset,
uint32_t aReplaceLength)
: mNormalizedString(std::forward<nsAutoString>(aWhiteSpaces)),
mReplaceStartOffset(aStartOffset),
mReplaceEndOffset(aStartOffset + aReplaceLength) {
MOZ_ASSERT(ReplaceLength() >= mNormalizedString.Length());
}
ReplaceWhiteSpacesData GetMinimizedData(const Text& aText) const {
if (!ReplaceLength()) {
return *this;
}
const nsTextFragment& textFragment = aText.TextFragment();
const auto minimizedReplaceStart = [&]() -> uint32_t {
if (mNormalizedString.IsEmpty()) {
return mReplaceStartOffset;
}
const uint32_t firstDiffCharOffset =
textFragment.FindFirstDifferentCharOffset(mNormalizedString,
mReplaceStartOffset);
if (firstDiffCharOffset == nsTextFragment::kNotFound) {
// We don't need to insert new white-spaces,
return mReplaceStartOffset + mNormalizedString.Length();
}
return firstDiffCharOffset;
}();
const auto minimizedReplaceEnd = [&]() -> uint32_t {
if (mNormalizedString.IsEmpty()) {
return mReplaceEndOffset;
}
if (minimizedReplaceStart ==
mReplaceStartOffset + mNormalizedString.Length()) {
// Note that here may be invisible white-spaces before
// mReplaceEndOffset. Then, this value may be larger than
// minimizedReplaceStart.
MOZ_ASSERT(mReplaceEndOffset >= minimizedReplaceStart);
return mReplaceEndOffset;
}
if (ReplaceLength() != mNormalizedString.Length()) {
// If we're deleting some invisible white-spaces, don't shrink the end
// of the replacing range because it may shrink mNormalizedString too
// much.
return mReplaceEndOffset;
}
const auto lastDiffCharOffset =
textFragment.RFindFirstDifferentCharOffset(mNormalizedString,
mReplaceEndOffset);
MOZ_ASSERT(lastDiffCharOffset != nsTextFragment::kNotFound);
return lastDiffCharOffset == nsTextFragment::kNotFound
? mReplaceEndOffset
: lastDiffCharOffset + 1u;
}();
if (minimizedReplaceStart == mReplaceStartOffset &&
minimizedReplaceEnd == mReplaceEndOffset) {
return *this;
}
const uint32_t precedingUnnecessaryLength =
minimizedReplaceStart - mReplaceStartOffset;
const uint32_t followingUnnecessaryLength =
mReplaceEndOffset - minimizedReplaceEnd;
return ReplaceWhiteSpacesData(
Substring(mNormalizedString, precedingUnnecessaryLength,
mNormalizedString.Length() - (precedingUnnecessaryLength +
followingUnnecessaryLength)),
minimizedReplaceStart, minimizedReplaceEnd - minimizedReplaceStart);
}
[[nodiscard]] uint32_t ReplaceLength() const {
return mReplaceEndOffset - mReplaceStartOffset;
}
[[nodiscard]] uint32_t DeletingInvisibleWhiteSpaces() const {
return ReplaceLength() - mNormalizedString.Length();
}
[[nodiscard]] ReplaceWhiteSpacesData operator+(
const ReplaceWhiteSpacesData& aOther) const {
if (!ReplaceLength()) {
return aOther;
}
if (!aOther.ReplaceLength()) {
return *this;
}
MOZ_ASSERT(mReplaceEndOffset == aOther.mReplaceStartOffset);
return ReplaceWhiteSpacesData(
nsAutoString(mNormalizedString + aOther.mNormalizedString),
mReplaceStartOffset, aOther.mReplaceEndOffset);
}
nsAutoString mNormalizedString;
const uint32_t mReplaceStartOffset = 0u;
const uint32_t mReplaceEndOffset = 0u;
};
} // namespace mozilla
#endif // #ifndef HTMLEditorNestedClasses_h

View File

@@ -33,6 +33,7 @@ namespace mozilla {
using namespace dom;
using LeafNodeType = HTMLEditUtils::LeafNodeType;
using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
template nsresult WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
@@ -817,6 +818,271 @@ Result<MoveNodeResult, nsresult> WhiteSpaceVisibilityKeeper::
return std::move(unwrappedMoveContentResult);
}
// static
Result<EditorDOMPoint, nsresult>
WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt(
HTMLEditor& aHTMLEditor, const EditorDOMPointInText& aPointToSplit) {
MOZ_ASSERT(aPointToSplit.IsSetAndValid());
MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible());
if (EditorUtils::IsWhiteSpacePreformatted(
*aPointToSplit.ContainerAs<Text>())) {
return aPointToSplit.To<EditorDOMPoint>();
}
const OwningNonNull<Text> textNode = *aPointToSplit.ContainerAs<Text>();
if (!textNode->TextDataLength()) {
// Delete if it's an empty `Text` node and removable.
if (!HTMLEditUtils::IsRemovableNode(*textNode)) {
// It's logically odd to call this for non-editable `Text`, but it may
// happen if surrounding white-space sequence contains empty non-editable
// `Text`. In that case, the caller needs to normalize its preceding
// `Text` nodes too.
return EditorDOMPoint();
}
const nsCOMPtr<nsINode> parentNode = textNode->GetParentNode();
const nsCOMPtr<nsIContent> nextSibling = textNode->GetNextSibling();
nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(textNode);
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
return Err(rv);
}
if (NS_WARN_IF(nextSibling && nextSibling->GetParentNode() != parentNode)) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
return EditorDOMPoint(nextSibling);
}
const HTMLEditor::ReplaceWhiteSpacesData replacePrecedingWhiteSpacesData =
aPointToSplit.IsStartOfContainer() ||
// Chrome does not normalize the left `Text` at least when it ends
// with an NBSP.
aPointToSplit.IsPreviousCharNBSP()
? HTMLEditor::ReplaceWhiteSpacesData()
: aHTMLEditor.GetPrecedingNormalizedStringToSplitAt(aPointToSplit);
const HTMLEditor::ReplaceWhiteSpacesData replaceFollowingWhiteSpaceData =
aHTMLEditor.GetFollowingNormalizedStringToSplitAt(aPointToSplit);
const HTMLEditor::ReplaceWhiteSpacesData replaceWhiteSpacesData =
(replacePrecedingWhiteSpacesData + replaceFollowingWhiteSpaceData)
.GetMinimizedData(*textNode);
if (!replaceWhiteSpacesData.ReplaceLength()) {
return aPointToSplit.To<EditorDOMPoint>();
}
if (replaceWhiteSpacesData.mNormalizedString.IsEmpty() &&
replaceWhiteSpacesData.ReplaceLength() == textNode->TextDataLength()) {
// If there is only invisible white-spaces, mNormalizedString is empty
// string but replace length is same the the `Text` length. In this case, we
// should delete the `Text` to avoid empty `Text` to stay in the DOM tree.
const nsCOMPtr<nsINode> parentNode = textNode->GetParentNode();
const nsCOMPtr<nsIContent> nextSibling = textNode->GetNextSibling();
nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(textNode);
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
return Err(rv);
}
if (NS_WARN_IF(nextSibling && nextSibling->GetParentNode() != parentNode)) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
return EditorDOMPoint(nextSibling);
}
Result<InsertTextResult, nsresult> replaceWhiteSpacesResultOrError =
aHTMLEditor.ReplaceTextWithTransaction(textNode, replaceWhiteSpacesData);
if (MOZ_UNLIKELY(replaceWhiteSpacesResultOrError.isErr())) {
NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
return replaceWhiteSpacesResultOrError.propagateErr();
}
replaceWhiteSpacesResultOrError.unwrap().IgnoreCaretPointSuggestion();
const uint32_t offsetToSplit =
aPointToSplit.Offset() - replacePrecedingWhiteSpacesData.ReplaceLength() +
replacePrecedingWhiteSpacesData.mNormalizedString.Length();
if (NS_WARN_IF(textNode->TextDataLength() < offsetToSplit)) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
return EditorDOMPoint(textNode, offsetToSplit);
}
// static
Result<EditorDOMPoint, nsresult>
WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt(
HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToSplit) {
MOZ_ASSERT(aPointToSplit.IsSet());
MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible());
// If the insertion point is not in composed doc, we're probably initializing
// an element which will be inserted. In such case, the caller should own the
// responsibility for normalizing the white-spaces.
if (!aPointToSplit.IsInComposedDoc()) {
return aPointToSplit;
}
EditorDOMPoint pointToSplit(aPointToSplit);
{
AutoTrackDOMPoint trackPointToSplit(aHTMLEditor.RangeUpdaterRef(),
&pointToSplit);
Result<EditorDOMPoint, nsresult> pointToSplitOrError =
WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces(aHTMLEditor,
pointToSplit);
if (MOZ_UNLIKELY(pointToSplitOrError.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpaces() failed");
return pointToSplitOrError.propagateErr();
}
}
if (NS_WARN_IF(!pointToSplit.IsInContentNode())) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
if (pointToSplit.IsInTextNode()) {
Result<EditorDOMPoint, nsresult> pointToSplitOrError =
WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt(
aHTMLEditor, pointToSplit.AsInText());
if (MOZ_UNLIKELY(pointToSplitOrError.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt() "
"failed");
return pointToSplitOrError.propagateErr();
}
pointToSplit = pointToSplitOrError.unwrap().To<EditorDOMPoint>();
if (NS_WARN_IF(!pointToSplit.IsInContentNode())) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
// If we normalize white-spaces in middle of the `Text`, we don't need to
// touch surrounding `Text` nodes.
if (pointToSplit.IsMiddleOfContainer()) {
return pointToSplit;
}
}
// Preceding and/or following white-space sequence may be across multiple
// `Text` nodes. Then, they may become unexpectedly visible without
// normalizing the white-spaces. Therefore, we need to list up all possible
// `Text` nodes first. Then, normalize them unless the `Text` is not
const RefPtr<Element> closestBlockElement =
HTMLEditUtils::GetInclusiveAncestorElement(
*pointToSplit.ContainerAs<nsIContent>(),
HTMLEditUtils::ClosestBlockElement,
BlockInlineCheck::UseComputedDisplayStyle);
AutoTArray<OwningNonNull<Text>, 3> precedingTextNodes, followingTextNodes;
if (!pointToSplit.IsInTextNode() || pointToSplit.IsStartOfContainer()) {
for (nsCOMPtr<nsIContent> previousContent =
HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
pointToSplit, {LeafNodeType::LeafNodeOrChildBlock},
BlockInlineCheck::UseComputedDisplayStyle,
closestBlockElement);
previousContent;
previousContent =
HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
*previousContent, {LeafNodeType::LeafNodeOrChildBlock},
BlockInlineCheck::UseComputedDisplayStyle,
closestBlockElement)) {
if (auto* const textNode = Text::FromNode(previousContent)) {
if (!HTMLEditUtils::IsSimplyEditableNode(*textNode) &&
textNode->TextDataLength()) {
break;
}
// Chrome does not normalize preceding `Text` at least when it ends with
// an NBSP.
if (textNode->TextDataLength() &&
textNode->TextFragment().CharAt(textNode->TextLength() - 1u) ==
HTMLEditUtils::kNBSP) {
break;
}
precedingTextNodes.AppendElement(*textNode);
if (textNode->TextIsOnlyWhitespace()) {
// white-space only `Text` will be removed, so, we need to check
// preceding one too.
continue;
}
break;
}
if (auto* const element = Element::FromNode(previousContent)) {
if (HTMLEditUtils::IsBlockElement(
*element, BlockInlineCheck::UseComputedDisplayStyle) ||
HTMLEditUtils::IsNonEditableReplacedContent(*element)) {
break;
}
// Ignore invisible inline elements
}
}
}
if (!pointToSplit.IsInTextNode() || pointToSplit.IsEndOfContainer()) {
for (nsCOMPtr<nsIContent> nextContent =
HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
pointToSplit, {LeafNodeType::LeafNodeOrChildBlock},
BlockInlineCheck::UseComputedDisplayStyle,
closestBlockElement);
nextContent;
nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
*nextContent, {LeafNodeType::LeafNodeOrChildBlock},
BlockInlineCheck::UseComputedDisplayStyle, closestBlockElement)) {
if (auto* const textNode = Text::FromNode(nextContent)) {
if (!HTMLEditUtils::IsSimplyEditableNode(*textNode) &&
textNode->TextDataLength()) {
break;
}
followingTextNodes.AppendElement(*textNode);
if (textNode->TextIsOnlyWhitespace() &&
EditorUtils::IsWhiteSpacePreformatted(*textNode)) {
// white-space only `Text` will be removed, so, we need to check next
// one too.
continue;
}
break;
}
if (auto* const element = Element::FromNode(nextContent)) {
if (HTMLEditUtils::IsBlockElement(
*element, BlockInlineCheck::UseComputedDisplayStyle) ||
HTMLEditUtils::IsNonEditableReplacedContent(*element)) {
break;
}
// Ignore invisible inline elements
}
}
}
AutoTrackDOMPoint trackPointToSplit(aHTMLEditor.RangeUpdaterRef(),
&pointToSplit);
for (const auto& textNode : precedingTextNodes) {
Result<EditorDOMPoint, nsresult> normalizeWhiteSpacesResultOrError =
WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt(
aHTMLEditor, EditorDOMPointInText::AtEndOf(textNode));
if (MOZ_UNLIKELY(normalizeWhiteSpacesResultOrError.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt() "
"failed");
return normalizeWhiteSpacesResultOrError.propagateErr();
}
if (normalizeWhiteSpacesResultOrError.inspect().IsInTextNode() &&
!normalizeWhiteSpacesResultOrError.inspect().IsStartOfContainer()) {
// The white-space sequence started from middle of this node, so, we need
// to do this for the preceding nodes.
break;
}
}
for (const auto& textNode : followingTextNodes) {
Result<EditorDOMPoint, nsresult> normalizeWhiteSpacesResultOrError =
WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt(
aHTMLEditor, EditorDOMPointInText(textNode, 0u));
if (MOZ_UNLIKELY(normalizeWhiteSpacesResultOrError.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitTextNodeAt() "
"failed");
return normalizeWhiteSpacesResultOrError.propagateErr();
}
if (normalizeWhiteSpacesResultOrError.inspect().IsInTextNode() &&
!normalizeWhiteSpacesResultOrError.inspect().IsEndOfContainer()) {
// The white-space sequence ended in middle of this node, so, we need
// to do this for the following nodes.
break;
}
}
trackPointToSplit.FlushAndStopTracking();
if (NS_WARN_IF(!pointToSplit.IsInContentNode())) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
return std::move(pointToSplit);
}
// static
Result<CreateLineBreakResult, nsresult>
WhiteSpaceVisibilityKeeper::InsertLineBreak(
@@ -826,6 +1092,9 @@ WhiteSpaceVisibilityKeeper::InsertLineBreak(
return Err(NS_ERROR_INVALID_ARG);
}
EditorDOMPoint pointToInsert(aPointToInsert);
// 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.
// meanwhile, the pre case is handled in HandleInsertText() in
// HTMLEditSubActionHandler.cpp
@@ -841,7 +1110,8 @@ WhiteSpaceVisibilityKeeper::InsertLineBreak(
.GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt(aPointToInsert);
EditorDOMRange invisibleTrailingWhiteSpaceRangeOfCurrentLine =
textFragmentDataAtInsertionPoint
.GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt(aPointToInsert);
.GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt(
aPointToInsert);
const Maybe<const VisibleWhiteSpacesData> visibleWhiteSpaces =
!invisibleLeadingWhiteSpaceRangeOfNewLine.IsPositioned() ||
!invisibleTrailingWhiteSpaceRangeOfCurrentLine.IsPositioned()
@@ -852,10 +1122,10 @@ WhiteSpaceVisibilityKeeper::InsertLineBreak(
? visibleWhiteSpaces.ref().ComparePoint(aPointToInsert)
: PointPosition::NotInSameDOMTree;
EditorDOMPoint pointToInsert(aPointToInsert);
EditorDOMPoint atNBSPReplaceableWithSP;
if (!invisibleLeadingWhiteSpaceRangeOfNewLine.IsPositioned() &&
(pointPositionWithVisibleWhiteSpaces == PointPosition::MiddleOfFragment ||
(pointPositionWithVisibleWhiteSpaces ==
PointPosition::MiddleOfFragment ||
pointPositionWithVisibleWhiteSpaces == PointPosition::EndOfFragment)) {
atNBSPReplaceableWithSP =
textFragmentDataAtInsertionPoint
@@ -936,8 +1206,8 @@ WhiteSpaceVisibilityKeeper::InsertLineBreak(
textFragmentDataAtInsertionPoint
.GetEndOfCollapsibleASCIIWhiteSpaces<EditorDOMPointInText>(
atNextCharOfInsertionPoint, nsIEditor::eNone,
// XXX Shouldn't be "No"? Skipping non-editable nodes may
// have visible content.
// XXX Shouldn't be "No"? Skipping non-editable nodes
// may have visible content.
IgnoreNonEditableNodes::Yes);
nsresult rv =
WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes(
@@ -1009,7 +1279,8 @@ WhiteSpaceVisibilityKeeper::InsertLineBreak(
*atNBSPReplacedWithASCIIWhiteSpace.ContainerAs<Text>()),
atNBSPReplacedWithASCIIWhiteSpace.Offset(), 1, u" "_ns);
if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed failed");
NS_WARNING(
"HTMLEditor::ReplaceTextWithTransaction() failed failed");
return replaceTextResult.propagateErr();
}
// Ignore caret suggestion because there was
@@ -1023,6 +1294,21 @@ WhiteSpaceVisibilityKeeper::InsertLineBreak(
}
}
}
} else {
Result<EditorDOMPoint, nsresult>
normalizeSurroundingWhiteSpacesResultOrError =
WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt(
aHTMLEditor, aPointToInsert);
if (MOZ_UNLIKELY(normalizeSurroundingWhiteSpacesResultOrError.isErr())) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt() failed");
return normalizeSurroundingWhiteSpacesResultOrError.propagateErr();
}
pointToInsert = normalizeSurroundingWhiteSpacesResultOrError.unwrap();
if (NS_WARN_IF(!pointToInsert.IsSet())) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
}
Result<CreateLineBreakResult, nsresult> insertBRElementResultOrError =
aHTMLEditor.InsertLineBreak(WithTransaction::Yes, aLineBreakType,

View File

@@ -130,6 +130,15 @@ class WhiteSpaceVisibilityKeeper final {
const EditorDOMPoint& aPointToSplit,
const Element& aSplittingBlockElement);
/**
* Normalize surrounding white-spaces of aPointToSplit. This may normalize
* 2 `Text` nodes if the point is surrounded by them.
* Note that this is designed only for the new normalizer.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<EditorDOMPoint, nsresult>
NormalizeWhiteSpacesToSplitAt(HTMLEditor& aHTMLEditor,
const EditorDOMPoint& aPointToSplit);
/**
* MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement() merges
* first line in aRightBlockElement into end of aLeftBlockElement which
@@ -347,6 +356,17 @@ class WhiteSpaceVisibilityKeeper final {
HTMLEditor& aHTMLEditor, const EditorDOMRangeInTexts& aRangeToReplace,
const nsAString& aReplaceString);
/**
* Normalize surrounding white-spaces of aPointToSplit.
*
* @return The split point which you specified before. Note that the result
* may be different from aPointToSplit if this deletes some invisible
* white-spaces.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<EditorDOMPoint, nsresult>
NormalizeWhiteSpacesToSplitTextNodeAt(
HTMLEditor& aHTMLEditor, const EditorDOMPointInText& aPointToSplit);
/**
* Delete leading or trailing invisible white-spaces around block boundaries
* or collapsed white-spaces in a white-space sequence if aPoint is around

View File

@@ -1,24 +0,0 @@
[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