/* -*- 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 "nsDeque.h" #include "nsINode.h" #include "nsRange.h" #include "Document.h" namespace mozilla::dom { Result, ErrorResult> RangeContentCache::Get(nsRange* aRange1, nsRange* aRange2) { auto cachedContent1 = mCache.Lookup(aRange1); auto cachedContent2 = mCache.Lookup(aRange2); // intermediate bool flags are necessary because the `LookupResult` objects // are invalidated after an insertion. const bool needsToInsert1 = !cachedContent1; const bool needsToInsert2 = !cachedContent2; if (!needsToInsert1 && !needsToInsert2) { return std::tuple{*cachedContent1, *cachedContent2}; } if (needsToInsert1) { Result content1 = TextDirectiveUtil::RangeContentAsFoldCase(aRange1); if (MOZ_UNLIKELY(content1.isErr())) { return content1.propagateErr(); } mCache.InsertOrUpdate(aRange1, content1.unwrap()); } if (needsToInsert2) { Result content2 = TextDirectiveUtil::RangeContentAsFoldCase(aRange2); if (MOZ_UNLIKELY(content2.isErr())) { return content2.propagateErr(); } mCache.InsertOrUpdate(aRange2, content2.unwrap()); } return std::tuple{*mCache.Lookup(aRange1), *mCache.Lookup(aRange2)}; } 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::CreateFromInputRange(const nsRange* aInputRange) { 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, ErrorResult> maybeFullStartRange = MaybeCreateStartToBlockBoundaryRange(*aInputRange); if (MOZ_UNLIKELY(maybeFullStartRange.isErr())) { return maybeFullStartRange.propagateErr(); } RefPtr fullStartRange = maybeFullStartRange.unwrap(); RefPtr 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 startRange; RefPtr 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, 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.CreateTextDirectiveString()); return instance; } /* static */ Result TextDirectiveCandidate::CreateFromStartAndEndRange(const nsRange* aStartRange, const nsRange* aEndRange) { MOZ_ASSERT(aStartRange); MOZ_ASSERT(aEndRange); ErrorResult rv; RefPtr startRange = aStartRange->CloneRange(); RefPtr endRange = aEndRange->CloneRange(); RefPtr fullRange = nsRange::Create(startRange->StartRef(), endRange->EndRef(), rv); if (MOZ_UNLIKELY(rv.Failed())) { return Err(std::move(rv)); } Result, ErrorResult> maybeFullStartRange = MaybeCreateStartToBlockBoundaryRange(*fullRange); if (MOZ_UNLIKELY(maybeFullStartRange.isErr())) { return maybeFullStartRange.propagateErr(); } RefPtr 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, ErrorResult> maybeFullEndRange = MaybeCreateEndToBlockBoundaryRange(*fullRange); if (MOZ_UNLIKELY(maybeFullEndRange.isErr())) { return maybeFullStartRange.propagateErr(); } RefPtr 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.CreateTextDirectiveString()); return instance; } Result TextDirectiveCandidate::CloneWith( RefPtr&& aNewPrefixRange, RefPtr&& aNewStartRange, RefPtr&& aNewEndRange, RefPtr&& aNewSuffixRange) const { MOZ_ASSERT( mFullPrefixRange && mFullSuffixRange, "TextDirectiveCandidate::CloneWith(): Source object is in invalid state"); TextDirectiveCandidate clone(mStartRange, mFullStartRange, mEndRange, mFullEndRange, mPrefixRange, mFullPrefixRange, mSuffixRange, mFullSuffixRange); if (aNewPrefixRange) { clone.mPrefixRange = std::move(aNewPrefixRange); } if (aNewStartRange) { clone.mStartRange = std::move(aNewStartRange); } // mEndRange is null if exact matching is used. MOZ_ASSERT_IF(aNewEndRange, mEndRange); if (aNewEndRange) { clone.mEndRange = std::move(aNewEndRange); } if (aNewSuffixRange) { clone.mSuffixRange = std::move(aNewSuffixRange); } // 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, ErrorResult> TextDirectiveCandidate::CreateNewCandidatesForMatches( const nsTArray& 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 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, 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{}; } // 2.3 Insert the elements of `newCandidatesForThisMatch` into // `newCandidates`. newCandidates.AppendElements(std::move(newCandidatesForThisMatch)); } // 3. Return `newCandidates`. return newCandidates; } Result, 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 newCandidates; auto createRangeExtendedUntilMismatch = [&aRangeContentCache]( nsRange* thisFullRange, nsRange* otherFullRange, TextScanDirection expandDirection, Maybe expandLimit = Nothing{}) -> Result, ErrorResult> { ErrorResult rv; auto foldCaseContents = aRangeContentCache.Get(thisFullRange, otherFullRange); if (MOZ_UNLIKELY(foldCaseContents.isErr())) { return foldCaseContents.propagateErr(); } const auto& [thisFullRangeFoldCase, otherFullRangeFoldCase] = foldCaseContents.unwrap(); 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(nullptr); } size_t indexFromStartOfFullRange = expandDirection == TextScanDirection::Right ? equalSubstringLength : thisFullRangeFoldCase.Length() - equalSubstringLength; Result 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 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, func = __FUNCTION__]( const char* contextTermName, RefPtr&& extendedPrefix, RefPtr&& extendedStart, RefPtr&& extendedEnd, RefPtr&& extendedSuffix) -> Result { if (!extendedPrefix && !extendedStart && !extendedEnd && !extendedSuffix) { TEXT_FRAGMENT_LOG_FN("Could not extend the {} because it is ambiguous.", func, contextTermName); return Ok(); } Result extendedCandidate = candidate->CloneWith(std::move(extendedPrefix), std::move(extendedStart), std::move(extendedEnd), std::move(extendedSuffix)); 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_TRY( createRangeExtendedUntilMismatch( mFullPrefixRange, aOther.mFullPrefixRange, TextScanDirection::Left) .andThen([&createAndAddCandidate](RefPtr&& extendedRange) { return createAndAddCandidate("prefix", std::move(extendedRange), nullptr, nullptr, nullptr); })); MOZ_TRY( createRangeExtendedUntilMismatch( mFullSuffixRange, aOther.mFullSuffixRange, TextScanDirection::Right) .andThen([&createAndAddCandidate](RefPtr&& extendedRange) { return createAndAddCandidate("suffix", nullptr, nullptr, nullptr, std::move(extendedRange)); })); MOZ_ASSERT(UseExactMatch() == aOther.UseExactMatch()); if (UseExactMatch()) { return std::move(newCandidates); } MOZ_TRY( createRangeExtendedUntilMismatch(mFullStartRange, aOther.mFullStartRange, TextScanDirection::Right, Some(mEndRange->StartRef())) .andThen([&createAndAddCandidate](RefPtr&& extendedRange) { return createAndAddCandidate( "start", nullptr, std::move(extendedRange), nullptr, nullptr); })); MOZ_TRY( createRangeExtendedUntilMismatch(mFullEndRange, aOther.mFullEndRange, TextScanDirection::Left) .andThen([&createAndAddCandidate](RefPtr&& extendedRange) { return createAndAddCandidate("end", nullptr, nullptr, std::move(extendedRange), nullptr); })); return std::move(newCandidates); } nsTArray TextDirectiveCandidate::FilterNonMatchingCandidates( const nsTArray& aMatches, RangeContentCache& aRangeContentCache) { AutoTArray stillMatching; for (const auto* match : aMatches) { if (Result matchesFoldCase = ThisCandidateMatchesOther(*match, aRangeContentCache); MOZ_LIKELY(matchesFoldCase.isOk()) && matchesFoldCase.unwrap()) { stillMatching.AppendElement(match); } } return std::move(stillMatching); } /* static */ Result, ErrorResult> TextDirectiveCandidate::MaybeCreateStartToBlockBoundaryRange( const nsRange& aRange) { ErrorResult rv; // this call returns `Nothing` if the block boundary is outside of the input // range. Result, ErrorResult> fullRange = TextDirectiveUtil::FindBlockBoundaryInRange(aRange, TextScanDirection::Right) .andThen([&aRange, &rv]( auto boundary) -> Result, 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, ErrorResult> TextDirectiveCandidate::MaybeCreateEndToBlockBoundaryRange( const nsRange& aRange) { ErrorResult rv; // this call returns `Nothing` if the block boundary is outside of the input // range. Result, ErrorResult> fullRange = TextDirectiveUtil::FindBlockBoundaryInRange(aRange, TextScanDirection::Left) .andThen([&aRange, &rv]( auto boundary) -> Result, 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; } Result TextDirectiveCandidate::ThisCandidateMatchesOther( const TextDirectiveCandidate& aOther, RangeContentCache& aRangeContentCache) const { auto shortRangeIsSubsetOfFullRange = [&aRangeContentCache]( nsRange* shortRange, nsRange* fullRange, const std::function& comparator) -> Result { auto contents = aRangeContentCache.Get(shortRange, fullRange); if (MOZ_UNLIKELY(contents.isErr())) { return contents.propagateErr(); } const auto& [shortContent, fullContent] = contents.unwrap(); return comparator(shortContent, fullContent) == shortContent.Length(); }; Result prefixIsSubset = shortRangeIsSubsetOfFullRange(mPrefixRange, aOther.mFullPrefixRange, TextDirectiveUtil::FindCommonSuffix); if (MOZ_UNLIKELY(prefixIsSubset.isErr())) { return prefixIsSubset.propagateErr(); } if (!prefixIsSubset.unwrap()) { return false; } Result suffixIsSubset = shortRangeIsSubsetOfFullRange(mSuffixRange, aOther.mFullSuffixRange, TextDirectiveUtil::FindCommonPrefix); if (MOZ_UNLIKELY(suffixIsSubset.isErr())) { return suffixIsSubset.propagateErr(); } if (!suffixIsSubset.unwrap()) { return false; } MOZ_ASSERT(UseExactMatch() == aOther.UseExactMatch()); if (UseExactMatch()) { return true; } Result startIsSubset = shortRangeIsSubsetOfFullRange( mStartRange, aOther.mFullStartRange, TextDirectiveUtil::FindCommonPrefix); if (MOZ_UNLIKELY(startIsSubset.isErr())) { return startIsSubset.propagateErr(); } if (!startIsSubset.unwrap()) { return false; } Result endIsSubset = shortRangeIsSubsetOfFullRange( mEndRange, aOther.mFullEndRange, TextDirectiveUtil::FindCommonSuffix); if (MOZ_UNLIKELY(endIsSubset.isErr())) { return endIsSubset.propagateErr(); } return endIsSubset.unwrap(); } /* static */ Result, RefPtr>, ErrorResult> TextDirectiveCandidate::CreatePrefixRanges( const RangeBoundary& aRangeBoundary) { MOZ_ASSERT(aRangeBoundary.IsSetAndValid()); ErrorResult rv; RangeBoundary previousNonWhitespacePoint = TextDirectiveUtil::MoveBoundaryToPreviousNonWhitespacePosition( aRangeBoundary); RefPtr prefixRange = nsRange::Create(previousNonWhitespacePoint, previousNonWhitespacePoint, rv); if (rv.Failed()) { return Err(std::move(rv)); } Result fullPrefixStartBoundary = TextDirectiveUtil::FindNextBlockBoundary(prefixRange->EndRef(), TextScanDirection::Left); if (MOZ_UNLIKELY(fullPrefixStartBoundary.isErr())) { return fullPrefixStartBoundary.propagateErr(); } RefPtr fullPrefixRange = nsRange::Create( fullPrefixStartBoundary.unwrap(), prefixRange->EndRef(), rv); if (rv.Failed()) { return Err(std::move(rv)); } return std::tuple{prefixRange, fullPrefixRange}; } /* static */ Result, RefPtr>, ErrorResult> TextDirectiveCandidate::CreateSuffixRanges( const RangeBoundary& aRangeBoundary) { MOZ_ASSERT(aRangeBoundary.IsSetAndValid()); RangeBoundary nextNonWhitespacePoint = TextDirectiveUtil::MoveBoundaryToNextNonWhitespacePosition( aRangeBoundary); ErrorResult rv; RefPtr suffixRange = nsRange::Create(nextNonWhitespacePoint, nextNonWhitespacePoint, rv); if (rv.Failed()) { return Err(std::move(rv)); } Result fullSuffixEndBoundary = TextDirectiveUtil::FindNextBlockBoundary(suffixRange->EndRef(), TextScanDirection::Right); if (MOZ_UNLIKELY(fullSuffixEndBoundary.isErr())) { return fullSuffixEndBoundary.propagateErr(); } RefPtr 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 TextDirectiveCandidate::CreateTextDirectiveString() { Result 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(); } void TextDirectiveCandidate::LogCurrentState(const char* aCallerFunc) const { if (!TextDirectiveUtil::ShouldLog()) { return; } auto getRangeContent = [](nsRange* range) { auto content = TextDirectiveUtil::RangeContentAsString(range).unwrapOr( u""_ns); return NS_ConvertUTF16toUTF8(content.IsEmpty() ? u""_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(""_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(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) : mDocument(aDocument), mInputRange(aInputRange), mTextDirective(std::move(aTextDirective)) {} /* static */ mozilla::Result 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 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 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. Result maybeTextDirectiveCandidate = TextDirectiveCandidate::CreateFromInputRange( inputRangeExtendedToWordBoundaries); if (MOZ_UNLIKELY(maybeTextDirectiveCandidate.isErr())) { return maybeTextDirectiveCandidate.propagateErr(); } auto textDirectiveCandidate = maybeTextDirectiveCandidate.unwrap(); TextDirectiveCreator creator(aDocument, inputRangeExtendedToWordBoundaries, std::move(textDirectiveCandidate)); // 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&& previousMatches) -> Result { return creator.CreateTextDirectiveFromMatches(previousMatches); }); } Result, ErrorResult> TextDirectiveCreator::FindAllMatchingCandidates() { ErrorResult rv; nsTArray textDirectiveMatches; if (mTextDirective.UseExactMatch()) { Result rangeContent = TextDirectiveUtil::RangeContentAsString(mInputRange); if (MOZ_UNLIKELY(rangeContent.isErr())) { return rangeContent.propagateErr(); } Result>, ErrorResult> maybeRangeMatches = FindAllMatchingRanges(rangeContent.unwrap()); 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); if (MOZ_UNLIKELY(candidate.isErr())) { return candidate.propagateErr(); } textDirectiveMatches.AppendElement(candidate.unwrap()); } return textDirectiveMatches; } Result startRangeContent = TextDirectiveUtil::RangeContentAsString(mTextDirective.StartRange()); if (MOZ_UNLIKELY(startRangeContent.isErr())) { return startRangeContent.propagateErr(); } Result>, ErrorResult> maybeStartRangeMatches = FindAllMatchingRanges(startRangeContent.unwrap()); if (MOZ_UNLIKELY(maybeStartRangeMatches.isErr())) { return maybeStartRangeMatches.propagateErr(); } auto startRangeMatches = maybeStartRangeMatches.unwrap(); if (mWatchdog.IsDone()) { return textDirectiveMatches; } Result endRangeContent = TextDirectiveUtil::RangeContentAsString(mTextDirective.EndRange()); if (MOZ_UNLIKELY(endRangeContent.isErr())) { return endRangeContent.propagateErr(); } Result>, ErrorResult> maybeEndRangeMatches = FindAllMatchingRanges(endRangeContent.unwrap()); if (MOZ_UNLIKELY(maybeEndRangeMatches.isErr())) { return maybeEndRangeMatches.propagateErr(); } auto endRangeMatchesArray = maybeEndRangeMatches.unwrap(); if (mWatchdog.IsDone()) { return textDirectiveMatches; } nsDeque endRangeMatches; for (auto& element : endRangeMatchesArray) { endRangeMatches.Push(element.get()); } size_t counter = 0; for (const auto& matchStartRange : startRangeMatches) { if (++counter % 100 == 0 && mWatchdog.IsDone()) { return textDirectiveMatches; } for (auto* matchEndRange : endRangeMatches) { Maybe compare = nsContentUtils::ComparePoints( matchStartRange->EndRef(), matchEndRange->StartRef()); if (!compare || *compare != -1) { endRangeMatches.PopFront(); continue; } auto candidate = TextDirectiveCandidate::CreateFromStartAndEndRange( matchStartRange, matchEndRange); if (MOZ_UNLIKELY(candidate.isErr())) { return candidate.propagateErr(); } textDirectiveMatches.AppendElement(candidate.unwrap()); } } return textDirectiveMatches; } Result>, ErrorResult> TextDirectiveCreator::FindAllMatchingRanges(const nsString& aSearchQuery) { MOZ_ASSERT(!aSearchQuery.IsEmpty()); ErrorResult rv; RangeBoundary documentStart{&mDocument, 0u}; RefPtr searchRange = nsRange::Create(documentStart, mInputRange->EndRef(), rv); if (rv.Failed()) { return Err(std::move(rv)); } nsTArray> matchingRanges; size_t counter = 0; while (!searchRange->Collapsed()) { if (++counter % 100 == 0 && mWatchdog.IsDone()) { return matchingRanges; } RefPtr searchResult = TextDirectiveUtil::FindStringInRange( searchRange, aSearchQuery, true, true); 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(), mInputRange->EndRef())) { // 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); RangeBoundary newStartBoundary = TextDirectiveUtil::MoveRangeBoundaryOneWord(searchResult->StartRef(), TextScanDirection::Right); searchRange->SetStart(newStartBoundary.AsRaw(), rv); } 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; } Result TextDirectiveCreator::CreateTextDirectiveFromMatches( const nsTArray& 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 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, ErrorResult> maybeNewCandidates = currentCandidate.CreateNewCandidatesForMatches(matches, mRangeContentCache); if (MOZ_UNLIKELY(maybeNewCandidates.isErr())) { return maybeNewCandidates.propagateErr(); } nsTArray 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. nsTArray>> candidatesAndMatchesForNextIteration(newCandidates.Length()); // 1.4 For each element `newCandidate` in `newCandidates`: for (auto& newCandidate : 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`. candidatesAndMatchesForNextIteration.AppendElement(std::pair{ std::move(newCandidate), newCandidate.FilterNonMatchingCandidates( matches, mRangeContentCache)}); } // 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