tor-browser

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

parsing-utils.js (27641B)


      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 // This file holds various CSS parsing and rewriting utilities.
      6 // Some entry points of note are:
      7 // parseDeclarations - parse a CSS rule into declarations
      8 // parsePseudoClassesAndAttributes - parse selector and extract
      9 //     pseudo-classes
     10 // parseSingleValue - parse a single CSS property value
     11 
     12 "use strict";
     13 
     14 const {
     15  InspectorCSSParserWrapper,
     16 } = require("resource://devtools/shared/css/lexer.js");
     17 
     18 loader.lazyRequireGetter(
     19  this,
     20  "CSS_ANGLEUNIT",
     21  "resource://devtools/shared/css/constants.js",
     22  true
     23 );
     24 
     25 const SELECTOR_ATTRIBUTE = (exports.SELECTOR_ATTRIBUTE = 1);
     26 const SELECTOR_ELEMENT = (exports.SELECTOR_ELEMENT = 2);
     27 const SELECTOR_PSEUDO_CLASS = (exports.SELECTOR_PSEUDO_CLASS = 3);
     28 
     29 // When commenting out a declaration, we put this character into the
     30 // comment opener so that future parses of the commented text know to
     31 // bypass the property name validity heuristic.
     32 const COMMENT_PARSING_HEURISTIC_BYPASS_CHAR =
     33  (exports.COMMENT_PARSING_HEURISTIC_BYPASS_CHAR = "!");
     34 
     35 /**
     36 * A generator function that lexes a CSS source string, yielding the
     37 * CSS tokens.  Comment tokens are dropped.
     38 *
     39 * @param {string} string - CSS source string
     40 * @yields {CSSToken} The next CSSToken that is lexed
     41 * @see CSSToken for details about the returned tokens
     42 */
     43 function* cssTokenizer(string) {
     44  const lexer = new InspectorCSSParserWrapper(string);
     45  while (true) {
     46    const token = lexer.nextToken();
     47    if (!token) {
     48      break;
     49    }
     50    // None of the existing consumers want comments.
     51    if (token.tokenType !== "Comment") {
     52      yield token;
     53    }
     54  }
     55 }
     56 
     57 /**
     58 * Pass |string| to the CSS lexer and return an array of all the
     59 * returned tokens.  Comment tokens are not included.  In addition to
     60 * the usual information, each token will have starting and ending
     61 * line and column information attached.  Specifically, each token
     62 * has an additional "loc" attribute.  This attribute is an object
     63 * of the form {line: L, column: C}.  Lines and columns are both zero
     64 * based.
     65 *
     66 * It's best not to add new uses of this function.  In general it is
     67 * simpler and better to use the CSSToken offsets, rather than line
     68 * and column.  Also, this function lexes the entire input string at
     69 * once, rather than lazily yielding a token stream.  Use
     70 * |cssTokenizer| or |getCSSLexer| instead.
     71 *
     72 * @param {string} string The input string.
     73 * @returns {CSSToken[]} An array of tokens (@see CSSToken) that have
     74 *        line and column information.
     75 */
     76 function cssTokenizerWithLineColumn(string) {
     77  const lexer = new InspectorCSSParserWrapper(string);
     78  const result = [];
     79  let prevToken = undefined;
     80  while (true) {
     81    const token = lexer.nextToken();
     82    const lineNumber = lexer.lineNumber;
     83    const columnNumber = lexer.columnNumber;
     84 
     85    if (prevToken) {
     86      prevToken.loc.end = {
     87        line: lineNumber,
     88        column: columnNumber,
     89      };
     90    }
     91 
     92    if (!token) {
     93      break;
     94    }
     95 
     96    if (token.tokenType === "Comment") {
     97      // We've already dealt with the previous token's location.
     98      prevToken = undefined;
     99    } else {
    100      const startLoc = {
    101        line: lineNumber,
    102        column: columnNumber,
    103      };
    104      token.loc = { start: startLoc };
    105 
    106      result.push(token);
    107      prevToken = token;
    108    }
    109  }
    110 
    111  return result;
    112 }
    113 
    114 /**
    115 * Escape a comment body.  Find the comment start and end strings in a
    116 * string and inserts backslashes so that the resulting text can
    117 * itself be put inside a comment.
    118 *
    119 * @param {string} inputString
    120 *                 input string
    121 * @returns {string} the escaped result
    122 */
    123 function escapeCSSComment(inputString) {
    124  const result = inputString.replace(/\/(\\*)\*/g, "/\\$1*");
    125  return result.replace(/\*(\\*)\//g, "*\\$1/");
    126 }
    127 
    128 /**
    129 * Un-escape a comment body.  This undoes any comment escaping that
    130 * was done by escapeCSSComment.  That is, given input like "/\*
    131 * comment *\/", it will strip the backslashes.
    132 *
    133 * @param {string} inputString
    134 *                 input string
    135 * @returns {string} the un-escaped result
    136 */
    137 function unescapeCSSComment(inputString) {
    138  const result = inputString.replace(/\/\\(\\*)\*/g, "/$1*");
    139  return result.replace(/\*\\(\\*)\//g, "*$1/");
    140 }
    141 
    142 /**
    143 * Parsed CSS declaration
    144 *
    145 * @typedef {object} ParsedDeclaration
    146 * @property {string} name - The name of the declaration
    147 * @property {string} value - The (authored) value of the declaration (i.e. not the computed value)
    148 * @property {string} priority - "important" if the declaration ends with `!important`,
    149 *           empty string otherwise.
    150 * @property {string} terminator - String to use to terminate the declaration, usually ""
    151 *           to mean no additional termination is needed.
    152 * @property {number[]} offsets - Holds the offsets of the start and end of the declaration
    153 *           text, in a form suitable for use with String.substring.
    154 * @property {number[]} colonOffsets - Holds the start and end locations of the colon (":")
    155 *           that separates the property name from the value.
    156 * @property {number[]} [commentOffsets] - If the declaration appears in a comment, holds the
    157 *           offsets of the start and end of the enclosing comment.
    158 * @property {boolean} [isCustomProperty] - Is this a CSS custom property (aka CSS variable)
    159 *           declaration. Only set when the declaration is a custom property so we save
    160 *           some cycles for non custom property declaration when sending them to the client.
    161 */
    162 
    163 /**
    164 * A helper function for @see parseDeclarations that handles parsing
    165 * of comment text.  This wraps a recursive call to parseDeclarations
    166 * with the processing needed to ensure that offsets in the result
    167 * refer back to the original, unescaped, input string.
    168 *
    169 * @param {Function} isCssPropertyKnown
    170 *        A function to check if the CSS property is known. This is either an
    171 *        internal server function or from the CssPropertiesFront.
    172 * @param {string} commentText The text of the comment, without the
    173 *                             delimiters.
    174 * @param {number} startOffset The offset of the comment opener
    175 *                             in the original text.
    176 * @param {number} endOffset The offset of the comment closer
    177 *                           in the original text.
    178 * @returns {ParsedDeclaration[]} Array of parsed declarations.
    179 */
    180 function parseCommentDeclarations(
    181  isCssPropertyKnown,
    182  commentText,
    183  startOffset,
    184  endOffset
    185 ) {
    186  let commentOverride = false;
    187  if (commentText === "") {
    188    return [];
    189  } else if (commentText[0] === COMMENT_PARSING_HEURISTIC_BYPASS_CHAR) {
    190    // This is the special sign that the comment was written by
    191    // rewriteDeclarations and so we should bypass the usual
    192    // heuristic.
    193    commentOverride = true;
    194    commentText = commentText.substring(1);
    195  }
    196 
    197  const rewrittenText = unescapeCSSComment(commentText);
    198 
    199  // We might have rewritten an embedded comment.  For example
    200  // /\* ... *\/ would turn into /* ... */.
    201  // This rewriting is necessary for proper lexing, but it means
    202  // that the offsets we get back can be off.  So now we compute
    203  // a map so that we can rewrite offsets later.  The map is the same
    204  // length as |rewrittenText| and tells us how to map an index
    205  // into |rewrittenText| to an index into |commentText|.
    206  //
    207  // First, we find the location of each comment starter or closer in
    208  // |rewrittenText|.  At these spots we put a 1 into |rewrites|.
    209  // Then we walk the array again, using the elements to compute a
    210  // delta, which we use to make the final mapping.
    211  //
    212  // Note we allocate one extra entry because we can see an ending
    213  // offset that is equal to the length.
    214  const rewrites = new Array(rewrittenText.length + 1).fill(0);
    215 
    216  const commentRe = /\/\\*\*|\*\\*\//g;
    217  while (true) {
    218    const matchData = commentRe.exec(rewrittenText);
    219    if (!matchData) {
    220      break;
    221    }
    222    rewrites[matchData.index] = 1;
    223  }
    224 
    225  let delta = 0;
    226  for (let i = 0; i <= rewrittenText.length; ++i) {
    227    delta += rewrites[i];
    228    // |startOffset| to add the offset from the comment starter, |+2|
    229    // for the length of the "/*", then |i| and |delta| as described
    230    // above.
    231    rewrites[i] = startOffset + 2 + i + delta;
    232    if (commentOverride) {
    233      ++rewrites[i];
    234    }
    235  }
    236 
    237  // Note that we pass "false" for parseComments here.  It doesn't
    238  // seem worthwhile to support declarations in comments-in-comments
    239  // here, as there's no way to generate those using the tools, and
    240  // users would be crazy to write such things.
    241  const newDecls = parseDeclarationsInternal(
    242    isCssPropertyKnown,
    243    rewrittenText,
    244    false,
    245    true,
    246    commentOverride
    247  );
    248  for (const decl of newDecls) {
    249    decl.offsets[0] = rewrites[decl.offsets[0]];
    250    decl.offsets[1] = rewrites[decl.offsets[1]];
    251    decl.colonOffsets[0] = rewrites[decl.colonOffsets[0]];
    252    decl.colonOffsets[1] = rewrites[decl.colonOffsets[1]];
    253    decl.commentOffsets = [startOffset, endOffset];
    254  }
    255  return newDecls;
    256 }
    257 
    258 /**
    259 * A helper function for parseDeclarationsInternal that creates a new
    260 * empty declaration.
    261 *
    262 * @returns {ParsedDeclaration} an empty declaration that matches what is returned by parseDeclarations
    263 */
    264 function getEmptyDeclaration() {
    265  return {
    266    name: "",
    267    value: "",
    268    priority: "",
    269    terminator: "",
    270    offsets: [undefined, undefined],
    271    colonOffsets: false,
    272  };
    273 }
    274 
    275 /**
    276 * Like trim, but only trims CSS-allowed whitespace.
    277 *
    278 * @param {string} str - The string to trim
    279 */
    280 function cssTrim(str) {
    281  const match = /^[ \t\r\n\f]*(.*?)[ \t\r\n\f]*$/.exec(str);
    282  if (match) {
    283    return match[1];
    284  }
    285  return str;
    286 }
    287 
    288 /**
    289 * A helper function that does all the parsing work for
    290 * parseDeclarations.  This is separate because it has some arguments
    291 * that don't make sense in isolation.
    292 *
    293 * The return value and arguments are like parseDeclarations, with
    294 * these additional arguments.
    295 *
    296 * @param {Function} isCssPropertyKnown
    297 *        Function to check if the CSS property is known.
    298 * @param {string} inputString
    299 *        An input string of CSS
    300 * @param {boolean} parseComments
    301 *        If true, try to parse the contents of comments as well.
    302 *        A comment will only be parsed if it occurs outside of
    303 *        the body of some other declaration.
    304 * @param {boolean} inComment
    305 *        If true, assume that this call is parsing some text
    306 *        which came from a comment in another declaration.
    307 *        In this case some heuristics are used to avoid parsing
    308 *        text which isn't obviously a series of declarations.
    309 * @param {boolean} commentOverride
    310 *        This only makes sense when inComment=true.
    311 *        When true, assume that the comment was generated by
    312 *        rewriteDeclarations, and skip the usual name-checking
    313 *        heuristic.
    314 * @returns {ParsedDeclaration[]} Array of parsed declarations.
    315 */
    316 // eslint-disable-next-line complexity
    317 function parseDeclarationsInternal(
    318  isCssPropertyKnown,
    319  inputString,
    320  parseComments,
    321  inComment,
    322  commentOverride
    323 ) {
    324  if (inputString === null || inputString === undefined) {
    325    throw new Error("empty input string");
    326  }
    327 
    328  const lexer = new InspectorCSSParserWrapper(inputString, {
    329    trackEOFChars: true,
    330  });
    331 
    332  let declarations = [getEmptyDeclaration()];
    333  let lastProp = declarations[0];
    334 
    335  // This tracks the various CSS blocks the current token is in currently.
    336  // This is a stack we push to when a block is opened, and we pop from when a block is
    337  // closed. Within a block, colons and semicolons don't advance the way they do outside
    338  // of blocks.
    339  let currentBlocks = [];
    340 
    341  // This tracks the "!important" parsing state.  The states are:
    342  // 0 - haven't seen anything
    343  // 1 - have seen "!", looking for "important" next (possibly after
    344  //     whitespace).
    345  // 2 - have seen "!important"
    346  let importantState = 0;
    347  // This is true if we saw whitespace or comments between the "!" and
    348  // the "important".
    349  let importantWS = false;
    350 
    351  // This tracks the nesting parsing state
    352  let isInNested = false;
    353  let nestingLevel = 0;
    354 
    355  let current = "";
    356 
    357  const resetStateForNextDeclaration = () => {
    358    current = "";
    359    currentBlocks = [];
    360    importantState = 0;
    361    importantWS = false;
    362    declarations.push(getEmptyDeclaration());
    363    lastProp = declarations.at(-1);
    364  };
    365 
    366  while (true) {
    367    const token = lexer.nextToken();
    368    if (!token) {
    369      break;
    370    }
    371 
    372    // Update the start and end offsets of the declaration, but only
    373    // when we see a significant token.
    374    if (token.tokenType !== "WhiteSpace" && token.tokenType !== "Comment") {
    375      if (lastProp.offsets[0] === undefined) {
    376        lastProp.offsets[0] = token.startOffset;
    377      }
    378      lastProp.offsets[1] = token.endOffset;
    379    } else if (
    380      lastProp.name &&
    381      !current &&
    382      !importantState &&
    383      !lastProp.priority &&
    384      lastProp.colonOffsets[1]
    385    ) {
    386      // Whitespace appearing after the ":" is attributed to it.
    387      lastProp.colonOffsets[1] = token.endOffset;
    388    } else if (importantState === 1) {
    389      importantWS = true;
    390    }
    391 
    392    if (
    393      token.tokenType === "Ident" &&
    394      token.text[0] == "-" &&
    395      token.text[1] == "-" &&
    396      token.text.length > 2
    397    ) {
    398      if (!lastProp.name) {
    399        lastProp.isCustomProperty = true;
    400      }
    401    }
    402 
    403    if (
    404      // If we're not already in a nested rule
    405      !isInNested &&
    406      // and there's an opening curly bracket
    407      token.tokenType === "CurlyBracketBlock" &&
    408      // and we're not inside a function or an attribute
    409      !currentBlocks.length
    410    ) {
    411      // Assume we're encountering a nested rule.
    412 
    413      if (inComment) {
    414        // If we're in a comment, we still want to retrieve all the "top" level declarations,
    415        // e.g. for `/* color: red; & > span { color: blue; } color: yellow; */`, we do want
    416        // to get the red and yellow declarations.
    417        isInNested = true;
    418        nestingLevel = 1;
    419        continue;
    420      }
    421 
    422      // If we're not in a comment, once we encounter a nested rule, we can stop;
    423      // even if there are declarations after the nested rules, they will be retrieved in
    424      // a different (CSSNestedDeclaration) rule.
    425      declarations.pop();
    426      break;
    427    } else if (isInNested) {
    428      if (token.tokenType == "CurlyBracketBlock") {
    429        nestingLevel++;
    430      } else if (token.tokenType == "CloseCurlyBracket") {
    431        nestingLevel--;
    432      }
    433 
    434      // If we were in a nested rule, and we saw the last closing curly bracket,
    435      // reset the state to parse possible declarations declared after the nested rule.
    436      if (nestingLevel === 0) {
    437        isInNested = false;
    438        // We need to remove the previous pending declaration and reset the state
    439        declarations.pop();
    440        resetStateForNextDeclaration();
    441      }
    442      continue;
    443    } else if (
    444      token.tokenType === "CloseParenthesis" ||
    445      token.tokenType === "CloseSquareBracket"
    446    ) {
    447      // Closing the last block that was opened.
    448      currentBlocks.pop();
    449      current += token.text;
    450    } else if (
    451      token.tokenType === "ParenthesisBlock" ||
    452      token.tokenType === "SquareBracketBlock"
    453    ) {
    454      // Opening a new block.
    455      currentBlocks.push(token.text);
    456      current += token.text;
    457    } else if (token.tokenType === "Function") {
    458      // Opening a function is like opening a new block, so push one to the stack.
    459      currentBlocks.push("(");
    460      current += token.text;
    461    } else if (token.tokenType === "Colon") {
    462      // Either way, a "!important" we've seen is no longer valid now.
    463      importantState = 0;
    464      importantWS = false;
    465      if (!lastProp.name) {
    466        // Set the current declaration name if there's no name yet
    467        lastProp.name = cssTrim(current);
    468        lastProp.colonOffsets = [token.startOffset, token.endOffset];
    469        current = "";
    470        currentBlocks = [];
    471 
    472        // When parsing a comment body, if the left-hand-side is not a
    473        // valid property name, then drop it and stop parsing.
    474        if (
    475          inComment &&
    476          !commentOverride &&
    477          !isCssPropertyKnown(lastProp.name)
    478        ) {
    479          lastProp.name = null;
    480          break;
    481        }
    482      } else {
    483        // Otherwise, just append ':' to the current value (declaration value
    484        // with colons)
    485        current += ":";
    486      }
    487    } else if (token.tokenType === "Semicolon" && !currentBlocks.length) {
    488      lastProp.terminator = "";
    489      // When parsing a comment, if the name hasn't been set, then we
    490      // have probably just seen an ordinary semicolon used in text,
    491      // so drop this and stop parsing.
    492      if (inComment && !lastProp.name) {
    493        current = "";
    494        currentBlocks = [];
    495        break;
    496      }
    497      if (importantState === 2) {
    498        lastProp.priority = "important";
    499      } else if (importantState === 1) {
    500        current += "!";
    501        if (importantWS) {
    502          current += " ";
    503        }
    504      }
    505      lastProp.value = cssTrim(current);
    506      resetStateForNextDeclaration();
    507    } else if (token.tokenType === "Ident") {
    508      if (token.text === "important" && importantState === 1) {
    509        importantState = 2;
    510      } else {
    511        if (importantState > 0) {
    512          current += "!";
    513          if (importantWS) {
    514            current += " ";
    515          }
    516          if (importantState === 2) {
    517            current += "important ";
    518          }
    519          importantState = 0;
    520          importantWS = false;
    521        }
    522        current += token.text;
    523      }
    524    } else if (token.tokenType === "Delim" && token.text === "!") {
    525      importantState = 1;
    526    } else if (token.tokenType === "WhiteSpace") {
    527      if (current !== "") {
    528        current = current.trimEnd() + " ";
    529      }
    530    } else if (token.tokenType === "Comment") {
    531      if (parseComments && !lastProp.name && !lastProp.value) {
    532        const commentText = inputString.substring(
    533          token.startOffset + 2,
    534          token.endOffset - 2
    535        );
    536        const newDecls = parseCommentDeclarations(
    537          isCssPropertyKnown,
    538          commentText,
    539          token.startOffset,
    540          token.endOffset
    541        );
    542 
    543        // Insert the new declarations just before the final element.
    544        const lastDecl = declarations.pop();
    545        declarations = [...declarations, ...newDecls, lastDecl];
    546      } else {
    547        current = current.trimEnd() + " ";
    548      }
    549    } else {
    550      if (importantState > 0) {
    551        current += "!";
    552        if (importantWS) {
    553          current += " ";
    554        }
    555        if (importantState === 2) {
    556          current += "important ";
    557        }
    558        importantState = 0;
    559        importantWS = false;
    560      }
    561      current += inputString.substring(token.startOffset, token.endOffset);
    562    }
    563  }
    564 
    565  // Handle whatever trailing properties or values might still be there
    566  if (current) {
    567    // If nested rule doesn't have closing bracket
    568    if (isInNested && nestingLevel > 0) {
    569      // We need to remove the previous (nested) pending declaration
    570      declarations.pop();
    571    } else if (!lastProp.name) {
    572      // Ignore this case in comments.
    573      if (!inComment) {
    574        // Trailing property found, e.g. p1:v1;p2:v2;p3
    575        lastProp.name = cssTrim(current);
    576      }
    577    } else {
    578      // Trailing value found, i.e. value without an ending ;
    579      if (importantState === 2) {
    580        lastProp.priority = "important";
    581      } else if (importantState === 1) {
    582        current += "!";
    583      }
    584      lastProp.value = cssTrim(current);
    585      const terminator = lexer.performEOFFixup("");
    586      lastProp.terminator = terminator + ";";
    587      // If the input was unterminated, attribute the remainder to
    588      // this property.  This avoids some bad behavior when rewriting
    589      // an unterminated comment.
    590      if (terminator) {
    591        lastProp.offsets[1] = inputString.length;
    592      }
    593    }
    594  }
    595 
    596  // Remove declarations that have neither a name nor a value
    597  declarations = declarations.filter(prop => prop.name || prop.value);
    598 
    599  return declarations;
    600 }
    601 
    602 /**
    603 * Returns an array of CSS declarations given a string.
    604 * For example, `parseDeclarations(isCssPropertyKnown, "--h: 1px; width: 1px !important; height: var(--h);")`
    605 * would return:
    606 * [{
    607 *   name: "--h",
    608 *   value: "1px",
    609 *   priority: "",
    610 *   terminator: "",
    611 *   offsets: [0,9],
    612 *   colonOffsets: [3,5],
    613 *   isCustomProperty: true
    614 * }, {
    615 *   name: "width",
    616 *   value: "1px",
    617 *   priority: "important",
    618 *   terminator: "",
    619 *   offsets: [10,32],
    620 *   colonOffsets: [15,17]
    621 * }, {
    622 *   name: "height",
    623 *   value: "var(--h)",
    624 *   priority: "",
    625 *   terminator: "",
    626 *   offsets: [33,50],
    627 *   colonOffsets: [39,41],
    628 * }]
    629 *
    630 * The input string is assumed to only contain declarations so `{` and `}`
    631 * characters will be treated as part of either the property or value,
    632 * depending where it's found.
    633 *
    634 * @param {Function} isCssPropertyKnown
    635 *        A function to check if the CSS property is known. This is either an
    636 *        internal server function or from the CssPropertiesFront.
    637 *        that are supported by the server.
    638 * @param {string} inputString
    639 *        An input string of CSS
    640 * @param {boolean} parseComments
    641 *        If true, try to parse the contents of comments as well.
    642 *        A comment will only be parsed if it occurs outside of
    643 *        the body of some other declaration.
    644 * @returns {ParsedDeclaration[]} Array of parsed declarations.
    645 */
    646 function parseDeclarations(
    647  isCssPropertyKnown,
    648  inputString,
    649  parseComments = false
    650 ) {
    651  return parseDeclarationsInternal(
    652    isCssPropertyKnown,
    653    inputString,
    654    parseComments,
    655    false,
    656    false
    657  );
    658 }
    659 
    660 /**
    661 * Like @see parseDeclarations, but removes properties that do not have a name.
    662 *
    663 * @param {Function} isCssPropertyKnown
    664 *        A function to check if the CSS property is known. This is either an
    665 *        internal server function or from the CssPropertiesFront.
    666 *        that are supported by the server.
    667 * @param {string} inputString
    668 *        An input string of CSS
    669 * @param {boolean} parseComments
    670 *        If true, try to parse the contents of comments as well.
    671 *        A comment will only be parsed if it occurs outside of
    672 *        the body of some other declaration.
    673 * @returns {ParsedDeclaration[]} Array of parsed declarations.
    674 */
    675 function parseNamedDeclarations(
    676  isCssPropertyKnown,
    677  inputString,
    678  parseComments = false
    679 ) {
    680  return parseDeclarations(
    681    isCssPropertyKnown,
    682    inputString,
    683    parseComments
    684  ).filter(item => !!item.name);
    685 }
    686 
    687 /**
    688 * Returns an array of the parsed CSS selector value and type given a string.
    689 *
    690 * The components making up the CSS selector can be extracted into 3 different
    691 * types: element, attribute and pseudoclass. The object that is appended to
    692 * the returned array contains the value related to one of the 3 types described
    693 * along with the actual type.
    694 *
    695 * The following are the 3 types that can be returned in the object signature:
    696 * (1) SELECTOR_ATTRIBUTE
    697 * (2) SELECTOR_ELEMENT
    698 * (3) SELECTOR_PSEUDO_CLASS
    699 *
    700 * @param {string} value
    701 *        The CSS selector text.
    702 * @returns {Array} an array of objects with the following signature:
    703 *         [{ "value": string, "type": integer }, ...]
    704 */
    705 // eslint-disable-next-line complexity
    706 function parsePseudoClassesAndAttributes(value) {
    707  if (!value) {
    708    throw new Error("empty input string");
    709  }
    710 
    711  // See InspectorCSSToken dictionnary in InspectorUtils.webidl for more information
    712  // about the tokens.
    713  const tokensIterator = cssTokenizer(value);
    714  const result = [];
    715  let current = "";
    716  let functionCount = 0;
    717  let hasAttribute = false;
    718  let hasColon = false;
    719 
    720  for (const token of tokensIterator) {
    721    if (token.tokenType === "Ident") {
    722      current += value.substring(token.startOffset, token.endOffset);
    723 
    724      if (hasColon && !functionCount) {
    725        if (current) {
    726          result.push({ value: current, type: SELECTOR_PSEUDO_CLASS });
    727        }
    728 
    729        current = "";
    730        hasColon = false;
    731      }
    732    } else if (token.tokenType === "Colon") {
    733      if (!hasColon) {
    734        if (current) {
    735          result.push({ value: current, type: SELECTOR_ELEMENT });
    736        }
    737 
    738        current = "";
    739        hasColon = true;
    740      }
    741 
    742      current += token.text;
    743    } else if (token.tokenType === "Function") {
    744      current += value.substring(token.startOffset, token.endOffset);
    745      functionCount++;
    746    } else if (token.tokenType === "CloseParenthesis") {
    747      current += token.text;
    748 
    749      if (hasColon && functionCount == 1) {
    750        if (current) {
    751          result.push({ value: current, type: SELECTOR_PSEUDO_CLASS });
    752        }
    753 
    754        current = "";
    755        functionCount--;
    756        hasColon = false;
    757      } else {
    758        functionCount--;
    759      }
    760    } else if (token.tokenType === "SquareBracketBlock") {
    761      if (!hasAttribute && !functionCount) {
    762        if (current) {
    763          result.push({ value: current, type: SELECTOR_ELEMENT });
    764        }
    765 
    766        current = "";
    767        hasAttribute = true;
    768      }
    769 
    770      current += token.text;
    771    } else if (token.tokenType === "CloseSquareBracket") {
    772      current += token.text;
    773 
    774      if (hasAttribute && !functionCount) {
    775        if (current) {
    776          result.push({ value: current, type: SELECTOR_ATTRIBUTE });
    777        }
    778 
    779        current = "";
    780        hasAttribute = false;
    781      }
    782    } else {
    783      current += value.substring(token.startOffset, token.endOffset);
    784    }
    785  }
    786 
    787  if (current) {
    788    result.push({ value: current, type: SELECTOR_ELEMENT });
    789  }
    790 
    791  return result;
    792 }
    793 
    794 /**
    795 * Expects a single CSS value to be passed as the input and parses the value
    796 * and priority.
    797 *
    798 * @param {Function} isCssPropertyKnown
    799 *        A function to check if the CSS property is known. This is either an
    800 *        internal server function or from the CssPropertiesFront.
    801 *        that are supported by the server.
    802 * @param {string} value
    803 *        The value from the text editor.
    804 * @returns {object} an object with 'value' and 'priority' properties.
    805 */
    806 function parseSingleValue(isCssPropertyKnown, value) {
    807  const declaration = parseDeclarations(
    808    isCssPropertyKnown,
    809    "a: " + value + ";"
    810  )[0];
    811  return {
    812    value: declaration ? declaration.value : "",
    813    priority: declaration ? declaration.priority : "",
    814  };
    815 }
    816 
    817 /**
    818 * Convert an angle value to degree.
    819 *
    820 * @param {number} angleValue The angle value.
    821 * @param {CSS_ANGLEUNIT} angleUnit The angleValue's angle unit.
    822 * @returns {number} An angle value in degree.
    823 */
    824 function getAngleValueInDegrees(angleValue, angleUnit) {
    825  switch (angleUnit) {
    826    case CSS_ANGLEUNIT.deg:
    827      return angleValue;
    828    case CSS_ANGLEUNIT.grad:
    829      return angleValue * 0.9;
    830    case CSS_ANGLEUNIT.rad:
    831      return (angleValue * 180) / Math.PI;
    832    case CSS_ANGLEUNIT.turn:
    833      return angleValue * 360;
    834    default:
    835      throw new Error("No matched angle unit.");
    836  }
    837 }
    838 
    839 exports.cssTokenizer = cssTokenizer;
    840 exports.cssTokenizerWithLineColumn = cssTokenizerWithLineColumn;
    841 exports.escapeCSSComment = escapeCSSComment;
    842 exports.unescapeCSSComment = unescapeCSSComment;
    843 exports.parseDeclarations = parseDeclarations;
    844 exports.parseNamedDeclarations = parseNamedDeclarations;
    845 // parseCommentDeclarations is exported for testing.
    846 exports._parseCommentDeclarations = parseCommentDeclarations;
    847 exports.parsePseudoClassesAndAttributes = parsePseudoClassesAndAttributes;
    848 exports.parseSingleValue = parseSingleValue;
    849 exports.getAngleValueInDegrees = getAngleValueInDegrees;