Bug 1940377 - part 7: Make HTMLEditor normalize surrounding white-spaces of commit string r=m_kato
For avoiding invisible white-space issues around composition string and for avoiding making `CompositionTransaction` confused, we normalize white-spaces around composition string boundaries with using NBSPs more than usual. Therefore, we need to recover them as usual when ending the composition. Differential Revision: https://phabricator.services.mozilla.com/D239469
This commit is contained in:
@@ -1194,7 +1194,7 @@ Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
|
|||||||
NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed");
|
NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed");
|
||||||
return insertEmptyTextResultOrError.propagateErr();
|
return insertEmptyTextResultOrError.propagateErr();
|
||||||
}
|
}
|
||||||
const InsertTextResult insertEmptyTextResult =
|
InsertTextResult insertEmptyTextResult =
|
||||||
insertEmptyTextResultOrError.unwrap();
|
insertEmptyTextResultOrError.unwrap();
|
||||||
nsresult rv = EnsureNoFollowingUnnecessaryLineBreak(
|
nsresult rv = EnsureNoFollowingUnnecessaryLineBreak(
|
||||||
insertEmptyTextResult.EndOfInsertedTextRef());
|
insertEmptyTextResult.EndOfInsertedTextRef());
|
||||||
@@ -1203,6 +1203,26 @@ Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
|
|||||||
"HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed");
|
"HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed");
|
||||||
return Err(rv);
|
return Err(rv);
|
||||||
}
|
}
|
||||||
|
const EditorDOMPoint& endOfInsertedText =
|
||||||
|
insertEmptyTextResult.EndOfInsertedTextRef();
|
||||||
|
if (StaticPrefs::editor_white_space_normalization_blink_compatible() &&
|
||||||
|
endOfInsertedText.IsInTextNode() &&
|
||||||
|
!endOfInsertedText.IsStartOfContainer()) {
|
||||||
|
nsresult rv = WhiteSpaceVisibilityKeeper::
|
||||||
|
NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces(
|
||||||
|
*this, endOfInsertedText.AsInText().PreviousPoint());
|
||||||
|
if (NS_FAILED(rv)) {
|
||||||
|
NS_WARNING(
|
||||||
|
"WhiteSpaceVisibilityKeeper::"
|
||||||
|
"NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces()"
|
||||||
|
" failed");
|
||||||
|
return Err(rv);
|
||||||
|
}
|
||||||
|
if (NS_WARN_IF(
|
||||||
|
!endOfInsertedText.IsInContentNodeAndValidInComposedDoc())) {
|
||||||
|
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
|
||||||
|
}
|
||||||
|
}
|
||||||
// If we replaced non-empty composition string with an empty string,
|
// If we replaced non-empty composition string with an empty string,
|
||||||
// its preceding character may be a collapsible ASCII white-space.
|
// its preceding character may be a collapsible ASCII white-space.
|
||||||
// Therefore, we may need to insert a padding <br> after the white-space.
|
// Therefore, we may need to insert a padding <br> after the white-space.
|
||||||
|
|||||||
@@ -2895,6 +2895,52 @@ WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString(
|
|||||||
!pointToInsert.IsSetAndValidInComposedDoc())) {
|
!pointToInsert.IsSetAndValidInComposedDoc())) {
|
||||||
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
|
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
|
||||||
}
|
}
|
||||||
|
// If we're starting composition, we won't normalizing surrounding
|
||||||
|
// white-spaces until end of the composition. Additionally, at that time,
|
||||||
|
// we need to assume all white-spaces of surrounding white-spaces are
|
||||||
|
// visible because canceling composition may cause previous white-space
|
||||||
|
// invisible temporarily. Therefore, we should normalize surrounding
|
||||||
|
// white-spaces to delete invisible white-spaces contained in the sequence.
|
||||||
|
// E.g., `NBSP SP SP NBSP`, in this case, one of the SP is invisible.
|
||||||
|
if (EditorBase::InsertingTextForStartingComposition(aPurpose) &&
|
||||||
|
pointToInsert.IsInTextNode()) {
|
||||||
|
const auto whiteSpaceOffset = [&]() -> Maybe<uint32_t> {
|
||||||
|
if (!pointToInsert.IsEndOfContainer() &&
|
||||||
|
pointToInsert.IsCharCollapsibleASCIISpaceOrNBSP()) {
|
||||||
|
return Some(pointToInsert.Offset());
|
||||||
|
}
|
||||||
|
if (!pointToInsert.IsStartOfContainer() &&
|
||||||
|
pointToInsert.IsPreviousCharCollapsibleASCIISpaceOrNBSP()) {
|
||||||
|
return Some(pointToInsert.Offset() - 1u);
|
||||||
|
}
|
||||||
|
return Nothing();
|
||||||
|
}();
|
||||||
|
if (whiteSpaceOffset.isSome()) {
|
||||||
|
Maybe<AutoTrackDOMPoint> trackPointToInsert;
|
||||||
|
if (pointToInsert.Offset() != *whiteSpaceOffset) {
|
||||||
|
trackPointToInsert.emplace(aHTMLEditor.RangeUpdaterRef(),
|
||||||
|
&pointToInsert);
|
||||||
|
}
|
||||||
|
Result<EditorDOMPoint, nsresult> pointToInsertOrError =
|
||||||
|
WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAt(
|
||||||
|
aHTMLEditor,
|
||||||
|
EditorDOMPointInText(pointToInsert.ContainerAs<Text>(),
|
||||||
|
*whiteSpaceOffset));
|
||||||
|
if (MOZ_UNLIKELY(pointToInsertOrError.isErr())) {
|
||||||
|
NS_WARNING(
|
||||||
|
"WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAt() failed");
|
||||||
|
return pointToInsertOrError.propagateErr();
|
||||||
|
}
|
||||||
|
if (trackPointToInsert.isSome()) {
|
||||||
|
trackPointToInsert.reset();
|
||||||
|
} else {
|
||||||
|
pointToInsert = pointToInsertOrError.unwrap();
|
||||||
|
}
|
||||||
|
if (NS_WARN_IF(!pointToInsert.IsInContentNodeAndValidInComposedDoc())) {
|
||||||
|
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (NS_WARN_IF(!pointToInsert.IsInContentNode())) {
|
if (NS_WARN_IF(!pointToInsert.IsInContentNode())) {
|
||||||
@@ -2932,12 +2978,160 @@ WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString(
|
|||||||
Result<InsertTextResult, nsresult> insertOrReplaceTextResultOrError =
|
Result<InsertTextResult, nsresult> insertOrReplaceTextResultOrError =
|
||||||
aHTMLEditor.InsertOrReplaceTextWithTransaction(pointToInsert,
|
aHTMLEditor.InsertOrReplaceTextWithTransaction(pointToInsert,
|
||||||
insertTextData);
|
insertTextData);
|
||||||
NS_WARNING_ASSERTION(insertOrReplaceTextResultOrError.isOk(),
|
if (MOZ_UNLIKELY(insertOrReplaceTextResultOrError.isErr())) {
|
||||||
"HTMLEditor::ReplaceTextWithTransaction() failed");
|
NS_WARNING("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;
|
return insertOrReplaceTextResultOrError;
|
||||||
|
}
|
||||||
|
// If the composition is committed, we should normalize surrounding
|
||||||
|
// white-spaces of the commit string.
|
||||||
|
if (aPurpose != InsertTextFor::CompositionEnd &&
|
||||||
|
aPurpose != InsertTextFor::CompositionStartAndEnd) {
|
||||||
|
return insertOrReplaceTextResultOrError;
|
||||||
|
}
|
||||||
|
InsertTextResult insertOrReplaceTextResult =
|
||||||
|
insertOrReplaceTextResultOrError.unwrap();
|
||||||
|
const EditorDOMPointInText endOfCommitString =
|
||||||
|
insertOrReplaceTextResult.EndOfInsertedTextRef().GetAsInText();
|
||||||
|
if (!endOfCommitString.IsSet() || endOfCommitString.IsContainerEmpty()) {
|
||||||
|
return std::move(insertOrReplaceTextResult);
|
||||||
|
}
|
||||||
|
if (NS_WARN_IF(endOfCommitString.Offset() <
|
||||||
|
insertTextData.mNormalizedString.Length())) {
|
||||||
|
insertOrReplaceTextResult.IgnoreCaretPointSuggestion();
|
||||||
|
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
|
||||||
|
}
|
||||||
|
const EditorDOMPointInText startOfCommitString(
|
||||||
|
endOfCommitString.ContainerAs<Text>(),
|
||||||
|
endOfCommitString.Offset() - insertTextData.mNormalizedString.Length());
|
||||||
|
MOZ_ASSERT(insertOrReplaceTextResult.EndOfInsertedTextRef() ==
|
||||||
|
insertOrReplaceTextResult.CaretPointRef());
|
||||||
|
EditorDOMPoint pointToPutCaret = insertOrReplaceTextResult.UnwrapCaretPoint();
|
||||||
|
// First, normalize the trailing white-spaces if there is. Note that its
|
||||||
|
// sequence may start from before the commit string. In such case, the
|
||||||
|
// another call of NormalizeWhiteSpacesAt() won't update the DOM.
|
||||||
|
if (endOfCommitString.IsMiddleOfContainer()) {
|
||||||
|
nsresult rv = WhiteSpaceVisibilityKeeper::
|
||||||
|
NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces(
|
||||||
|
aHTMLEditor, endOfCommitString.PreviousPoint());
|
||||||
|
if (NS_FAILED(rv)) {
|
||||||
|
NS_WARNING(
|
||||||
|
"WhiteSpaceVisibilityKeeper::"
|
||||||
|
"NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces() "
|
||||||
|
"failed");
|
||||||
|
return Err(rv);
|
||||||
|
}
|
||||||
|
if (NS_WARN_IF(!pointToPutCaret.IsSetAndValidInComposedDoc())) {
|
||||||
|
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Finally, normalize the leading white-spaces if there is and not a part of
|
||||||
|
// the trailing white-spaces.
|
||||||
|
if (!startOfCommitString.IsStartOfContainer()) {
|
||||||
|
nsresult rv = WhiteSpaceVisibilityKeeper::
|
||||||
|
NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces(
|
||||||
|
aHTMLEditor, startOfCommitString.PreviousPoint());
|
||||||
|
if (NS_FAILED(rv)) {
|
||||||
|
NS_WARNING(
|
||||||
|
"WhiteSpaceVisibilityKeeper::"
|
||||||
|
"NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces() "
|
||||||
|
"failed");
|
||||||
|
return Err(rv);
|
||||||
|
}
|
||||||
|
if (NS_WARN_IF(!pointToPutCaret.IsSetAndValidInComposedDoc())) {
|
||||||
|
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EditorDOMPoint endOfCommitStringAfterNormalized = pointToPutCaret;
|
||||||
|
return InsertTextResult(std::move(endOfCommitStringAfterNormalized),
|
||||||
|
CaretPoint(std::move(pointToPutCaret)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
nsresult WhiteSpaceVisibilityKeeper::
|
||||||
|
NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces(
|
||||||
|
HTMLEditor& aHTMLEditor, const EditorDOMPointInText& aPoint) {
|
||||||
|
MOZ_ASSERT(StaticPrefs::editor_white_space_normalization_blink_compatible());
|
||||||
|
MOZ_ASSERT(aPoint.IsSet());
|
||||||
|
MOZ_ASSERT(!aPoint.IsEndOfContainer());
|
||||||
|
|
||||||
|
if (EditorUtils::IsWhiteSpacePreformatted(*aPoint.ContainerAs<Text>())) {
|
||||||
|
return NS_OK;
|
||||||
|
}
|
||||||
|
Text& textNode = *aPoint.ContainerAs<Text>();
|
||||||
|
const bool isNewLinePreformatted =
|
||||||
|
EditorUtils::IsNewLinePreformatted(textNode);
|
||||||
|
const auto IsCollapsibleChar = [&](char16_t aChar) {
|
||||||
|
return aChar == HTMLEditUtils::kNewLine ? !isNewLinePreformatted
|
||||||
|
: nsCRT::IsAsciiSpace(aChar);
|
||||||
|
};
|
||||||
|
const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) {
|
||||||
|
return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar);
|
||||||
|
};
|
||||||
|
const auto whiteSpaceOffset = [&]() -> Maybe<uint32_t> {
|
||||||
|
if (IsCollapsibleCharOrNBSP(aPoint.Char())) {
|
||||||
|
return Some(aPoint.Offset());
|
||||||
|
}
|
||||||
|
if (!aPoint.IsAtLastContent() &&
|
||||||
|
IsCollapsibleCharOrNBSP(aPoint.NextChar())) {
|
||||||
|
return Some(aPoint.Offset() + 1u);
|
||||||
|
}
|
||||||
|
return Nothing();
|
||||||
|
}();
|
||||||
|
if (whiteSpaceOffset.isNothing()) {
|
||||||
|
return NS_OK;
|
||||||
|
}
|
||||||
|
const uint32_t firstOffset = [&]() {
|
||||||
|
for (const uint32_t offset : Reversed(IntegerRange(*whiteSpaceOffset))) {
|
||||||
|
if (!IsCollapsibleCharOrNBSP(textNode.TextFragment().CharAt(offset))) {
|
||||||
|
return offset + 1u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0u;
|
||||||
|
}();
|
||||||
|
const uint32_t endOffset = [&]() {
|
||||||
|
for (const uint32_t offset :
|
||||||
|
IntegerRange(*whiteSpaceOffset + 1, textNode.TextDataLength())) {
|
||||||
|
if (!IsCollapsibleCharOrNBSP(textNode.TextFragment().CharAt(offset))) {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return textNode.TextDataLength();
|
||||||
|
}();
|
||||||
|
nsAutoString normalizedString;
|
||||||
|
const char16_t precedingChar =
|
||||||
|
!firstOffset ? static_cast<char16_t>(0)
|
||||||
|
: textNode.TextFragment().CharAt(firstOffset - 1u);
|
||||||
|
const char16_t followingChar =
|
||||||
|
endOffset == textNode.TextDataLength()
|
||||||
|
? static_cast<char16_t>(0)
|
||||||
|
: textNode.TextFragment().CharAt(endOffset);
|
||||||
|
HTMLEditor::GenerateWhiteSpaceSequence(
|
||||||
|
normalizedString, endOffset - firstOffset,
|
||||||
|
!firstOffset ? HTMLEditor::CharPointData::InSameTextNode(
|
||||||
|
HTMLEditor::CharPointType::TextEnd)
|
||||||
|
: HTMLEditor::CharPointData::InSameTextNode(
|
||||||
|
precedingChar == HTMLEditUtils::kNewLine
|
||||||
|
? HTMLEditor::CharPointType::PreformattedLineBreak
|
||||||
|
: HTMLEditor::CharPointType::VisibleChar),
|
||||||
|
endOffset == textNode.TextDataLength()
|
||||||
|
? HTMLEditor::CharPointData::InSameTextNode(
|
||||||
|
HTMLEditor::CharPointType::TextEnd)
|
||||||
|
: HTMLEditor::CharPointData::InSameTextNode(
|
||||||
|
followingChar == HTMLEditUtils::kNewLine
|
||||||
|
? HTMLEditor::CharPointType::PreformattedLineBreak
|
||||||
|
: HTMLEditor::CharPointType::VisibleChar));
|
||||||
|
MOZ_ASSERT(normalizedString.Length() == endOffset - firstOffset);
|
||||||
|
const OwningNonNull<Text> text(textNode);
|
||||||
|
Result<InsertTextResult, nsresult> normalizeWhiteSpaceSequenceResultOrError =
|
||||||
|
aHTMLEditor.ReplaceTextWithTransaction(
|
||||||
|
text, firstOffset, endOffset - firstOffset, normalizedString);
|
||||||
|
if (MOZ_UNLIKELY(normalizeWhiteSpaceSequenceResultOrError.isErr())) {
|
||||||
|
NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
|
||||||
|
return normalizeWhiteSpaceSequenceResultOrError.unwrapErr();
|
||||||
|
}
|
||||||
|
normalizeWhiteSpaceSequenceResultOrError.unwrap()
|
||||||
|
.IgnoreCaretPointSuggestion();
|
||||||
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// static
|
// static
|
||||||
|
|||||||
@@ -320,6 +320,14 @@ class WhiteSpaceVisibilityKeeper final {
|
|||||||
HTMLEditor::InsertTextTo::ExistingTextNodeIfAvailable, aPurpose);
|
HTMLEditor::InsertTextTo::ExistingTextNodeIfAvailable, aPurpose);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize white-space sequence containing aPoint or starts from next to
|
||||||
|
* aPoint. This assumes all white-spaces in the sequence is visible.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult
|
||||||
|
NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces(
|
||||||
|
HTMLEditor& aHTMLEditor, const EditorDOMPointInText& aPoint);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete previous white-space of aPoint. This automatically keeps visibility
|
* Delete previous white-space of aPoint. This automatically keeps visibility
|
||||||
* of white-spaces around aPoint. E.g., may remove invisible leading
|
* of white-spaces around aPoint. E.g., may remove invisible leading
|
||||||
|
|||||||
Reference in New Issue
Block a user