tor-browser

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

TextContent.ts (3472B)


      1 /**
      2 * @license
      3 * Copyright 2022 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 interface NonTrivialValueNode extends Node {
      8  value: string;
      9 }
     10 
     11 const TRIVIAL_VALUE_INPUT_TYPES = new Set(['checkbox', 'image', 'radio']);
     12 
     13 /**
     14 * Determines if the node has a non-trivial value property.
     15 *
     16 * @internal
     17 */
     18 const isNonTrivialValueNode = (node: Node): node is NonTrivialValueNode => {
     19  if (node instanceof HTMLSelectElement) {
     20    return true;
     21  }
     22  if (node instanceof HTMLTextAreaElement) {
     23    return true;
     24  }
     25  if (
     26    node instanceof HTMLInputElement &&
     27    !TRIVIAL_VALUE_INPUT_TYPES.has(node.type)
     28  ) {
     29    return true;
     30  }
     31  return false;
     32 };
     33 
     34 const UNSUITABLE_NODE_NAMES = new Set(['SCRIPT', 'STYLE']);
     35 
     36 /**
     37 * Determines whether a given node is suitable for text matching.
     38 *
     39 * @internal
     40 */
     41 export const isSuitableNodeForTextMatching = (node: Node): boolean => {
     42  return (
     43    !UNSUITABLE_NODE_NAMES.has(node.nodeName) && !document.head?.contains(node)
     44  );
     45 };
     46 
     47 /**
     48 * @internal
     49 */
     50 export interface TextContent {
     51  // Contains the full text of the node.
     52  full: string;
     53  // Contains the text immediately beneath the node.
     54  immediate: string[];
     55 }
     56 
     57 /**
     58 * Maps {@link Node}s to their computed {@link TextContent}.
     59 */
     60 const textContentCache = new WeakMap<Node, TextContent>();
     61 const eraseFromCache = (node: Node | null) => {
     62  while (node) {
     63    textContentCache.delete(node);
     64    if (node instanceof ShadowRoot) {
     65      node = node.host;
     66    } else {
     67      node = node.parentNode;
     68    }
     69  }
     70 };
     71 
     72 /**
     73 * Erases the cache when the tree has mutated text.
     74 */
     75 const observedNodes = new WeakSet<Node>();
     76 const textChangeObserver = new MutationObserver(mutations => {
     77  for (const mutation of mutations) {
     78    eraseFromCache(mutation.target);
     79  }
     80 });
     81 
     82 /**
     83 * Builds the text content of a node using some custom logic.
     84 *
     85 * @remarks
     86 * The primary reason this function exists is due to {@link ShadowRoot}s not having
     87 * text content.
     88 *
     89 * @internal
     90 */
     91 export const createTextContent = (root: Node): TextContent => {
     92  let value = textContentCache.get(root);
     93  if (value) {
     94    return value;
     95  }
     96  value = {full: '', immediate: []};
     97  if (!isSuitableNodeForTextMatching(root)) {
     98    return value;
     99  }
    100 
    101  let currentImmediate = '';
    102  if (isNonTrivialValueNode(root)) {
    103    value.full = root.value;
    104    value.immediate.push(root.value);
    105 
    106    root.addEventListener(
    107      'input',
    108      event => {
    109        eraseFromCache(event.target as HTMLInputElement);
    110      },
    111      {once: true, capture: true},
    112    );
    113  } else {
    114    for (let child = root.firstChild; child; child = child.nextSibling) {
    115      if (child.nodeType === Node.TEXT_NODE) {
    116        value.full += child.nodeValue ?? '';
    117        currentImmediate += child.nodeValue ?? '';
    118        continue;
    119      }
    120      if (currentImmediate) {
    121        value.immediate.push(currentImmediate);
    122      }
    123      currentImmediate = '';
    124      if (child.nodeType === Node.ELEMENT_NODE) {
    125        value.full += createTextContent(child).full;
    126      }
    127    }
    128    if (currentImmediate) {
    129      value.immediate.push(currentImmediate);
    130    }
    131    if (root instanceof Element && root.shadowRoot) {
    132      value.full += createTextContent(root.shadowRoot).full;
    133    }
    134 
    135    if (!observedNodes.has(root)) {
    136      textChangeObserver.observe(root, {
    137        childList: true,
    138        characterData: true,
    139        subtree: true,
    140      });
    141      observedNodes.add(root);
    142    }
    143  }
    144  textContentCache.set(root, value);
    145  return value;
    146 };