/* -*- 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 "ContentIterator.h" #include "Document.h" #include "Text.h" #include "fragmentdirectives_ffi_generated.h" #include "mozilla/ContentIterator.h" #include "mozilla/ResultVariant.h" #include "mozilla/SelectionMovementUtils.h" #include "mozilla/intl/WordBreaker.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" namespace mozilla::dom { LazyLogModule gFragmentDirectiveLog("FragmentDirective"); /* static */ Result TextDirectiveUtil::RangeContentAsString( AbstractRange* 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->DataBuffer().AppendTo(content, startOffset, endOffset - startOffset); } content.CompressWhitespace(); 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( nsFind* aFinder, const RangeBoundary& aSearchStart, const RangeBoundary& aSearchEnd, const nsAString& aQuery, bool aWordStartBounded, bool aWordEndBounded) { MOZ_DIAGNOSTIC_ASSERT(aFinder); TEXT_FRAGMENT_LOG("query='{}', wordStartBounded='{}', wordEndBounded='{}'.\n", NS_ConvertUTF16toUTF8(aQuery), aWordStartBounded, aWordEndBounded); aFinder->SetWordStartBounded(aWordStartBounded); aFinder->SetWordEndBounded(aWordEndBounded); aFinder->SetCaseSensitive(false); RefPtr result = aFinder->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 */ bool TextDirectiveUtil::IsWhitespaceAtPosition(const Text* aText, uint32_t aPos) { if (!aText || aText->Length() == 0 || aPos >= aText->Length()) { return false; } const CharacterDataBuffer& characterDataBuffer = aText->DataBuffer(); const char NBSP_CHAR = char(0xA0); if (characterDataBuffer.Is2b()) { const char16_t* content = characterDataBuffer.Get2b(); return IsSpaceCharacter(content[aPos]) || content[aPos] == char16_t(NBSP_CHAR); } const char* content = characterDataBuffer.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::TableCell || 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 */ 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); } } // https://wicg.github.io/scroll-to-text-fragment/#find-a-range-from-a-text-directive // Steps 2.2.3, 2.3.4 /* static */ RangeBoundary TextDirectiveUtil::MoveToNextBoundaryPoint( const RangeBoundary& aPoint) { MOZ_DIAGNOSTIC_ASSERT(aPoint.IsSetAndValid()); Text* node = Text::FromNode(aPoint.GetContainer()); MOZ_ASSERT(node); uint32_t pos = *aPoint.Offset(RangeBoundary::OffsetFilter::kValidOrInvalidOffsets); if (!node) { return RangeBoundary{}; } ++pos; if (pos < node->Length() && node->GetCharacterDataBuffer()->IsLowSurrogateFollowingHighSurrogateAt( pos)) { ++pos; } return {node, pos}; } /* static */ bool TextDirectiveUtil::WordIsJustWhitespaceOrPunctuation( const nsAString& aString, uint32_t aWordBegin, uint32_t aWordEnd) { MOZ_ASSERT(aWordEnd <= aString.Length()); MOZ_ASSERT(aWordBegin < aWordEnd); auto word = aString.View().substr(aWordBegin, aWordEnd - aWordBegin); return std::all_of(word.cbegin(), word.cend(), [](const char16_t ch) { return nsContentUtils::IsHTMLWhitespaceOrNBSP(ch) || mozilla::IsPunctuationForWordSelect(ch); }); } } // namespace mozilla::dom