/* -*- 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 "TextDirectiveUtil.h" #include "nsComputedDOMStyle.h" #include "nsDOMAttributeMap.h" #include "nsFind.h" #include "nsFrameSelection.h" #include "nsGkAtoms.h" #include "nsIFrame.h" #include "nsINode.h" #include "nsIURI.h" #include "nsRange.h" #include "nsString.h" #include "nsTArray.h" #include "nsUnicharUtils.h" #include "ContentIterator.h" #include "Document.h" #include "fragmentdirectives_ffi_generated.h" #include "Text.h" #include "mozilla/ContentIterator.h" #include "mozilla/ResultVariant.h" #include "mozilla/intl/WordBreaker.h" #include "mozilla/SelectionMovementUtils.h" namespace mozilla::dom { LazyLogModule gFragmentDirectiveLog("FragmentDirective"); /* static */ Result TextDirectiveUtil::RangeContentAsString( nsRange* aRange) { nsString content; if (!aRange || aRange->Collapsed()) { return content; } UnsafePreContentIterator iter; nsresult rv = iter.Init(aRange); if (NS_FAILED(rv)) { return Err(ErrorResult(rv)); } for (; !iter.IsDone(); iter.Next()) { nsINode* current = iter.GetCurrentNode(); if (!TextDirectiveUtil::NodeIsVisibleTextNode(*current) || TextDirectiveUtil::NodeIsPartOfNonSearchableSubTree(*current)) { continue; } const uint32_t startOffset = current == aRange->GetStartContainer() ? aRange->StartOffset() : 0; const uint32_t endOffset = std::min(current == aRange->GetEndContainer() ? aRange->EndOffset() : current->Length(), current->Length()); const Text* text = Text::FromNode(current); text->TextFragment().AppendTo(content, startOffset, endOffset - startOffset); } content.CompressWhitespace(); return content; } /* static */ Result TextDirectiveUtil::RangeContentAsFoldCase(nsRange* aRange) { Result contentResult = RangeContentAsString(aRange); if (MOZ_UNLIKELY(contentResult.isErr())) { return contentResult.propagateErr(); } nsString content = contentResult.unwrap(); content.CompressWhitespace(); ToFoldedCase(content); return content; } /* static */ bool TextDirectiveUtil::NodeIsVisibleTextNode( const nsINode& aNode) { const Text* text = Text::FromNode(aNode); if (!text) { return false; } const nsIFrame* frame = text->GetPrimaryFrame(); return frame && frame->StyleVisibility()->IsVisible(); } /* static */ RefPtr TextDirectiveUtil::FindStringInRange( const RangeBoundary& aSearchStart, const RangeBoundary& aSearchEnd, const nsAString& aQuery, bool aWordStartBounded, bool aWordEndBounded, nsContentUtils::NodeIndexCache* aCache) { TEXT_FRAGMENT_LOG("query='{}', wordStartBounded='{}', wordEndBounded='{}'.\n", NS_ConvertUTF16toUTF8(aQuery), aWordStartBounded, aWordEndBounded); RefPtr finder = new nsFind(); finder->SetWordStartBounded(aWordStartBounded); finder->SetWordEndBounded(aWordEndBounded); finder->SetCaseSensitive(false); finder->SetNodeIndexCache(aCache); RefPtr result = finder->FindFromRangeBoundaries(aQuery, aSearchStart, aSearchEnd); if (!result || result->Collapsed()) { TEXT_FRAGMENT_LOG("Did not find query '{}'", NS_ConvertUTF16toUTF8(aQuery)); } else { auto rangeToString = [](nsRange* range) -> nsCString { nsString rangeString; range->ToString(rangeString, IgnoreErrors()); return NS_ConvertUTF16toUTF8(rangeString); }; TEXT_FRAGMENT_LOG("find returned '{}'", rangeToString(result)); } return result; } /* static */ RangeBoundary TextDirectiveUtil::MoveRangeBoundaryOneWord( const RangeBoundary& aRangeBoundary, TextScanDirection aDirection) { MOZ_ASSERT(aRangeBoundary.IsSetAndValid()); PeekOffsetOptions options = {PeekOffsetOption::JumpLines, PeekOffsetOption::StopAtScroller, PeekOffsetOption::IsKeyboardSelect}; Result newBoundary = SelectionMovementUtils::MoveRangeBoundaryToSomewhere( aRangeBoundary, aDirection == TextScanDirection::Left ? nsDirection::eDirPrevious : nsDirection::eDirNext, aDirection == TextScanDirection::Left ? CaretAssociationHint::Before : CaretAssociationHint::After, intl::BidiEmbeddingLevel::DefaultLTR(), nsSelectionAmount::eSelectWord, options); return newBoundary.unwrapOr({}); } /* static */ bool TextDirectiveUtil::IsWhitespaceAtPosition(const Text* aText, uint32_t aPos) { if (!aText || aText->Length() == 0 || aPos >= aText->Length()) { return false; } const nsTextFragment& frag = aText->TextFragment(); const char NBSP_CHAR = char(0xA0); if (frag.Is2b()) { const char16_t* content = frag.Get2b(); return IsSpaceCharacter(content[aPos]) || content[aPos] == char16_t(NBSP_CHAR); } const char* content = frag.Get1b(); return IsSpaceCharacter(content[aPos]) || content[aPos] == NBSP_CHAR; } /* static */ bool TextDirectiveUtil::NodeIsSearchInvisible(nsINode& aNode) { if (!aNode.IsElement()) { return false; } // 2. If the node serializes as void. nsAtom* nodeNameAtom = aNode.NodeInfo()->NameAtom(); if (FragmentOrElement::IsHTMLVoid(nodeNameAtom)) { return true; } // 3. Is any of the following types: HTMLIFrameElement, HTMLImageElement, // HTMLMeterElement, HTMLObjectElement, HTMLProgressElement, HTMLStyleElement, // HTMLScriptElement, HTMLVideoElement, HTMLAudioElement if (aNode.IsAnyOfHTMLElements( nsGkAtoms::iframe, nsGkAtoms::image, nsGkAtoms::meter, nsGkAtoms::object, nsGkAtoms::progress, nsGkAtoms::style, nsGkAtoms::script, nsGkAtoms::video, nsGkAtoms::audio)) { return true; } // 4. Is a select element whose multiple content attribute is absent. if (aNode.IsHTMLElement(nsGkAtoms::select)) { return aNode.GetAttributes()->GetNamedItem(u"multiple"_ns) == nullptr; } // This is tested last because it's the most expensive check. // 1. The computed value of its 'display' property is 'none'. const Element* nodeAsElement = Element::FromNode(aNode); const RefPtr computedStyle = nsComputedDOMStyle::GetComputedStyleNoFlush(nodeAsElement); return !computedStyle || computedStyle->StyleDisplay()->mDisplay == StyleDisplay::None; } /* static */ bool TextDirectiveUtil::NodeHasBlockLevelDisplay(nsINode& aNode) { if (!aNode.IsElement()) { return false; } const Element* nodeAsElement = Element::FromNode(aNode); const RefPtr computedStyle = nsComputedDOMStyle::GetComputedStyleNoFlush(nodeAsElement); if (!computedStyle) { return false; } const StyleDisplay& styleDisplay = computedStyle->StyleDisplay()->mDisplay; return styleDisplay == StyleDisplay::Block || styleDisplay == StyleDisplay::Table || styleDisplay == StyleDisplay::FlowRoot || styleDisplay == StyleDisplay::Grid || styleDisplay == StyleDisplay::Flex || styleDisplay.IsListItem(); } /* static */ nsINode* TextDirectiveUtil::GetBlockAncestorForNode( nsINode* aNode) { // 1. Let curNode be node. RefPtr curNode = aNode; // 2. While curNode is non-null while (curNode) { // 2.1. If curNode is not a Text node and it has block-level display then // return curNode. if (!curNode->IsText() && NodeHasBlockLevelDisplay(*curNode)) { return curNode; } // 2.2. Otherwise, set curNode to curNode’s parent. curNode = curNode->GetParentNode(); } // 3.Return node’s node document's document element. return aNode->GetOwnerDocument(); } /* static */ bool TextDirectiveUtil::NodeIsPartOfNonSearchableSubTree( nsINode& aNode) { nsINode* node = &aNode; do { if (NodeIsSearchInvisible(*node)) { return true; } } while ((node = node->GetParentOrShadowHostNode())); return false; } /* static */ bool TextDirectiveUtil::IsAtWordBoundary(const nsAString& aText, uint32_t aPosition) { const intl::WordRange wordRange = intl::WordBreaker::FindWord(aText, aPosition); return wordRange.mBegin == aPosition || wordRange.mEnd == aPosition; } /* static */ RangeBoundary TextDirectiveUtil::GetBoundaryPointAtIndex( uint32_t aIndex, const nsTArray>& aTextNodeList, IsEndIndex aIsEndIndex) { // 1. Let counted be 0. uint32_t counted = 0; // 2. For each curNode of nodes: for (Text* curNode : aTextNodeList) { // 2.1. Let nodeEnd be counted + curNode’s length. uint32_t nodeEnd = counted + curNode->Length(); // 2.2. If isEnd is true, add 1 to nodeEnd. if (aIsEndIndex == IsEndIndex::Yes) { ++nodeEnd; } // 2.3. If nodeEnd is greater than index then: if (nodeEnd > aIndex) { // 2.3.1. Return the boundary point (curNode, index − counted). return RangeBoundary(curNode->AsNode(), aIndex - counted); } // 2.4. Increment counted by curNode’s length. counted += curNode->Length(); } return {}; } /* static */ void TextDirectiveUtil::AdvanceStartToNextNonWhitespacePosition( nsRange& aRange) { // 1. While range is not collapsed: while (!aRange.Collapsed()) { // 1.1. Let node be range's start node. RefPtr node = aRange.GetStartContainer(); MOZ_ASSERT(node); // 1.2. Let offset be range's start offset. const uint32_t offset = aRange.StartOffset(); // 1.3. If node is part of a non-searchable subtree or if node is not a // visible text node or if offset is equal to node's length then: if (NodeIsPartOfNonSearchableSubTree(*node) || !NodeIsVisibleTextNode(*node) || offset == node->Length()) { // 1.3.1. Set range's start node to the next node, in shadow-including // tree order. // 1.3.2. Set range's start offset to 0. if (NS_FAILED(aRange.SetStart(node->GetNextNode(), 0))) { return; } // 1.3.3. Continue. continue; } const Text* text = Text::FromNode(node); MOZ_ASSERT(text); // These steps are moved to `IsWhitespaceAtPosition()`. // 1.4. If the substring data of node at offset offset and count 6 is equal // to the string " " then: // 1.4.1. Add 6 to range’s start offset. // 1.5. Otherwise, if the substring data of node at offset offset and count // 5 is equal to the string " " then: // 1.5.1. Add 5 to range’s start offset. // 1.6. Otherwise: // 1.6.1 Let cp be the code point at the offset index in node’s data. // 1.6.2 If cp does not have the White_Space property set, return. // 1.6.3 Add 1 to range’s start offset. if (!IsWhitespaceAtPosition(text, offset)) { return; } aRange.SetStart(node, offset + 1); } } /* static */ RangeBoundary TextDirectiveUtil::MoveBoundaryToNextNonWhitespacePosition( const RangeBoundary& aRangeBoundary) { MOZ_ASSERT(aRangeBoundary.IsSetAndValid()); nsINode* node = aRangeBoundary.GetContainer(); uint32_t offset = *aRangeBoundary.Offset(RangeBoundary::OffsetFilter::kValidOffsets); while (node) { if (TextDirectiveUtil::NodeIsPartOfNonSearchableSubTree(*node) || !TextDirectiveUtil::NodeIsVisibleTextNode(*node) || offset == node->Length()) { nsINode* newNode = node->GetNextNode(); if (!newNode) { // jjaschke: I don't see a situation where this could happen. However, // let's return the original range boundary as fallback. return aRangeBoundary; } node = newNode; offset = 0; continue; } const Text* text = Text::FromNode(node); MOZ_ASSERT(text); if (TextDirectiveUtil::IsWhitespaceAtPosition(text, offset)) { ++offset; continue; } return {node, offset}; } MOZ_ASSERT_UNREACHABLE("All code paths must return in the loop."); return {}; } /* static */ RangeBoundary TextDirectiveUtil::MoveBoundaryToPreviousNonWhitespacePosition( const RangeBoundary& aRangeBoundary) { MOZ_ASSERT(aRangeBoundary.IsSetAndValid()); nsINode* node = aRangeBoundary.GetContainer(); uint32_t offset = *aRangeBoundary.Offset(RangeBoundary::OffsetFilter::kValidOffsets); // Decrement offset by one so that the actual previous character is used. This // means that we need to increment the offset by 1 when we have found the // non-whitespace character. while (node) { if (TextDirectiveUtil::NodeIsPartOfNonSearchableSubTree(*node) || !TextDirectiveUtil::NodeIsVisibleTextNode(*node) || offset == 0) { nsIContent* newNode = node->GetPrevNode(); if (!newNode) { // jjaschke: I don't see a situation where this could happen. However, // let's return the original range boundary as fallback. return aRangeBoundary; } node = newNode; offset = node->Length(); continue; } const Text* text = Text::FromNode(node); MOZ_ASSERT(text); if (TextDirectiveUtil::IsWhitespaceAtPosition(text, offset - 1)) { --offset; continue; } return {node, offset}; } MOZ_ASSERT_UNREACHABLE("All code paths must return in the loop."); return {}; } /* static */ Result TextDirectiveUtil::FindNextBlockBoundary(const RangeBoundary& aRangeBoundary, TextScanDirection aDirection) { MOZ_ASSERT(aRangeBoundary.IsSetAndValid()); auto findNextBlockBoundaryInternal = [aDirection](const RangeBoundary& rangeBoundary) -> Result { PeekOffsetOptions options = { PeekOffsetOption::JumpLines, PeekOffsetOption::StopAtScroller, PeekOffsetOption::IsKeyboardSelect, PeekOffsetOption::Extend}; return SelectionMovementUtils::MoveRangeBoundaryToSomewhere( rangeBoundary, aDirection == TextScanDirection::Left ? nsDirection::eDirPrevious : nsDirection::eDirNext, CaretAssociationHint::After, intl::BidiEmbeddingLevel::DefaultLTR(), nsSelectionAmount::eSelectParagraph, options) .mapErr([](nsresult rv) { return ErrorResult(rv); }); }; auto maybeNewBoundary = findNextBlockBoundaryInternal(aRangeBoundary); if (MOZ_UNLIKELY(maybeNewBoundary.isErr())) { return maybeNewBoundary.propagateErr(); } auto newBoundary = maybeNewBoundary.unwrap(); while (NormalizedRangeBoundariesAreEqual(aRangeBoundary, newBoundary)) { maybeNewBoundary = findNextBlockBoundaryInternal(newBoundary); if (MOZ_UNLIKELY(maybeNewBoundary.isErr())) { return maybeNewBoundary.propagateErr(); } if (maybeNewBoundary.inspect() == newBoundary) { return newBoundary; // we reached the end or so? } newBoundary = maybeNewBoundary.unwrap(); } return newBoundary; } /* static */ Result, ErrorResult> TextDirectiveUtil::FindBlockBoundaryInRange(const nsRange& aRange, TextScanDirection aDirection) { if (aRange.Collapsed()) { return Result, ErrorResult>(Nothing{}); } if (aDirection == TextScanDirection::Right) { Result maybeBoundary = FindNextBlockBoundary(aRange.StartRef(), TextScanDirection::Right); if (MOZ_UNLIKELY(maybeBoundary.isErr())) { return maybeBoundary.propagateErr(); } RangeBoundary boundary = maybeBoundary.unwrap(); Maybe compare = nsContentUtils::ComparePoints(boundary, aRange.EndRef()); if (!compare || *compare != -1) { // *compare == -1 means that the found block boundary is after the range // end, and therefore outside of the range. return Result, ErrorResult>(Nothing{}); } return Some(boundary); } Result maybeBoundary = FindNextBlockBoundary(aRange.EndRef(), TextScanDirection::Left); if (MOZ_UNLIKELY(maybeBoundary.isErr())) { return maybeBoundary.propagateErr(); } RangeBoundary boundary = maybeBoundary.unwrap(); auto compare = nsContentUtils::ComparePoints(aRange.StartRef(), boundary); if (!compare || *compare != -1) { // *compare == 1 means that the found block boundary is before the range // start boundary, and therefore outside of the range. return Result, ErrorResult>(Nothing{}); } return Some(boundary); } /* static */ bool TextDirectiveUtil::NormalizedRangeBoundariesAreEqual( const RangeBoundary& aRangeBoundary1, const RangeBoundary& aRangeBoundary2, nsContentUtils::NodeIndexCache* aCache /* = nullptr */) { MOZ_ASSERT(aRangeBoundary1.IsSetAndValid() && aRangeBoundary2.IsSetAndValid()); if (aRangeBoundary1 == aRangeBoundary2) { return true; } auto textSubStringIsOnlyWhitespace = [](const Text* textNode, uint32_t startIndex, uint32_t endIndex) { MOZ_ASSERT(textNode); if (startIndex > endIndex) { std::swap(startIndex, endIndex); } MOZ_ASSERT(startIndex < textNode->Length()); if (startIndex == endIndex) { return true; } const nsTextFragment& textFragment = textNode->TextFragment(); for (uint32_t i = startIndex; i < endIndex; ++i) { char16_t ch = textFragment.CharAt(i); if (!nsContentUtils::IsHTMLWhitespaceOrNBSP(ch)) { return false; } } return true; }; const nsINode* node1 = aRangeBoundary1.GetContainer(); const nsINode* node2 = aRangeBoundary2.GetContainer(); size_t offset1 = *aRangeBoundary1.Offset(RangeBoundary::OffsetFilter::kValidOffsets); size_t offset2 = *aRangeBoundary2.Offset(RangeBoundary::OffsetFilter::kValidOffsets); if (node1 == node2) { if (const Text* text = Text::FromNodeOrNull(node1)) { return textSubStringIsOnlyWhitespace(text, offset1, offset2); } return offset1 == offset2; } mozilla::UnsafePreContentIterator iter; // ContentIterator classes require boundaries to be in correct order. auto comp = nsContentUtils::ComparePoints(aRangeBoundary1, aRangeBoundary2, aCache); if (!comp) { return false; } if (*comp == 0) { return true; } const auto& [firstBoundary, secondBoundary] = *comp == -1 ? std::tuple{&aRangeBoundary1, &aRangeBoundary2} : std::tuple{&aRangeBoundary2, &aRangeBoundary1}; if (NS_FAILED(iter.InitWithoutValidatingPoints(firstBoundary->AsRaw(), secondBoundary->AsRaw()))) { return false; } for (; !iter.IsDone(); iter.Next()) { auto* node = iter.GetCurrentNode(); if (!node || !TextDirectiveUtil::NodeIsVisibleTextNode(*node)) { continue; } if (node == firstBoundary->GetContainer()) { auto firstOffset = *firstBoundary->Offset(RangeBoundary::OffsetFilter::kValidOffsets); if (firstOffset == node->Length()) { // if this is the start node, it's a text node and the offset is at the // end, continue with the next node. continue; } if (const Text* text = Text::FromNodeOrNull(node)) { if (textSubStringIsOnlyWhitespace(text, firstOffset, text->Length())) { continue; } } } if (node == secondBoundary->GetContainer()) { auto secondOffset = *secondBoundary->Offset(RangeBoundary::OffsetFilter::kValidOffsets); if (secondOffset == 0) { // if this is the end node, it's a text node and the offset is 0, return // true. return true; } if (const Text* text = Text::FromNodeOrNull(node)) { if (textSubStringIsOnlyWhitespace(text, 0, secondOffset)) { return true; } } } if (node->Length() && !node->AsText()->TextIsOnlyWhitespace()) { // if the text node only contains whitespace, ignore it; otherwise, the // boundaries are not at the same spot. return false; } } return true; } /* static */ Result TextDirectiveUtil::ExtendRangeToWordBoundaries(nsRange& aRange) { MOZ_ASSERT(!aRange.Collapsed()); PeekOffsetOptions options = { PeekOffsetOption::JumpLines, PeekOffsetOption::StopAtScroller, PeekOffsetOption::IsKeyboardSelect, PeekOffsetOption::Extend}; // To extend a range `inputRange` to its word boundaries, perform these steps: // 1. To extend the start boundary: // 1.1 Let `newStartBoundary` be a range boundary, initially null. // 1.2 Create a new range boundary `rangeStartWordEndBoundary` at the next // word end boundary at `inputRange`s start point. // 1.3 Then, create a new range boundary `rangeStartWordStartBoundary` // at the previous word start boundary of `rangeStartWordEndBoundary` // 1.4 If `rangeStartWordStartBoundary` is not at the same (normalized) // position as `inputRange`s start point, let `newStartBoundary` be // `rangeStartWordStartBoundary`. Result, ErrorResult> newStartBoundary = SelectionMovementUtils::MoveRangeBoundaryToSomewhere( aRange.StartRef(), nsDirection::eDirNext, CaretAssociationHint::After, intl::BidiEmbeddingLevel::DefaultLTR(), nsSelectionAmount::eSelectWord, options) .andThen([&options](const RangeBoundary& rangeStartWordEndBoundary) { return SelectionMovementUtils::MoveRangeBoundaryToSomewhere( rangeStartWordEndBoundary, nsDirection::eDirPrevious, CaretAssociationHint::Before, intl::BidiEmbeddingLevel::DefaultLTR(), nsSelectionAmount::eSelectWord, options); }) .map([&rangeStart = aRange.StartRef()]( RangeBoundary&& rangeStartWordStartBoundary) { return NormalizedRangeBoundariesAreEqual( rangeStartWordStartBoundary, rangeStart) ? Nothing{} : Some(std::move(rangeStartWordStartBoundary)); }) .mapErr([](nsresult rv) { return ErrorResult(rv); }); if (MOZ_UNLIKELY(newStartBoundary.isErr())) { return newStartBoundary.propagateErr(); } // 2. To extend the end boundary: // 2.1 Let `newEndBoundary` be a range boundary, initially null. // 2.2 Create a new range boundary `rangeEndWordStartBoundary` at the previous // word start boundary at `inputRange`s end point. // 2.3 Then, create a new range boundary `rangeEndWordEndBoundary` at the next // word end boundary from `rangeEndWordStartBoundary`. // 2.4 If `rangeEndWordEndBoundary` is not at the same (normalized) position // as `inputRange`s end point, let `newEndBoundary` be // `rangEndWordEndBoundary`. Result, ErrorResult> newEndBoundary = SelectionMovementUtils::MoveRangeBoundaryToSomewhere( aRange.EndRef(), nsDirection::eDirPrevious, CaretAssociationHint::Before, intl::BidiEmbeddingLevel::DefaultLTR(), nsSelectionAmount::eSelectWord, options) .andThen([&options](const RangeBoundary& rangeEndWordStartBoundary) { return SelectionMovementUtils::MoveRangeBoundaryToSomewhere( rangeEndWordStartBoundary, nsDirection::eDirNext, CaretAssociationHint::After, intl::BidiEmbeddingLevel::DefaultLTR(), nsSelectionAmount::eSelectWord, options); }) .map([&rangeEnd = aRange.EndRef()](RangeBoundary&& rangeEndWordEndBoundary) { return NormalizedRangeBoundariesAreEqual(rangeEndWordEndBoundary, rangeEnd) ? Nothing{} : Some(std::move(rangeEndWordEndBoundary)); }) .mapErr([](auto rv) { return ErrorResult(rv); }); if (MOZ_UNLIKELY(newEndBoundary.isErr())) { return newEndBoundary.propagateErr(); } // 3. If `newStartBoundary` is not null, set `inputRange`s start point to // `newStartBoundary`. MOZ_TRY(newStartBoundary.andThen( [&aRange](Maybe&& boundary) -> Result { if (boundary.isNothing() || !boundary->IsSetAndValid()) { return Ok(); } ErrorResult rv; aRange.SetStart(boundary->AsRaw(), rv); if (MOZ_UNLIKELY(rv.Failed())) { return Err(std::move(rv)); } return Ok(); })); // 4. If `newEndBoundary` is not null, set `inputRange`s end point to // `newEndBoundary`. MOZ_TRY(newEndBoundary.andThen( [&aRange](Maybe&& boundary) -> Result { if (boundary.isNothing() || !boundary->IsSetAndValid()) { return Ok(); } ErrorResult rv; aRange.SetEnd(boundary->AsRaw(), rv); if (MOZ_UNLIKELY(rv.Failed())) { return Err(std::move(rv)); } return Ok(); })); return Ok(); } /* static */ Result TextDirectiveUtil::CreateTextDirectiveFromRanges(nsRange* aPrefix, nsRange* aStart, nsRange* aEnd, nsRange* aSuffix) { MOZ_ASSERT(aStart && !aStart->Collapsed()); ErrorResult rv; TextDirective textDirective; MOZ_TRY(RangeContentAsString(aStart).andThen( [&textDirective](nsString start) -> Result { textDirective.start = std::move(start); return Ok(); })); MOZ_TRY(RangeContentAsString(aPrefix).andThen( [&textDirective](nsString prefix) -> Result { textDirective.prefix = std::move(prefix); return Ok(); })); MOZ_TRY(RangeContentAsString(aEnd).andThen( [&textDirective](nsString end) -> Result { textDirective.end = std::move(end); return Ok(); })); MOZ_TRY(RangeContentAsString(aSuffix).andThen( [&textDirective](nsString suffix) -> Result { textDirective.suffix = std::move(suffix); return Ok(); })); return textDirective; } uint32_t TextDirectiveUtil::FindCommonPrefix(const nsAString& aFoldedStr1, const nsAString& aFoldedStr2) { const uint32_t maxCommonLength = std::min(aFoldedStr1.Length(), aFoldedStr2.Length()); uint32_t commonLength = 0; const char16_t* iter1 = aFoldedStr1.BeginReading(); const char16_t* iter2 = aFoldedStr2.BeginReading(); while (commonLength < maxCommonLength) { if (*iter1 != *iter2) { break; } ++iter1; ++iter2; ++commonLength; } // this condition ensures that if the high surrogate is removed if the low // surrogate does not match. if (commonLength && NS_IS_HIGH_SURROGATE(*(iter1 - 1))) { --commonLength; } return commonLength; } uint32_t TextDirectiveUtil::FindCommonSuffix(const nsAString& aFoldedStr1, const nsAString& aFoldedStr2) { const uint32_t maxCommonLength = std::min(aFoldedStr1.Length(), aFoldedStr2.Length()); uint32_t commonLength = 0; const char16_t* iter1 = aFoldedStr1.EndReading(); const char16_t* iter2 = aFoldedStr2.EndReading(); while (commonLength != maxCommonLength) { if (*(iter1 - 1) != *(iter2 - 1)) { break; } --iter1; --iter2; ++commonLength; } // this condition ensures that a matching low surrogate is removed if the high // surrogate does not match. if (commonLength && NS_IS_LOW_SURROGATE(*iter1)) { --commonLength; } return commonLength; } RangeBoundary TextDirectiveUtil::CreateRangeBoundaryByMovingOffsetFromRangeStart( nsRange* aRange, uint32_t aLogicalOffset) { MOZ_ASSERT(!aRange->Collapsed()); nsINode* node = aRange->GetStartContainer(); uint32_t startOffset = aRange->StartOffset(); uint32_t remaining = startOffset + aLogicalOffset; while (node && remaining) { if (NodeIsPartOfNonSearchableSubTree(*node) || !NodeIsVisibleTextNode(*node)) { node = node->GetNextNode(); continue; } MOZ_ASSERT_IF(node == aRange->GetEndContainer(), remaining <= node->Length()); if (node->Length() <= remaining) { remaining -= node->Length(); node = node->GetNextNode(); continue; } return {node, remaining}; } return {node, remaining}; } } // namespace mozilla::dom