TextDirectiveUtil.cpp (10051B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim:set ts=2 sw=2 sts=2 et cindent: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 #include "TextDirectiveUtil.h" 7 8 #include "ContentIterator.h" 9 #include "Document.h" 10 #include "Text.h" 11 #include "fragmentdirectives_ffi_generated.h" 12 #include "mozilla/ContentIterator.h" 13 #include "mozilla/ResultVariant.h" 14 #include "mozilla/SelectionMovementUtils.h" 15 #include "mozilla/intl/WordBreaker.h" 16 #include "nsComputedDOMStyle.h" 17 #include "nsDOMAttributeMap.h" 18 #include "nsFind.h" 19 #include "nsFrameSelection.h" 20 #include "nsGkAtoms.h" 21 #include "nsIFrame.h" 22 #include "nsINode.h" 23 #include "nsIURI.h" 24 #include "nsRange.h" 25 #include "nsString.h" 26 #include "nsTArray.h" 27 #include "nsUnicharUtils.h" 28 29 namespace mozilla::dom { 30 LazyLogModule gFragmentDirectiveLog("FragmentDirective"); 31 32 /* static */ 33 Result<nsString, ErrorResult> TextDirectiveUtil::RangeContentAsString( 34 AbstractRange* aRange) { 35 nsString content; 36 if (!aRange || aRange->Collapsed()) { 37 return content; 38 } 39 UnsafePreContentIterator iter; 40 nsresult rv = iter.Init(aRange); 41 if (NS_FAILED(rv)) { 42 return Err(ErrorResult(rv)); 43 } 44 for (; !iter.IsDone(); iter.Next()) { 45 nsINode* current = iter.GetCurrentNode(); 46 if (!TextDirectiveUtil::NodeIsVisibleTextNode(*current) || 47 TextDirectiveUtil::NodeIsPartOfNonSearchableSubTree(*current)) { 48 continue; 49 } 50 const uint32_t startOffset = 51 current == aRange->GetStartContainer() ? aRange->StartOffset() : 0; 52 const uint32_t endOffset = 53 std::min(current == aRange->GetEndContainer() ? aRange->EndOffset() 54 : current->Length(), 55 current->Length()); 56 const Text* text = Text::FromNode(current); 57 text->DataBuffer().AppendTo(content, startOffset, endOffset - startOffset); 58 } 59 content.CompressWhitespace(); 60 return content; 61 } 62 63 /* static */ bool TextDirectiveUtil::NodeIsVisibleTextNode( 64 const nsINode& aNode) { 65 const Text* text = Text::FromNode(aNode); 66 if (!text) { 67 return false; 68 } 69 const nsIFrame* frame = text->GetPrimaryFrame(); 70 return frame && frame->StyleVisibility()->IsVisible(); 71 } 72 73 /* static */ RefPtr<nsRange> TextDirectiveUtil::FindStringInRange( 74 nsFind* aFinder, const RangeBoundary& aSearchStart, 75 const RangeBoundary& aSearchEnd, const nsAString& aQuery, 76 bool aWordStartBounded, bool aWordEndBounded) { 77 MOZ_DIAGNOSTIC_ASSERT(aFinder); 78 TEXT_FRAGMENT_LOG("query='{}', wordStartBounded='{}', wordEndBounded='{}'.\n", 79 NS_ConvertUTF16toUTF8(aQuery), aWordStartBounded, 80 aWordEndBounded); 81 aFinder->SetWordStartBounded(aWordStartBounded); 82 aFinder->SetWordEndBounded(aWordEndBounded); 83 aFinder->SetCaseSensitive(false); 84 RefPtr<nsRange> result = 85 aFinder->FindFromRangeBoundaries(aQuery, aSearchStart, aSearchEnd); 86 if (!result || result->Collapsed()) { 87 TEXT_FRAGMENT_LOG("Did not find query '{}'", NS_ConvertUTF16toUTF8(aQuery)); 88 } else { 89 auto rangeToString = [](nsRange* range) -> nsCString { 90 nsString rangeString; 91 range->ToString(rangeString, IgnoreErrors()); 92 return NS_ConvertUTF16toUTF8(rangeString); 93 }; 94 TEXT_FRAGMENT_LOG("find returned '{}'", rangeToString(result)); 95 } 96 return result; 97 } 98 99 /* static */ bool TextDirectiveUtil::IsWhitespaceAtPosition(const Text* aText, 100 uint32_t aPos) { 101 if (!aText || aText->Length() == 0 || aPos >= aText->Length()) { 102 return false; 103 } 104 const CharacterDataBuffer& characterDataBuffer = aText->DataBuffer(); 105 const char NBSP_CHAR = char(0xA0); 106 if (characterDataBuffer.Is2b()) { 107 const char16_t* content = characterDataBuffer.Get2b(); 108 return IsSpaceCharacter(content[aPos]) || 109 content[aPos] == char16_t(NBSP_CHAR); 110 } 111 const char* content = characterDataBuffer.Get1b(); 112 return IsSpaceCharacter(content[aPos]) || content[aPos] == NBSP_CHAR; 113 } 114 115 /* static */ bool TextDirectiveUtil::NodeIsSearchInvisible(nsINode& aNode) { 116 if (!aNode.IsElement()) { 117 return false; 118 } 119 // 2. If the node serializes as void. 120 nsAtom* nodeNameAtom = aNode.NodeInfo()->NameAtom(); 121 if (FragmentOrElement::IsHTMLVoid(nodeNameAtom)) { 122 return true; 123 } 124 // 3. Is any of the following types: HTMLIFrameElement, HTMLImageElement, 125 // HTMLMeterElement, HTMLObjectElement, HTMLProgressElement, HTMLStyleElement, 126 // HTMLScriptElement, HTMLVideoElement, HTMLAudioElement 127 if (aNode.IsAnyOfHTMLElements( 128 nsGkAtoms::iframe, nsGkAtoms::image, nsGkAtoms::meter, 129 nsGkAtoms::object, nsGkAtoms::progress, nsGkAtoms::style, 130 nsGkAtoms::script, nsGkAtoms::video, nsGkAtoms::audio)) { 131 return true; 132 } 133 // 4. Is a select element whose multiple content attribute is absent. 134 if (aNode.IsHTMLElement(nsGkAtoms::select)) { 135 return aNode.GetAttributes()->GetNamedItem(u"multiple"_ns) == nullptr; 136 } 137 // This is tested last because it's the most expensive check. 138 // 1. The computed value of its 'display' property is 'none'. 139 const Element* nodeAsElement = Element::FromNode(aNode); 140 const RefPtr<const ComputedStyle> computedStyle = 141 nsComputedDOMStyle::GetComputedStyleNoFlush(nodeAsElement); 142 return !computedStyle || 143 computedStyle->StyleDisplay()->mDisplay == StyleDisplay::None; 144 } 145 146 /* static */ bool TextDirectiveUtil::NodeHasBlockLevelDisplay(nsINode& aNode) { 147 if (!aNode.IsElement()) { 148 return false; 149 } 150 const Element* nodeAsElement = Element::FromNode(aNode); 151 const RefPtr<const ComputedStyle> computedStyle = 152 nsComputedDOMStyle::GetComputedStyleNoFlush(nodeAsElement); 153 if (!computedStyle) { 154 return false; 155 } 156 const StyleDisplay& styleDisplay = computedStyle->StyleDisplay()->mDisplay; 157 return styleDisplay == StyleDisplay::Block || 158 styleDisplay == StyleDisplay::Table || 159 styleDisplay == StyleDisplay::TableCell || 160 styleDisplay == StyleDisplay::FlowRoot || 161 styleDisplay == StyleDisplay::Grid || 162 styleDisplay == StyleDisplay::Flex || styleDisplay.IsListItem(); 163 } 164 165 /* static */ nsINode* TextDirectiveUtil::GetBlockAncestorForNode( 166 nsINode* aNode) { 167 // 1. Let curNode be node. 168 RefPtr<nsINode> curNode = aNode; 169 // 2. While curNode is non-null 170 while (curNode) { 171 // 2.1. If curNode is not a Text node and it has block-level display then 172 // return curNode. 173 if (!curNode->IsText() && NodeHasBlockLevelDisplay(*curNode)) { 174 return curNode; 175 } 176 // 2.2. Otherwise, set curNode to curNode’s parent. 177 curNode = curNode->GetParentNode(); 178 } 179 // 3.Return node’s node document's document element. 180 return aNode->GetOwnerDocument(); 181 } 182 183 /* static */ bool TextDirectiveUtil::NodeIsPartOfNonSearchableSubTree( 184 nsINode& aNode) { 185 nsINode* node = &aNode; 186 do { 187 if (NodeIsSearchInvisible(*node)) { 188 return true; 189 } 190 } while ((node = node->GetParentOrShadowHostNode())); 191 return false; 192 } 193 194 /* static */ void TextDirectiveUtil::AdvanceStartToNextNonWhitespacePosition( 195 nsRange& aRange) { 196 // 1. While range is not collapsed: 197 while (!aRange.Collapsed()) { 198 // 1.1. Let node be range's start node. 199 RefPtr<nsINode> node = aRange.GetStartContainer(); 200 MOZ_ASSERT(node); 201 // 1.2. Let offset be range's start offset. 202 const uint32_t offset = aRange.StartOffset(); 203 // 1.3. If node is part of a non-searchable subtree or if node is not a 204 // visible text node or if offset is equal to node's length then: 205 if (NodeIsPartOfNonSearchableSubTree(*node) || 206 !NodeIsVisibleTextNode(*node) || offset == node->Length()) { 207 // 1.3.1. Set range's start node to the next node, in shadow-including 208 // tree order. 209 // 1.3.2. Set range's start offset to 0. 210 if (NS_FAILED(aRange.SetStart(node->GetNextNode(), 0))) { 211 return; 212 } 213 // 1.3.3. Continue. 214 continue; 215 } 216 const Text* text = Text::FromNode(node); 217 MOZ_ASSERT(text); 218 // These steps are moved to `IsWhitespaceAtPosition()`. 219 // 1.4. If the substring data of node at offset offset and count 6 is equal 220 // to the string " " then: 221 // 1.4.1. Add 6 to range’s start offset. 222 // 1.5. Otherwise, if the substring data of node at offset offset and count 223 // 5 is equal to the string " " then: 224 // 1.5.1. Add 5 to range’s start offset. 225 // 1.6. Otherwise: 226 // 1.6.1 Let cp be the code point at the offset index in node’s data. 227 // 1.6.2 If cp does not have the White_Space property set, return. 228 // 1.6.3 Add 1 to range’s start offset. 229 if (!IsWhitespaceAtPosition(text, offset)) { 230 return; 231 } 232 233 aRange.SetStart(node, offset + 1); 234 } 235 } 236 // https://wicg.github.io/scroll-to-text-fragment/#find-a-range-from-a-text-directive 237 // Steps 2.2.3, 2.3.4 238 /* static */ 239 RangeBoundary TextDirectiveUtil::MoveToNextBoundaryPoint( 240 const RangeBoundary& aPoint) { 241 MOZ_DIAGNOSTIC_ASSERT(aPoint.IsSetAndValid()); 242 Text* node = Text::FromNode(aPoint.GetContainer()); 243 MOZ_ASSERT(node); 244 uint32_t pos = 245 *aPoint.Offset(RangeBoundary::OffsetFilter::kValidOrInvalidOffsets); 246 if (!node) { 247 return RangeBoundary{}; 248 } 249 ++pos; 250 if (pos < node->Length() && 251 node->GetCharacterDataBuffer()->IsLowSurrogateFollowingHighSurrogateAt( 252 pos)) { 253 ++pos; 254 } 255 return {node, pos}; 256 } 257 258 /* static */ bool TextDirectiveUtil::WordIsJustWhitespaceOrPunctuation( 259 const nsAString& aString, uint32_t aWordBegin, uint32_t aWordEnd) { 260 MOZ_ASSERT(aWordEnd <= aString.Length()); 261 MOZ_ASSERT(aWordBegin < aWordEnd); 262 263 auto word = aString.View().substr(aWordBegin, aWordEnd - aWordBegin); 264 return std::all_of(word.cbegin(), word.cend(), [](const char16_t ch) { 265 return nsContentUtils::IsHTMLWhitespaceOrNBSP(ch) || 266 mozilla::IsPunctuationForWordSelect(ch); 267 }); 268 } 269 270 } // namespace mozilla::dom