tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 "&nbsp;" 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 "&nbsp" 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