tor-browser

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

css-logic.js (26708B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const LINE_BREAK_RE = /\r\n?|\n|\u2028|\u2029/;
      8 const MAX_DATA_URL_LENGTH = 40;
      9 /**
     10 * Provide access to the style information in a page.
     11 * CssLogic uses the standard DOM API, and the Gecko InspectorUtils API to
     12 * access styling information in the page, and present this to the user in a way
     13 * that helps them understand:
     14 * - why their expectations may not have been fulfilled
     15 * - how browsers process CSS
     16 *
     17 * @class
     18 */
     19 
     20 loader.lazyRequireGetter(
     21  this,
     22  "InspectorCSSParserWrapper",
     23  "resource://devtools/shared/css/lexer.js",
     24  true
     25 );
     26 loader.lazyRequireGetter(
     27  this,
     28  "getTabPrefs",
     29  "resource://devtools/shared/indentation.js",
     30  true
     31 );
     32 loader.lazyRequireGetter(
     33  this,
     34  "getNodeDisplayName",
     35  "resource://devtools/server/actors/inspector/utils.js",
     36  true
     37 );
     38 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     39 const styleInspectorL10N = new LocalizationHelper(
     40  "devtools/shared/locales/styleinspector.properties"
     41 );
     42 
     43 /**
     44 * Special values for filter, in addition to an href these values can be used
     45 */
     46 exports.FILTER = {
     47  // show properties for all user style sheets.
     48  USER: "user",
     49  // USER, plus user-agent (i.e. browser) style sheets
     50  UA: "ua",
     51 };
     52 
     53 /**
     54 * Each rule has a status, the bigger the number, the better placed it is to
     55 * provide styling information.
     56 *
     57 * These statuses are localized inside the styleinspector.properties
     58 * string bundle.
     59 *
     60 * @see csshtmltree.js RuleView._cacheStatusNames()
     61 */
     62 exports.STATUS = {
     63  BEST: 3,
     64  MATCHED: 2,
     65  PARENT_MATCH: 1,
     66  UNMATCHED: 0,
     67  UNKNOWN: -1,
     68 };
     69 
     70 /**
     71 * Mapping of CSS at-Rule className to CSSRule type name.
     72 */
     73 exports.CSSAtRuleClassNameType = {
     74  CSSContainerRule: "container",
     75  CSSCounterStyleRule: "counter-style",
     76  CSSDocumentRule: "document",
     77  CSSFontFaceRule: "font-face",
     78  CSSFontFeatureValuesRule: "font-feature-values",
     79  CSSImportRule: "import",
     80  CSSKeyframeRule: "keyframe",
     81  CSSKeyframesRule: "keyframes",
     82  CSSLayerBlockRule: "layer",
     83  CSSMediaRule: "media",
     84  CSSNamespaceRule: "namespace",
     85  CSSPageRule: "page",
     86  CSSScopeRule: "scope",
     87  CSSStartingStyleRule: "starting-style",
     88  CSSSupportsRule: "supports",
     89 };
     90 
     91 /**
     92 * Get Rule type as human-readable string (ex: "@media", "@container", …)
     93 *
     94 * @param {CSSRule} cssRule
     95 * @returns {string}
     96 */
     97 exports.getCSSAtRuleTypeName = function (cssRule) {
     98  const ruleClassName = ChromeUtils.getClassName(cssRule);
     99  const atRuleTypeName = exports.CSSAtRuleClassNameType[ruleClassName];
    100  if (atRuleTypeName) {
    101    return "@" + atRuleTypeName;
    102  }
    103 
    104  return "";
    105 };
    106 
    107 /**
    108 * Lookup a l10n string in the shared styleinspector string bundle.
    109 *
    110 * @param {string} name
    111 *        The key to lookup.
    112 * @returns {string} A localized version of the given key.
    113 */
    114 exports.l10n = name => styleInspectorL10N.getStr(name);
    115 exports.l10nFormatStr = (name, ...args) =>
    116  styleInspectorL10N.getFormatStr(name, ...args);
    117 
    118 /**
    119 * Is the given property sheet an author stylesheet?
    120 *
    121 * @param {CSSStyleSheet} sheet a stylesheet
    122 * @return {boolean} true if the given stylesheet is an author stylesheet,
    123 * false otherwise.
    124 */
    125 exports.isAuthorStylesheet = function (sheet) {
    126  return sheet.parsingMode === "author";
    127 };
    128 
    129 /**
    130 * Is the given property sheet a user stylesheet?
    131 *
    132 * @param {CSSStyleSheet} sheet a stylesheet
    133 * @return {boolean} true if the given stylesheet is a user stylesheet,
    134 * false otherwise.
    135 */
    136 exports.isUserStylesheet = function (sheet) {
    137  return sheet.parsingMode === "user";
    138 };
    139 
    140 /**
    141 * Is the given property sheet a agent stylesheet?
    142 *
    143 * @param {CSSStyleSheet} sheet a stylesheet
    144 * @return {boolean} true if the given stylesheet is a agent stylesheet,
    145 * false otherwise.
    146 */
    147 exports.isAgentStylesheet = function (sheet) {
    148  return sheet.parsingMode === "agent";
    149 };
    150 
    151 /**
    152 * Return a shortened version of a style sheet's source.
    153 *
    154 * @param {CSSStyleSheet} sheet the DOM object for the style sheet.
    155 */
    156 exports.shortSource = function (sheet) {
    157  if (!sheet) {
    158    return exports.l10n("rule.sourceInline");
    159  }
    160 
    161  if (!sheet.href) {
    162    return exports.l10n(
    163      sheet.constructed ? "rule.sourceConstructed" : "rule.sourceInline"
    164    );
    165  }
    166 
    167  let name = sheet.href;
    168 
    169  // If the sheet is a data URL, return a trimmed version of it.
    170  const dataUrl = sheet.href.trim().match(/^data:.*?,((?:.|\r|\n)*)$/);
    171  if (dataUrl) {
    172    name =
    173      dataUrl[1].length > MAX_DATA_URL_LENGTH
    174        ? `${dataUrl[1].substr(0, MAX_DATA_URL_LENGTH - 1)}…`
    175        : dataUrl[1];
    176  } else {
    177    // We try, in turn, the filename, filePath, query string, whole thing
    178    const url = URL.parse(sheet.href);
    179    if (url) {
    180      if (url.pathname) {
    181        const index = url.pathname.lastIndexOf("/");
    182        if (index !== -1 && index < url.pathname.length) {
    183          name = url.pathname.slice(index + 1);
    184        } else {
    185          name = url.pathname;
    186        }
    187      } else if (url.query) {
    188        name = url.query;
    189      }
    190    } // else some UA-provided stylesheets are not valid URLs.
    191  }
    192 
    193  try {
    194    name = decodeURIComponent(name);
    195  } catch (e) {
    196    // This may still fail if the URL contains invalid % numbers (for ex)
    197  }
    198 
    199  return name;
    200 };
    201 
    202 /**
    203 * Return the style sheet's source, handling element, inline and constructed stylesheets.
    204 *
    205 * @param {CSSStyleSheet} sheet the DOM object for the style sheet.
    206 */
    207 exports.longSource = function (sheet) {
    208  if (!sheet) {
    209    return exports.l10n("rule.sourceInline");
    210  }
    211 
    212  if (!sheet.href) {
    213    return exports.l10n(
    214      sheet.constructed ? "rule.sourceConstructed" : "rule.sourceInline"
    215    );
    216  }
    217 
    218  return sheet.href;
    219 };
    220 
    221 const TAB_CHARS = "\t";
    222 const SPACE_CHARS = " ";
    223 
    224 function getLineCountInComments(text) {
    225  let count = 0;
    226 
    227  for (const comment of text.match(/\/\*(?:.|\n)*?\*\//gm) || []) {
    228    count += comment.split("\n").length + 1;
    229  }
    230 
    231  return count;
    232 }
    233 
    234 /**
    235 * Prettify minified CSS text.
    236 * This prettifies CSS code where there is no indentation in usual places while
    237 * keeping original indentation as-is elsewhere.
    238 *
    239 * Returns an object with the resulting prettified source and a list of mappings of
    240 * token positions between the original and the prettified source. Each single mapping
    241 * is an object that looks like this:
    242 *
    243 * {
    244 *  original: {line: {number}, column: {number}},
    245 *  generated: {line: {number}, column: {number}},
    246 * }
    247 *
    248 * @param  {string} text
    249 *         The CSS source to prettify.
    250 * @param  {number} ruleCount
    251 *         The number of CSS rules expected in the CSS source.
    252 *         Set to null to force the text to be pretty-printed.
    253 *
    254 * @return {object}
    255 *         Object with the prettified source and source mappings.
    256 *          {
    257 *            result: {String}  // Prettified source
    258 *            mappings: {Array} // List of objects with mappings for lines and columns
    259 *                              // between the original source and prettified source
    260 *          }
    261 */
    262 // eslint-disable-next-line complexity
    263 function prettifyCSS(text, ruleCount) {
    264  if (prettifyCSS.LINE_SEPARATOR == null) {
    265    const os = Services.appinfo.OS;
    266    prettifyCSS.LINE_SEPARATOR = os === "WINNT" ? "\r\n" : "\n";
    267  }
    268 
    269  // Stylesheets may start and end with HTML comment tags (possibly with whitespaces
    270  // before and after). Remove those first. Don't do anything if there aren't any.
    271  const trimmed = text.trim();
    272  if (trimmed.startsWith("<!--")) {
    273    text = trimmed.replace(/^<!--/, "").replace(/-->$/, "").trim();
    274  }
    275 
    276  const originalText = text;
    277  text = text.trim();
    278 
    279  // don't attempt to prettify if there's more than one line per rule, excluding comments.
    280  const lineCount = text.split("\n").length - 1 - getLineCountInComments(text);
    281  if (ruleCount !== null && lineCount >= ruleCount) {
    282    return { result: originalText, mappings: [] };
    283  }
    284 
    285  // We reformat the text using a simple state machine.  The
    286  // reformatting preserves most of the input text, changing only
    287  // whitespace.  The rules are:
    288  //
    289  // * After a "{" or ";" symbol, ensure there is a newline and
    290  //   indentation before the next non-comment, non-whitespace token.
    291  // * Additionally after a "{" symbol, increase the indentation.
    292  // * A "}" symbol ensures there is a preceding newline, and
    293  //   decreases the indentation level.
    294  // * Ensure there is whitespace before a "{".
    295  //
    296  // This approach can be confused sometimes, but should do ok on a
    297  // minified file.
    298  let indent = "";
    299  let indentLevel = 0;
    300  const lexer = new InspectorCSSParserWrapper(text);
    301  // List of mappings of token positions from original source to prettified source.
    302  const mappings = [];
    303  // Line and column offsets used to shift the token positions after prettyfication.
    304  let lineOffset = 0;
    305  let columnOffset = 0;
    306  let indentOffset = 0;
    307  let result = "";
    308  let pushbackToken = undefined;
    309 
    310  // A helper function that reads tokens, looking for the next
    311  // non-comment, non-whitespace token.  Comment and whitespace tokens
    312  // are appended to |result|.  If this encounters EOF, it returns
    313  // null.  Otherwise it returns the last whitespace token that was
    314  // seen.  This function also updates |pushbackToken|.
    315  const readUntilSignificantToken = () => {
    316    while (true) {
    317      const token = lexer.nextToken();
    318      if (!token || token.tokenType !== "WhiteSpace") {
    319        pushbackToken = token;
    320        return token;
    321      }
    322      // Saw whitespace.  Before committing to it, check the next
    323      // token.
    324      const nextToken = lexer.nextToken();
    325      if (!nextToken || nextToken.tokenType !== "Comment") {
    326        pushbackToken = nextToken;
    327        return token;
    328      }
    329      // Saw whitespace + comment.  Update the result and continue.
    330      result = result + text.substring(token.startOffset, nextToken.endOffset);
    331    }
    332  };
    333 
    334  // State variables for readUntilNewlineNeeded.
    335  //
    336  // Starting index of the accumulated tokens.
    337  let startIndex;
    338  // Ending index of the accumulated tokens.
    339  let endIndex;
    340  // True if any non-whitespace token was seen.
    341  let anyNonWS;
    342  // True if the terminating token is "}".
    343  let isCloseBrace;
    344  // True if the terminating token is a new line character.
    345  let isNewLine;
    346  // True if the token just before the terminating token was
    347  // whitespace.
    348  let lastWasWS;
    349  // True if the current token is inside a CSS selector.
    350  let isInSelector = true;
    351  // True if the current token is inside an at-rule definition.
    352  let isInAtRuleDefinition = false;
    353 
    354  // A helper function that reads tokens until there is a reason to
    355  // insert a newline.  This updates the state variables as needed.
    356  // If this encounters EOF, it returns null.  Otherwise it returns
    357  // the final token read.  Note that if the returned token is "{",
    358  // then it will not be included in the computed start/end token
    359  // range.  This is used to handle whitespace insertion before a "{".
    360  const readUntilNewlineNeeded = () => {
    361    let token;
    362    while (true) {
    363      if (pushbackToken) {
    364        token = pushbackToken;
    365        pushbackToken = undefined;
    366      } else {
    367        token = lexer.nextToken();
    368      }
    369      if (!token) {
    370        endIndex = text.length;
    371        break;
    372      }
    373 
    374      const line = lexer.lineNumber;
    375      const column = lexer.columnNumber;
    376      mappings.push({
    377        original: {
    378          line,
    379          column,
    380        },
    381        generated: {
    382          line: lineOffset + line,
    383          column: columnOffset,
    384        },
    385      });
    386      // Shift the column offset for the next token by the current token's length.
    387      columnOffset += token.endOffset - token.startOffset;
    388 
    389      if (token.tokenType === "AtKeyword") {
    390        isInAtRuleDefinition = true;
    391      }
    392 
    393      // A "}" symbol must be inserted later, to deal with indentation
    394      // and newline.
    395      if (token.tokenType === "CloseCurlyBracket") {
    396        isInSelector = true;
    397        isCloseBrace = true;
    398        break;
    399      } else if (token.tokenType === "CurlyBracketBlock") {
    400        if (isInAtRuleDefinition) {
    401          isInAtRuleDefinition = false;
    402        } else {
    403          isInSelector = false;
    404        }
    405        break;
    406      }
    407 
    408      if (token.tokenType === "WhiteSpace") {
    409        if (LINE_BREAK_RE.test(token.text)) {
    410          // If we encounter a new line after a significant token, we can
    411          // move on to the next significant token.
    412          // This avoids messing with declarations with no semi-colon preceding
    413          // a closing brace, eg `{\n  color: red\n  }`
    414          isNewLine = true;
    415          break;
    416        }
    417      } else {
    418        anyNonWS = true;
    419      }
    420 
    421      if (startIndex === undefined) {
    422        startIndex = token.startOffset;
    423      }
    424      endIndex = token.endOffset;
    425 
    426      if (token.tokenType === "Semicolon") {
    427        break;
    428      }
    429 
    430      if (
    431        token.tokenType === "Comma" &&
    432        isInSelector &&
    433        !isInAtRuleDefinition
    434      ) {
    435        break;
    436      }
    437 
    438      lastWasWS = token.tokenType === "WhiteSpace";
    439    }
    440    return token;
    441  };
    442 
    443  // Get preference of the user regarding what to use for indentation,
    444  // spaces or tabs.
    445  const tabPrefs = getTabPrefs();
    446  const baseIndentString = tabPrefs.indentWithTabs
    447    ? TAB_CHARS
    448    : SPACE_CHARS.repeat(tabPrefs.indentUnit);
    449 
    450  while (true) {
    451    // Set the initial state.
    452    startIndex = undefined;
    453    endIndex = undefined;
    454    anyNonWS = false;
    455    isCloseBrace = false;
    456    isNewLine = false;
    457    lastWasWS = false;
    458 
    459    // Read tokens until we see a reason to insert a newline.
    460    let token = readUntilNewlineNeeded();
    461 
    462    // Append any saved up text to the result, applying indentation.
    463    if (startIndex !== undefined) {
    464      if (isCloseBrace && !anyNonWS) {
    465        // If we saw only whitespace followed by a "}", then we don't
    466        // need anything here.
    467      } else {
    468        result = result + indent + text.substring(startIndex, endIndex);
    469        if (isNewLine) {
    470          lineOffset = lineOffset - 1;
    471        }
    472        if (isCloseBrace) {
    473          result += prettifyCSS.LINE_SEPARATOR;
    474          lineOffset = lineOffset + 1;
    475        }
    476      }
    477    }
    478 
    479    if (isCloseBrace) {
    480      // Even if the stylesheet contains extra closing braces, the indent level should
    481      // remain > 0.
    482      indentLevel = Math.max(0, indentLevel - 1);
    483      indent = baseIndentString.repeat(indentLevel);
    484 
    485      // FIXME: This is incorrect and should be fixed in Bug 1839297
    486      if (tabPrefs.indentWithTabs) {
    487        indentOffset = 4 * indentLevel;
    488      } else {
    489        indentOffset = 1 * indentLevel;
    490      }
    491      result = result + indent + "}";
    492    }
    493 
    494    if (!token) {
    495      break;
    496    }
    497 
    498    if (token.tokenType === "CurlyBracketBlock") {
    499      if (!lastWasWS) {
    500        result += " ";
    501        columnOffset++;
    502      }
    503      result += "{";
    504      indentLevel++;
    505      indent = baseIndentString.repeat(indentLevel);
    506      indentOffset = indent.length;
    507 
    508      // FIXME: This is incorrect and should be fixed in Bug 1839297
    509      if (tabPrefs.indentWithTabs) {
    510        indentOffset = 4 * indentLevel;
    511      } else {
    512        indentOffset = 1 * indentLevel;
    513      }
    514    }
    515 
    516    // Now it is time to insert a newline.  However first we want to
    517    // deal with any trailing comments.
    518    token = readUntilSignificantToken();
    519 
    520    // "Early" bail-out if the text does not appear to be minified.
    521    // Here we ignore the case where whitespace appears at the end of
    522    // the text.
    523    if (
    524      ruleCount !== null &&
    525      pushbackToken &&
    526      token &&
    527      token.tokenType === "WhiteSpace" &&
    528      /\n/g.test(text.substring(token.startOffset, token.endOffset))
    529    ) {
    530      return { result: originalText, mappings: [] };
    531    }
    532 
    533    // Finally time for that newline.
    534    result = result + prettifyCSS.LINE_SEPARATOR;
    535 
    536    // Update line and column offsets for the new line.
    537    lineOffset = lineOffset + 1;
    538    columnOffset = 0 + indentOffset;
    539 
    540    // Maybe we hit EOF.
    541    if (!pushbackToken) {
    542      break;
    543    }
    544  }
    545 
    546  return { result, mappings };
    547 }
    548 
    549 exports.prettifyCSS = prettifyCSS;
    550 
    551 /**
    552 * Given a node, check to see if it is a ::marker, ::before, or ::after element.
    553 * If so, return the node that is accessible from within the document
    554 * (the parent of the anonymous node), along with which pseudo element
    555 * it was.  Otherwise, return the node itself.
    556 *
    557 * @returns {object}
    558 *            - {DOMNode} node: The non-anonymous node
    559 *            - {string|null} pseudo: The label representing the anonymous node
    560 *                                    (e.g. '::marker',  '::before', '::after', '::view-transition',
    561 *                                    '::view-transition-group(root)', …).
    562 *                                    null if node isn't an anonymous node or isn't handled
    563 *                                    yet.
    564 */
    565 function getBindingElementAndPseudo(node) {
    566  let bindingElement = node;
    567  let pseudo = null;
    568  const { implementedPseudoElement } = node;
    569  if (implementedPseudoElement) {
    570    // we only want to explicitly handle the elements we're displaying in the markup view
    571    if (
    572      implementedPseudoElement === "::marker" ||
    573      implementedPseudoElement === "::before" ||
    574      implementedPseudoElement === "::after" ||
    575      implementedPseudoElement === "::backdrop"
    576    ) {
    577      pseudo = getNodeDisplayName(node);
    578      bindingElement = node.parentNode;
    579    } else if (implementedPseudoElement.startsWith("::view-transition")) {
    580      pseudo = getNodeDisplayName(node);
    581      // The binding for all view transition pseudo element is the <html> element, i.e. we
    582      // can't use `node.parentNode` as for`::view-transition-old` element, we'd get the
    583      // `::view-transition-group`, which is not the binding element.
    584      bindingElement = node.getRootNode().documentElement;
    585    }
    586  }
    587 
    588  return {
    589    bindingElement,
    590    pseudo,
    591  };
    592 }
    593 exports.getBindingElementAndPseudo = getBindingElementAndPseudo;
    594 
    595 /**
    596 * Returns css rules for a given a node.
    597 * This function can handle ::before or ::after pseudo element as well as
    598 * normal element.
    599 */
    600 function getMatchingCSSRules(node) {
    601  const { bindingElement, pseudo } = getBindingElementAndPseudo(node);
    602  const rules = InspectorUtils.getMatchingCSSRules(bindingElement, pseudo);
    603  return rules;
    604 }
    605 exports.getMatchingCSSRules = getMatchingCSSRules;
    606 
    607 /**
    608 * Returns true if the given node has visited state.
    609 */
    610 function hasVisitedState(node) {
    611  if (!Element.isInstance(node)) {
    612    return false;
    613  }
    614 
    615  // ElementState::VISITED
    616  const ELEMENT_STATE_VISITED = 1 << 18;
    617 
    618  return (
    619    !!(InspectorUtils.getContentState(node) & ELEMENT_STATE_VISITED) ||
    620    InspectorUtils.hasPseudoClassLock(node, ":visited")
    621  );
    622 }
    623 exports.hasVisitedState = hasVisitedState;
    624 
    625 /**
    626 * Find the position of [element] in [nodeList].
    627 *
    628 * @returns an index of the match, or -1 if there is no match
    629 */
    630 function positionInNodeList(element, nodeList) {
    631  for (let i = 0; i < nodeList.length; i++) {
    632    if (element === nodeList[i]) {
    633      return i;
    634    }
    635  }
    636  return -1;
    637 }
    638 
    639 /**
    640 * For a provided node, find the appropriate container/node couple so that
    641 * container.contains(node) and a CSS selector can be created from the
    642 * container to the node.
    643 */
    644 function findNodeAndContainer(node) {
    645  const shadowRoot = node.containingShadowRoot;
    646  while (node?.isNativeAnonymous) {
    647    node = node.parentNode;
    648  }
    649 
    650  if (shadowRoot) {
    651    // If the node is under a shadow root, the shadowRoot contains the node and
    652    // we can find the node via shadowRoot.querySelector(path).
    653    return {
    654      containingDocOrShadow: shadowRoot,
    655      node,
    656    };
    657  }
    658 
    659  // Otherwise, get the root binding parent to get a non anonymous element that
    660  // will be accessible from the ownerDocument.
    661  return {
    662    containingDocOrShadow: node.ownerDocument,
    663    node,
    664  };
    665 }
    666 
    667 /**
    668 * Find a unique CSS selector for a given element
    669 *
    670 * @returns a string such that:
    671 *   - ele.containingDocOrShadow.querySelector(reply) === ele
    672 *   - ele.containingDocOrShadow.querySelectorAll(reply).length === 1
    673 */
    674 const findCssSelector = function (ele) {
    675  const { node, containingDocOrShadow } = findNodeAndContainer(ele);
    676  ele = node;
    677 
    678  if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
    679    // findCssSelector received element not inside container.
    680    return "";
    681  }
    682 
    683  const cssEscape = ele.ownerGlobal.CSS.escape;
    684 
    685  // document.querySelectorAll("#id") returns multiple if elements share an ID
    686  if (
    687    ele.id &&
    688    containingDocOrShadow.querySelectorAll("#" + cssEscape(ele.id)).length === 1
    689  ) {
    690    return "#" + cssEscape(ele.id);
    691  }
    692 
    693  // Inherently unique by tag name
    694  const tagName = ele.localName;
    695  if (tagName === "html") {
    696    return "html";
    697  }
    698  if (tagName === "head") {
    699    return "head";
    700  }
    701  if (tagName === "body") {
    702    return "body";
    703  }
    704 
    705  // We might be able to find a unique class name
    706  let selector, index, matches;
    707  for (let i = 0; i < ele.classList.length; i++) {
    708    // Is this className unique by itself?
    709    selector = "." + cssEscape(ele.classList.item(i));
    710    matches = containingDocOrShadow.querySelectorAll(selector);
    711    if (matches.length === 1) {
    712      return selector;
    713    }
    714    // Maybe it's unique with a tag name?
    715    selector = cssEscape(tagName) + selector;
    716    matches = containingDocOrShadow.querySelectorAll(selector);
    717    if (matches.length === 1) {
    718      return selector;
    719    }
    720    // Maybe it's unique using a tag name and nth-child
    721    index = positionInNodeList(ele, ele.parentNode.children) + 1;
    722    selector = selector + ":nth-child(" + index + ")";
    723    matches = containingDocOrShadow.querySelectorAll(selector);
    724    if (matches.length === 1) {
    725      return selector;
    726    }
    727  }
    728 
    729  // Not unique enough yet.
    730  index = positionInNodeList(ele, ele.parentNode.children) + 1;
    731  selector = cssEscape(tagName) + ":nth-child(" + index + ")";
    732  if (ele.parentNode !== containingDocOrShadow) {
    733    selector = findCssSelector(ele.parentNode) + " > " + selector;
    734  }
    735  return selector;
    736 };
    737 exports.findCssSelector = findCssSelector;
    738 
    739 /**
    740 * Get the full CSS path for a given element.
    741 *
    742 * @returns a string that can be used as a CSS selector for the element. It might not
    743 * match the element uniquely. It does however, represent the full path from the root
    744 * node to the element.
    745 */
    746 function getCssPath(ele) {
    747  const { node, containingDocOrShadow } = findNodeAndContainer(ele);
    748  ele = node;
    749  if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
    750    // getCssPath received element not inside container.
    751    return "";
    752  }
    753 
    754  const nodeGlobal = ele.ownerGlobal.Node;
    755 
    756  const getElementSelector = element => {
    757    if (!element.localName) {
    758      return "";
    759    }
    760 
    761    let label =
    762      element.nodeName == element.nodeName.toUpperCase()
    763        ? element.localName.toLowerCase()
    764        : element.localName;
    765 
    766    if (element.id) {
    767      label += "#" + element.id;
    768    }
    769 
    770    if (element.classList) {
    771      for (const cl of element.classList) {
    772        label += "." + cl;
    773      }
    774    }
    775 
    776    return label;
    777  };
    778 
    779  const paths = [];
    780 
    781  while (ele) {
    782    if (!ele || ele.nodeType !== nodeGlobal.ELEMENT_NODE) {
    783      break;
    784    }
    785 
    786    paths.splice(0, 0, getElementSelector(ele));
    787    ele = ele.parentNode;
    788  }
    789 
    790  return paths.length ? paths.join(" ") : "";
    791 }
    792 exports.getCssPath = getCssPath;
    793 
    794 /**
    795 * Get the xpath for a given element.
    796 *
    797 * @param {DomNode} ele
    798 * @returns a string that can be used as an XPath to find the element uniquely.
    799 */
    800 function getXPath(ele) {
    801  const { node, containingDocOrShadow } = findNodeAndContainer(ele);
    802  ele = node;
    803  if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
    804    // getXPath received element not inside container.
    805    return "";
    806  }
    807 
    808  // Create a short XPath for elements with IDs.
    809  if (ele.id) {
    810    return `//*[@id="${ele.id}"]`;
    811  }
    812 
    813  // Otherwise walk the DOM up and create a part for each ancestor.
    814  const parts = [];
    815 
    816  const nodeGlobal = ele.ownerGlobal.Node;
    817  // Use nodeName (instead of localName) so namespace prefix is included (if any).
    818  while (ele && ele.nodeType === nodeGlobal.ELEMENT_NODE) {
    819    let nbOfPreviousSiblings = 0;
    820    let hasNextSiblings = false;
    821 
    822    // Count how many previous same-name siblings the element has.
    823    let sibling = ele.previousSibling;
    824    while (sibling) {
    825      // Ignore document type declaration.
    826      if (
    827        sibling.nodeType !== nodeGlobal.DOCUMENT_TYPE_NODE &&
    828        sibling.nodeName == ele.nodeName
    829      ) {
    830        nbOfPreviousSiblings++;
    831      }
    832 
    833      sibling = sibling.previousSibling;
    834    }
    835 
    836    // Check if the element has at least 1 next same-name sibling.
    837    sibling = ele.nextSibling;
    838    while (sibling) {
    839      if (sibling.nodeName == ele.nodeName) {
    840        hasNextSiblings = true;
    841        break;
    842      }
    843      sibling = sibling.nextSibling;
    844    }
    845 
    846    const prefix = ele.prefix ? ele.prefix + ":" : "";
    847    const nth =
    848      nbOfPreviousSiblings || hasNextSiblings
    849        ? `[${nbOfPreviousSiblings + 1}]`
    850        : "";
    851 
    852    parts.push(prefix + ele.localName + nth);
    853 
    854    ele = ele.parentNode;
    855  }
    856 
    857  return parts.length ? "/" + parts.reverse().join("/") : "";
    858 }
    859 exports.getXPath = getXPath;
    860 
    861 /**
    862 * Build up a regular expression that matches a CSS variable token. This is an
    863 * ident token that starts with two dashes "--".
    864 *
    865 * https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
    866 */
    867 var NON_ASCII = "[^\\x00-\\x7F]";
    868 var ESCAPE = "\\\\[^\n\r]";
    869 var VALID_CHAR = ["[_a-z0-9-]", NON_ASCII, ESCAPE].join("|");
    870 var IS_VARIABLE_TOKEN = new RegExp(`^--(${VALID_CHAR})*$`, "i");
    871 
    872 /**
    873 * Check that this is a CSS variable.
    874 *
    875 * @param {string} input
    876 * @return {boolean}
    877 */
    878 function isCssVariable(input) {
    879  return !!input.match(IS_VARIABLE_TOKEN);
    880 }
    881 exports.isCssVariable = isCssVariable;
    882 
    883 /**
    884 * This is a list of all the element backed pseudo elements.
    885 *
    886 * From https://drafts.csswg.org/css-pseudo-4/#element-backed :
    887 * > The element-backed pseudo-elements, interact with most CSS and other platform features
    888 * > as if they were real elements (and, in fact, often are real elements that are
    889 * > not otherwise selectable).
    890 *
    891 * Those pseudo elements are not displayed in the markup view, but declarations in rules
    892 * targetting them can then be inherited by their "children", and so we need to retrieve
    893 * those rules to surface them in the Inspector (e.g. in "Inherited" sections in the Rules
    894 * view, in the matched selectors section in the Computed panel, …).
    895 *
    896 * Any new element-backed pseudo elements should be added into this Set.
    897 */
    898 exports.ELEMENT_BACKED_PSEUDO_ELEMENTS = new Set([
    899  "::details-content",
    900  "::file-selector-button",
    901 ]);