1
0
Fork 0
firefox/dom/base/TextDirectiveCreator.cpp
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

1086 lines
46 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "TextDirectiveCreator.h"
#include "TextDirectiveUtil.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/ResultVariant.h"
#include "mozilla/dom/fragmentdirectives_ffi_generated.h"
#include "mozilla/glean/DomMetrics.h"
#include "nsDeque.h"
#include "nsINode.h"
#include "nsRange.h"
#include "Document.h"
namespace mozilla::dom {
Result<const nsString*, ErrorResult> RangeContentCache::GetOrCreate(
nsRange* aRange) {
if (!aRange) {
return nullptr;
}
if (const auto rangeContent = mCache.Lookup(aRange)) {
return rangeContent->get();
}
return TextDirectiveUtil::RangeContentAsFoldCase(aRange).andThen(
[cache = &mCache, range = RefPtr(aRange)](
nsString&& content) -> Result<const nsString*, ErrorResult> {
return cache
->InsertOrUpdate(range, MakeUnique<nsString>(std::move(content)))
.get();
});
}
TextDirectiveCandidate::TextDirectiveCandidate(
nsRange* aStartRange, nsRange* aFullStartRange, nsRange* aEndRange,
nsRange* aFullEndRange, nsRange* aPrefixRange, nsRange* aFullPrefixRange,
nsRange* aSuffixRange, nsRange* aFullSuffixRange)
: mStartRange(aStartRange),
mFullStartRange(aFullStartRange),
mEndRange(aEndRange),
mFullEndRange(aFullEndRange),
mPrefixRange(aPrefixRange),
mFullPrefixRange(aFullPrefixRange),
mSuffixRange(aSuffixRange),
mFullSuffixRange(aFullSuffixRange) {}
/* static */
Result<TextDirectiveCandidate, ErrorResult>
TextDirectiveCandidate::CreateFromInputRange(
const nsRange* aInputRange, RangeContentCache& aRangeContentCache) {
MOZ_ASSERT(aInputRange);
ErrorResult rv;
// To create a fully-populated text directive candidate from a given Range
// `inputRange`, follow these steps:
// 1. Let `fullStartRange`, `fullEndRange`, `startRange` and `endRange` be
// Ranges, initially null.
// 2. Let `firstBlockBoundary` be the result of searching for the first block
// boundary inside of `inputRange`.
// 3. If `firstBlockBoundary` is not null:
// 3.1. Set the boundary points of `fullStartRange` to the start point of
// `inputRange` and the found block boundary.
Result<RefPtr<nsRange>, ErrorResult> maybeFullStartRange =
MaybeCreateStartToBlockBoundaryRange(*aInputRange);
if (MOZ_UNLIKELY(maybeFullStartRange.isErr())) {
return maybeFullStartRange.propagateErr();
}
RefPtr<nsRange> fullStartRange = maybeFullStartRange.unwrap();
RefPtr<nsRange> fullEndRange;
// 4. Let `useExactMatching` be true if `fullStartRange` is null.
// Note: This means that the context terms for this candidate will only be
// prefix and suffix, instead of also start and end.
const bool useExactMatching = !fullStartRange;
RefPtr<nsRange> startRange;
RefPtr<nsRange> endRange;
// 5. If `useExactMatching` is true:
if (useExactMatching) {
// 5.1 Let `startRange` be a Range with the same boundary points as
// `inputRange`.
startRange = aInputRange->CloneRange();
} else {
// 6. Otherwise:
// 6.1 Search for the first block boundary inside of `inputRange`, starting
// from `inputRange`s end point.
// 6.2 Assert: There is a block boundary in `inputRange`.
// Note: Since searching from `inputRange`s start found a block boundary,
// searching from the end also must find one.
// 6.3 Let `fullEndRange` be a range starting at lastBlockBoundary and
// ending at `inputRange`s end point.
Result<RefPtr<nsRange>, ErrorResult> maybeFullEndRange =
MaybeCreateEndToBlockBoundaryRange(*aInputRange);
if (MOZ_UNLIKELY(maybeFullEndRange.isErr())) {
return maybeFullEndRange.propagateErr();
}
fullEndRange = maybeFullEndRange.unwrap();
MOZ_ASSERT(fullEndRange,
"Searching from start found a range boundary in the range, so "
"searching from the end must find one as well");
// 6.4 Let `startRange` be a Range which starts at the start of `inputRange`
// and ends at the first word boundary after `inputRange`s start point.
startRange =
nsRange::Create(aInputRange->StartRef(),
TextDirectiveUtil::MoveRangeBoundaryOneWord(
aInputRange->StartRef(), TextScanDirection::Right),
rv);
if (MOZ_UNLIKELY(rv.Failed())) {
return Err(std::move(rv));
}
// 6.5 Let `endRange` be a Range which starts at the beginning of the last
// word of `inputRange` and ends at the end of `inputRange`.
endRange =
nsRange::Create(TextDirectiveUtil::MoveRangeBoundaryOneWord(
aInputRange->EndRef(), TextScanDirection::Left),
aInputRange->EndRef(), rv);
if (rv.Failed()) {
return Err(std::move(rv));
}
}
// 7. Let `prefixRange` be a Range, collapsed at the start point of
// `inputRange`.
// 8. Let `prefixBlockBoundary` be the result of finding the next block
// boundary, starting at `inputRange`s start point and going to the left.
// Note: If `inputRange`s start point is a block boundary, this should extend
// to the _next_ block boundary.
// 9. Let `fullPrefixRange` be a Range which starts at
// `prefixBlockBoundary` and ends at `inputRange`s start point.
auto prefixRanges = CreatePrefixRanges(startRange->StartRef());
if (MOZ_UNLIKELY(prefixRanges.isErr())) {
return prefixRanges.propagateErr();
}
auto [prefixRange, fullPrefixRange] = prefixRanges.unwrap();
// 10. Let `suffixRange` be a Range, collapsed at the end point of
// `inputRange`.
// 11. Let `suffixBlockBoundary` be the result of finding the next block
// boundary, starting at `inputRange`s end point and going to the right.
// Note: If `inputRange`s end point is a block boundary, this should extend to
// the _next_ block boundary.
// 12. Let `fullSuffixRange` be a Range which starts at `inputRange`s end
// point and ends at `suffixBlockBoundary`.
auto suffixRanges =
CreateSuffixRanges(endRange ? endRange->EndRef() : startRange->EndRef());
if (MOZ_UNLIKELY(suffixRanges.isErr())) {
return suffixRanges.propagateErr();
}
auto [suffixRange, fullSuffixRange] = suffixRanges.unwrap();
// 13. Return a structure which contains `startRange`, `fullStartRange`,
// `endRange`, `fullEndRange`, `prefixRange`, `fullPrefixRange`, `suffixRange`
// and `fullSuffixRange`.
auto instance = TextDirectiveCandidate{
startRange, fullStartRange, endRange, fullEndRange,
prefixRange, fullPrefixRange, suffixRange, fullSuffixRange};
MOZ_TRY(instance.CreateFoldCaseContents(aRangeContentCache));
MOZ_TRY(instance.CreateTextDirectiveString());
return instance;
}
/* static */
Result<TextDirectiveCandidate, ErrorResult>
TextDirectiveCandidate::CreateFromStartAndEndRange(
const nsRange* aStartRange, const nsRange* aEndRange,
RangeContentCache& aRangeContentCache) {
MOZ_ASSERT(aStartRange);
MOZ_ASSERT(aEndRange);
ErrorResult rv;
RefPtr<nsRange> startRange = aStartRange->CloneRange();
RefPtr<nsRange> endRange = aEndRange->CloneRange();
RefPtr<nsRange> fullRange =
nsRange::Create(startRange->StartRef(), endRange->EndRef(), rv);
if (MOZ_UNLIKELY(rv.Failed())) {
return Err(std::move(rv));
}
Result<RefPtr<nsRange>, ErrorResult> maybeFullStartRange =
MaybeCreateStartToBlockBoundaryRange(*fullRange);
if (MOZ_UNLIKELY(maybeFullStartRange.isErr())) {
return maybeFullStartRange.propagateErr();
}
RefPtr<nsRange> fullStartRange = maybeFullStartRange.unwrap();
// This function is called in a context where there is not necessarily a block
// boundary between `aStartRange` and `aEndRange`.
// Therefore, it is not an error if `fullStartRange` is null (unlike in
// `CreateFromInputRange()`).
// If there is no block boundary, use the full range as `fullStartRange` and
// `fullEndRange`.
if (!fullStartRange) {
fullStartRange = fullRange->CloneRange();
}
Result<RefPtr<nsRange>, ErrorResult> maybeFullEndRange =
MaybeCreateEndToBlockBoundaryRange(*fullRange);
if (MOZ_UNLIKELY(maybeFullEndRange.isErr())) {
return maybeFullStartRange.propagateErr();
}
RefPtr<nsRange> fullEndRange = maybeFullStartRange.unwrap();
if (!fullEndRange) {
fullEndRange = fullRange->CloneRange();
}
auto prefixRanges = CreatePrefixRanges(startRange->StartRef());
if (MOZ_UNLIKELY(prefixRanges.isErr())) {
return prefixRanges.propagateErr();
}
auto [prefixRange, fullPrefixRange] = prefixRanges.unwrap();
auto suffixRanges = CreateSuffixRanges(endRange->EndRef());
if (MOZ_UNLIKELY(suffixRanges.isErr())) {
return suffixRanges.propagateErr();
}
auto [suffixRange, fullSuffixRange] = suffixRanges.unwrap();
auto instance = TextDirectiveCandidate{
startRange, fullStartRange, endRange, fullEndRange,
prefixRange, fullPrefixRange, suffixRange, fullSuffixRange};
MOZ_TRY(instance.CreateFoldCaseContents(aRangeContentCache));
MOZ_TRY(instance.CreateTextDirectiveString());
return instance;
}
Result<TextDirectiveCandidate, ErrorResult> TextDirectiveCandidate::CloneWith(
RefPtr<nsRange>&& aNewPrefixRange, RefPtr<nsRange>&& aNewStartRange,
RefPtr<nsRange>&& aNewEndRange, RefPtr<nsRange>&& aNewSuffixRange,
RangeContentCache& aRangeContentCache) const {
MOZ_ASSERT(
mFullPrefixRange && mFullSuffixRange,
"TextDirectiveCandidate::CloneWith(): Source object is in invalid state");
// start with a shallow copy. This copies all ranges, and all fold case
// content raw pointers.
TextDirectiveCandidate clone(*this);
// Replace the pointers for all ranges which are present in the parameter
// list, and create new fold case representations.
if (aNewPrefixRange) {
clone.mPrefixRange = std::move(aNewPrefixRange);
Result<const nsString*, ErrorResult> maybeContent =
aRangeContentCache.GetOrCreate(clone.mPrefixRange);
if (MOZ_UNLIKELY(maybeContent.isErr())) {
return maybeContent.propagateErr();
}
clone.mFoldCaseContents.prefix_content = maybeContent.unwrap();
}
if (aNewStartRange) {
clone.mStartRange = std::move(aNewStartRange);
Result<const nsString*, ErrorResult> maybeContent =
aRangeContentCache.GetOrCreate(clone.mStartRange);
if (MOZ_UNLIKELY(maybeContent.isErr())) {
return maybeContent.propagateErr();
}
clone.mFoldCaseContents.start_content = maybeContent.unwrap();
}
// mEndRange is null if exact matching is used.
MOZ_ASSERT_IF(aNewEndRange, mEndRange);
if (aNewEndRange) {
clone.mEndRange = std::move(aNewEndRange);
Result<const nsString*, ErrorResult> maybeContent =
aRangeContentCache.GetOrCreate(clone.mEndRange);
if (MOZ_UNLIKELY(maybeContent.isErr())) {
return maybeContent.propagateErr();
}
clone.mFoldCaseContents.end_content = maybeContent.unwrap();
}
if (aNewSuffixRange) {
clone.mSuffixRange = std::move(aNewSuffixRange);
Result<const nsString*, ErrorResult> maybeContent =
aRangeContentCache.GetOrCreate(clone.mSuffixRange);
if (MOZ_UNLIKELY(maybeContent.isErr())) {
return maybeContent.propagateErr();
}
clone.mFoldCaseContents.suffix_content = maybeContent.unwrap();
}
// from now on, the cloned candidate is immutable. Therefore it is allowed to
// create a text directive string representation, which will be stored in the
// object and used later.
MOZ_TRY(clone.CreateTextDirectiveString());
return clone;
}
Result<nsTArray<TextDirectiveCandidate>, ErrorResult>
TextDirectiveCandidate::CreateNewCandidatesForMatches(
const nsTArray<const TextDirectiveCandidate*>& aMatches,
RangeContentCache& aRangeContentCache) {
TEXT_FRAGMENT_LOG("Creating new candidates for candidate {}",
mTextDirectiveString);
// To Create a list of new candidates for a text directive candidate given a
// set of text directive candidates `matches`, follow these steps:
// 1. Let `newCandidates` be a list of text directive candidates, initially
// empty.
nsTArray<TextDirectiveCandidate> newCandidates;
// 2. For each `match` in `matches`:
for (const auto* match : aMatches) {
// 2.1 Let `newCandidatesForThisMatch` be the result of the Create New
// Candidates for a given match algorithm.
Result<nsTArray<TextDirectiveCandidate>, ErrorResult>
maybeNewCandidatesForThisMatch =
CreateNewCandidatesForGivenMatch(*match, aRangeContentCache);
if (MOZ_UNLIKELY(maybeNewCandidatesForThisMatch.isErr())) {
return maybeNewCandidatesForThisMatch.propagateErr();
}
auto newCandidatesForThisMatch = maybeNewCandidatesForThisMatch.unwrap();
// 2.2 If `newCandidatesForThisMatch` is empty, return an empty list.
// Note: If it was not possible to create a new match (ie. it was not
// possible to find a unique match by extending any of the context terms),
// it is not possible to create a text directive for the given input range.
// In this case, return an empty array to indicate this to the caller.
if (newCandidatesForThisMatch.IsEmpty()) {
return nsTArray<TextDirectiveCandidate>{};
}
// 2.3 Insert the elements of `newCandidatesForThisMatch` into
// `newCandidates`.
newCandidates.AppendElements(std::move(newCandidatesForThisMatch));
}
// 3. Return `newCandidates`.
return newCandidates;
}
Result<nsTArray<TextDirectiveCandidate>, ErrorResult>
TextDirectiveCandidate::CreateNewCandidatesForGivenMatch(
const TextDirectiveCandidate& aOther,
RangeContentCache& aRangeContentCache) const {
// To Create New Candidates For A Given Match given a text directive candidate
// `this` and a text directive candidate `other`, follow these steps:
// 1. Let `newCandidates` be a list of text directive candidates, initially
// empty.
// 2. For `contextTerm` in ['prefix´, 'suffix´, 'start', 'end']:
// 2.1 Let `extendedRange` be a range, initially null.
// 2.2 Let `thisFullRange` be the fully-expanded range for
// `contextTerm` of `this` candidate, and `otherFullRange` the
// fully-expanded range for `contextTerm` of the other candidate.
// 2.3 if `contextTerm` is 'start' or 'end' and exact matching is used,
// continue.
// 2.4 Let `thisFullRangeContent` and `otherFullRangeContent` be the fold-case
// representations of `thisFullRange` and `otherFullRange`.
// 2.5 If `contextTerm` is 'prefix´ or 'end', let `commonContext` be the
// common suffix of `thisFullRangeContent` and `otherFullRangeContent`.
// Otherwise, let `commonContext` be the common prefix.
// 2.6 If `commonContext` is equal to `thisFullRangeContent`, continue.
// 2.7 If 'contextTerm' is 'prefix' or 'end':
// 2.7.1 Create a range boundary `newRangeBoundary` by moving the length of
// the common suffix, starting from the end point of the fully
// expanded range of the property.
// 2.7.2 Move `newRangeBoundary` one word to the left.
// 2.7.3 Assert: `newRangeBoundary` is not before the start boundary of the
// fully expanded context term (this would conflict with step 2.6).
// 2.7.4 Initialize `extendedRange` using `newRangeBoundary` as start point
// and the end point of the fully expanded range of `this` as end point.
// 2.8 else:
// 2.8.1 Create a range boundary `newRangeBoundary` by moving the length of
// the common prefix, starting from the start point of the fully
// expanded range of the property.
// 2.8.2 Move `newRangeBoundary` one word to the right.
// 2.8.3 Assert: `newRangeBoundary` is not behind the end boundary of the
// fully expanded context term (this would conflict with step 2.6).
// 2.8.4 Initialize `extendedRange` using the start point of the fully
// expanded range of `this` as start point and `newRangeBoundary` as end
// point.
// 2.9 If `contextTerm` is `start`:
// 2.9.1 Let `expandLimit` be the start point of the non-expanded end range.
// 2.9.2 If `newRangeBoundary` is behind `expandLimit`, set `newRangeBoundary`
// to `expandLimit`.
// 2.10 If `contextTerm` is `end`:
// 2.10.1 Let `expandLimit` be the end point of the non-expanded start range.
// 2.10.2 If `newRangeBoundary` is before `expandLimit`, set
// `newRangeBoundary` to `expandLimit`.
// 2.9 Clone the text directive candidate `this`, replacing `contextTerm`
// with `extendedRange` and insert it into `newCandidates`.
// 3. Return `newCandidates`.
AutoTArray<TextDirectiveCandidate, 4> newCandidates;
auto createRangeExtendedUntilMismatch =
[](nsRange* thisFullRange, const nsString& thisFullRangeFoldCase,
nsRange* otherFullRange, const nsString& otherFullRangeFoldCase,
TextScanDirection expandDirection,
Maybe<RangeBoundary> expandLimit =
Nothing{}) -> Result<RefPtr<nsRange>, ErrorResult> {
ErrorResult rv;
auto equalSubstringLength =
expandDirection == TextScanDirection::Right
? TextDirectiveUtil::FindCommonPrefix(thisFullRangeFoldCase,
otherFullRangeFoldCase)
: TextDirectiveUtil::FindCommonSuffix(thisFullRangeFoldCase,
otherFullRangeFoldCase);
if (equalSubstringLength == thisFullRangeFoldCase.Length()) {
// even when the range is fully extended, it would match `other`.
// Therefore, it can be skipped.
return RefPtr<nsRange>(nullptr);
}
size_t indexFromStartOfFullRange =
expandDirection == TextScanDirection::Right
? equalSubstringLength
: thisFullRangeFoldCase.Length() - equalSubstringLength;
Result<RangeBoundary, ErrorResult> maybeNewRangeBoundary =
TextDirectiveUtil::CreateRangeBoundaryByMovingOffsetFromRangeStart(
thisFullRange, indexFromStartOfFullRange);
if (MOZ_UNLIKELY(maybeNewRangeBoundary.isErr())) {
return maybeNewRangeBoundary.propagateErr();
}
RangeBoundary newRangeBoundary =
TextDirectiveUtil::MoveRangeBoundaryOneWord(
maybeNewRangeBoundary.unwrap(), expandDirection);
if (expandLimit.isSome()) {
// in the range-based matching case it needs to be ensured that start and
// end don't overlap.
if (auto compareResult =
nsContentUtils::ComparePoints(newRangeBoundary, *expandLimit)) {
if ((expandDirection == TextScanDirection::Right &&
*compareResult != 1) ||
(expandDirection == TextScanDirection::Left &&
*compareResult != -1)) {
newRangeBoundary = *expandLimit;
}
}
}
RefPtr<nsRange> newRange = thisFullRange->CloneRange();
if (expandDirection == TextScanDirection::Right) {
newRange->SetEnd(newRangeBoundary.AsRaw(), rv);
if (MOZ_UNLIKELY(rv.Failed())) {
return Err(std::move(rv));
}
} else {
newRange->SetStart(newRangeBoundary.AsRaw(), rv);
if (MOZ_UNLIKELY(rv.Failed())) {
return Err(std::move(rv));
}
}
return newRange;
};
auto createAndAddCandidate =
[candidate = this, &newCandidates, &aRangeContentCache,
func = __FUNCTION__](
const char* contextTermName, RefPtr<nsRange>&& extendedPrefix,
RefPtr<nsRange>&& extendedStart, RefPtr<nsRange>&& extendedEnd,
RefPtr<nsRange>&& extendedSuffix) -> Result<Ok, ErrorResult> {
if (!extendedPrefix && !extendedStart && !extendedEnd && !extendedSuffix) {
TEXT_FRAGMENT_LOG_FN("Could not extend the {} because it is ambiguous.",
func, contextTermName);
return Ok();
}
Result<TextDirectiveCandidate, ErrorResult> extendedCandidate =
candidate->CloneWith(std::move(extendedPrefix),
std::move(extendedStart), std::move(extendedEnd),
std::move(extendedSuffix), aRangeContentCache);
if (MOZ_UNLIKELY(extendedCandidate.isErr())) {
return extendedCandidate.propagateErr();
}
newCandidates.AppendElement(extendedCandidate.unwrap());
TEXT_FRAGMENT_LOG_FN("Created candidate by extending the {}: {}", func,
contextTermName,
newCandidates.LastElement().TextDirectiveString());
return Ok();
};
MOZ_ASSERT(mFoldCaseContents.full_prefix_content &&
aOther.mFoldCaseContents.full_prefix_content);
MOZ_TRY(
createRangeExtendedUntilMismatch(
mFullPrefixRange, *mFoldCaseContents.full_prefix_content,
aOther.mFullPrefixRange,
*aOther.mFoldCaseContents.full_prefix_content,
TextScanDirection::Left)
.andThen([&createAndAddCandidate](RefPtr<nsRange>&& extendedRange) {
return createAndAddCandidate("prefix", std::move(extendedRange),
nullptr, nullptr, nullptr);
}));
MOZ_ASSERT(mFoldCaseContents.full_suffix_content &&
aOther.mFoldCaseContents.full_suffix_content);
MOZ_TRY(
createRangeExtendedUntilMismatch(
mFullSuffixRange, *mFoldCaseContents.full_suffix_content,
aOther.mFullSuffixRange,
*aOther.mFoldCaseContents.full_suffix_content,
TextScanDirection::Right)
.andThen([&createAndAddCandidate](RefPtr<nsRange>&& extendedRange) {
return createAndAddCandidate("suffix", nullptr, nullptr, nullptr,
std::move(extendedRange));
}));
MOZ_ASSERT(UseExactMatch() == aOther.UseExactMatch());
if (UseExactMatch()) {
return std::move(newCandidates);
}
MOZ_ASSERT(mFoldCaseContents.full_start_content &&
aOther.mFoldCaseContents.full_start_content);
MOZ_TRY(
createRangeExtendedUntilMismatch(
mFullStartRange, *mFoldCaseContents.full_start_content,
aOther.mFullStartRange, *aOther.mFoldCaseContents.full_start_content,
TextScanDirection::Right, Some(mEndRange->StartRef()))
.andThen([&createAndAddCandidate](RefPtr<nsRange>&& extendedRange) {
return createAndAddCandidate(
"start", nullptr, std::move(extendedRange), nullptr, nullptr);
}));
MOZ_ASSERT(mFoldCaseContents.full_end_content &&
aOther.mFoldCaseContents.full_end_content);
MOZ_TRY(
createRangeExtendedUntilMismatch(
mFullEndRange, *mFoldCaseContents.full_end_content,
aOther.mFullEndRange, *aOther.mFoldCaseContents.full_end_content,
TextScanDirection::Left)
.andThen([&createAndAddCandidate](RefPtr<nsRange>&& extendedRange) {
return createAndAddCandidate("end", nullptr, nullptr,
std::move(extendedRange), nullptr);
}));
return std::move(newCandidates);
}
nsTArray<const TextDirectiveCandidate*>
TextDirectiveCandidate::FilterNonMatchingCandidates(
const nsTArray<const TextDirectiveCandidate*>& aMatches) {
AutoTArray<const TextDirectiveCandidate*, 8> stillMatching;
for (const auto* match : aMatches) {
if (ThisCandidateMatchesOther(*match)) {
stillMatching.AppendElement(match);
}
}
return std::move(stillMatching);
}
/* static */ Result<RefPtr<nsRange>, ErrorResult>
TextDirectiveCandidate::MaybeCreateStartToBlockBoundaryRange(
const nsRange& aRange) {
ErrorResult rv;
// this call returns `Nothing` if the block boundary is outside of the input
// range.
Result<RefPtr<nsRange>, ErrorResult> fullRange =
TextDirectiveUtil::FindBlockBoundaryInRange(aRange,
TextScanDirection::Right)
.andThen([&aRange, &rv](
auto boundary) -> Result<RefPtr<nsRange>, ErrorResult> {
RefPtr range =
boundary ? nsRange::Create(aRange.StartRef(), *boundary, rv)
: nullptr;
if (MOZ_UNLIKELY(rv.Failed())) {
return Err(std::move(rv));
}
return range;
});
return fullRange;
}
/* static */ Result<RefPtr<nsRange>, ErrorResult>
TextDirectiveCandidate::MaybeCreateEndToBlockBoundaryRange(
const nsRange& aRange) {
ErrorResult rv;
// this call returns `Nothing` if the block boundary is outside of the input
// range.
Result<RefPtr<nsRange>, ErrorResult> fullRange =
TextDirectiveUtil::FindBlockBoundaryInRange(aRange,
TextScanDirection::Left)
.andThen([&aRange, &rv](
auto boundary) -> Result<RefPtr<nsRange>, ErrorResult> {
RefPtr range = boundary
? nsRange::Create(*boundary, aRange.EndRef(), rv)
: nullptr;
if (MOZ_UNLIKELY(rv.Failed())) {
return Err(std::move(rv));
}
return range;
});
return fullRange;
}
bool TextDirectiveCandidate::ThisCandidateMatchesOther(
const TextDirectiveCandidate& aOther) const {
if (TextDirectiveUtil::FindCommonSuffix(
*mFoldCaseContents.prefix_content,
*aOther.mFoldCaseContents.full_prefix_content) !=
mFoldCaseContents.prefix_content->Length()) {
return false;
}
if (TextDirectiveUtil::FindCommonPrefix(
*mFoldCaseContents.suffix_content,
*aOther.mFoldCaseContents.full_suffix_content) !=
mFoldCaseContents.suffix_content->Length()) {
return false;
}
MOZ_ASSERT(UseExactMatch() == aOther.UseExactMatch());
if (UseExactMatch()) {
return true;
}
if (TextDirectiveUtil::FindCommonPrefix(
*mFoldCaseContents.start_content,
*aOther.mFoldCaseContents.full_start_content) !=
mFoldCaseContents.start_content->Length()) {
return false;
}
return TextDirectiveUtil::FindCommonSuffix(
*mFoldCaseContents.end_content,
*aOther.mFoldCaseContents.full_end_content) ==
mFoldCaseContents.end_content->Length();
}
/* static */
Result<std::tuple<RefPtr<nsRange>, RefPtr<nsRange>>, ErrorResult>
TextDirectiveCandidate::CreatePrefixRanges(
const RangeBoundary& aRangeBoundary) {
MOZ_ASSERT(aRangeBoundary.IsSetAndValid());
ErrorResult rv;
RangeBoundary previousNonWhitespacePoint =
TextDirectiveUtil::MoveBoundaryToPreviousNonWhitespacePosition(
aRangeBoundary);
RefPtr<nsRange> prefixRange = nsRange::Create(previousNonWhitespacePoint,
previousNonWhitespacePoint, rv);
if (rv.Failed()) {
return Err(std::move(rv));
}
Result<RangeBoundary, ErrorResult> fullPrefixStartBoundary =
TextDirectiveUtil::FindNextBlockBoundary(prefixRange->EndRef(),
TextScanDirection::Left);
if (MOZ_UNLIKELY(fullPrefixStartBoundary.isErr())) {
return fullPrefixStartBoundary.propagateErr();
}
RefPtr<nsRange> fullPrefixRange = nsRange::Create(
fullPrefixStartBoundary.unwrap(), prefixRange->EndRef(), rv);
if (rv.Failed()) {
return Err(std::move(rv));
}
return std::tuple{prefixRange, fullPrefixRange};
}
/* static */
Result<std::tuple<RefPtr<nsRange>, RefPtr<nsRange>>, ErrorResult>
TextDirectiveCandidate::CreateSuffixRanges(
const RangeBoundary& aRangeBoundary) {
MOZ_ASSERT(aRangeBoundary.IsSetAndValid());
RangeBoundary nextNonWhitespacePoint =
TextDirectiveUtil::MoveBoundaryToNextNonWhitespacePosition(
aRangeBoundary);
ErrorResult rv;
RefPtr<nsRange> suffixRange =
nsRange::Create(nextNonWhitespacePoint, nextNonWhitespacePoint, rv);
if (rv.Failed()) {
return Err(std::move(rv));
}
Result<RangeBoundary, ErrorResult> fullSuffixEndBoundary =
TextDirectiveUtil::FindNextBlockBoundary(suffixRange->EndRef(),
TextScanDirection::Right);
if (MOZ_UNLIKELY(fullSuffixEndBoundary.isErr())) {
return fullSuffixEndBoundary.propagateErr();
}
RefPtr<nsRange> fullSuffixRange = nsRange::Create(
suffixRange->StartRef(), fullSuffixEndBoundary.unwrap(), rv);
if (rv.Failed()) {
return Err(std::move(rv));
}
return std::tuple{suffixRange, fullSuffixRange};
}
const nsCString& TextDirectiveCandidate::TextDirectiveString() const {
return mTextDirectiveString;
}
Result<Ok, ErrorResult> TextDirectiveCandidate::CreateTextDirectiveString() {
Result<TextDirective, ErrorResult> maybeTextDirective =
TextDirectiveUtil::CreateTextDirectiveFromRanges(
mPrefixRange, mStartRange, mEndRange, mSuffixRange);
if (MOZ_UNLIKELY(maybeTextDirective.isErr())) {
return maybeTextDirective.propagateErr();
}
TextDirective textDirective = maybeTextDirective.unwrap();
create_text_directive(&textDirective, &mTextDirectiveString);
return Ok();
}
Result<Ok, ErrorResult> TextDirectiveCandidate::CreateFoldCaseContents(
RangeContentCache& aRangeContentCache) {
auto getFoldCase = [&aRangeContentCache](
nsRange* range,
const nsString*& content) -> Result<Ok, ErrorResult> {
return aRangeContentCache.GetOrCreate(range).andThen(
[&content](const nsString*&& value) -> Result<Ok, ErrorResult> {
content = value;
return Ok();
});
};
MOZ_ASSERT(mFullPrefixRange);
MOZ_ASSERT(mFullSuffixRange);
MOZ_ASSERT(mStartRange);
MOZ_TRY(getFoldCase(mFullPrefixRange, mFoldCaseContents.full_prefix_content));
MOZ_TRY(getFoldCase(mFullStartRange, mFoldCaseContents.full_start_content));
MOZ_TRY(getFoldCase(mFullEndRange, mFoldCaseContents.full_end_content));
MOZ_TRY(getFoldCase(mFullSuffixRange, mFoldCaseContents.full_suffix_content));
MOZ_TRY(getFoldCase(mPrefixRange, mFoldCaseContents.prefix_content));
MOZ_TRY(getFoldCase(mStartRange, mFoldCaseContents.start_content));
MOZ_TRY(getFoldCase(mEndRange, mFoldCaseContents.end_content));
MOZ_TRY(getFoldCase(mSuffixRange, mFoldCaseContents.suffix_content));
mFoldCaseContents.use_exact_matching = UseExactMatch();
return Ok();
}
void TextDirectiveCandidate::LogCurrentState(const char* aCallerFunc) const {
if (!TextDirectiveUtil::ShouldLog()) {
return;
}
auto getRangeContent = [](nsRange* range) {
auto content = TextDirectiveUtil::RangeContentAsString(range).unwrapOr(
u"<nsRange::ToString() failed>"_ns);
return NS_ConvertUTF16toUTF8(content.IsEmpty() ? u"<empty range>"_ns
: content);
};
auto fullTextDirectiveString =
TextDirectiveUtil::CreateTextDirectiveFromRanges(
mFullPrefixRange, mFullStartRange ? mFullStartRange : mStartRange,
mFullEndRange, mFullSuffixRange)
.map([](const auto& textDirective) {
nsCString value;
create_text_directive(&textDirective, &value);
return value;
})
.unwrapOr("<creating text directive failed>"_ns);
TEXT_FRAGMENT_LOG_FN(
"State of text directive candidate {:p}:\nPercent-encoded string: "
"{}\n\nCurrent context terms:\nPrefix: {}\nStart: {}\nEnd: {}\nSuffix: "
"{}\n\nMaximum expanded context terms:\nPercent-encoded string: "
"{}\nPrefix:\n{}\nStart:\n{}\nEnd:\n{}\nSuffix:\n{}",
aCallerFunc, static_cast<const void*>(this), mTextDirectiveString,
getRangeContent(mPrefixRange), getRangeContent(mStartRange),
getRangeContent(mEndRange), getRangeContent(mSuffixRange),
fullTextDirectiveString, getRangeContent(mFullPrefixRange),
getRangeContent(mFullStartRange), getRangeContent(mFullEndRange),
getRangeContent(mFullSuffixRange));
}
TextDirectiveCreator::TextDirectiveCreator(
Document& aDocument, nsRange* aInputRange,
TextDirectiveCandidate&& aTextDirective,
RangeContentCache&& aRangeContentCache)
: mDocument(aDocument),
mInputRange(aInputRange),
mTextDirective(std::move(aTextDirective)),
mRangeContentCache(std::move(aRangeContentCache)) {}
/* static */
mozilla::Result<nsCString, ErrorResult>
TextDirectiveCreator::CreateTextDirectiveFromRange(Document& aDocument,
nsRange* aInputRange) {
MOZ_ASSERT(aInputRange);
MOZ_ASSERT(!aInputRange->Collapsed());
// To create a text directive string from a given range, follow these steps:
// 1. extend inputRange to the next word boundaries in outward direction.
RefPtr<nsRange> inputRangeExtendedToWordBoundaries =
aInputRange->CloneRange();
MOZ_TRY(TextDirectiveUtil::ExtendRangeToWordBoundaries(
*inputRangeExtendedToWordBoundaries));
// 2. If the content of the extended range is empty, return an empty string.
if (inputRangeExtendedToWordBoundaries->Collapsed()) {
return nsCString{};
}
Result<nsString, ErrorResult> rangeContent =
TextDirectiveUtil::RangeContentAsString(
inputRangeExtendedToWordBoundaries);
if (MOZ_UNLIKELY(rangeContent.isErr())) {
return rangeContent.propagateErr();
}
if (rangeContent.unwrap().IsEmpty()) {
return nsCString{};
}
// 3. Create a fully-populated text directive candidate
// textDirectiveCandidate.
RangeContentCache rangeContentCache;
Result<TextDirectiveCandidate, ErrorResult> maybeTextDirectiveCandidate =
TextDirectiveCandidate::CreateFromInputRange(
inputRangeExtendedToWordBoundaries, rangeContentCache);
if (MOZ_UNLIKELY(maybeTextDirectiveCandidate.isErr())) {
return maybeTextDirectiveCandidate.propagateErr();
}
auto textDirectiveCandidate = maybeTextDirectiveCandidate.unwrap();
TextDirectiveCreator creator(aDocument, inputRangeExtendedToWordBoundaries,
std::move(textDirectiveCandidate),
std::move(rangeContentCache));
// 4. Create a list of fully populated text directive candidates.
// 5. Run the create text directive from matches algorithm and return its
// result.
return creator.FindAllMatchingCandidates().andThen(
[&creator](nsTArray<TextDirectiveCandidate>&& previousMatches)
-> Result<nsCString, ErrorResult> {
return creator.CreateTextDirectiveFromMatches(previousMatches);
});
}
Result<nsTArray<TextDirectiveCandidate>, ErrorResult>
TextDirectiveCreator::FindAllMatchingCandidates() {
ErrorResult rv;
nsTArray<TextDirectiveCandidate> textDirectiveMatches;
if (mTextDirective.UseExactMatch()) {
Result<nsString, ErrorResult> rangeContent =
TextDirectiveUtil::RangeContentAsString(mInputRange);
if (MOZ_UNLIKELY(rangeContent.isErr())) {
return rangeContent.propagateErr();
}
Result<nsTArray<RefPtr<nsRange>>, ErrorResult> maybeRangeMatches =
FindAllMatchingRanges(rangeContent.unwrap(),
mTextDirective.StartRange()->EndRef());
if (MOZ_UNLIKELY(maybeRangeMatches.isErr())) {
return maybeRangeMatches.propagateErr();
}
if (mWatchdog.IsDone()) {
return textDirectiveMatches;
}
auto rangeMatches = maybeRangeMatches.unwrap();
textDirectiveMatches.SetCapacity(rangeMatches.Length());
for (const auto& rangeMatch : rangeMatches) {
auto candidate = TextDirectiveCandidate::CreateFromInputRange(
rangeMatch, mRangeContentCache);
if (MOZ_UNLIKELY(candidate.isErr())) {
return candidate.propagateErr();
}
textDirectiveMatches.AppendElement(candidate.unwrap());
}
return textDirectiveMatches;
}
Result<nsString, ErrorResult> startRangeContent =
TextDirectiveUtil::RangeContentAsString(mTextDirective.StartRange());
if (MOZ_UNLIKELY(startRangeContent.isErr())) {
return startRangeContent.propagateErr();
}
Result<nsTArray<RefPtr<nsRange>>, ErrorResult> maybeStartRangeMatches =
FindAllMatchingRanges(startRangeContent.unwrap(),
mTextDirective.StartRange()->EndRef());
if (MOZ_UNLIKELY(maybeStartRangeMatches.isErr())) {
return maybeStartRangeMatches.propagateErr();
}
auto startRangeMatches = maybeStartRangeMatches.unwrap();
if (mWatchdog.IsDone()) {
return textDirectiveMatches;
}
Result<nsString, ErrorResult> endRangeContent =
TextDirectiveUtil::RangeContentAsString(mTextDirective.EndRange());
if (MOZ_UNLIKELY(endRangeContent.isErr())) {
return endRangeContent.propagateErr();
}
Result<nsTArray<RefPtr<nsRange>>, ErrorResult> maybeEndRangeMatches =
FindAllMatchingRanges(endRangeContent.unwrap(),
mTextDirective.EndRange()->EndRef());
if (MOZ_UNLIKELY(maybeEndRangeMatches.isErr())) {
return maybeEndRangeMatches.propagateErr();
}
auto endRangeMatchesArray = maybeEndRangeMatches.unwrap();
if (mWatchdog.IsDone()) {
return textDirectiveMatches;
}
nsDeque<nsRange> endRangeMatches;
for (auto& element : endRangeMatchesArray) {
endRangeMatches.Push(element.get());
}
endRangeMatches.Push(mTextDirective.EndRange());
size_t counter = 0;
for (const auto& matchStartRange : startRangeMatches) {
for (auto* matchEndRange : endRangeMatches) {
if (++counter % 100 == 0 && mWatchdog.IsDone()) {
return textDirectiveMatches;
}
Maybe<int32_t> compare = nsContentUtils::ComparePoints(
matchStartRange->EndRef(), matchEndRange->StartRef());
if (!compare || *compare != -1) {
endRangeMatches.PopFront();
continue;
}
auto candidate = TextDirectiveCandidate::CreateFromStartAndEndRange(
matchStartRange, matchEndRange, mRangeContentCache);
if (MOZ_UNLIKELY(candidate.isErr())) {
return candidate.propagateErr();
}
textDirectiveMatches.AppendElement(candidate.unwrap());
}
}
return textDirectiveMatches;
}
Result<nsTArray<RefPtr<nsRange>>, ErrorResult>
TextDirectiveCreator::FindAllMatchingRanges(const nsString& aSearchQuery,
const RangeBoundary& aSearchEnd) {
MOZ_ASSERT(!aSearchQuery.IsEmpty());
ErrorResult rv;
nsContentUtils::NodeIndexCache nodeIndexCache;
RangeBoundary documentStart{&mDocument, 0u};
RefPtr<nsRange> searchRange = nsRange::Create(documentStart, aSearchEnd, rv);
if (rv.Failed()) {
return Err(std::move(rv));
}
nsTArray<RefPtr<nsRange>> matchingRanges;
size_t counter = 0;
RangeBoundary searchStart = searchRange->StartRef();
const RangeBoundary searchEnd = searchRange->EndRef();
while (!searchRange->Collapsed()) {
if (++counter % 100 == 0 && mWatchdog.IsDone()) {
return matchingRanges;
}
RefPtr<nsRange> searchResult = TextDirectiveUtil::FindStringInRange(
searchStart, searchEnd, aSearchQuery, true, true, &nodeIndexCache);
if (!searchResult) {
// This would mean we reached a weird edge case in which the search query
// is not in the search range.
break;
}
if (TextDirectiveUtil::NormalizedRangeBoundariesAreEqual(
searchResult->EndRef(), searchEnd, &nodeIndexCache)) {
// It is safe to assume that this algorithm reached the end of all
// potential ranges if the search result is equal to the search query.
// Therefore, this range is not added to the results.
break;
}
matchingRanges.AppendElement(searchResult);
searchStart = TextDirectiveUtil::MoveRangeBoundaryOneWord(
searchResult->StartRef(), TextScanDirection::Right);
}
TEXT_FRAGMENT_LOG(
"Found {} matches for the input '{}' in the document until the end of "
"the input range.",
matchingRanges.Length(), NS_ConvertUTF16toUTF8(aSearchQuery));
return matchingRanges;
}
nsTArray<
std::pair<TextDirectiveCandidate, nsTArray<const TextDirectiveCandidate*>>>
TextDirectiveCreator::FindMatchesForCandidates(
nsTArray<TextDirectiveCandidate>&& aCandidates,
const nsTArray<const TextDirectiveCandidate*>& aMatches) {
AutoTArray<const TextDirectiveCandidateContents*, 32> candidateContents;
candidateContents.SetCapacity(aCandidates.Length());
for (const auto& c : aCandidates) {
candidateContents.AppendElement(&c.RangeContents());
}
AutoTArray<const TextDirectiveCandidateContents*, 32> matchContents;
matchContents.SetCapacity(aMatches.Length());
for (const auto* m : aMatches) {
matchContents.AppendElement(&m->RangeContents());
}
nsTArray<nsTArray<size_t>> matchIndices;
fragment_directive_filter_non_matching_candidates(
&candidateContents, &matchContents, &matchIndices);
nsTArray<std::pair<TextDirectiveCandidate,
nsTArray<const TextDirectiveCandidate*>>>
candidatesAndMatches(aCandidates.Length());
for (size_t index = 0; index < aCandidates.Length(); ++index) {
AutoTArray<const TextDirectiveCandidate*, 8> stillMatching;
for (auto i : matchIndices[index]) {
stillMatching.AppendElement(aMatches[i]);
}
candidatesAndMatches.AppendElement(
std::pair(std::move(aCandidates[index]), std::move(stillMatching)));
}
return candidatesAndMatches;
}
Result<nsCString, ErrorResult>
TextDirectiveCreator::CreateTextDirectiveFromMatches(
const nsTArray<TextDirectiveCandidate>& aTextDirectiveMatches) {
if (mWatchdog.IsDone()) {
TEXT_FRAGMENT_LOG(
"Hitting {}s timeout.",
StaticPrefs::dom_text_fragments_create_text_fragment_timeout_seconds());
return nsCString{};
}
TextDirectiveCandidate currentCandidate = std::move(mTextDirective);
if (aTextDirectiveMatches.IsEmpty()) {
TEXT_FRAGMENT_LOG(
"There are no conflicting matches. Returning text directive '{}'.",
currentCandidate.TextDirectiveString());
return currentCandidate.TextDirectiveString();
}
TEXT_FRAGMENT_LOG("Found {} text directive matches to eliminate",
aTextDirectiveMatches.Length());
// To create a text directive string which points to the input range using a
// text directive candidate `currentCandidate` and a given set of
// fully-expanded text directive candidates `matches` representing other
// matches of input range, follow these steps:
// Implementation note:
// `aTextDirectiveMatches` keeps ownership of the `TextDirectiveCandidate`s
// during this algorithm and is const. `matches` doesn't have ownership and
// will change its content during the following loop.
nsTArray<const TextDirectiveCandidate*> matches(
aTextDirectiveMatches.Length());
for (const auto& match : aTextDirectiveMatches) {
matches.AppendElement(&match);
}
// 1. While `matches` is not empty:
size_t loopCounter = 0;
while (!matches.IsEmpty()) {
++loopCounter;
if (mWatchdog.IsDone()) {
TEXT_FRAGMENT_LOG(
"Hitting {}s timeout.",
StaticPrefs::
dom_text_fragments_create_text_fragment_timeout_seconds());
return nsCString{};
}
TEXT_FRAGMENT_LOG(
"Entering loop {}. {} matches left. Current candidate state:\n",
loopCounter, matches.Length());
currentCandidate.LogCurrentState(__FUNCTION__);
if (TextDirectiveUtil::ShouldLog()) {
TEXT_FRAGMENT_LOG("State of remaining matches:");
for (const auto* match : matches) {
match->LogCurrentState(__FUNCTION__);
}
}
// 1.1 Let `newCandidates` be a list of text directive candidates, created
// by running the Create New Candidates For Matches algorithm.
Result<nsTArray<TextDirectiveCandidate>, ErrorResult> maybeNewCandidates =
currentCandidate.CreateNewCandidatesForMatches(matches,
mRangeContentCache);
if (MOZ_UNLIKELY(maybeNewCandidates.isErr())) {
return maybeNewCandidates.propagateErr();
}
nsTArray<TextDirectiveCandidate> newCandidates =
maybeNewCandidates.unwrap();
// 1.2 If `newCandidates` is empty:
if (newCandidates.IsEmpty()) {
TEXT_FRAGMENT_LOG(
"It is not possible to create a text directive that matches the "
"input string.");
// 1.2.1 Return an empty string.
return nsCString{};
}
// 1.3 Let `candidatesAndMatchesForNextIteration` be a list of tuple,
// where the first element is a text directive candidate and the
// second is a list of text directive candidates.
// Note: This will map all elements of `matches` which would still match
// against the new candidate. These matches would need to be
// examined further in the next iteration.
// 1.4 For each element `newCandidate` in `newCandidates`:
// 1.4.1 Insert a new element into
// `candidatesAndMatchesForNextIteration`, consisting of
// `newCandidate` and the result of running the Filter Non-matching
// Candidates algorithm using `newCandidate` and `matches`.
nsTArray<std::pair<TextDirectiveCandidate,
nsTArray<const TextDirectiveCandidate*>>>
candidatesAndMatchesForNextIteration =
FindMatchesForCandidates(std::move(newCandidates), matches);
// 1.5 Sort the elements of `candidatesAndMatchesForNextIteration`
// ascending using the following criteria:
// 1. The number of remaining matches
// 2. If the number of remaining matches is equal, sort by the length
// of the created text directive string.
candidatesAndMatchesForNextIteration.Sort(
[](const auto& a, const auto& b) -> int {
// sorting by the number of still matching candidates which still
// exist has higher priority ...
const int difference = a.second.Length() - b.second.Length();
if (difference != 0) {
return difference;
}
// ... than sorting by the resulting text directive string for
// elements which have the same number of matching candidates.
return a.first.TextDirectiveString().Length() -
b.first.TextDirectiveString().Length();
});
// 1.6 Let `currentCandidate` be the first element of the first element of
// `candidatesAndMatchesForNextIteration`.
// 1.7. Let `matches` be the second element of the first element of
// `candidatesAndMatchesForNextIteration`.
auto& [bestCandidate, matchesForCandidate] =
candidatesAndMatchesForNextIteration.ElementAt(0);
currentCandidate = std::move(bestCandidate);
matches = std::move(matchesForCandidate);
}
// 2. Return the text directive string of `currentCandidate`.
return currentCandidate.TextDirectiveString();
}
} // namespace mozilla::dom