Greasy Fork is available in English.

extended-css

A javascript library that allows using extended CSS selectors (:has, :contains, etc)

Tätä skriptiä ei tulisi asentaa suoraan. Se on kirjasto muita skriptejä varten sisällytettäväksi metadirektiivillä // @require https://update.greasyfork.org/scripts/452263/1135232/extended-css.js.

// ==UserScript==
// @name extended-css
// @name:zh-CN extended-css
// @version 2.0.36
// @namespace https://adguard.com/
// @author AdguardTeam
// @contributor AdguardTeam
// @contributors AdguardTeam
// @developer AdguardTeam
// @copyright GPL-3.0
// @license GPL-3.0
// @description A javascript library that allows using extended CSS selectors (:has, :contains, etc) 
// @description:zh 一个让用户可以使用扩展 CSS 选择器的库
// @description:zh-CN 一个让用户可以使用扩展 CSS 选择器的库
// @description:zh_CN 一个让用户可以使用扩展 CSS 选择器的库
// @homepage https://github.com/AdguardTeam/ExtendedCss
// @homepageURL https://github.com/AdguardTeam/ExtendedCss
// ==/UserScript==
/**
 * @adguard/extended-css - v2.0.36 - Thu Jan 05 2023
 * https://github.com/AdguardTeam/ExtendedCss#homepage
 * Copyright (c) 2023 AdGuard. Licensed GPL-3.0
 */
var ExtendedCss = (function () {
  'use strict';

  function _typeof(obj) {
    "@babel/helpers - typeof";

    return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) {
      return typeof obj;
    } : function (obj) {
      return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
    }, _typeof(obj);
  }

  function _toPrimitive(input, hint) {
    if (_typeof(input) !== "object" || input === null) return input;
    var prim = input[Symbol.toPrimitive];
    if (prim !== undefined) {
      var res = prim.call(input, hint || "default");
      if (_typeof(res) !== "object") return res;
      throw new TypeError("@@toPrimitive must return a primitive value.");
    }
    return (hint === "string" ? String : Number)(input);
  }

  function _toPropertyKey(arg) {
    var key = _toPrimitive(arg, "string");
    return _typeof(key) === "symbol" ? key : String(key);
  }

  function _defineProperty(obj, key, value) {
    key = _toPropertyKey(key);
    if (key in obj) {
      Object.defineProperty(obj, key, {
        value: value,
        enumerable: true,
        configurable: true,
        writable: true
      });
    } else {
      obj[key] = value;
    }
    return obj;
  }

  let NodeType;

  /**
   * Universal interface for all node types.
   */
  (function (NodeType) {
    NodeType["SelectorList"] = "SelectorList";
    NodeType["Selector"] = "Selector";
    NodeType["RegularSelector"] = "RegularSelector";
    NodeType["ExtendedSelector"] = "ExtendedSelector";
    NodeType["AbsolutePseudoClass"] = "AbsolutePseudoClass";
    NodeType["RelativePseudoClass"] = "RelativePseudoClass";
  })(NodeType || (NodeType = {}));
  /**
   * Class needed for creating ast nodes while selector parsing.
   * Used for SelectorList, Selector, ExtendedSelector.
   */
  class AnySelectorNode {
    /**
     * Creates new ast node.
     *
     * @param type Ast node type.
     */
    constructor(type) {
      _defineProperty(this, "children", []);
      this.type = type;
    }

    /**
     * Adds child node to children array.
     *
     * @param child Ast node.
     */
    addChild(child) {
      this.children.push(child);
    }
  }

  /**
   * Class needed for creating RegularSelector ast node while selector parsing.
   */
  class RegularSelectorNode extends AnySelectorNode {
    /**
     * Creates RegularSelector ast node.
     *
     * @param value Value of RegularSelector node.
     */
    constructor(value) {
      super(NodeType.RegularSelector);
      this.value = value;
    }
  }

  /**
   * Class needed for creating RelativePseudoClass ast node while selector parsing.
   */
  class RelativePseudoClassNode extends AnySelectorNode {
    /**
     * Creates RegularSelector ast node.
     *
     * @param name Name of RelativePseudoClass node.
     */
    constructor(name) {
      super(NodeType.RelativePseudoClass);
      this.name = name;
    }
  }

  /**
   * Class needed for creating AbsolutePseudoClass ast node while selector parsing.
   */
  class AbsolutePseudoClassNode extends AnySelectorNode {
    /**
     * Creates AbsolutePseudoClass ast node.
     *
     * @param name Name of AbsolutePseudoClass node.
     */
    constructor(name) {
      super(NodeType.AbsolutePseudoClass);
      _defineProperty(this, "value", '');
      this.name = name;
    }
  }

  /* eslint-disable jsdoc/require-description-complete-sentence */

  /**
   * Root node.
   *
   * SelectorList
   *   : Selector
   *     ...
   *   ;
   */

  /**
   * Selector node.
   *
   * Selector
   *   : RegularSelector
   *   | ExtendedSelector
   *     ...
   *   ;
   */

  /**
   * Regular selector node.
   * It can be selected by querySelectorAll().
   *
   * RegularSelector
   *   : type
   *   : value
   *   ;
   */

  /**
   * Extended selector node.
   *
   * ExtendedSelector
   *   : AbsolutePseudoClass
   *   | RelativePseudoClass
   *   ;
   */

  /**
   * Absolute extended pseudo-class node,
   * i.e. none-selector args.
   *
   * AbsolutePseudoClass
   *   : type
   *   : name
   *   : value
   *   ;
   */

  /**
   * Relative extended pseudo-class node
   * i.e. selector as arg.
   *
   * RelativePseudoClass
   *   : type
   *   : name
   *   : SelectorList
   *   ;
   */

  //
  //  ast example
  //
  //  div.banner > div:has(span, p), a img.ad
  //
  //  SelectorList - div.banner > div:has(span, p), a img.ad
  //      Selector - div.banner > div:has(span, p)
  //          RegularSelector - div.banner > div
  //          ExtendedSelector - :has(span, p)
  //              PseudoClassSelector - :has
  //              SelectorList - span, p
  //                  Selector - span
  //                      RegularSelector - span
  //                  Selector - p
  //                      RegularSelector - p
  //      Selector - a img.ad
  //          RegularSelector - a img.ad
  //

  const LEFT_SQUARE_BRACKET = '[';
  const RIGHT_SQUARE_BRACKET = ']';
  const LEFT_PARENTHESIS = '(';
  const RIGHT_PARENTHESIS = ')';
  const LEFT_CURLY_BRACKET = '{';
  const RIGHT_CURLY_BRACKET = '}';
  const BRACKETS = {
    SQUARE: {
      LEFT: LEFT_SQUARE_BRACKET,
      RIGHT: RIGHT_SQUARE_BRACKET
    },
    PARENTHESES: {
      LEFT: LEFT_PARENTHESIS,
      RIGHT: RIGHT_PARENTHESIS
    },
    CURLY: {
      LEFT: LEFT_CURLY_BRACKET,
      RIGHT: RIGHT_CURLY_BRACKET
    }
  };
  const SLASH = '/';
  const BACKSLASH = '\\';
  const SPACE = ' ';
  const COMMA = ',';
  const DOT = '.';
  const SEMICOLON = ';';
  const COLON = ':';
  const SINGLE_QUOTE = '\'';
  const DOUBLE_QUOTE = '"';

  // do not consider hyphen `-` as separated mark
  // to avoid pseudo-class names splitting
  // e.g. 'matches-css' or 'if-not'

  const CARET = '^';
  const DOLLAR_SIGN = '$';
  const EQUAL_SIGN = '=';
  const TAB = '\t';
  const CARRIAGE_RETURN = '\r';
  const LINE_FEED = '\n';
  const FORM_FEED = '\f';
  const WHITE_SPACE_CHARACTERS = [SPACE, TAB, CARRIAGE_RETURN, LINE_FEED, FORM_FEED];

  // for universal selector and attributes
  const ASTERISK = '*';
  const ID_MARKER = '#';
  const CLASS_MARKER = DOT;
  const DESCENDANT_COMBINATOR = SPACE;
  const CHILD_COMBINATOR = '>';
  const NEXT_SIBLING_COMBINATOR = '+';
  const SUBSEQUENT_SIBLING_COMBINATOR = '~';
  const COMBINATORS = [DESCENDANT_COMBINATOR, CHILD_COMBINATOR, NEXT_SIBLING_COMBINATOR, SUBSEQUENT_SIBLING_COMBINATOR];
  const SUPPORTED_SELECTOR_MARKS = [LEFT_SQUARE_BRACKET, RIGHT_SQUARE_BRACKET, LEFT_PARENTHESIS, RIGHT_PARENTHESIS, LEFT_CURLY_BRACKET, RIGHT_CURLY_BRACKET, SLASH, BACKSLASH, SEMICOLON, COLON, COMMA, SINGLE_QUOTE, DOUBLE_QUOTE, CARET, DOLLAR_SIGN, ASTERISK, ID_MARKER, CLASS_MARKER, DESCENDANT_COMBINATOR, CHILD_COMBINATOR, NEXT_SIBLING_COMBINATOR, SUBSEQUENT_SIBLING_COMBINATOR, TAB, CARRIAGE_RETURN, LINE_FEED, FORM_FEED];

  // absolute:
  const CONTAINS_PSEUDO = 'contains';
  const HAS_TEXT_PSEUDO = 'has-text';
  const ABP_CONTAINS_PSEUDO = '-abp-contains';
  const MATCHES_CSS_PSEUDO = 'matches-css';
  const MATCHES_CSS_BEFORE_PSEUDO = 'matches-css-before';
  const MATCHES_CSS_AFTER_PSEUDO = 'matches-css-after';
  const MATCHES_ATTR_PSEUDO_CLASS_MARKER = 'matches-attr';
  const MATCHES_PROPERTY_PSEUDO_CLASS_MARKER = 'matches-property';
  const XPATH_PSEUDO_CLASS_MARKER = 'xpath';
  const NTH_ANCESTOR_PSEUDO_CLASS_MARKER = 'nth-ancestor';
  const CONTAINS_PSEUDO_NAMES = [CONTAINS_PSEUDO, HAS_TEXT_PSEUDO, ABP_CONTAINS_PSEUDO];

  /**
   * Pseudo-class :upward() can get number or selector arg
   * and if the arg is selector it should be standard, not extended
   * so :upward pseudo-class is always absolute.
   */
  const UPWARD_PSEUDO_CLASS_MARKER = 'upward';

  /**
   * Pseudo-class `:remove()` and pseudo-property `remove`
   * are used for element actions, not for element selecting.
   *
   * Selector text should not contain the pseudo-class
   * so selector parser should consider it as invalid
   * and both are handled by stylesheet parser.
   */
  const REMOVE_PSEUDO_MARKER = 'remove';

  // relative:
  const HAS_PSEUDO_CLASS_MARKER = 'has';
  const ABP_HAS_PSEUDO_CLASS_MARKER = '-abp-has';
  const HAS_PSEUDO_CLASS_MARKERS = [HAS_PSEUDO_CLASS_MARKER, ABP_HAS_PSEUDO_CLASS_MARKER];
  const IS_PSEUDO_CLASS_MARKER = 'is';
  const NOT_PSEUDO_CLASS_MARKER = 'not';
  const ABSOLUTE_PSEUDO_CLASSES = [CONTAINS_PSEUDO, HAS_TEXT_PSEUDO, ABP_CONTAINS_PSEUDO, MATCHES_CSS_PSEUDO, MATCHES_CSS_BEFORE_PSEUDO, MATCHES_CSS_AFTER_PSEUDO, MATCHES_ATTR_PSEUDO_CLASS_MARKER, MATCHES_PROPERTY_PSEUDO_CLASS_MARKER, XPATH_PSEUDO_CLASS_MARKER, NTH_ANCESTOR_PSEUDO_CLASS_MARKER, UPWARD_PSEUDO_CLASS_MARKER];
  const RELATIVE_PSEUDO_CLASSES = [...HAS_PSEUDO_CLASS_MARKERS, IS_PSEUDO_CLASS_MARKER, NOT_PSEUDO_CLASS_MARKER];
  const SUPPORTED_PSEUDO_CLASSES = [...ABSOLUTE_PSEUDO_CLASSES, ...RELATIVE_PSEUDO_CLASSES];

  // these pseudo-classes should be part of RegularSelector value
  // if its arg does not contain extended selectors.
  // the ast will be checked after the selector is completely parsed
  const OPTIMIZATION_PSEUDO_CLASSES = [NOT_PSEUDO_CLASS_MARKER, IS_PSEUDO_CLASS_MARKER];

  /**
   * ':scope' is used for extended pseudo-class :has(), if-not(), :is() and :not().
   */
  const SCOPE_CSS_PSEUDO_CLASS = ':scope';

  /**
   * ':after' and ':before' are needed for :matches-css() pseudo-class
   * all other are needed for :has() limitation after regular pseudo-elements.
   *
   * @see {@link https://bugs.chromium.org/p/chromium/issues/detail?id=669058#c54} [case 3]
   */
  const REGULAR_PSEUDO_ELEMENTS = {
    AFTER: 'after',
    BACKDROP: 'backdrop',
    BEFORE: 'before',
    CUE: 'cue',
    CUE_REGION: 'cue-region',
    FIRST_LETTER: 'first-letter',
    FIRST_LINE: 'first-line',
    FILE_SELECTION_BUTTON: 'file-selector-button',
    GRAMMAR_ERROR: 'grammar-error',
    MARKER: 'marker',
    PART: 'part',
    PLACEHOLDER: 'placeholder',
    SELECTION: 'selection',
    SLOTTED: 'slotted',
    SPELLING_ERROR: 'spelling-error',
    TARGET_TEXT: 'target-text'
  };
  const CONTENT_CSS_PROPERTY = 'content';
  const PSEUDO_PROPERTY_POSITIVE_VALUE = 'true';
  const DEBUG_PSEUDO_PROPERTY_GLOBAL_VALUE = 'global';
  const NO_SELECTOR_ERROR_PREFIX = 'Selector should be defined before';
  const STYLESHEET_ERROR_PREFIX = {
    NO_STYLE: 'No style declaration at stylesheet part',
    NO_SELECTOR: `${NO_SELECTOR_ERROR_PREFIX} style declaration in stylesheet`,
    INVALID_STYLE: 'Invalid style declaration at stylesheet part',
    UNCLOSED_STYLE: 'Unclosed style declaration at stylesheet part',
    NO_PROPERTY: 'Missing style property in declaration at stylesheet part',
    NO_VALUE: 'Missing style value in declaration at stylesheet part',
    NO_STYLE_OR_REMOVE: 'Invalid stylesheet - no style declared or :remove() pseudo-class used',
    NO_COMMENT: 'Comments in stylesheet are not supported'
  };
  const REMOVE_ERROR_PREFIX = {
    INVALID_REMOVE: 'Invalid :remove() pseudo-class in selector',
    NO_TARGET_SELECTOR: `${NO_SELECTOR_ERROR_PREFIX} :remove() pseudo-class`,
    MULTIPLE_USAGE: 'Pseudo-class :remove() appears more than once in selector',
    INVALID_POSITION: 'Pseudo-class :remove() should be at the end of selector'
  };
  const MATCHING_ELEMENT_ERROR_PREFIX = 'Error while matching element';
  const MAX_STYLE_PROTECTION_COUNT = 50;

  /**
   * Regexp that matches backward compatible syntaxes.
   */
  const REGEXP_VALID_OLD_SYNTAX = /\[-(?:ext)-([a-z-_]+)=(["'])((?:(?=(\\?))\4.)*?)\2\]/g;

  /**
   * Marker for checking invalid selector after old-syntax normalizing by selector converter.
   */
  const INVALID_OLD_SYNTAX_MARKER = '[-ext-';

  /**
   * Complex replacement function.
   * Undo quote escaping inside of an extended selector.
   *
   * @param match     Whole matched string.
   * @param name      Group 1.
   * @param quoteChar Group 2.
   * @param rawValue  Group 3.
   *
   * @returns Converted string.
   */
  const evaluateMatch = (match, name, quoteChar, rawValue) => {
    // Unescape quotes
    const re = new RegExp(`([^\\\\]|^)\\\\${quoteChar}`, 'g');
    const value = rawValue.replace(re, `$1${quoteChar}`);
    return `:${name}(${value})`;
  };

  // ':scope' pseudo may be at start of :has() argument
  // but ExtCssDocument.querySelectorAll() already use it for selecting exact element descendants
  const reScope = /\(:scope >/g;
  const SCOPE_REPLACER = '(>';
  const MATCHES_CSS_PSEUDO_ELEMENT_REGEXP = /(:matches-css)-(before|after)\(/g;
  const convertMatchesCss = (match, extendedPseudoClass, regularPseudoElement) => {
    // ':matches-css-before('  -->  ':matches-css(before, '
    // ':matches-css-after('   -->  ':matches-css(after, '
    return `${extendedPseudoClass}${BRACKETS.PARENTHESES.LEFT}${regularPseudoElement}${COMMA}`;
  };

  /**
   * Handles old syntax and :scope inside :has().
   *
   * @param selector Trimmed selector to normalize.
   *
   * @returns Normalized selector.
   * @throws An error on invalid old extended syntax selector.
   */
  const normalize = selector => {
    const normalizedSelector = selector.replace(REGEXP_VALID_OLD_SYNTAX, evaluateMatch).replace(reScope, SCOPE_REPLACER).replace(MATCHES_CSS_PSEUDO_ELEMENT_REGEXP, convertMatchesCss);

    // validate old syntax after normalizing
    // e.g. '[-ext-matches-css-before=\'content:  /^[A-Z][a-z]'
    if (normalizedSelector.includes(INVALID_OLD_SYNTAX_MARKER)) {
      throw new Error(`Invalid extended-css old syntax selector: '${selector}'`);
    }
    return normalizedSelector;
  };

  /**
   * Prepares the rawSelector before tokenization:
   * 1. Trims it.
   * 2. Converts old syntax `[-ext-pseudo-class="..."]` to new one `:pseudo-class(...)`.
   * 3. Handles :scope pseudo inside :has() pseudo-class arg.
   *
   * @param rawSelector Selector with no style declaration.
   * @returns Prepared selector with no style declaration.
   */
  const convert = rawSelector => {
    const trimmedSelector = rawSelector.trim();
    return normalize(trimmedSelector);
  };

  let TokenType;
  (function (TokenType) {
    TokenType["Mark"] = "mark";
    TokenType["Word"] = "word";
  })(TokenType || (TokenType = {}));
  /**
   * Splits `input` string into tokens.
   *
   * @param input Input string to tokenize.
   * @param supportedMarks Array of supported marks to considered as `TokenType.Mark`;
   * all other will be considered as `TokenType.Word`.
   *
   * @returns Array of tokens.
   */
  const tokenize = (input, supportedMarks) => {
    // buffer is needed for words collecting while iterating
    let buffer = '';
    // result collection
    const tokens = [];
    const selectorSymbols = input.split('');
    // iterate through selector chars and collect tokens
    selectorSymbols.forEach((symbol, i) => {
      if (supportedMarks.includes(symbol)) {
        tokens.push({
          type: TokenType.Mark,
          value: symbol
        });
        return;
      }
      buffer += symbol;
      const nextSymbol = selectorSymbols[i + 1];
      // string end has been reached if nextSymbol is undefined
      if (!nextSymbol || supportedMarks.includes(nextSymbol)) {
        tokens.push({
          type: TokenType.Word,
          value: buffer
        });
        buffer = '';
      }
    });
    return tokens;
  };

  /**
   * Prepares `rawSelector` and splits it into tokens.
   *
   * @param rawSelector Raw css selector.
   *
   * @returns Array of tokens supported for selector.
   */
  const tokenizeSelector = rawSelector => {
    const selector = convert(rawSelector);
    return tokenize(selector, SUPPORTED_SELECTOR_MARKS);
  };

  /**
   * Splits `attribute` into tokens.
   *
   * @param attribute Input attribute.
   *
   * @returns Array of tokens supported for attribute.
   */
  const tokenizeAttribute = attribute => {
    // equal sigh `=` in attribute is considered as `TokenType.Mark`
    return tokenize(attribute, [...SUPPORTED_SELECTOR_MARKS, EQUAL_SIGN]);
  };

  /**
   * Some browsers do not support Array.prototype.flat()
   * e.g. Opera 42 which is used for browserstack tests.
   *
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat}
   *
   * @param input Array needed to be flatten.
   *
   * @returns Flatten array.
   * @throws An error if array cannot be flatten.
   */
  const flatten = input => {
    const stack = [];
    input.forEach(el => stack.push(el));
    const res = [];
    while (stack.length) {
      // pop value from stack
      const next = stack.pop();
      if (!next) {
        throw new Error('Unable to make array flat');
      }
      if (Array.isArray(next)) {
        // push back array items, won't modify the original input
        next.forEach(el => stack.push(el));
      } else {
        res.push(next);
      }
    }
    // reverse to restore input order
    return res.reverse();
  };

  /**
   * Returns first item from `array`.
   *
   * @param array Input array.
   *
   * @returns First array item, or `undefined` if there is no such item.
   */
  const getFirst = array => {
    return array[0];
  };

  /**
   * Returns last item from array.
   *
   * @param array Input array.
   *
   * @returns Last array item, or `undefined` if there is no such item.
   */
  const getLast = array => {
    return array[array.length - 1];
  };

  /**
   * Returns array item which is previous to the last one
   * e.g. for `[5, 6, 7, 8]` returns `7`.
   *
   * @param array Input array.
   *
   * @returns Previous to last array item, or `undefined` if there is no such item.
   */
  const getPrevToLast = array => {
    return array[array.length - 2];
  };

  /**
   * Takes array of ast node `children` and returns the child by the `index`.
   *
   * @param array Array of ast node children.
   * @param index Index of needed child in the array.
   * @param errorMessage Optional error message to throw.
   *
   * @returns Array item at `index` position.
   * @throws An error if there is no child with specified `index` in array.
   */
  const getItemByIndex = (array, index, errorMessage) => {
    const indexChild = array[index];
    if (!indexChild) {
      throw new Error(errorMessage || `No array item found by index ${index}`);
    }
    return indexChild;
  };

  const NO_REGULAR_SELECTOR_ERROR = 'At least one of Selector node children should be RegularSelector';

  /**
   * Checks whether the type of `astNode` is SelectorList.
   *
   * @param astNode Ast node.
   *
   * @returns True if astNode.type === SelectorList.
   */
  const isSelectorListNode = astNode => {
    return (astNode === null || astNode === void 0 ? void 0 : astNode.type) === NodeType.SelectorList;
  };

  /**
   * Checks whether the type of `astNode` is Selector.
   *
   * @param astNode Ast node.
   *
   * @returns True if astNode.type === Selector.
   */
  const isSelectorNode = astNode => {
    return (astNode === null || astNode === void 0 ? void 0 : astNode.type) === NodeType.Selector;
  };

  /**
   * Checks whether the type of `astNode` is RegularSelector.
   *
   * @param astNode Ast node.
   *
   * @returns True if astNode.type === RegularSelector.
   */
  const isRegularSelectorNode = astNode => {
    return (astNode === null || astNode === void 0 ? void 0 : astNode.type) === NodeType.RegularSelector;
  };

  /**
   * Checks whether the type of `astNode` is ExtendedSelector.
   *
   * @param astNode Ast node.
   *
   * @returns True if astNode.type === ExtendedSelector.
   */
  const isExtendedSelectorNode = astNode => {
    return astNode.type === NodeType.ExtendedSelector;
  };

  /**
   * Checks whether the type of `astNode` is AbsolutePseudoClass.
   *
   * @param astNode Ast node.
   *
   * @returns True if astNode.type === AbsolutePseudoClass.
   */
  const isAbsolutePseudoClassNode = astNode => {
    return (astNode === null || astNode === void 0 ? void 0 : astNode.type) === NodeType.AbsolutePseudoClass;
  };

  /**
   * Checks whether the type of `astNode` is RelativePseudoClass.
   *
   * @param astNode Ast node.
   *
   * @returns True if astNode.type === RelativePseudoClass.
   */
  const isRelativePseudoClassNode = astNode => {
    return (astNode === null || astNode === void 0 ? void 0 : astNode.type) === NodeType.RelativePseudoClass;
  };

  /**
   * Returns name of `astNode`.
   *
   * @param astNode AbsolutePseudoClass or RelativePseudoClass node.
   *
   * @returns Name of `astNode`.
   * @throws An error on unsupported ast node or no name found.
   */
  const getNodeName = astNode => {
    if (astNode === null) {
      throw new Error('Ast node should be defined');
    }
    if (!isAbsolutePseudoClassNode(astNode) && !isRelativePseudoClassNode(astNode)) {
      throw new Error('Only AbsolutePseudoClass or RelativePseudoClass ast node can have a name');
    }
    if (!astNode.name) {
      throw new Error('Extended pseudo-class should have a name');
    }
    return astNode.name;
  };

  /**
   * Returns value of `astNode`.
   *
   * @param astNode RegularSelector or AbsolutePseudoClass node.
   * @param errorMessage Optional error message if no value found.
   *
   * @returns Value of `astNode`.
   * @throws An error on unsupported ast node or no value found.
   */
  const getNodeValue = (astNode, errorMessage) => {
    if (astNode === null) {
      throw new Error('Ast node should be defined');
    }
    if (!isRegularSelectorNode(astNode) && !isAbsolutePseudoClassNode(astNode)) {
      throw new Error('Only RegularSelector ot AbsolutePseudoClass ast node can have a value');
    }
    if (!astNode.value) {
      throw new Error(errorMessage || 'Ast RegularSelector ot AbsolutePseudoClass node should have a value');
    }
    return astNode.value;
  };

  /**
   * Returns only RegularSelector nodes from `children`.
   *
   * @param children Array of ast node children.
   *
   * @returns Array of RegularSelector nodes.
   */
  const getRegularSelectorNodes = children => {
    return children.filter(isRegularSelectorNode);
  };

  /**
   * Returns the first RegularSelector node from `children`.
   *
   * @param children Array of ast node children.
   * @param errorMessage Optional error message if no value found.
   *
   * @returns Ast RegularSelector node.
   * @throws An error if no RegularSelector node found.
   */
  const getFirstRegularChild = (children, errorMessage) => {
    const regularSelectorNodes = getRegularSelectorNodes(children);
    const firstRegularSelectorNode = getFirst(regularSelectorNodes);
    if (!firstRegularSelectorNode) {
      throw new Error(errorMessage || NO_REGULAR_SELECTOR_ERROR);
    }
    return firstRegularSelectorNode;
  };

  /**
   * Returns the last RegularSelector node from `children`.
   *
   * @param children Array of ast node children.
   *
   * @returns Ast RegularSelector node.
   * @throws An error if no RegularSelector node found.
   */
  const getLastRegularChild = children => {
    const regularSelectorNodes = getRegularSelectorNodes(children);
    const lastRegularSelectorNode = getLast(regularSelectorNodes);
    if (!lastRegularSelectorNode) {
      throw new Error(NO_REGULAR_SELECTOR_ERROR);
    }
    return lastRegularSelectorNode;
  };

  /**
   * Returns the only child of `node`.
   *
   * @param node Ast node.
   * @param errorMessage Error message.
   *
   * @returns The only child of ast node.
   * @throws An error if none or more than one child found.
   */
  const getNodeOnlyChild = (node, errorMessage) => {
    if (node.children.length !== 1) {
      throw new Error(errorMessage);
    }
    const onlyChild = getFirst(node.children);
    if (!onlyChild) {
      throw new Error(errorMessage);
    }
    return onlyChild;
  };

  /**
   * Takes ExtendedSelector node and returns its only child.
   *
   * @param extendedSelectorNode ExtendedSelector ast node.
   *
   * @returns AbsolutePseudoClass or RelativePseudoClass.
   * @throws An error if there is no specific pseudo-class ast node.
   */
  const getPseudoClassNode = extendedSelectorNode => {
    return getNodeOnlyChild(extendedSelectorNode, 'Extended selector should be specified');
  };

  /**
   * Takes RelativePseudoClass node and returns its only child
   * which is relative SelectorList node.
   *
   * @param pseudoClassNode RelativePseudoClass.
   *
   * @returns Relative SelectorList node.
   * @throws An error if no selector list found.
   */
  const getRelativeSelectorListNode = pseudoClassNode => {
    if (!isRelativePseudoClassNode(pseudoClassNode)) {
      throw new Error('Only RelativePseudoClass node can have relative SelectorList node as child');
    }
    return getNodeOnlyChild(pseudoClassNode, `Missing arg for :${getNodeName(pseudoClassNode)}() pseudo-class`);
  };

  const ATTRIBUTE_CASE_INSENSITIVE_FLAG = 'i';

  /**
   * Limited list of available symbols before slash `/`
   * to check whether it is valid regexp pattern opening.
   */
  const POSSIBLE_MARKS_BEFORE_REGEXP = {
    COMMON: [
    // e.g. ':matches-attr(/data-/)'
    BRACKETS.PARENTHESES.LEFT,
    // e.g. `:matches-attr('/data-/')`
    SINGLE_QUOTE,
    // e.g. ':matches-attr("/data-/")'
    DOUBLE_QUOTE,
    // e.g. ':matches-attr(check=/data-v-/)'
    EQUAL_SIGN,
    // e.g. ':matches-property(inner./_test/=null)'
    DOT,
    // e.g. ':matches-css(height:/20px/)'
    COLON,
    // ':matches-css-after( content  :   /(\\d+\\s)*me/  )'
    SPACE],
    CONTAINS: [
    // e.g. ':contains(/text/)'
    BRACKETS.PARENTHESES.LEFT,
    // e.g. `:contains('/text/')`
    SINGLE_QUOTE,
    // e.g. ':contains("/text/")'
    DOUBLE_QUOTE]
  };

  /**
   * Checks whether the passed token is supported extended pseudo-class.
   *
   * @param tokenValue Token value to check.
   *
   * @returns True if `tokenValue` is one of supported extended pseudo-class names.
   */
  const isSupportedPseudoClass = tokenValue => {
    return SUPPORTED_PSEUDO_CLASSES.includes(tokenValue);
  };

  /**
   * Checks whether the passed pseudo-class `name` should be optimized,
   * i.e. :not() and :is().
   *
   * @param name Pseudo-class name.
   *
   * @returns True if `name` is one if pseudo-class which should be optimized.
   */
  const isOptimizationPseudoClass = name => {
    return OPTIMIZATION_PSEUDO_CLASSES.includes(name);
  };

  /**
   * Checks whether next to "space" token is a continuation of regular selector being processed.
   *
   * @param nextTokenType Type of token next to current one.
   * @param nextTokenValue Value of token next to current one.
   *
   * @returns True if next token seems to be a part of current regular selector.
   */
  const doesRegularContinueAfterSpace = (nextTokenType, nextTokenValue) => {
    // regular selector does not continues after the current token
    if (!nextTokenType || !nextTokenValue) {
      return false;
    }
    return COMBINATORS.includes(nextTokenValue) || nextTokenType === TokenType.Word
    // e.g. '#main *:has(> .ad)'
    || nextTokenValue === ASTERISK || nextTokenValue === ID_MARKER || nextTokenValue === CLASS_MARKER
    // e.g. 'div :where(.content)'
    || nextTokenValue === COLON
    // e.g. "div[class*=' ']"
    || nextTokenValue === SINGLE_QUOTE
    // e.g. 'div[class*=" "]'
    || nextTokenValue === DOUBLE_QUOTE || nextTokenValue === BRACKETS.SQUARE.LEFT;
  };

  /**
   * Checks whether the regexp pattern for pseudo-class arg starts.
   * Needed for `context.isRegexpOpen` flag.
   *
   * @param context Selector parser context.
   * @param prevTokenValue Value of previous token.
   * @param bufferNodeValue Value of bufferNode.
   *
   * @returns True if current token seems to be a start of regexp pseudo-class arg pattern.
   * @throws An error on invalid regexp pattern.
   */
  const isRegexpOpening = (context, prevTokenValue, bufferNodeValue) => {
    const lastExtendedPseudoClassName = getLast(context.extendedPseudoNamesStack);
    if (!lastExtendedPseudoClassName) {
      throw new Error('Regexp pattern allowed only in arg of extended pseudo-class');
    }
    // for regexp pattens the slash should not be escaped
    // const isRegexpPatternSlash = prevTokenValue !== BACKSLASH;
    // regexp pattern can be set as arg of pseudo-class
    // which means limited list of available symbols before slash `/`;
    // for :contains() pseudo-class regexp pattern should be at the beginning of arg
    if (CONTAINS_PSEUDO_NAMES.includes(lastExtendedPseudoClassName)) {
      return POSSIBLE_MARKS_BEFORE_REGEXP.CONTAINS.includes(prevTokenValue);
    }
    if (prevTokenValue === SLASH && lastExtendedPseudoClassName !== XPATH_PSEUDO_CLASS_MARKER) {
      const rawArgDesc = bufferNodeValue ? `in arg part: '${bufferNodeValue}'` : 'arg';
      throw new Error(`Invalid regexp pattern for :${lastExtendedPseudoClassName}() pseudo-class ${rawArgDesc}`);
    }

    // for other pseudo-classes regexp pattern can be either the whole arg or its part
    return POSSIBLE_MARKS_BEFORE_REGEXP.COMMON.includes(prevTokenValue);
  };

  /**
   * Checks whether the attribute starts.
   *
   * @param tokenValue Value of current token.
   * @param prevTokenValue Previous token value.
   *
   * @returns True if combination of current and previous token seems to be **a start** of attribute.
   */
  const isAttributeOpening = (tokenValue, prevTokenValue) => {
    return tokenValue === BRACKETS.SQUARE.LEFT && prevTokenValue !== BACKSLASH;
  };

  /**
   * Checks whether the attribute ends.
   *
   * @param context Selector parser context.
   *
   * @returns True if combination of current and previous token seems to be **an end** of attribute.
   * @throws An error on invalid attribute.
   */
  const isAttributeClosing = context => {
    var _getPrevToLast;
    if (!context.isAttributeBracketsOpen) {
      return false;
    }
    // valid attributes may have extra spaces inside.
    // we get rid of them just to simplify the checking and they are skipped only here:
    //   - spaces will be collected to the ast with spaces as they were declared is selector
    //   - extra spaces in attribute are not relevant to attribute syntax validity
    //     e.g. 'a[ title ]' is the same as 'a[title]'
    //          'div[style *= "MARGIN" i]' is the same as 'div[style*="MARGIN"i]'
    const noSpaceAttr = context.attributeBuffer.split(SPACE).join('');
    // tokenize the prepared attribute string
    const attrTokens = tokenizeAttribute(noSpaceAttr);
    const firstAttrToken = getFirst(attrTokens);
    const firstAttrTokenType = firstAttrToken === null || firstAttrToken === void 0 ? void 0 : firstAttrToken.type;
    const firstAttrTokenValue = firstAttrToken === null || firstAttrToken === void 0 ? void 0 : firstAttrToken.value;
    // signal an error on any mark-type token except backslash
    // e.g. '[="margin"]'
    if (firstAttrTokenType === TokenType.Mark
    // backslash is allowed at start of attribute
    // e.g. '[\\:data-service-slot]'
    && firstAttrTokenValue !== BACKSLASH) {
      // eslint-disable-next-line max-len
      throw new Error(`'[${context.attributeBuffer}]' is not a valid attribute due to '${firstAttrTokenValue}' at start of it`);
    }
    const lastAttrToken = getLast(attrTokens);
    const lastAttrTokenType = lastAttrToken === null || lastAttrToken === void 0 ? void 0 : lastAttrToken.type;
    const lastAttrTokenValue = lastAttrToken === null || lastAttrToken === void 0 ? void 0 : lastAttrToken.value;
    if (lastAttrTokenValue === EQUAL_SIGN) {
      // e.g. '[style=]'
      throw new Error(`'[${context.attributeBuffer}]' is not a valid attribute due to '${EQUAL_SIGN}'`);
    }
    const equalSignIndex = attrTokens.findIndex(token => {
      return token.type === TokenType.Mark && token.value === EQUAL_SIGN;
    });
    const prevToLastAttrTokenValue = (_getPrevToLast = getPrevToLast(attrTokens)) === null || _getPrevToLast === void 0 ? void 0 : _getPrevToLast.value;
    if (equalSignIndex === -1) {
      // if there is no '=' inside attribute,
      // it must be just attribute name which means the word-type token before closing bracket
      // e.g. 'div[style]'
      if (lastAttrTokenType === TokenType.Word) {
        return true;
      }
      return prevToLastAttrTokenValue === BACKSLASH
      // some weird attribute are valid too
      // e.g. '[class\\"ads-article\\"]'
      && (lastAttrTokenValue === DOUBLE_QUOTE
      // e.g. "[class\\'ads-article\\']"
      || lastAttrTokenValue === SINGLE_QUOTE);
    }

    // get the value of token next to `=`
    const nextToEqualSignToken = getItemByIndex(attrTokens, equalSignIndex + 1);
    const nextToEqualSignTokenValue = nextToEqualSignToken.value;
    // check whether the attribute value wrapper in quotes
    const isAttrValueQuote = nextToEqualSignTokenValue === SINGLE_QUOTE || nextToEqualSignTokenValue === DOUBLE_QUOTE;

    // for no quotes after `=` the last token before `]` should be a word-type one
    // e.g. 'div[style*=margin]'
    //      'div[style*=MARGIN i]'
    if (!isAttrValueQuote) {
      if (lastAttrTokenType === TokenType.Word) {
        return true;
      }
      // otherwise signal an error
      // e.g. 'table[style*=border: 0px"]'
      throw new Error(`'[${context.attributeBuffer}]' is not a valid attribute`);
    }

    // otherwise if quotes for value are present
    // the last token before `]` can still be word-type token
    // e.g. 'div[style*="MARGIN" i]'
    if (lastAttrTokenType === TokenType.Word && (lastAttrTokenValue === null || lastAttrTokenValue === void 0 ? void 0 : lastAttrTokenValue.toLocaleLowerCase()) === ATTRIBUTE_CASE_INSENSITIVE_FLAG) {
      return prevToLastAttrTokenValue === nextToEqualSignTokenValue;
    }

    // eventually if there is quotes for attribute value and last token is not a word,
    // the closing mark should be the same quote as opening one
    return lastAttrTokenValue === nextToEqualSignTokenValue;
  };

  /**
   * Checks whether the `tokenValue` is a whitespace character.
   *
   * @param tokenValue Token value.
   *
   * @returns True if `tokenValue` is a whitespace character.
   */
  const isWhiteSpaceChar = tokenValue => {
    if (!tokenValue) {
      return false;
    }
    return WHITE_SPACE_CHARACTERS.includes(tokenValue);
  };

  /**
   * Checks whether the passed `str` is a name of supported absolute extended pseudo-class,
   * e.g. :contains(), :matches-css() etc.
   *
   * @param str Token value to check.
   *
   * @returns True if `str` is one of absolute extended pseudo-class names.
   */
  const isAbsolutePseudoClass = str => {
    return ABSOLUTE_PSEUDO_CLASSES.includes(str);
  };

  /**
   * Checks whether the passed `str` is a name of supported relative extended pseudo-class,
   * e.g. :has(), :not() etc.
   *
   * @param str Token value to check.
   *
   * @returns True if `str` is one of relative extended pseudo-class names.
   */
  const isRelativePseudoClass = str => {
    return RELATIVE_PSEUDO_CLASSES.includes(str);
  };

  /**
   * Returns the node which is being collected
   * or null if there is no such one.
   *
   * @param context Selector parser context.
   *
   * @returns Buffer node or null.
   */
  const getBufferNode = context => {
    if (context.pathToBufferNode.length === 0) {
      return null;
    }
    // buffer node is always the last in the pathToBufferNode stack
    return getLast(context.pathToBufferNode) || null;
  };

  /**
   * Returns the parent node to the 'buffer node' — which is the one being collected —
   * or null if there is no such one.
   *
   * @param context Selector parser context.
   *
   * @returns Parent node of buffer node or null.
   */
  const getBufferNodeParent = context => {
    // at least two nodes should exist — the buffer node and its parent
    // otherwise return null
    if (context.pathToBufferNode.length < 2) {
      return null;
    }
    // since the buffer node is always the last in the pathToBufferNode stack
    // its parent is previous to it in the stack
    return getPrevToLast(context.pathToBufferNode) || null;
  };

  /**
   * Returns last RegularSelector ast node.
   * Needed for parsing of the complex selector with extended pseudo-class inside it.
   *
   * @param context Selector parser context.
   *
   * @returns Ast RegularSelector node.
   * @throws An error if:
   * - bufferNode is absent;
   * - type of bufferNode is unsupported;
   * - no RegularSelector in bufferNode.
   */
  const getContextLastRegularSelectorNode = context => {
    const bufferNode = getBufferNode(context);
    if (!bufferNode) {
      throw new Error('No bufferNode found');
    }
    if (!isSelectorNode(bufferNode)) {
      throw new Error('Unsupported bufferNode type');
    }
    const lastRegularSelectorNode = getLastRegularChild(bufferNode.children);
    context.pathToBufferNode.push(lastRegularSelectorNode);
    return lastRegularSelectorNode;
  };

  /**
   * Updates needed buffer node value while tokens iterating.
   * For RegularSelector also collects token values to context.attributeBuffer
   * for proper attribute parsing.
   *
   * @param context Selector parser context.
   * @param tokenValue Value of current token.
   *
   * @throws An error if:
   * - no bufferNode;
   * - bufferNode.type is not RegularSelector or AbsolutePseudoClass.
   */
  const updateBufferNode = (context, tokenValue) => {
    const bufferNode = getBufferNode(context);
    if (bufferNode === null) {
      throw new Error('No bufferNode to update');
    }
    if (isAbsolutePseudoClassNode(bufferNode)) {
      bufferNode.value += tokenValue;
    } else if (isRegularSelectorNode(bufferNode)) {
      bufferNode.value += tokenValue;
      if (context.isAttributeBracketsOpen) {
        context.attributeBuffer += tokenValue;
      }
    } else {
      // eslint-disable-next-line max-len
      throw new Error(`${bufferNode.type} node cannot be updated. Only RegularSelector and AbsolutePseudoClass are supported`);
    }
  };

  /**
   * Adds SelectorList node to context.ast at the start of ast collecting.
   *
   * @param context Selector parser context.
   */
  const addSelectorListNode = context => {
    const selectorListNode = new AnySelectorNode(NodeType.SelectorList);
    context.ast = selectorListNode;
    context.pathToBufferNode.push(selectorListNode);
  };

  /**
   * Adds new node to buffer node children.
   * New added node will be considered as buffer node after it.
   *
   * @param context Selector parser context.
   * @param type Type of node to add.
   * @param tokenValue Optional, defaults to `''`, value of processing token.
   *
   * @throws An error if no bufferNode.
   */
  const addAstNodeByType = function (context, type) {
    let tokenValue = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
    const bufferNode = getBufferNode(context);
    if (bufferNode === null) {
      throw new Error('No buffer node');
    }
    let node;
    if (type === NodeType.RegularSelector) {
      node = new RegularSelectorNode(tokenValue);
    } else if (type === NodeType.AbsolutePseudoClass) {
      node = new AbsolutePseudoClassNode(tokenValue);
    } else if (type === NodeType.RelativePseudoClass) {
      node = new RelativePseudoClassNode(tokenValue);
    } else {
      // SelectorList || Selector || ExtendedSelector
      node = new AnySelectorNode(type);
    }
    bufferNode.addChild(node);
    context.pathToBufferNode.push(node);
  };

  /**
   * The very beginning of ast collecting.
   *
   * @param context Selector parser context.
   * @param tokenValue Value of regular selector.
   */
  const initAst = (context, tokenValue) => {
    addSelectorListNode(context);
    addAstNodeByType(context, NodeType.Selector);
    // RegularSelector node is always the first child of Selector node
    addAstNodeByType(context, NodeType.RegularSelector, tokenValue);
  };

  /**
   * Inits selector list subtree for relative extended pseudo-classes, e.g. :has(), :not().
   *
   * @param context Selector parser context.
   * @param tokenValue Optional, defaults to `''`, value of inner regular selector.
   */
  const initRelativeSubtree = function (context) {
    let tokenValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
    addAstNodeByType(context, NodeType.SelectorList);
    addAstNodeByType(context, NodeType.Selector);
    addAstNodeByType(context, NodeType.RegularSelector, tokenValue);
  };

  /**
   * Goes to closest parent specified by type.
   * Actually updates path to buffer node for proper ast collecting of selectors while parsing.
   *
   * @param context Selector parser context.
   * @param parentType Type of needed parent node in ast.
   */
  const upToClosest = (context, parentType) => {
    for (let i = context.pathToBufferNode.length - 1; i >= 0; i -= 1) {
      var _context$pathToBuffer;
      if (((_context$pathToBuffer = context.pathToBufferNode[i]) === null || _context$pathToBuffer === void 0 ? void 0 : _context$pathToBuffer.type) === parentType) {
        context.pathToBufferNode = context.pathToBufferNode.slice(0, i + 1);
        break;
      }
    }
  };

  /**
   * Returns needed buffer node updated due to complex selector parsing.
   *
   * @param context Selector parser context.
   *
   * @returns Ast node for following selector parsing.
   * @throws An error if there is no upper SelectorNode is ast.
   */
  const getUpdatedBufferNode = context => {
    // it may happen during the parsing of selector list
    // which is an argument of relative pseudo-class
    // e.g. '.banner:has(~span, ~p)'
    // parser position is here  ↑
    // so if after the comma the buffer node type is SelectorList and parent type is RelativePseudoClass
    // we should simply return the current buffer node
    const bufferNode = getBufferNode(context);
    if (bufferNode && isSelectorListNode(bufferNode) && isRelativePseudoClassNode(getBufferNodeParent(context))) {
      return bufferNode;
    }
    upToClosest(context, NodeType.Selector);
    const selectorNode = getBufferNode(context);
    if (!selectorNode) {
      throw new Error('No SelectorNode, impossible to continue selector parsing by ExtendedCss');
    }
    const lastSelectorNodeChild = getLast(selectorNode.children);
    const hasExtended = lastSelectorNodeChild && isExtendedSelectorNode(lastSelectorNodeChild)
    // parser position might be inside standard pseudo-class brackets which has space
    // e.g. 'div:contains(/а/):nth-child(100n + 2)'
    && context.standardPseudoBracketsStack.length === 0;
    const supposedPseudoClassNode = hasExtended && getFirst(lastSelectorNodeChild.children);
    let newNeededBufferNode = selectorNode;
    if (supposedPseudoClassNode) {
      // name of pseudo-class for last extended-node child for Selector node
      const lastExtendedPseudoName = hasExtended && supposedPseudoClassNode.name;
      const isLastExtendedNameRelative = lastExtendedPseudoName && isRelativePseudoClass(lastExtendedPseudoName);
      const isLastExtendedNameAbsolute = lastExtendedPseudoName && isAbsolutePseudoClass(lastExtendedPseudoName);
      const hasRelativeExtended = isLastExtendedNameRelative && context.extendedPseudoBracketsStack.length > 0 && context.extendedPseudoBracketsStack.length === context.extendedPseudoNamesStack.length;
      const hasAbsoluteExtended = isLastExtendedNameAbsolute && lastExtendedPseudoName === getLast(context.extendedPseudoNamesStack);
      if (hasRelativeExtended) {
        // return relative selector node to update later
        context.pathToBufferNode.push(lastSelectorNodeChild);
        newNeededBufferNode = supposedPseudoClassNode;
      } else if (hasAbsoluteExtended) {
        // return absolute selector node to update later
        context.pathToBufferNode.push(lastSelectorNodeChild);
        newNeededBufferNode = supposedPseudoClassNode;
      }
    } else if (hasExtended) {
      // return selector node to add new regular selector node later
      newNeededBufferNode = selectorNode;
    } else {
      // otherwise return last regular selector node to update later
      newNeededBufferNode = getContextLastRegularSelectorNode(context);
    }
    // update the path to buffer node properly
    context.pathToBufferNode.push(newNeededBufferNode);
    return newNeededBufferNode;
  };

  /**
   * Checks values of few next tokens on colon token `:` and:
   *  - updates buffer node for following standard pseudo-class;
   *  - adds extended selector ast node for following extended pseudo-class;
   *  - validates some cases of `:remove()` and `:has()` usage.
   *
   * @param context Selector parser context.
   * @param selector Selector.
   * @param tokenValue Value of current token.
   * @param nextTokenValue Value of token next to current one.
   * @param nextToNextTokenValue Value of token next to next to current one.
   *
   * @throws An error on :remove() pseudo-class in selector
   * or :has() inside regular pseudo limitation.
   */
  const handleNextTokenOnColon = (context, selector, tokenValue, nextTokenValue, nextToNextTokenValue) => {
    if (!nextTokenValue) {
      throw new Error(`Invalid colon ':' at the end of selector: '${selector}'`);
    }
    if (!isSupportedPseudoClass(nextTokenValue.toLowerCase())) {
      if (nextTokenValue.toLowerCase() === REMOVE_PSEUDO_MARKER) {
        // :remove() pseudo-class should be handled before
        // as it is not about element selecting but actions with elements
        // e.g. 'body > div:empty:remove()'
        throw new Error(`${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${selector}'`);
      }
      // if following token is not an extended pseudo
      // the colon should be collected to value of RegularSelector
      // e.g. '.entry_text:nth-child(2)'
      updateBufferNode(context, tokenValue);
      // check the token after the pseudo and do balance parentheses later
      // only if it is functional pseudo-class (standard with brackets, e.g. ':lang()').
      // no brackets balance needed for such case,
      // parser position is on first colon after the 'div':
      // e.g. 'div:last-child:has(button.privacy-policy__btn)'
      if (nextToNextTokenValue && nextToNextTokenValue === BRACKETS.PARENTHESES.LEFT
      // no brackets balance needed for parentheses inside attribute value
      // e.g. 'a[href="javascript:void(0)"]'   <-- parser position is on colon `:`
      // before `void`           ↑
      && !context.isAttributeBracketsOpen) {
        context.standardPseudoNamesStack.push(nextTokenValue);
      }
    } else {
      // it is supported extended pseudo-class.
      // Disallow :has() inside the pseudos accepting only compound selectors
      // https://bugs.chromium.org/p/chromium/issues/detail?id=669058#c54 [2]
      if (HAS_PSEUDO_CLASS_MARKERS.includes(nextTokenValue) && context.standardPseudoNamesStack.length > 0) {
        // eslint-disable-next-line max-len
        throw new Error(`Usage of :${nextTokenValue}() pseudo-class is not allowed inside regular pseudo: '${getLast(context.standardPseudoNamesStack)}'`);
      } else {
        // stop RegularSelector value collecting
        upToClosest(context, NodeType.Selector);
        // add ExtendedSelector to Selector children
        addAstNodeByType(context, NodeType.ExtendedSelector);
      }
    }
  };

  // limit applying of wildcard :is() and :not() pseudo-class only to html children
  // e.g. ':is(.page, .main) > .banner' or '*:not(span):not(p)'
  const IS_OR_NOT_PSEUDO_SELECTING_ROOT = `html ${ASTERISK}`;

  /**
   * Checks if there are any ExtendedSelector node in selector list.
   *
   * @param selectorList Ast SelectorList node.
   *
   * @returns True if `selectorList` has any inner ExtendedSelector node.
   */
  const hasExtendedSelector = selectorList => {
    return selectorList.children.some(selectorNode => {
      return selectorNode.children.some(selectorNodeChild => {
        return isExtendedSelectorNode(selectorNodeChild);
      });
    });
  };

  /**
   * Converts selector list of RegularSelector nodes to string.
   *
   * @param selectorList Ast SelectorList node.
   *
   * @returns String representation for selector list of regular selectors.
   */
  const selectorListOfRegularsToString = selectorList => {
    // if there is no ExtendedSelector in relative SelectorList
    // it means that each Selector node has single child — RegularSelector node
    // and their values should be combined to string
    const standardCssSelectors = selectorList.children.map(selectorNode => {
      const selectorOnlyChild = getNodeOnlyChild(selectorNode, 'Ast Selector node should have RegularSelector node');
      return getNodeValue(selectorOnlyChild);
    });
    return standardCssSelectors.join(`${COMMA}${SPACE}`);
  };

  /**
   * Updates children of `node` replacing them with `newChildren`.
   * Important: modifies input `node` which is passed by reference.
   *
   * @param node Ast node to update.
   * @param newChildren Array of new children for ast node.
   *
   * @returns Updated ast node.
   */
  const updateNodeChildren = (node, newChildren) => {
    node.children = newChildren;
    return node;
  };

  /**
   * Recursively checks whether the ExtendedSelector node should be optimized.
   * It has to be recursive because RelativePseudoClass has inner SelectorList node.
   *
   * @param currExtendedSelectorNode Ast ExtendedSelector node.
   *
   * @returns True is ExtendedSelector should be optimized.
   */
  const shouldOptimizeExtendedSelector = currExtendedSelectorNode => {
    if (currExtendedSelectorNode === null) {
      return false;
    }
    const extendedPseudoClassNode = getPseudoClassNode(currExtendedSelectorNode);
    const pseudoName = getNodeName(extendedPseudoClassNode);
    if (isAbsolutePseudoClass(pseudoName)) {
      return false;
    }
    const relativeSelectorList = getRelativeSelectorListNode(extendedPseudoClassNode);
    const innerSelectorNodes = relativeSelectorList.children;
    // simple checking for standard selectors in arg of :not() or :is() pseudo-class
    // e.g. 'div > *:is(div, a, span)'
    if (isOptimizationPseudoClass(pseudoName)) {
      const areAllSelectorNodeChildrenRegular = innerSelectorNodes.every(selectorNode => {
        try {
          const selectorOnlyChild = getNodeOnlyChild(selectorNode, 'Selector node should have RegularSelector');
          // it means that the only child is RegularSelector and it can be optimized
          return isRegularSelectorNode(selectorOnlyChild);
        } catch (e) {
          return false;
        }
      });
      if (areAllSelectorNodeChildrenRegular) {
        return true;
      }
    }
    // for other extended pseudo-classes than :not() and :is()
    return innerSelectorNodes.some(selectorNode => {
      return selectorNode.children.some(selectorNodeChild => {
        if (!isExtendedSelectorNode(selectorNodeChild)) {
          return false;
        }
        // check inner ExtendedSelector recursively
        // e.g. 'div:has(*:not(.header))'
        return shouldOptimizeExtendedSelector(selectorNodeChild);
      });
    });
  };

  /**
   * Returns optimized ExtendedSelector node if it can be optimized
   * or null if ExtendedSelector is fully optimized while function execution
   * which means that value of `prevRegularSelectorNode` is updated.
   *
   * @param currExtendedSelectorNode Current ExtendedSelector node to optimize.
   * @param prevRegularSelectorNode Previous RegularSelector node.
   *
   * @returns Ast node or null.
   */
  const getOptimizedExtendedSelector = (currExtendedSelectorNode, prevRegularSelectorNode) => {
    if (!currExtendedSelectorNode) {
      return null;
    }
    const extendedPseudoClassNode = getPseudoClassNode(currExtendedSelectorNode);
    const relativeSelectorList = getRelativeSelectorListNode(extendedPseudoClassNode);
    const hasInnerExtendedSelector = hasExtendedSelector(relativeSelectorList);
    if (!hasInnerExtendedSelector) {
      // if there is no extended selectors for :not() or :is()
      // e.g. 'div:not(.content, .main)'
      const relativeSelectorListStr = selectorListOfRegularsToString(relativeSelectorList);
      const pseudoName = getNodeName(extendedPseudoClassNode);
      // eslint-disable-next-line max-len
      const optimizedExtendedStr = `${COLON}${pseudoName}${BRACKETS.PARENTHESES.LEFT}${relativeSelectorListStr}${BRACKETS.PARENTHESES.RIGHT}`;
      prevRegularSelectorNode.value = `${getNodeValue(prevRegularSelectorNode)}${optimizedExtendedStr}`;
      return null;
    }

    // eslint-disable-next-line @typescript-eslint/no-use-before-define
    const optimizedRelativeSelectorList = optimizeSelectorListNode(relativeSelectorList);
    const optimizedExtendedPseudoClassNode = updateNodeChildren(extendedPseudoClassNode, [optimizedRelativeSelectorList]);
    return updateNodeChildren(currExtendedSelectorNode, [optimizedExtendedPseudoClassNode]);
  };

  /**
   * Combines values of `previous` and `current` RegularSelector nodes.
   * It may happen during the optimization when ExtendedSelector between RegularSelector node was optimized.
   *
   * @param current Current RegularSelector node.
   * @param previous Previous RegularSelector node.
   */
  const optimizeCurrentRegularSelector = (current, previous) => {
    previous.value = `${getNodeValue(previous)}${SPACE}${getNodeValue(current)}`;
  };

  /**
   * Optimizes ast Selector node.
   *
   * @param selectorNode Ast Selector node.
   *
   * @returns Optimized ast node.
   * @throws An error while collecting optimized nodes.
   */
  const optimizeSelectorNode = selectorNode => {
    // non-optimized list of SelectorNode children
    const rawSelectorNodeChildren = selectorNode.children;
    // for collecting optimized children list
    const optimizedChildrenList = [];
    let currentIndex = 0;
    // iterate through all children in non-optimized ast Selector node
    while (currentIndex < rawSelectorNodeChildren.length) {
      const currentChild = getItemByIndex(rawSelectorNodeChildren, currentIndex, 'currentChild should be specified');
      // no need to optimize the very first child which is always RegularSelector node
      if (currentIndex === 0) {
        optimizedChildrenList.push(currentChild);
      } else {
        const prevRegularChild = getLastRegularChild(optimizedChildrenList);
        if (isExtendedSelectorNode(currentChild)) {
          // start checking with point is null
          let optimizedExtendedSelector = null;
          // check whether the optimization is needed
          let isOptimizationNeeded = shouldOptimizeExtendedSelector(currentChild);
          // update optimizedExtendedSelector so it can be optimized recursively
          // i.e. `getOptimizedExtendedSelector(optimizedExtendedSelector)` below
          optimizedExtendedSelector = currentChild;
          while (isOptimizationNeeded) {
            // recursively optimize ExtendedSelector until no optimization needed
            // e.g. div > *:is(.banner:not(.block))
            optimizedExtendedSelector = getOptimizedExtendedSelector(optimizedExtendedSelector, prevRegularChild);
            isOptimizationNeeded = shouldOptimizeExtendedSelector(optimizedExtendedSelector);
          }
          // if it was simple :not() of :is() with standard selector arg
          // e.g. 'div:not([class][id])'
          // or   '.main > *:is([data-loaded], .banner)'
          // after the optimization the ExtendedSelector node become part of RegularSelector
          // so nothing to save eventually
          // otherwise the optimized ExtendedSelector should be saved
          // e.g. 'div:has(:not([class]))'
          if (optimizedExtendedSelector !== null) {
            optimizedChildrenList.push(optimizedExtendedSelector);
            // if optimization is not needed
            const optimizedPseudoClass = getPseudoClassNode(optimizedExtendedSelector);
            const optimizedPseudoName = getNodeName(optimizedPseudoClass);
            // parent element checking is used to apply :is() and :not() pseudo-classes as extended.
            // as there is no parentNode for root element (html)
            // so element selection should be limited to it's children
            // e.g. '*:is(:has(.page))' -> 'html *:is(has(.page))'
            // or   '*:not(:has(span))' -> 'html *:not(:has(span))'
            if (getNodeValue(prevRegularChild) === ASTERISK && isOptimizationPseudoClass(optimizedPseudoName)) {
              prevRegularChild.value = IS_OR_NOT_PSEUDO_SELECTING_ROOT;
            }
          }
        } else if (isRegularSelectorNode(currentChild)) {
          // in non-optimized ast, RegularSelector node may follow ExtendedSelector which should be optimized
          // for example, for 'div:not(.content) > .banner' schematically it looks like
          // non-optimized ast: [
          //   1. RegularSelector: 'div'
          //   2. ExtendedSelector: 'not(.content)'
          //   3. RegularSelector: '> .banner'
          // ]
          // which after the ExtendedSelector looks like
          // partly optimized ast: [
          //   1. RegularSelector: 'div:not(.content)'
          //   2. RegularSelector: '> .banner'
          // ]
          // so second RegularSelector value should be combined with first one
          // optimized ast: [
          //   1. RegularSelector: 'div:not(.content) > .banner'
          // ]
          // here we check **children of selectorNode** after previous optimization if it was
          const lastOptimizedChild = getLast(optimizedChildrenList) || null;
          if (isRegularSelectorNode(lastOptimizedChild)) {
            optimizeCurrentRegularSelector(currentChild, prevRegularChild);
          }
        }
      }
      currentIndex += 1;
    }
    return updateNodeChildren(selectorNode, optimizedChildrenList);
  };

  /**
   * Optimizes ast SelectorList node.
   *
   * @param selectorListNode SelectorList node.
   *
   * @returns Optimized ast node.
   */
  const optimizeSelectorListNode = selectorListNode => {
    return updateNodeChildren(selectorListNode, selectorListNode.children.map(s => optimizeSelectorNode(s)));
  };

  /**
   * Optimizes ast:
   * If arg of :not() and :is() pseudo-classes does not contain extended selectors,
   * native Document.querySelectorAll() can be used to query elements.
   * It means that ExtendedSelector ast nodes can be removed
   * and value of relevant RegularSelector node should be updated accordingly.
   *
   * @param ast Non-optimized ast.
   *
   * @returns Optimized ast.
   */
  const optimizeAst = ast => {
    // ast is basically the selector list of selectors
    return optimizeSelectorListNode(ast);
  };

  // limit applying of :xpath() pseudo-class to 'any' element
  // https://github.com/AdguardTeam/ExtendedCss/issues/115
  const XPATH_PSEUDO_SELECTING_ROOT = 'body';
  const NO_WHITESPACE_ERROR_PREFIX = 'No white space is allowed before or after extended pseudo-class name in selector';

  /**
   * Parses selector into ast for following element selection.
   *
   * @param selector Selector to parse.
   *
   * @returns Parsed ast.
   * @throws An error on invalid selector.
   */
  const parse$1 = selector => {
    const tokens = tokenizeSelector(selector);
    const context = {
      ast: null,
      pathToBufferNode: [],
      extendedPseudoNamesStack: [],
      extendedPseudoBracketsStack: [],
      standardPseudoNamesStack: [],
      standardPseudoBracketsStack: [],
      isAttributeBracketsOpen: false,
      attributeBuffer: '',
      isRegexpOpen: false,
      shouldOptimize: false
    };
    let i = 0;
    while (i < tokens.length) {
      const token = tokens[i];
      if (!token) {
        break;
      }
      // Token to process
      const {
        type: tokenType,
        value: tokenValue
      } = token;

      // needed for SPACE and COLON tokens checking
      const nextToken = tokens[i + 1];
      const nextTokenType = nextToken === null || nextToken === void 0 ? void 0 : nextToken.type;
      const nextTokenValue = nextToken === null || nextToken === void 0 ? void 0 : nextToken.value;

      // needed for limitations
      // - :not() and :is() root element
      // - :has() usage
      // - white space before and after pseudo-class name
      const nextToNextToken = tokens[i + 2];
      const nextToNextTokenValue = nextToNextToken === null || nextToNextToken === void 0 ? void 0 : nextToNextToken.value;

      // needed for COLON token checking for none-specified regular selector before extended one
      // e.g. 'p, :hover'
      // or   '.banner, :contains(ads)'
      const previousToken = tokens[i - 1];
      const prevTokenType = previousToken === null || previousToken === void 0 ? void 0 : previousToken.type;
      const prevTokenValue = previousToken === null || previousToken === void 0 ? void 0 : previousToken.value;

      // needed for proper parsing of regexp pattern arg
      // e.g. ':matches-css(background-image: /^url\(https:\/\/example\.org\//)'
      const previousToPreviousToken = tokens[i - 2];
      const prevToPrevTokenValue = previousToPreviousToken === null || previousToPreviousToken === void 0 ? void 0 : previousToPreviousToken.value;
      let bufferNode = getBufferNode(context);
      switch (tokenType) {
        case TokenType.Word:
          if (bufferNode === null) {
            // there is no buffer node only in one case — no ast collecting has been started
            initAst(context, tokenValue);
          } else if (isSelectorListNode(bufferNode)) {
            // add new selector to selector list
            addAstNodeByType(context, NodeType.Selector);
            addAstNodeByType(context, NodeType.RegularSelector, tokenValue);
          } else if (isRegularSelectorNode(bufferNode)) {
            updateBufferNode(context, tokenValue);
          } else if (isExtendedSelectorNode(bufferNode)) {
            // No white space is allowed between the name of extended pseudo-class
            // and its opening parenthesis
            // https://www.w3.org/TR/selectors-4/#pseudo-classes
            // e.g. 'span:contains (text)'
            if (isWhiteSpaceChar(nextTokenValue) && nextToNextTokenValue === BRACKETS.PARENTHESES.LEFT) {
              throw new Error(`${NO_WHITESPACE_ERROR_PREFIX}: '${selector}'`);
            }
            const lowerCaseTokenValue = tokenValue.toLowerCase();
            // save pseudo-class name for brackets balance checking
            context.extendedPseudoNamesStack.push(lowerCaseTokenValue);
            // extended pseudo-class name are parsed in lower case
            // as they should be case-insensitive
            // https://www.w3.org/TR/selectors-4/#pseudo-classes
            if (isAbsolutePseudoClass(lowerCaseTokenValue)) {
              addAstNodeByType(context, NodeType.AbsolutePseudoClass, lowerCaseTokenValue);
            } else {
              // if it is not absolute pseudo-class, it must be relative one
              // add RelativePseudoClass with tokenValue as pseudo-class name to ExtendedSelector children
              addAstNodeByType(context, NodeType.RelativePseudoClass, lowerCaseTokenValue);
              // for :not() and :is() pseudo-classes parsed ast should be optimized later
              if (isOptimizationPseudoClass(lowerCaseTokenValue)) {
                context.shouldOptimize = true;
              }
            }
          } else if (isAbsolutePseudoClassNode(bufferNode)) {
            // collect absolute pseudo-class arg
            updateBufferNode(context, tokenValue);
          } else if (isRelativePseudoClassNode(bufferNode)) {
            initRelativeSubtree(context, tokenValue);
          }
          break;
        case TokenType.Mark:
          switch (tokenValue) {
            case COMMA:
              if (!bufferNode || typeof bufferNode !== 'undefined' && !nextTokenValue) {
                // consider the selector is invalid if there is no bufferNode yet (e.g. ', a')
                // or there is nothing after the comma while bufferNode is defined (e.g. 'div, ')
                throw new Error(`'${selector}' is not a valid selector`);
              } else if (isRegularSelectorNode(bufferNode)) {
                if (context.isAttributeBracketsOpen) {
                  // the comma might be inside element attribute value
                  // e.g. 'div[data-comma="0,1"]'
                  updateBufferNode(context, tokenValue);
                } else {
                  // new Selector should be collected to upper SelectorList
                  upToClosest(context, NodeType.SelectorList);
                }
              } else if (isAbsolutePseudoClassNode(bufferNode)) {
                // the comma inside arg of absolute extended pseudo
                // e.g. 'div:xpath(//h3[contains(text(),"Share it!")]/..)'
                updateBufferNode(context, tokenValue);
              } else if (isSelectorNode(bufferNode)) {
                // new Selector should be collected to upper SelectorList
                // if parser position is on Selector node
                upToClosest(context, NodeType.SelectorList);
              }
              break;
            case SPACE:
              // it might be complex selector with extended pseudo-class inside it
              // and the space is between that complex selector and following regular selector
              // parser position is on ` ` before `span` now:
              // e.g. 'div:has(img).banner span'
              // so we need to check whether the new ast node should be added (example above)
              // or previous regular selector node should be updated
              if (isRegularSelectorNode(bufferNode)
              // no need to update the buffer node if attribute value is being parsed
              // e.g. 'div:not([id])[style="position: absolute; z-index: 10000;"]'
              // parser position inside attribute    ↑
              && !context.isAttributeBracketsOpen) {
                bufferNode = getUpdatedBufferNode(context);
              }
              if (isRegularSelectorNode(bufferNode)) {
                // standard selectors with white space between colon and name of pseudo
                // are invalid for native document.querySelectorAll() anyway,
                // so throwing the error here is better
                // than proper parsing of invalid selector and passing it further.
                // first of all do not check attributes
                // e.g. div[style="text-align: center"]
                if (!context.isAttributeBracketsOpen
                // check the space after the colon and before the pseudo
                // e.g. '.block: nth-child(2)
                && (prevTokenValue === COLON && nextTokenType === TokenType.Word
                // or after the pseudo and before the opening parenthesis
                // e.g. '.block:nth-child (2)
                || prevTokenType === TokenType.Word && nextTokenValue === BRACKETS.PARENTHESES.LEFT)) {
                  throw new Error(`'${selector}' is not a valid selector`);
                }
                // collect current tokenValue to value of RegularSelector
                // if it is the last token or standard selector continues after the space.
                // otherwise it will be skipped
                if (!nextTokenValue || doesRegularContinueAfterSpace(nextTokenType, nextTokenValue)
                // we also should collect space inside attribute value
                // e.g. `[onclick^="window.open ('https://example.com/share?url="]`
                // parser position             ↑
                || context.isAttributeBracketsOpen) {
                  updateBufferNode(context, tokenValue);
                }
              }
              if (isAbsolutePseudoClassNode(bufferNode)) {
                // space inside extended pseudo-class arg
                // e.g. 'span:contains(some text)'
                updateBufferNode(context, tokenValue);
              }
              if (isRelativePseudoClassNode(bufferNode)) {
                // init with empty value RegularSelector
                // as the space is not needed for selector value
                // e.g. 'p:not( .content )'
                initRelativeSubtree(context);
              }
              if (isSelectorNode(bufferNode)) {
                // do NOT add RegularSelector if parser position on space BEFORE the comma in selector list
                // e.g. '.block:has(> img) , .banner)'
                if (doesRegularContinueAfterSpace(nextTokenType, nextTokenValue)) {
                  // regular selector might be after the extended one.
                  // extra space before combinator or selector should not be collected
                  // e.g. '.banner:upward(2) .block'
                  //      '.banner:upward(2) > .block'
                  // so no tokenValue passed to addAnySelectorNode()
                  addAstNodeByType(context, NodeType.RegularSelector);
                }
              }
              break;
            case DESCENDANT_COMBINATOR:
            case CHILD_COMBINATOR:
            case NEXT_SIBLING_COMBINATOR:
            case SUBSEQUENT_SIBLING_COMBINATOR:
            case SEMICOLON:
            case SLASH:
            case BACKSLASH:
            case SINGLE_QUOTE:
            case DOUBLE_QUOTE:
            case CARET:
            case DOLLAR_SIGN:
            case BRACKETS.CURLY.LEFT:
            case BRACKETS.CURLY.RIGHT:
            case ASTERISK:
            case ID_MARKER:
            case CLASS_MARKER:
            case BRACKETS.SQUARE.LEFT:
              // it might be complex selector with extended pseudo-class inside it
              // and the space is between that complex selector and following regular selector
              // e.g. 'div:has(img).banner'   // parser position is on `.` before `banner` now
              //      'div:has(img)[attr]'    // parser position is on `[` before `attr` now
              // so we need to check whether the new ast node should be added (example above)
              // or previous regular selector node should be updated
              if (COMBINATORS.includes(tokenValue)) {
                if (bufferNode === null) {
                  // cases where combinator at very beginning of a selector
                  // e.g. '> div'
                  // or   '~ .banner'
                  // or even '+js(overlay-buster)' which not a selector at all
                  // but may be validated by FilterCompiler so error message should be appropriate
                  throw new Error(`'${selector}' is not a valid selector`);
                }
                bufferNode = getUpdatedBufferNode(context);
              }
              if (bufferNode === null) {
                // no ast collecting has been started
                // e.g. '.banner > p'
                // or   '#top > div.ad'
                // or   '[class][style][attr]'
                // or   '*:not(span)'
                initAst(context, tokenValue);
                if (isAttributeOpening(tokenValue, prevTokenValue)) {
                  // e.g. '[class^="banner-"]'
                  context.isAttributeBracketsOpen = true;
                }
              } else if (isRegularSelectorNode(bufferNode)) {
                // collect the mark to the value of RegularSelector node
                updateBufferNode(context, tokenValue);
                if (isAttributeOpening(tokenValue, prevTokenValue)) {
                  // needed for proper handling element attribute value with comma
                  // e.g. 'div[data-comma="0,1"]'
                  context.isAttributeBracketsOpen = true;
                }
              } else if (isAbsolutePseudoClassNode(bufferNode)) {
                // collect the mark to the arg of AbsolutePseudoClass node
                updateBufferNode(context, tokenValue);
                // 'isRegexpOpen' flag is needed for brackets balancing inside extended pseudo-class arg
                if (tokenValue === SLASH && context.extendedPseudoNamesStack.length > 0) {
                  if (prevTokenValue === SLASH && prevToPrevTokenValue === BACKSLASH) {
                    // it may be specific url regexp pattern in arg of pseudo-class
                    // e.g. ':matches-css(background-image: /^url\(https:\/\/example\.org\//)'
                    // parser position is on final slash before `)`                        ↑
                    context.isRegexpOpen = false;
                  } else if (prevTokenValue && prevTokenValue !== BACKSLASH) {
                    if (isRegexpOpening(context, prevTokenValue, getNodeValue(bufferNode))) {
                      context.isRegexpOpen = !context.isRegexpOpen;
                    } else {
                      // otherwise force `isRegexpOpen` flag to `false`
                      context.isRegexpOpen = false;
                    }
                  }
                }
              } else if (isRelativePseudoClassNode(bufferNode)) {
                // add SelectorList to children of RelativePseudoClass node
                initRelativeSubtree(context, tokenValue);
                if (isAttributeOpening(tokenValue, prevTokenValue)) {
                  // besides of creating the relative subtree
                  // opening square bracket means start of attribute
                  // e.g. 'div:not([class="content"])'
                  //      'div:not([href*="window.print()"])'
                  context.isAttributeBracketsOpen = true;
                }
              } else if (isSelectorNode(bufferNode)) {
                // after the extended pseudo closing parentheses
                // parser position is on Selector node
                // and regular selector can be after the extended one
                // e.g. '.banner:upward(2)> .block'
                // or   '.inner:nth-ancestor(1)~ .banner'
                if (COMBINATORS.includes(tokenValue)) {
                  addAstNodeByType(context, NodeType.RegularSelector, tokenValue);
                } else if (!context.isRegexpOpen) {
                  // it might be complex selector with extended pseudo-class inside it.
                  // parser position is on `.` now:
                  // e.g. 'div:has(img).banner'
                  // so we need to get last regular selector node and update its value
                  bufferNode = getContextLastRegularSelectorNode(context);
                  updateBufferNode(context, tokenValue);
                  if (isAttributeOpening(tokenValue, prevTokenValue)) {
                    // handle attribute in compound selector after extended pseudo-class
                    // e.g. 'div:not(.top)[style="z-index: 10000;"]'
                    // parser position    ↑
                    context.isAttributeBracketsOpen = true;
                  }
                }
              } else if (isSelectorListNode(bufferNode)) {
                // add Selector to SelectorList
                addAstNodeByType(context, NodeType.Selector);
                // and RegularSelector as it is always the first child of Selector
                addAstNodeByType(context, NodeType.RegularSelector, tokenValue);
                if (isAttributeOpening(tokenValue, prevTokenValue)) {
                  // handle simple attribute selector in selector list
                  // e.g. '.banner, [class^="ad-"]'
                  context.isAttributeBracketsOpen = true;
                }
              }
              break;
            case BRACKETS.SQUARE.RIGHT:
              if (isRegularSelectorNode(bufferNode)) {
                // unescaped `]` in regular selector allowed only inside attribute value
                if (!context.isAttributeBracketsOpen && prevTokenValue !== BACKSLASH) {
                  // e.g. 'div]'
                  // eslint-disable-next-line max-len
                  throw new Error(`'${selector}' is not a valid selector due to '${tokenValue}' after '${getNodeValue(bufferNode)}'`);
                }
                // needed for proper parsing regular selectors after the attributes with comma
                // e.g. 'div[data-comma="0,1"] > img'
                if (isAttributeClosing(context)) {
                  context.isAttributeBracketsOpen = false;
                  // reset attribute buffer on closing `]`
                  context.attributeBuffer = '';
                }
                // collect the bracket to the value of RegularSelector node
                updateBufferNode(context, tokenValue);
              }
              if (isAbsolutePseudoClassNode(bufferNode)) {
                // :xpath() expended pseudo-class arg might contain square bracket
                // so it should be collected
                // e.g. 'div:xpath(//h3[contains(text(),"Share it!")]/..)'
                updateBufferNode(context, tokenValue);
              }
              break;
            case COLON:
              // No white space is allowed between the colon and the following name of the pseudo-class
              // https://www.w3.org/TR/selectors-4/#pseudo-classes
              // e.g. 'span: contains(text)'
              if (isWhiteSpaceChar(nextTokenValue) && nextToNextTokenValue && SUPPORTED_PSEUDO_CLASSES.includes(nextToNextTokenValue)) {
                throw new Error(`${NO_WHITESPACE_ERROR_PREFIX}: '${selector}'`);
              }
              if (bufferNode === null) {
                // no ast collecting has been started
                if (nextTokenValue === XPATH_PSEUDO_CLASS_MARKER) {
                  // limit applying of "naked" :xpath pseudo-class
                  // https://github.com/AdguardTeam/ExtendedCss/issues/115
                  initAst(context, XPATH_PSEUDO_SELECTING_ROOT);
                } else if (nextTokenValue === UPWARD_PSEUDO_CLASS_MARKER || nextTokenValue === NTH_ANCESTOR_PSEUDO_CLASS_MARKER) {
                  // selector should be specified before :nth-ancestor() or :upward()
                  // e.g. ':nth-ancestor(3)'
                  // or   ':upward(span)'
                  throw new Error(`${NO_SELECTOR_ERROR_PREFIX} :${nextTokenValue}() pseudo-class`);
                } else {
                  // make it more obvious if selector starts with pseudo with no tag specified
                  // e.g. ':has(a)' -> '*:has(a)'
                  // or   ':empty'  -> '*:empty'
                  initAst(context, ASTERISK);
                }

                // bufferNode should be updated for following checking
                bufferNode = getBufferNode(context);
              }
              if (isSelectorListNode(bufferNode)) {
                // bufferNode is SelectorList after comma has been parsed.
                // parser position is on colon now:
                // e.g. 'img,:not(.content)'
                addAstNodeByType(context, NodeType.Selector);
                // add empty value RegularSelector anyway as any selector should start with it
                // and check previous token on the next step
                addAstNodeByType(context, NodeType.RegularSelector);
                // bufferNode should be updated for following checking
                bufferNode = getBufferNode(context);
              }
              if (isRegularSelectorNode(bufferNode)) {
                // it can be extended or standard pseudo
                // e.g. '#share, :contains(share it)'
                // or   'div,:hover'
                // of   'div:has(+:contains(text))'  // position is after '+'
                if (prevTokenValue && COMBINATORS.includes(prevTokenValue) || prevTokenValue === COMMA) {
                  // case with colon at the start of string - e.g. ':contains(text)'
                  // is covered by 'bufferNode === null' above at start of COLON checking
                  updateBufferNode(context, ASTERISK);
                }
                handleNextTokenOnColon(context, selector, tokenValue, nextTokenValue, nextToNextTokenValue);
              }
              if (isSelectorNode(bufferNode)) {
                // e.g. 'div:contains(text):'
                if (!nextTokenValue) {
                  throw new Error(`Invalid colon ':' at the end of selector: '${selector}'`);
                }
                // after the extended pseudo closing parentheses
                // parser position is on Selector node
                // and there is might be another extended selector.
                // parser position is on colon before 'upward':
                // e.g. 'p:contains(PR):upward(2)'
                if (isSupportedPseudoClass(nextTokenValue.toLowerCase())) {
                  // if supported extended pseudo-class is next to colon
                  // add ExtendedSelector to Selector children
                  addAstNodeByType(context, NodeType.ExtendedSelector);
                } else if (nextTokenValue.toLowerCase() === REMOVE_PSEUDO_MARKER) {
                  // :remove() pseudo-class should be handled before
                  // as it is not about element selecting but actions with elements
                  // e.g. '#banner:upward(2):remove()'
                  throw new Error(`${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${selector}'`);
                } else {
                  // otherwise it is standard pseudo after extended pseudo-class in complex selector
                  // and colon should be collected to value of previous RegularSelector
                  // e.g. 'body *:not(input)::selection'
                  //      'input:matches-css(padding: 10):checked'
                  bufferNode = getContextLastRegularSelectorNode(context);
                  handleNextTokenOnColon(context, selector, tokenValue, nextTokenType, nextToNextTokenValue);
                }
              }
              if (isAbsolutePseudoClassNode(bufferNode)) {
                // :xpath() pseudo-class should be the last of extended pseudo-classes
                if (getNodeName(bufferNode) === XPATH_PSEUDO_CLASS_MARKER && nextTokenValue && SUPPORTED_PSEUDO_CLASSES.includes(nextTokenValue) && nextToNextTokenValue === BRACKETS.PARENTHESES.LEFT) {
                  throw new Error(`:xpath() pseudo-class should be the last in selector: '${selector}'`);
                }
                // collecting arg for absolute pseudo-class
                // e.g. 'div:matches-css(width:400px)'
                updateBufferNode(context, tokenValue);
              }
              if (isRelativePseudoClassNode(bufferNode)) {
                if (!nextTokenValue) {
                  // e.g. 'div:has(:'
                  throw new Error(`Invalid pseudo-class arg at the end of selector: '${selector}'`);
                }
                // make it more obvious if selector starts with pseudo with no tag specified
                // parser position is on colon inside :has() arg
                // e.g. 'div:has(:contains(text))'
                // or   'div:not(:empty)'
                initRelativeSubtree(context, ASTERISK);
                if (!isSupportedPseudoClass(nextTokenValue.toLowerCase())) {
                  // collect the colon to value of RegularSelector
                  // e.g. 'div:not(:empty)'
                  updateBufferNode(context, tokenValue);
                  // parentheses should be balanced only for functional pseudo-classes
                  // e.g. '.yellow:not(:nth-child(3))'
                  if (nextToNextTokenValue === BRACKETS.PARENTHESES.LEFT) {
                    context.standardPseudoNamesStack.push(nextTokenValue);
                  }
                } else {
                  // add ExtendedSelector to Selector children
                  // e.g. 'div:has(:contains(text))'
                  upToClosest(context, NodeType.Selector);
                  addAstNodeByType(context, NodeType.ExtendedSelector);
                }
              }
              break;
            case BRACKETS.PARENTHESES.LEFT:
              // start of pseudo-class arg
              if (isAbsolutePseudoClassNode(bufferNode)) {
                // no brackets balancing needed inside
                // 1. :xpath() extended pseudo-class arg
                // 2. regexp arg for other extended pseudo-classes
                if (getNodeName(bufferNode) !== XPATH_PSEUDO_CLASS_MARKER && context.isRegexpOpen) {
                  // if the parentheses is escaped it should be part of regexp
                  // collect it to arg of AbsolutePseudoClass
                  // e.g. 'div:matches-css(background-image: /^url\\("data:image\\/gif;base64.+/)'
                  updateBufferNode(context, tokenValue);
                } else {
                  // otherwise brackets should be balanced
                  // e.g. 'div:xpath(//h3[contains(text(),"Share it!")]/..)'
                  context.extendedPseudoBracketsStack.push(tokenValue);
                  // eslint-disable-next-line max-len
                  if (context.extendedPseudoBracketsStack.length > context.extendedPseudoNamesStack.length) {
                    updateBufferNode(context, tokenValue);
                  }
                }
              }
              if (isRegularSelectorNode(bufferNode)) {
                // continue RegularSelector value collecting for standard pseudo-classes
                // e.g. '.banner:where(div)'
                if (context.standardPseudoNamesStack.length > 0) {
                  updateBufferNode(context, tokenValue);
                  context.standardPseudoBracketsStack.push(tokenValue);
                }
                // parentheses inside attribute value should be part of RegularSelector value
                // e.g. 'div:not([href*="window.print()"])'   <-- parser position
                // is on the `(` after `print`       ↑
                if (context.isAttributeBracketsOpen) {
                  updateBufferNode(context, tokenValue);
                }
              }
              if (isRelativePseudoClassNode(bufferNode)) {
                // save opening bracket for balancing
                // e.g. 'div:not()'  // position is on `(`
                context.extendedPseudoBracketsStack.push(tokenValue);
              }
              break;
            case BRACKETS.PARENTHESES.RIGHT:
              if (isAbsolutePseudoClassNode(bufferNode)) {
                // no brackets balancing needed inside
                // 1. :xpath() extended pseudo-class arg
                // 2. regexp arg for other extended pseudo-classes
                if (getNodeName(bufferNode) !== XPATH_PSEUDO_CLASS_MARKER && context.isRegexpOpen) {
                  // if closing bracket is part of regexp
                  // simply save it to pseudo-class arg
                  updateBufferNode(context, tokenValue);
                } else {
                  // remove stacked open parentheses for brackets balance
                  // e.g. 'h3:contains((Ads))'
                  // or   'div:xpath(//h3[contains(text(),"Share it!")]/..)'
                  context.extendedPseudoBracketsStack.pop();
                  if (getNodeName(bufferNode) !== XPATH_PSEUDO_CLASS_MARKER) {
                    // for all other absolute pseudo-classes except :xpath()
                    // remove stacked name of extended pseudo-class
                    context.extendedPseudoNamesStack.pop();
                    // eslint-disable-next-line max-len
                    if (context.extendedPseudoBracketsStack.length > context.extendedPseudoNamesStack.length) {
                      // if brackets stack is not empty yet,
                      // save tokenValue to arg of AbsolutePseudoClass
                      // parser position on first closing bracket after 'Ads':
                      // e.g. 'h3:contains((Ads))'
                      updateBufferNode(context, tokenValue);
                    } else if (context.extendedPseudoBracketsStack.length >= 0 && context.extendedPseudoNamesStack.length >= 0) {
                      // assume it is combined extended pseudo-classes
                      // parser position on first closing bracket after 'advert':
                      // e.g. 'div:has(.banner, :contains(advert))'
                      upToClosest(context, NodeType.Selector);
                    }
                  } else {
                    // for :xpath()
                    // eslint-disable-next-line max-len
                    if (context.extendedPseudoBracketsStack.length < context.extendedPseudoNamesStack.length) {
                      // remove stacked name of extended pseudo-class
                      // if there are less brackets than pseudo-class names
                      // with means last removes bracket was closing for pseudo-class
                      context.extendedPseudoNamesStack.pop();
                    } else {
                      // otherwise the bracket is part of arg
                      updateBufferNode(context, tokenValue);
                    }
                  }
                }
              }
              if (isRegularSelectorNode(bufferNode)) {
                if (context.isAttributeBracketsOpen) {
                  // parentheses inside attribute value should be part of RegularSelector value
                  // e.g. 'div:not([href*="window.print()"])'   <-- parser position
                  // is on the `)` after `print(`       ↑
                  updateBufferNode(context, tokenValue);
                } else if (context.standardPseudoNamesStack.length > 0 && context.standardPseudoBracketsStack.length > 0) {
                  // standard pseudo-class was processing.
                  // collect the closing bracket to value of RegularSelector
                  // parser position is on bracket after 'class' now:
                  // e.g. 'div:where(.class)'
                  updateBufferNode(context, tokenValue);
                  // remove bracket and pseudo name from stacks
                  context.standardPseudoBracketsStack.pop();
                  const lastStandardPseudo = context.standardPseudoNamesStack.pop();
                  if (!lastStandardPseudo) {
                    // standard pseudo should be in standardPseudoNamesStack
                    // as related to standardPseudoBracketsStack
                    throw new Error(`Parsing error. Invalid selector: ${selector}`);
                  }
                  // Disallow :has() after regular pseudo-elements
                  // https://bugs.chromium.org/p/chromium/issues/detail?id=669058#c54 [3]
                  if (Object.values(REGULAR_PSEUDO_ELEMENTS).includes(lastStandardPseudo)
                  // check token which is next to closing parentheses and token after it
                  // parser position is on bracket after 'foo' now:
                  // e.g. '::part(foo):has(.a)'
                  && nextTokenValue === COLON && nextToNextTokenValue && HAS_PSEUDO_CLASS_MARKERS.includes(nextToNextTokenValue)) {
                    // eslint-disable-next-line max-len
                    throw new Error(`Usage of :${nextToNextTokenValue}() pseudo-class is not allowed after any regular pseudo-element: '${lastStandardPseudo}'`);
                  }
                } else {
                  // extended pseudo-class was processing.
                  // e.g. 'div:has(h3)'
                  // remove bracket and pseudo name from stacks
                  context.extendedPseudoBracketsStack.pop();
                  context.extendedPseudoNamesStack.pop();
                  upToClosest(context, NodeType.ExtendedSelector);
                  // go to upper selector for possible selector continuation after extended pseudo-class
                  // e.g. 'div:has(h3) > img'
                  upToClosest(context, NodeType.Selector);
                }
              }
              if (isSelectorNode(bufferNode)) {
                // after inner extended pseudo-class bufferNode is Selector.
                // parser position is on last bracket now:
                // e.g. 'div:has(.banner, :contains(ads))'
                context.extendedPseudoBracketsStack.pop();
                context.extendedPseudoNamesStack.pop();
                upToClosest(context, NodeType.ExtendedSelector);
                upToClosest(context, NodeType.Selector);
              }
              if (isRelativePseudoClassNode(bufferNode)) {
                // save opening bracket for balancing
                // e.g. 'div:not()'  // position is on `)`
                // context.extendedPseudoBracketsStack.push(tokenValue);
                if (context.extendedPseudoNamesStack.length > 0 && context.extendedPseudoBracketsStack.length > 0) {
                  context.extendedPseudoBracketsStack.pop();
                  context.extendedPseudoNamesStack.pop();
                }
              }
              break;
            case LINE_FEED:
            case FORM_FEED:
            case CARRIAGE_RETURN:
              // such characters at start and end of selector should be trimmed
              // so is there is one them among tokens, it is not valid selector
              throw new Error(`'${selector}' is not a valid selector`);
            case TAB:
              // allow tab only inside attribute value
              // as there are such valid rules in filter lists
              // e.g. 'div[style^="margin-right: auto;	text-align: left;',
              // parser position                      ↑
              if (isRegularSelectorNode(bufferNode) && context.isAttributeBracketsOpen) {
                updateBufferNode(context, tokenValue);
              } else {
                // otherwise not valid
                throw new Error(`'${selector}' is not a valid selector`);
              }
          }
          break;
        // no default statement for Marks as they are limited to SUPPORTED_SELECTOR_MARKS
        // and all other symbol combinations are tokenized as Word
        // so error for invalid Word will be thrown later while element selecting by parsed ast
        default:
          throw new Error(`Unknown type of token: '${tokenValue}'`);
      }
      i += 1;
    }
    if (context.ast === null) {
      throw new Error(`'${selector}' is not a valid selector`);
    }
    if (context.extendedPseudoNamesStack.length > 0 || context.extendedPseudoBracketsStack.length > 0) {
      // eslint-disable-next-line max-len
      throw new Error(`Unbalanced brackets for extended pseudo-class: '${getLast(context.extendedPseudoNamesStack)}'`);
    }
    if (context.isAttributeBracketsOpen) {
      throw new Error(`Unbalanced attribute brackets in selector: '${selector}'`);
    }
    return context.shouldOptimize ? optimizeAst(context.ast) : context.ast;
  };

  const natives = {
    MutationObserver: window.MutationObserver || window.WebKitMutationObserver
  };

  /**
   * As soon as possible stores native Node textContent getter to be used for contains pseudo-class
   * because elements' 'textContent' and 'innerText' properties might be mocked.
   *
   * @see {@link https://github.com/AdguardTeam/ExtendedCss/issues/127}
   */
  const nodeTextContentGetter = (() => {
    var _Object$getOwnPropert;
    const nativeNode = window.Node || Node;
    return (_Object$getOwnPropert = Object.getOwnPropertyDescriptor(nativeNode.prototype, 'textContent')) === null || _Object$getOwnPropert === void 0 ? void 0 : _Object$getOwnPropert.get;
  })();

  /**
   * Returns textContent of passed domElement.
   *
   * @param domElement DOM element.
   *
   * @returns DOM element textContent.
   */
  const getNodeTextContent = domElement => {
    return (nodeTextContentGetter === null || nodeTextContentGetter === void 0 ? void 0 : nodeTextContentGetter.apply(domElement)) || '';
  };

  /**
   * Returns element selector text based on it's tagName and attributes.
   *
   * @param element DOM element.
   *
   * @returns String representation of `element`.
   */
  const getElementSelectorDesc = element => {
    let selectorText = element.tagName.toLowerCase();
    selectorText += Array.from(element.attributes).map(attr => {
      return `[${attr.name}="${element.getAttribute(attr.name)}"]`;
    }).join('');
    return selectorText;
  };

  /**
   * Returns path to a DOM element as a selector string.
   *
   * @param inputEl Input element.
   *
   * @returns String path to a DOM element.
   * @throws An error if `inputEl` in not instance of `Element`.
   */
  const getElementSelectorPath = inputEl => {
    if (!(inputEl instanceof Element)) {
      throw new Error('Function received argument with wrong type');
    }
    let el;
    el = inputEl;
    const path = [];
    // we need to check '!!el' first because it is possible
    // that some ancestor of the inputEl was removed before it
    while (!!el && el.nodeType === Node.ELEMENT_NODE) {
      let selector = el.nodeName.toLowerCase();
      if (el.id && typeof el.id === 'string') {
        selector += `#${el.id}`;
        path.unshift(selector);
        break;
      }
      let sibling = el;
      let nth = 1;
      while (sibling.previousElementSibling) {
        sibling = sibling.previousElementSibling;
        if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName.toLowerCase() === selector) {
          nth += 1;
        }
      }
      if (nth !== 1) {
        selector += `:nth-of-type(${nth})`;
      }
      path.unshift(selector);
      el = el.parentElement;
    }
    return path.join(' > ');
  };

  /**
   * Checks whether the element is instance of HTMLElement.
   *
   * @param element Element to check.
   *
   * @returns True if `element` is HTMLElement.
   */
  const isHtmlElement = element => {
    return element instanceof HTMLElement;
  };

  /**
   * Takes `element` and returns its parent element.
   *
   * @param element Element.
   * @param errorMessage Optional error message to throw.
   *
   * @returns Parent of `element`.
   * @throws An error if element has no parent element.
   */
  const getParent = (element, errorMessage) => {
    const {
      parentElement
    } = element;
    if (!parentElement) {
      throw new Error(errorMessage || 'Element does no have parent element');
    }
    return parentElement;
  };

  const logger = {
    /**
     * Safe console.error version.
     */
    error: typeof console !== 'undefined' && console.error && console.error.bind ? console.error.bind(window.console) : console.error,
    /**
     * Safe console.info version.
     */
    info: typeof console !== 'undefined' && console.info && console.info.bind ? console.info.bind(window.console) : console.info
  };

  /**
   * Returns string without suffix.
   *
   * @param str Input string.
   * @param suffix Needed to remove.
   *
   * @returns String without suffix.
   */
  const removeSuffix = (str, suffix) => {
    const index = str.indexOf(suffix, str.length - suffix.length);
    if (index >= 0) {
      return str.substring(0, index);
    }
    return str;
  };

  /**
   * Replaces all `pattern`s with `replacement` in `input` string.
   * String.replaceAll() polyfill because it is not supported by old browsers, e.g. Chrome 55.
   *
   * @see {@link https://caniuse.com/?search=String.replaceAll}
   *
   * @param input Input string to process.
   * @param pattern Find in the input string.
   * @param replacement Replace the pattern with.
   *
   * @returns Modified string.
   */
  const replaceAll = (input, pattern, replacement) => {
    if (!input) {
      return input;
    }
    return input.split(pattern).join(replacement);
  };

  /**
   * Converts string pattern to regular expression.
   *
   * @param str String to convert.
   *
   * @returns Regular expression converted from pattern `str`.
   */
  const toRegExp = str => {
    if (str.startsWith(SLASH) && str.endsWith(SLASH)) {
      return new RegExp(str.slice(1, -1));
    }
    const escaped = str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    return new RegExp(escaped);
  };

  /**
   * Converts any simple type value to string type,
   * e.g. `undefined` -> `'undefined'`.
   *
   * @param value Any type value.
   *
   * @returns String representation of `value`.
   */
  const convertTypeIntoString = value => {
    let output;
    switch (value) {
      case undefined:
        output = 'undefined';
        break;
      case null:
        output = 'null';
        break;
      default:
        output = value.toString();
    }
    return output;
  };

  /**
   * Converts instance of string value into other simple types,
   * e.g. `'null'` -> `null`, `'true'` -> `true`.
   *
   * @param value String-type value.
   *
   * @returns Its own type representation of string-type `value`.
   */
  const convertTypeFromString = value => {
    const numValue = Number(value);
    let output;
    if (!Number.isNaN(numValue)) {
      output = numValue;
    } else {
      switch (value) {
        case 'undefined':
          output = undefined;
          break;
        case 'null':
          output = null;
          break;
        case 'true':
          output = true;
          break;
        case 'false':
          output = false;
          break;
        default:
          output = value;
      }
    }
    return output;
  };

  var BrowserName;
  (function (BrowserName) {
    BrowserName["Chrome"] = "Chrome";
    BrowserName["Firefox"] = "Firefox";
    BrowserName["Edge"] = "Edg";
    BrowserName["Opera"] = "Opera";
    BrowserName["Safari"] = "Safari";
    BrowserName["HeadlessChrome"] = "HeadlessChrome";
  })(BrowserName || (BrowserName = {}));
  const CHROMIUM_BRAND_NAME = 'Chromium';
  const GOOGLE_CHROME_BRAND_NAME = 'Google Chrome';

  /**
   * Simple check for Safari browser.
   */
  const isSafariBrowser = navigator.vendor === 'Apple Computer, Inc.';
  const SUPPORTED_BROWSERS_DATA = {
    [BrowserName.Chrome]: {
      // avoid Chromium-based Edge browser
      // 'EdgA' for android version
      MASK: /\s(Chrome)\/(\d+)\..+\s(?!.*(Edg|EdgA)\/)/,
      MIN_VERSION: 88
    },
    [BrowserName.Firefox]: {
      MASK: /\s(Firefox)\/(\d+)\./,
      MIN_VERSION: 84
    },
    [BrowserName.Edge]: {
      MASK: /\s(Edg)\/(\d+)\./,
      MIN_VERSION: 88
    },
    [BrowserName.Opera]: {
      MASK: /\s(OPR)\/(\d+)\./,
      MIN_VERSION: 80
    },
    [BrowserName.Safari]: {
      MASK: /\sVersion\/(\d{2}\.\d)(.+\s|\s)(Safari)\//,
      MIN_VERSION: 14
    },
    [BrowserName.HeadlessChrome]: {
      // support headless Chrome used by puppeteer
      MASK: /\s(HeadlessChrome)\/(\d+)\..+\s(?!.*Edg\/)/,
      // version should be the same as for BrowserName.Chrome
      MIN_VERSION: 88
    }
  };

  /**
   * Returns chromium brand object or null if not supported.
   * Chromium because of all browsers based on it should be supported as well
   * and it is universal way to check it.
   *
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/brands}
   *
   * @param uaDataBrands Array of user agent brand information.
   *
   * @returns Chromium brand data object or null if it is not supported.
   */
  const getChromiumBrand = uaDataBrands => {
    if (!uaDataBrands) {
      return null;
    }
    // for chromium-based browsers
    const chromiumBrand = uaDataBrands.find(brandData => {
      return brandData.brand === CHROMIUM_BRAND_NAME || brandData.brand === GOOGLE_CHROME_BRAND_NAME;
    });
    return chromiumBrand || null;
  };
  /**
   * Parses userAgent string and returns the data object for supported browsers;
   * otherwise returns null.
   *
   * @param userAgent User agent to parse.
   *
   * @returns Parsed userAgent data object if browser is supported, otherwise null.
   */
  const parseUserAgent = userAgent => {
    let browserName;
    let currentVersion;
    const browserNames = Object.values(BrowserName);
    for (let i = 0; i < browserNames.length; i += 1) {
      let match = null;
      const name = browserNames[i];
      if (name) {
        var _SUPPORTED_BROWSERS_D;
        match = (_SUPPORTED_BROWSERS_D = SUPPORTED_BROWSERS_DATA[name]) === null || _SUPPORTED_BROWSERS_D === void 0 ? void 0 : _SUPPORTED_BROWSERS_D.MASK.exec(userAgent);
      }
      if (match) {
        // for safari browser the order is different because of regexp
        if (match[3] === browserNames[i]) {
          browserName = match[3];
          currentVersion = Number(match[1]);
        } else {
          // for others first is name and second is version
          browserName = match[1];
          currentVersion = Number(match[2]);
        }
        if (!browserName || !currentVersion) {
          return null;
        }
        return {
          browserName,
          currentVersion
        };
      }
    }
    return null;
  };

  /**
   * Returns info about browser.
   *
   * @param userAgent User agent of browser.
   * @param uaDataBrands Array of user agent brand information if supported by browser.
   *
   * @returns Data object if browser is supported, otherwise null.
   */
  const getBrowserInfoAsSupported = (userAgent, uaDataBrands) => {
    const brandData = getChromiumBrand(uaDataBrands);
    if (!brandData) {
      const uaInfo = parseUserAgent(userAgent);
      if (!uaInfo) {
        return null;
      }
      const {
        browserName,
        currentVersion
      } = uaInfo;
      return {
        browserName,
        currentVersion
      };
    }

    // if navigator.userAgentData is supported
    const {
      brand,
      version
    } = brandData;
    // handle chromium-based browsers
    const browserName = brand === CHROMIUM_BRAND_NAME || brand === GOOGLE_CHROME_BRAND_NAME ? BrowserName.Chrome : brand;
    return {
      browserName,
      currentVersion: Number(version)
    };
  };

  /**
   * Checks whether the browser userAgent and userAgentData.brands is supported.
   *
   * @param userAgent User agent of browser.
   * @param uaDataBrands Array of user agent brand information if supported by browser.
   *
   * @returns True if browser is supported.
   */
  const isUserAgentSupported = (userAgent, uaDataBrands) => {
    var _SUPPORTED_BROWSERS_D2;
    // do not support Internet Explorer
    if (userAgent.includes('MSIE') || userAgent.includes('Trident/')) {
      return false;
    }

    // for local testing purposes
    if (userAgent.includes('jsdom')) {
      return true;
    }
    const browserData = getBrowserInfoAsSupported(userAgent, uaDataBrands);
    if (!browserData) {
      return false;
    }
    const {
      browserName,
      currentVersion
    } = browserData;
    if (!browserName || !currentVersion) {
      return false;
    }
    const minVersion = (_SUPPORTED_BROWSERS_D2 = SUPPORTED_BROWSERS_DATA[browserName]) === null || _SUPPORTED_BROWSERS_D2 === void 0 ? void 0 : _SUPPORTED_BROWSERS_D2.MIN_VERSION;
    if (!minVersion) {
      return false;
    }
    return currentVersion >= minVersion;
  };

  /**
   * Checks whether the current browser is supported.
   *
   * @returns True if *current* browser is supported.
   */
  const isBrowserSupported = () => {
    var _navigator$userAgentD;
    return isUserAgentSupported(navigator.userAgent, (_navigator$userAgentD = navigator.userAgentData) === null || _navigator$userAgentD === void 0 ? void 0 : _navigator$userAgentD.brands);
  };

  var CssProperty;
  (function (CssProperty) {
    CssProperty["Background"] = "background";
    CssProperty["BackgroundImage"] = "background-image";
    CssProperty["Content"] = "content";
    CssProperty["Opacity"] = "opacity";
  })(CssProperty || (CssProperty = {}));
  const REGEXP_ANY_SYMBOL = '.*';
  const REGEXP_WITH_FLAGS_REGEXP = /^\s*\/.*\/[gmisuy]*\s*$/;
  /**
   * Removes quotes for specified content value.
   *
   * For example, content style declaration with `::before` can be set as '-' (e.g. unordered list)
   * which displayed as simple dash `-` with no quotes.
   * But CSSStyleDeclaration.getPropertyValue('content') will return value
   * wrapped into quotes, e.g. '"-"', which should be removed
   * because filters maintainers does not use any quotes in real rules.
   *
   * @param str Input string.
   *
   * @returns String with no quotes for content value.
   */
  const removeContentQuotes = str => {
    return str.replace(/^(["'])([\s\S]*)\1$/, '$2');
  };

  /**
   * Adds quotes for specified background url value.
   *
   * If background-image is specified **without** quotes:
   * e.g. 'background: url(data:image/gif;base64,R0lGODlhAQA7)'.
   *
   * CSSStyleDeclaration.getPropertyValue('background-image') may return value **with** quotes:
   * e.g. 'background: url("data:image/gif;base64,R0lGODlhAQA7")'.
   *
   * So we add quotes for compatibility since filters maintainers might use quotes in real rules.
   *
   * @param str Input string.
   *
   * @returns String with unified quotes for background url value.
   */
  const addUrlPropertyQuotes = str => {
    if (!str.includes('url("')) {
      const re = /url\((.*?)\)/g;
      return str.replace(re, 'url("$1")');
    }
    return str;
  };

  /**
   * Adds quotes to url arg for consistent property value matching.
   */
  const addUrlQuotesTo = {
    regexpArg: str => {
      // e.g. /^url\\([a-z]{4}:[a-z]{5}/
      // or /^url\\(data\\:\\image\\/gif;base64.+/
      const re = /(\^)?url(\\)?\\\((\w|\[\w)/g;
      return str.replace(re, '$1url$2\\(\\"?$3');
    },
    noneRegexpArg: addUrlPropertyQuotes
  };

  /**
   * Escapes regular expression string.
   *
   * @see {@link https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/regexp}
   *
   * @param str Input string.
   *
   * @returns Escaped regular expression string.
   */
  const escapeRegExp = str => {
    // should be escaped . * + ? ^ $ { } ( ) | [ ] / \
    // except of * | ^
    const specials = ['.', '+', '?', '$', '{', '}', '(', ')', '[', ']', '\\', '/'];
    const specialsRegex = new RegExp(`[${specials.join('\\')}]`, 'g');
    return str.replace(specialsRegex, '\\$&');
  };

  /**
   * Converts :matches-css() arg property value match to regexp.
   *
   * @param rawValue Style match value pattern.
   *
   * @returns Arg of :matches-css() converted to regular expression.
   */
  const convertStyleMatchValueToRegexp = rawValue => {
    let value;
    if (rawValue.startsWith(SLASH) && rawValue.endsWith(SLASH)) {
      // For regex patterns double quotes `"` and backslashes `\` should be escaped
      value = addUrlQuotesTo.regexpArg(rawValue);
      value = value.slice(1, -1);
    } else {
      // For non-regex patterns parentheses `(` `)` and square brackets `[` `]`
      // should be unescaped, because their escaping in filter rules is required
      value = addUrlQuotesTo.noneRegexpArg(rawValue);
      value = value.replace(/\\([\\()[\]"])/g, '$1');
      value = escapeRegExp(value);
      // e.g. div:matches-css(background-image: url(data:*))
      value = replaceAll(value, ASTERISK, REGEXP_ANY_SYMBOL);
    }
    return new RegExp(value, 'i');
  };

  /**
   * Makes some properties values compatible.
   *
   * @param propertyName Name of style property.
   * @param propertyValue Value of style property.
   *
   * @returns Normalized values for some CSS properties.
   */
  const normalizePropertyValue = (propertyName, propertyValue) => {
    let normalized = '';
    switch (propertyName) {
      case CssProperty.Background:
      case CssProperty.BackgroundImage:
        // sometimes url property does not have quotes
        // so we add them for consistent matching
        normalized = addUrlPropertyQuotes(propertyValue);
        break;
      case CssProperty.Content:
        normalized = removeContentQuotes(propertyValue);
        break;
      case CssProperty.Opacity:
        // https://bugs.webkit.org/show_bug.cgi?id=93445
        normalized = isSafariBrowser ? (Math.round(parseFloat(propertyValue) * 100) / 100).toString() : propertyValue;
        break;
      default:
        normalized = propertyValue;
    }
    return normalized;
  };

  /**
   * Returns domElement style property value
   * by css property name and standard pseudo-element.
   *
   * @param domElement DOM element.
   * @param propertyName CSS property name.
   * @param regularPseudoElement Standard pseudo-element — '::before', '::after' etc.
   *
   * @returns String containing the value of a specified CSS property.
   */
  const getComputedStylePropertyValue = (domElement, propertyName, regularPseudoElement) => {
    const style = window.getComputedStyle(domElement, regularPseudoElement);
    const propertyValue = style.getPropertyValue(propertyName);
    return normalizePropertyValue(propertyName, propertyValue);
  };
  /**
   * Parses arg of absolute pseudo-class into 'name' and 'value' if set.
   *
   * Used for :matches-css() - with COLON as separator,
   * for :matches-attr() and :matches-property() - with EQUAL_SIGN as separator.
   *
   * @param pseudoArg Arg of pseudo-class.
   * @param separator Divider symbol.
   *
   * @returns Parsed 'matches' pseudo-class arg data.
   */
  const getPseudoArgData = (pseudoArg, separator) => {
    const index = pseudoArg.indexOf(separator);
    let name;
    let value;
    if (index > -1) {
      name = pseudoArg.substring(0, index).trim();
      value = pseudoArg.substring(index + 1).trim();
    } else {
      name = pseudoArg;
    }
    return {
      name,
      value
    };
  };
  /**
   * Parses :matches-css() pseudo-class arg
   * where regular pseudo-element can be a part of arg
   * e.g. 'div:matches-css(before, color: rgb(255, 255, 255))'    <-- obsolete `:matches-css-before()`.
   *
   * @param pseudoName Pseudo-class name.
   * @param rawArg Pseudo-class arg.
   *
   * @returns Parsed :matches-css() pseudo-class arg data.
   * @throws An error on invalid `rawArg`.
   */
  const parseStyleMatchArg = (pseudoName, rawArg) => {
    const {
      name,
      value
    } = getPseudoArgData(rawArg, COMMA);
    let regularPseudoElement = name;
    let styleMatchArg = value;

    // check whether the string part before the separator is valid regular pseudo-element,
    // otherwise `regularPseudoElement` is null, and `styleMatchArg` is rawArg
    if (!Object.values(REGULAR_PSEUDO_ELEMENTS).includes(name)) {
      regularPseudoElement = null;
      styleMatchArg = rawArg;
    }
    if (!styleMatchArg) {
      throw new Error(`Required style property argument part is missing in :${pseudoName}() arg: '${rawArg}'`);
    }

    // if regularPseudoElement is not `null`
    if (regularPseudoElement) {
      // pseudo-element should have two colon marks for Window.getComputedStyle() due to the syntax:
      // https://www.w3.org/TR/selectors-4/#pseudo-element-syntax
      // ':matches-css(before, content: ads)' ->> '::before'
      regularPseudoElement = `${COLON}${COLON}${regularPseudoElement}`;
    }
    return {
      regularPseudoElement,
      styleMatchArg
    };
  };

  /**
   * Checks whether the domElement is matched by :matches-css() arg.
   *
   * @param argsData Pseudo-class name, arg, and dom element to check.
   *
   @returns True if DOM element is matched.
   * @throws An error on invalid pseudo-class arg.
   */
  const isStyleMatched = argsData => {
    const {
      pseudoName,
      pseudoArg,
      domElement
    } = argsData;
    const {
      regularPseudoElement,
      styleMatchArg
    } = parseStyleMatchArg(pseudoName, pseudoArg);
    const {
      name: matchName,
      value: matchValue
    } = getPseudoArgData(styleMatchArg, COLON);
    if (!matchName || !matchValue) {
      throw new Error(`Required property name or value is missing in :${pseudoName}() arg: '${styleMatchArg}'`);
    }
    let valueRegexp;
    try {
      valueRegexp = convertStyleMatchValueToRegexp(matchValue);
    } catch (e) {
      logger.error(e);
      throw new Error(`Invalid argument of :${pseudoName}() pseudo-class: '${styleMatchArg}'`);
    }
    const value = getComputedStylePropertyValue(domElement, matchName, regularPseudoElement);
    return valueRegexp && valueRegexp.test(value);
  };

  /**
   * Validates string arg for :matches-attr() and :matches-property().
   *
   * @param arg Pseudo-class arg.
   *
   * @returns True if 'matches' pseudo-class string arg is valid.
   */
  const validateStrMatcherArg = arg => {
    if (arg.includes(SLASH)) {
      return false;
    }
    if (!/^[\w-]+$/.test(arg)) {
      return false;
    }
    return true;
  };

  /**
   * Returns valid arg for :matches-attr() and :matcher-property().
   *
   * @param rawArg Arg pattern.
   * @param [isWildcardAllowed=false] Flag for wildcard (`*`) using as pseudo-class arg.
   *
   * @returns Valid arg for :matches-attr() and :matcher-property().
   * @throws An error on invalid `rawArg`.
   */
  const getValidMatcherArg = function (rawArg) {
    let isWildcardAllowed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
    // if rawArg is missing for pseudo-class
    // e.g. :matches-attr()
    // error will be thrown before getValidMatcherArg() is called:
    // name or arg is missing in AbsolutePseudoClass

    let arg;
    if (rawArg.length > 1 && rawArg.startsWith(DOUBLE_QUOTE) && rawArg.endsWith(DOUBLE_QUOTE)) {
      rawArg = rawArg.slice(1, -1);
    }
    if (rawArg === '') {
      // e.g. :matches-property("")
      throw new Error('Argument should be specified. Empty arg is invalid.');
    }
    if (rawArg.startsWith(SLASH) && rawArg.endsWith(SLASH)) {
      // e.g. :matches-property("//")
      if (rawArg.length > 2) {
        arg = toRegExp(rawArg);
      } else {
        throw new Error(`Invalid regexp: '${rawArg}'`);
      }
    } else if (rawArg.includes(ASTERISK)) {
      if (rawArg === ASTERISK && !isWildcardAllowed) {
        // e.g. :matches-attr(*)
        throw new Error(`Argument should be more specific than ${rawArg}`);
      }
      arg = replaceAll(rawArg, ASTERISK, REGEXP_ANY_SYMBOL);
      arg = new RegExp(arg);
    } else {
      if (!validateStrMatcherArg(rawArg)) {
        throw new Error(`Invalid argument: '${rawArg}'`);
      }
      arg = rawArg;
    }
    return arg;
  };
  /**
   * Parses pseudo-class argument and returns parsed data.
   *
   * @param pseudoName Extended pseudo-class name.
   * @param pseudoArg Extended pseudo-class argument.
   *
   * @returns Parsed pseudo-class argument data.
   * @throws An error if attribute name is missing in pseudo-class arg.
   */
  const getRawMatchingData = (pseudoName, pseudoArg) => {
    const {
      name: rawName,
      value: rawValue
    } = getPseudoArgData(pseudoArg, EQUAL_SIGN);
    if (!rawName) {
      throw new Error(`Required attribute name is missing in :${pseudoName} arg: ${pseudoArg}`);
    }
    return {
      rawName,
      rawValue
    };
  };

  /**
   * Checks whether the domElement is matched by :matches-attr() arg.
   *
   * @param argsData Pseudo-class name, arg, and dom element to check.
   *
   @returns True if DOM element is matched.
   * @throws An error on invalid arg of pseudo-class.
   */
  const isAttributeMatched = argsData => {
    const {
      pseudoName,
      pseudoArg,
      domElement
    } = argsData;
    const elementAttributes = domElement.attributes;
    // no match if dom element has no attributes
    if (elementAttributes.length === 0) {
      return false;
    }
    const {
      rawName: rawAttrName,
      rawValue: rawAttrValue
    } = getRawMatchingData(pseudoName, pseudoArg);
    let attrNameMatch;
    try {
      attrNameMatch = getValidMatcherArg(rawAttrName);
    } catch (e) {
      // eslint-disable-line @typescript-eslint/no-explicit-any
      logger.error(e);
      throw new SyntaxError(e.message);
    }
    let isMatched = false;
    let i = 0;
    while (i < elementAttributes.length && !isMatched) {
      const attr = elementAttributes[i];
      if (!attr) {
        break;
      }
      const isNameMatched = attrNameMatch instanceof RegExp ? attrNameMatch.test(attr.name) : attrNameMatch === attr.name;
      if (!rawAttrValue) {
        // for rules with no attribute value specified
        // e.g. :matches-attr("/regex/") or :matches-attr("attr-name")
        isMatched = isNameMatched;
      } else {
        let attrValueMatch;
        try {
          attrValueMatch = getValidMatcherArg(rawAttrValue);
        } catch (e) {
          // eslint-disable-line @typescript-eslint/no-explicit-any
          logger.error(e);
          throw new SyntaxError(e.message);
        }
        const isValueMatched = attrValueMatch instanceof RegExp ? attrValueMatch.test(attr.value) : attrValueMatch === attr.value;
        isMatched = isNameMatched && isValueMatched;
      }
      i += 1;
    }
    return isMatched;
  };

  /**
   * Parses raw :matches-property() arg which may be chain of properties.
   *
   * @param input Argument of :matches-property().
   *
   * @returns Arg of :matches-property() as array of strings or regular expressions.
   * @throws An error on invalid chain.
   */
  const parseRawPropChain = input => {
    if (input.length > 1 && input.startsWith(DOUBLE_QUOTE) && input.endsWith(DOUBLE_QUOTE)) {
      input = input.slice(1, -1);
    }
    const chainChunks = input.split(DOT);
    const chainPatterns = [];
    let patternBuffer = '';
    let isRegexpPattern = false;
    let i = 0;
    while (i < chainChunks.length) {
      const chunk = getItemByIndex(chainChunks, i, `Invalid pseudo-class arg: '${input}'`);
      if (chunk.startsWith(SLASH) && chunk.endsWith(SLASH) && chunk.length > 2) {
        // regexp pattern with no dot in it, e.g. /propName/
        chainPatterns.push(chunk);
      } else if (chunk.startsWith(SLASH)) {
        // if chunk is a start of regexp pattern
        isRegexpPattern = true;
        patternBuffer += chunk;
      } else if (chunk.endsWith(SLASH)) {
        isRegexpPattern = false;
        // restore dot removed while splitting
        // e.g. testProp./.{1,5}/
        patternBuffer += `.${chunk}`;
        chainPatterns.push(patternBuffer);
        patternBuffer = '';
      } else {
        // if there are few dots in regexp pattern
        // so chunk might be in the middle of it
        if (isRegexpPattern) {
          patternBuffer += chunk;
        } else {
          // otherwise it is string pattern
          chainPatterns.push(chunk);
        }
      }
      i += 1;
    }
    if (patternBuffer.length > 0) {
      throw new Error(`Invalid regexp property pattern '${input}'`);
    }
    const chainMatchPatterns = chainPatterns.map(pattern => {
      if (pattern.length === 0) {
        // e.g. '.prop.id' or 'nested..test'
        throw new Error(`Empty pattern '${pattern}' is invalid in chain '${input}'`);
      }
      let validPattern;
      try {
        validPattern = getValidMatcherArg(pattern, true);
      } catch (e) {
        logger.error(e);
        throw new Error(`Invalid property pattern '${pattern}' in property chain '${input}'`);
      }
      return validPattern;
    });
    return chainMatchPatterns;
  };
  /**
   * Checks if the property exists in the base object (recursively).
   *
   * @param base Element to check.
   * @param chain Array of objects - parsed string property chain.
   * @param [output=[]] Result acc.
   *
   * @returns Array of parsed data — representation of `base`-related `chain`.
   */
  const filterRootsByRegexpChain = function (base, chain) {
    let output = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
    const tempProp = getFirst(chain);
    if (chain.length === 1) {
      let key;
      for (key in base) {
        if (tempProp instanceof RegExp) {
          if (tempProp.test(key)) {
            output.push({
              base,
              prop: key,
              value: base[key]
            });
          }
        } else if (tempProp === key) {
          output.push({
            base,
            prop: tempProp,
            value: base[key]
          });
        }
      }
      return output;
    }

    // if there is a regexp prop in input chain
    // e.g. 'unit./^ad.+/.src' for 'unit.ad-1gf2.src unit.ad-fgd34.src'),
    // every base keys should be tested by regexp and it can be more that one results
    if (tempProp instanceof RegExp) {
      const nextProp = chain.slice(1);
      const baseKeys = [];
      for (const key in base) {
        if (tempProp.test(key)) {
          baseKeys.push(key);
        }
      }
      baseKeys.forEach(key => {
        var _Object$getOwnPropert;
        const item = (_Object$getOwnPropert = Object.getOwnPropertyDescriptor(base, key)) === null || _Object$getOwnPropert === void 0 ? void 0 : _Object$getOwnPropert.value;
        filterRootsByRegexpChain(item, nextProp, output);
      });
    }
    if (base && typeof tempProp === 'string') {
      var _Object$getOwnPropert2;
      const nextBase = (_Object$getOwnPropert2 = Object.getOwnPropertyDescriptor(base, tempProp)) === null || _Object$getOwnPropert2 === void 0 ? void 0 : _Object$getOwnPropert2.value;
      chain = chain.slice(1);
      if (nextBase !== undefined) {
        filterRootsByRegexpChain(nextBase, chain, output);
      }
    }
    return output;
  };

  /**
   * Checks whether the domElement is matched by :matches-property() arg.
   *
   * @param argsData Pseudo-class name, arg, and dom element to check.
   *
   @returns True if DOM element is matched.
   * @throws An error on invalid prop in chain.
   */
  const isPropertyMatched = argsData => {
    const {
      pseudoName,
      pseudoArg,
      domElement
    } = argsData;
    const {
      rawName: rawPropertyName,
      rawValue: rawPropertyValue
    } = getRawMatchingData(pseudoName, pseudoArg);

    // chained property name cannot include '/' or '.'
    // so regex prop names with such escaped characters are invalid
    if (rawPropertyName.includes('\\/') || rawPropertyName.includes('\\.')) {
      throw new Error(`Invalid :${pseudoName} name pattern: ${rawPropertyName}`);
    }
    let propChainMatches;
    try {
      propChainMatches = parseRawPropChain(rawPropertyName);
    } catch (e) {
      // eslint-disable-line @typescript-eslint/no-explicit-any
      logger.error(e);
      throw new SyntaxError(e.message);
    }
    const ownerObjArr = filterRootsByRegexpChain(domElement, propChainMatches);
    if (ownerObjArr.length === 0) {
      return false;
    }
    let isMatched = true;
    if (rawPropertyValue) {
      let propValueMatch;
      try {
        propValueMatch = getValidMatcherArg(rawPropertyValue);
      } catch (e) {
        // eslint-disable-line @typescript-eslint/no-explicit-any
        logger.error(e);
        throw new SyntaxError(e.message);
      }
      if (propValueMatch) {
        for (let i = 0; i < ownerObjArr.length; i += 1) {
          var _ownerObjArr$i;
          const realValue = (_ownerObjArr$i = ownerObjArr[i]) === null || _ownerObjArr$i === void 0 ? void 0 : _ownerObjArr$i.value;
          if (propValueMatch instanceof RegExp) {
            isMatched = propValueMatch.test(convertTypeIntoString(realValue));
          } else {
            // handle 'null' and 'undefined' property values set as string
            if (realValue === 'null' || realValue === 'undefined') {
              isMatched = propValueMatch === realValue;
              break;
            }
            isMatched = convertTypeFromString(propValueMatch) === realValue;
          }
          if (isMatched) {
            break;
          }
        }
      }
    }
    return isMatched;
  };

  /**
   * Checks whether the textContent is matched by :contains arg.
   *
   * @param argsData Pseudo-class name, arg, and dom element to check.
   *
   @returns True if DOM element is matched.
   * @throws An error on invalid arg of pseudo-class.
   */
  const isTextMatched = argsData => {
    const {
      pseudoName,
      pseudoArg,
      domElement
    } = argsData;
    const textContent = getNodeTextContent(domElement);
    let isTextContentMatched;
    let pseudoArgToMatch = pseudoArg;
    if (pseudoArgToMatch.startsWith(SLASH) && REGEXP_WITH_FLAGS_REGEXP.test(pseudoArgToMatch)) {
      // regexp arg
      const flagsIndex = pseudoArgToMatch.lastIndexOf('/');
      const flagsStr = pseudoArgToMatch.substring(flagsIndex + 1);
      pseudoArgToMatch = pseudoArgToMatch.substring(0, flagsIndex + 1).slice(1, -1).replace(/\\([\\"])/g, '$1');
      let regex;
      try {
        regex = new RegExp(pseudoArgToMatch, flagsStr);
      } catch (e) {
        throw new Error(`Invalid argument of :${pseudoName}() pseudo-class: ${pseudoArg}`);
      }
      isTextContentMatched = regex.test(textContent);
    } else {
      // none-regexp arg
      pseudoArgToMatch = pseudoArgToMatch.replace(/\\([\\()[\]"])/g, '$1');
      isTextContentMatched = textContent.includes(pseudoArgToMatch);
    }
    return isTextContentMatched;
  };

  /**
   * Validates number arg for :nth-ancestor() and :upward() pseudo-classes.
   *
   * @param rawArg Raw arg of pseudo-class.
   * @param pseudoName Pseudo-class name.
   *
   * @returns Valid number arg for :nth-ancestor() and :upward().
   * @throws An error on invalid `rawArg`.
   */
  const getValidNumberAncestorArg = (rawArg, pseudoName) => {
    const deep = Number(rawArg);
    if (Number.isNaN(deep) || deep < 1 || deep >= 256) {
      throw new Error(`Invalid argument of :${pseudoName} pseudo-class: '${rawArg}'`);
    }
    return deep;
  };

  /**
   * Returns nth ancestor by 'deep' number arg OR undefined if ancestor range limit exceeded.
   *
   * @param domElement DOM element to find ancestor for.
   * @param nth Depth up to needed ancestor.
   * @param pseudoName Pseudo-class name.
   *
   * @returns Ancestor element found in DOM, or null if not found.
   * @throws An error on invalid `nth` arg.
   */
  const getNthAncestor = (domElement, nth, pseudoName) => {
    let ancestor = null;
    let i = 0;
    while (i < nth) {
      ancestor = domElement.parentElement;
      if (!ancestor) {
        throw new Error(`Out of DOM: Argument of :${pseudoName}() pseudo-class is too big — '${nth}'.`);
      }
      domElement = ancestor;
      i += 1;
    }
    return ancestor;
  };

  /**
   * Validates standard CSS selector.
   *
   * @param selector Standard selector.
   *
   * @returns True if standard CSS selector is valid.
   */
  const validateStandardSelector = selector => {
    let isValid;
    try {
      document.querySelectorAll(selector);
      isValid = true;
    } catch (e) {
      isValid = false;
    }
    return isValid;
  };

  /**
   * Wrapper to run matcher `callback` with `args`
   * and throw error with `errorMessage` if `callback` run fails.
   *
   * @param callback Matcher callback.
   * @param argsData Args needed for matcher callback.
   * @param errorMessage Error message.
   *
   * @returns True if `callback` returns true.
   * @throws An error if `callback` fails.
   */
  const matcherWrapper = (callback, argsData, errorMessage) => {
    let isMatched;
    try {
      isMatched = callback(argsData);
    } catch (e) {
      logger.error(e);
      throw new Error(errorMessage);
    }
    return isMatched;
  };

  /**
   * Generates common error message to throw while matching element `propDesc`.
   *
   * @param propDesc Text to describe what element 'prop' pseudo-class is trying to match.
   * @param pseudoName Pseudo-class name.
   * @param pseudoArg Pseudo-class arg.
   *
   * @returns Generated error message string.
   */
  const getAbsolutePseudoError = (propDesc, pseudoName, pseudoArg) => {
    // eslint-disable-next-line max-len
    return `${MATCHING_ELEMENT_ERROR_PREFIX} ${propDesc}, may be invalid :${pseudoName}() pseudo-class arg: '${pseudoArg}'`;
  };

  /**
   * Checks whether the domElement is matched by absolute extended pseudo-class argument.
   *
   * @param domElement Page element.
   * @param pseudoName Pseudo-class name.
   * @param pseudoArg Pseudo-class arg.
   *
   * @returns True if `domElement` is matched by absolute pseudo-class.
   * @throws An error on unknown absolute pseudo-class.
   */
  const isMatchedByAbsolutePseudo = (domElement, pseudoName, pseudoArg) => {
    let argsData;
    let errorMessage;
    let callback;
    switch (pseudoName) {
      case CONTAINS_PSEUDO:
      case HAS_TEXT_PSEUDO:
      case ABP_CONTAINS_PSEUDO:
        callback = isTextMatched;
        argsData = {
          pseudoName,
          pseudoArg,
          domElement
        };
        errorMessage = getAbsolutePseudoError('text content', pseudoName, pseudoArg);
        break;
      case MATCHES_CSS_PSEUDO:
      case MATCHES_CSS_AFTER_PSEUDO:
      case MATCHES_CSS_BEFORE_PSEUDO:
        callback = isStyleMatched;
        argsData = {
          pseudoName,
          pseudoArg,
          domElement
        };
        errorMessage = getAbsolutePseudoError('style', pseudoName, pseudoArg);
        break;
      case MATCHES_ATTR_PSEUDO_CLASS_MARKER:
        callback = isAttributeMatched;
        argsData = {
          domElement,
          pseudoName,
          pseudoArg
        };
        errorMessage = getAbsolutePseudoError('attributes', pseudoName, pseudoArg);
        break;
      case MATCHES_PROPERTY_PSEUDO_CLASS_MARKER:
        callback = isPropertyMatched;
        argsData = {
          domElement,
          pseudoName,
          pseudoArg
        };
        errorMessage = getAbsolutePseudoError('properties', pseudoName, pseudoArg);
        break;
      default:
        throw new Error(`Unknown absolute pseudo-class :${pseudoName}()`);
    }
    return matcherWrapper(callback, argsData, errorMessage);
  };
  const findByAbsolutePseudoPseudo = {
    /**
     * Returns list of nth ancestors relative to every dom node from domElements list.
     *
     * @param domElements DOM elements.
     * @param rawPseudoArg Number arg of :nth-ancestor() or :upward() pseudo-class.
     * @param pseudoName Pseudo-class name.
     *
     * @returns Array of ancestor DOM elements.
     */
    nthAncestor: (domElements, rawPseudoArg, pseudoName) => {
      const deep = getValidNumberAncestorArg(rawPseudoArg, pseudoName);
      const ancestors = domElements.map(domElement => {
        let ancestor = null;
        try {
          ancestor = getNthAncestor(domElement, deep, pseudoName);
        } catch (e) {
          logger.error(e);
        }
        return ancestor;
      }).filter(isHtmlElement);
      return ancestors;
    },
    /**
     * Returns list of elements by xpath expression, evaluated on every dom node from domElements list.
     *
     * @param domElements DOM elements.
     * @param rawPseudoArg Arg of :xpath() pseudo-class.
     *
     * @returns Array of DOM elements matched by xpath expression.
     */
    xpath: (domElements, rawPseudoArg) => {
      const foundElements = domElements.map(domElement => {
        const result = [];
        let xpathResult;
        try {
          xpathResult = document.evaluate(rawPseudoArg, domElement, null, window.XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
        } catch (e) {
          logger.error(e);
          throw new Error(`Invalid argument of :xpath pseudo-class: '${rawPseudoArg}'`);
        }
        let node = xpathResult.iterateNext();
        while (node) {
          if (isHtmlElement(node)) {
            result.push(node);
          }
          node = xpathResult.iterateNext();
        }
        return result;
      });
      return flatten(foundElements);
    },
    /**
     * Returns list of closest ancestors relative to every dom node from domElements list.
     *
     * @param domElements DOM elements.
     * @param rawPseudoArg Standard selector arg of :upward() pseudo-class.
     *
     * @returns Array of closest ancestor DOM elements.
     * @throws An error if `rawPseudoArg` is not a valid standard selector.
     */
    upward: (domElements, rawPseudoArg) => {
      if (!validateStandardSelector(rawPseudoArg)) {
        throw new Error(`Invalid argument of :upward pseudo-class: '${rawPseudoArg}'`);
      }
      const closestAncestors = domElements.map(domElement => {
        // closest to parent element should be found
        // otherwise `.base:upward(.base)` will return itself too, not only ancestor
        const parent = domElement.parentElement;
        if (!parent) {
          return null;
        }
        return parent.closest(rawPseudoArg);
      }).filter(isHtmlElement);
      return closestAncestors;
    }
  };

  /**
   * Calculated selector text which is needed to :has(), :is() and :not() pseudo-classes.
   * Contains calculated part (depends on the processed element)
   * and value of RegularSelector which is next to selector by.
   *
   * Native Document.querySelectorAll() does not select exact descendant elements
   * but match all page elements satisfying the selector,
   * so extra specification is needed for proper descendants selection
   * e.g. 'div:has(> img)'.
   *
   * Its calculation depends on extended selector.
   */

  /**
   * Combined `:scope` pseudo-class and **child** combinator — `:scope>`.
   */
  const scopeDirectChildren = `${SCOPE_CSS_PSEUDO_CLASS}${CHILD_COMBINATOR}`;

  /**
   * Combined `:scope` pseudo-class and **descendant** combinator — `:scope `.
   */
  const scopeAnyChildren = `${SCOPE_CSS_PSEUDO_CLASS}${DESCENDANT_COMBINATOR}`;

  /**
   * Interface for relative pseudo-class helpers args.
   */

  /**
   * Returns the first of RegularSelector child node for `selectorNode`.
   *
   * @param selectorNode Ast Selector node.
   * @param pseudoName Name of relative pseudo-class.
   *
   * @returns Ast RegularSelector node.
   */
  const getFirstInnerRegularChild = (selectorNode, pseudoName) => {
    return getFirstRegularChild(selectorNode.children, `RegularSelector is missing for :${pseudoName}() pseudo-class`);
  };

  // TODO: fix for <forgiving-relative-selector-list>
  // https://github.com/AdguardTeam/ExtendedCss/issues/154
  /**
   * Checks whether the element has all relative elements specified by pseudo-class arg.
   * Used for :has() pseudo-class.
   *
   * @param argsData Relative pseudo-class helpers args data.
   *
   * @returns True if **all selectors** from argsData.relativeSelectorList is **matched** for argsData.element.
   */
  const hasRelativesBySelectorList = argsData => {
    const {
      element,
      relativeSelectorList,
      pseudoName
    } = argsData;
    return relativeSelectorList.children
    // Array.every() is used here as each Selector node from SelectorList should exist on page
    .every(selectorNode => {
      // selectorList.children always starts with regular selector as any selector generally
      const relativeRegularSelector = getFirstInnerRegularChild(selectorNode, pseudoName);
      let specifiedSelector = '';
      let rootElement = null;
      const regularSelector = getNodeValue(relativeRegularSelector);
      if (regularSelector.startsWith(NEXT_SIBLING_COMBINATOR) || regularSelector.startsWith(SUBSEQUENT_SIBLING_COMBINATOR)) {
        /**
         * For matching the element by "element:has(+ next-sibling)" and "element:has(~ sibling)"
         * we check whether the element's parentElement has specific direct child combination,
         * e.g. 'h1:has(+ .share)' -> `h1Node.parentElement.querySelectorAll(':scope > h1 + .share')`.
         *
         * @see {@link https://www.w3.org/TR/selectors-4/#relational}
         */
        rootElement = element.parentElement;
        const elementSelectorText = getElementSelectorDesc(element);
        specifiedSelector = `${scopeDirectChildren}${elementSelectorText}${regularSelector}`;
      } else if (regularSelector === ASTERISK) {
        /**
         * :scope specification is needed for proper descendants selection
         * as native element.querySelectorAll() does not select exact element descendants
         * e.g. 'a:has(> img)' -> `aNode.querySelectorAll(':scope > img')`.
         *
         * For 'any selector' as arg of relative simplicity should be set for all inner elements
         * e.g. 'div:has(*)' -> `divNode.querySelectorAll(':scope *')`
         * which means empty div with no child element.
         */
        rootElement = element;
        specifiedSelector = `${scopeAnyChildren}${ASTERISK}`;
      } else {
        /**
         * As it described above, inner elements should be found using `:scope` pseudo-class
         * e.g. 'a:has(> img)' -> `aNode.querySelectorAll(':scope > img')`
         * OR '.block(div > span)' -> `blockClassNode.querySelectorAll(':scope div > span')`.
         */
        specifiedSelector = `${scopeAnyChildren}${regularSelector}`;
        rootElement = element;
      }
      if (!rootElement) {
        throw new Error(`Selection by :${pseudoName}() pseudo-class is not possible`);
      }
      let relativeElements;
      try {
        // eslint-disable-next-line @typescript-eslint/no-use-before-define
        relativeElements = getElementsForSelectorNode(selectorNode, rootElement, specifiedSelector);
      } catch (e) {
        logger.error(e);
        // fail for invalid selector
        throw new Error(`Invalid selector for :${pseudoName}() pseudo-class: '${regularSelector}'`);
      }
      return relativeElements.length > 0;
    });
  };

  /**
   * Checks whether the element is an any element specified by pseudo-class arg.
   * Used for :is() pseudo-class.
   *
   * @param argsData Relative pseudo-class helpers args data.
   *
   * @returns True if **any selector** from argsData.relativeSelectorList is **matched** for argsData.element.
   */
  const isAnyElementBySelectorList = argsData => {
    const {
      element,
      relativeSelectorList,
      pseudoName
    } = argsData;
    return relativeSelectorList.children
    // Array.some() is used here as any selector from selector list should exist on page
    .some(selectorNode => {
      // selectorList.children always starts with regular selector
      const relativeRegularSelector = getFirstInnerRegularChild(selectorNode, pseudoName);

      /**
       * For checking the element by 'div:is(.banner)'
       * we check whether the element's parentElement has any specific direct child.
       */
      const rootElement = getParent(element, `Selection by :${pseudoName}() pseudo-class is not possible`);

      /**
       * So we calculate the element "description" by it's tagname and attributes for targeting
       * and use it to specify the selection
       * e.g. `div:is(.banner)` --> `divNode.parentElement.querySelectorAll(':scope > .banner')`.
       */
      const specifiedSelector = `${scopeDirectChildren}${getNodeValue(relativeRegularSelector)}`;
      let anyElements;
      try {
        // eslint-disable-next-line @typescript-eslint/no-use-before-define
        anyElements = getElementsForSelectorNode(selectorNode, rootElement, specifiedSelector);
      } catch (e) {
        // do not fail on invalid selectors for :is()
        return false;
      }

      // TODO: figure out how to handle complex selectors with extended pseudo-classes
      // (check readme - extended-css-is-limitations)
      // because `element` and `anyElements` may be from different DOM levels
      return anyElements.includes(element);
    });
  };

  /**
   * Checks whether the element is not an element specified by pseudo-class arg.
   * Used for :not() pseudo-class.
   *
   * @param argsData Relative pseudo-class helpers args data.
   *
   * @returns True if **any selector** from argsData.relativeSelectorList is **not matched** for argsData.element.
   */
  const notElementBySelectorList = argsData => {
    const {
      element,
      relativeSelectorList,
      pseudoName
    } = argsData;
    return relativeSelectorList.children
    // Array.every() is used here as element should not be selected by any selector from selector list
    .every(selectorNode => {
      // selectorList.children always starts with regular selector
      const relativeRegularSelector = getFirstInnerRegularChild(selectorNode, pseudoName);

      /**
       * For checking the element by 'div:not([data="content"])
       * we check whether the element's parentElement has any specific direct child.
       */
      const rootElement = getParent(element, `Selection by :${pseudoName}() pseudo-class is not possible`);

      /**
       * So we calculate the element "description" by it's tagname and attributes for targeting
       * and use it to specify the selection
       * e.g. `div:not(.banner)` --> `divNode.parentElement.querySelectorAll(':scope > .banner')`.
       */
      const specifiedSelector = `${scopeDirectChildren}${getNodeValue(relativeRegularSelector)}`;
      let anyElements;
      try {
        // eslint-disable-next-line @typescript-eslint/no-use-before-define
        anyElements = getElementsForSelectorNode(selectorNode, rootElement, specifiedSelector);
      } catch (e) {
        // fail on invalid selectors for :not()
        logger.error(e);
        // eslint-disable-next-line max-len
        throw new Error(`Invalid selector for :${pseudoName}() pseudo-class: '${getNodeValue(relativeRegularSelector)}'`);
      }

      // TODO: figure out how to handle up-looking pseudo-classes inside :not()
      // (check readme - extended-css-not-limitations)
      // because `element` and `anyElements` may be from different DOM levels
      return !anyElements.includes(element);
    });
  };

  /**
   * Selects dom elements by value of RegularSelector.
   *
   * @param regularSelectorNode RegularSelector node.
   * @param root Root DOM element.
   * @param specifiedSelector @see {@link SpecifiedSelector}.
   *
   * @returns Array of DOM elements.
   * @throws An error if RegularSelector node value is an invalid selector.
   */
  const getByRegularSelector = (regularSelectorNode, root, specifiedSelector) => {
    const selectorText = specifiedSelector ? specifiedSelector : getNodeValue(regularSelectorNode);
    let selectedElements = [];
    try {
      selectedElements = Array.from(root.querySelectorAll(selectorText));
    } catch (e) {
      // eslint-disable-line @typescript-eslint/no-explicit-any
      throw new Error(`Error: unable to select by '${selectorText}' — ${e.message}`);
    }
    return selectedElements;
  };

  /**
   * Returns list of dom elements filtered or selected by ExtendedSelector node.
   *
   * @param domElements Array of DOM elements.
   * @param extendedSelectorNode ExtendedSelector node.
   *
   * @returns Array of DOM elements.
   * @throws An error on unknown pseudo-class,
   * absent or invalid arg of extended pseudo-class, etc.
   */
  const getByExtendedSelector = (domElements, extendedSelectorNode) => {
    let foundElements = [];
    const extendedPseudoClassNode = getPseudoClassNode(extendedSelectorNode);
    const pseudoName = getNodeName(extendedPseudoClassNode);
    if (isAbsolutePseudoClass(pseudoName)) {
      // absolute extended pseudo-classes should have an argument
      const absolutePseudoArg = getNodeValue(extendedPseudoClassNode, `Missing arg for :${pseudoName}() pseudo-class`);
      if (pseudoName === NTH_ANCESTOR_PSEUDO_CLASS_MARKER) {
        // :nth-ancestor()
        foundElements = findByAbsolutePseudoPseudo.nthAncestor(domElements, absolutePseudoArg, pseudoName);
      } else if (pseudoName === XPATH_PSEUDO_CLASS_MARKER) {
        // :xpath()
        try {
          document.createExpression(absolutePseudoArg, null);
        } catch (e) {
          throw new Error(`Invalid argument of :${pseudoName}() pseudo-class: '${absolutePseudoArg}'`);
        }
        foundElements = findByAbsolutePseudoPseudo.xpath(domElements, absolutePseudoArg);
      } else if (pseudoName === UPWARD_PSEUDO_CLASS_MARKER) {
        // :upward()
        if (Number.isNaN(Number(absolutePseudoArg))) {
          // so arg is selector, not a number
          foundElements = findByAbsolutePseudoPseudo.upward(domElements, absolutePseudoArg);
        } else {
          foundElements = findByAbsolutePseudoPseudo.nthAncestor(domElements, absolutePseudoArg, pseudoName);
        }
      } else {
        // all other absolute extended pseudo-classes
        // e.g. contains, matches-attr, etc.
        foundElements = domElements.filter(element => {
          return isMatchedByAbsolutePseudo(element, pseudoName, absolutePseudoArg);
        });
      }
    } else if (isRelativePseudoClass(pseudoName)) {
      const relativeSelectorList = getRelativeSelectorListNode(extendedPseudoClassNode);
      let relativePredicate;
      switch (pseudoName) {
        case HAS_PSEUDO_CLASS_MARKER:
        case ABP_HAS_PSEUDO_CLASS_MARKER:
          relativePredicate = element => hasRelativesBySelectorList({
            element,
            relativeSelectorList,
            pseudoName
          });
          break;
        case IS_PSEUDO_CLASS_MARKER:
          relativePredicate = element => isAnyElementBySelectorList({
            element,
            relativeSelectorList,
            pseudoName
          });
          break;
        case NOT_PSEUDO_CLASS_MARKER:
          relativePredicate = element => notElementBySelectorList({
            element,
            relativeSelectorList,
            pseudoName
          });
          break;
        default:
          throw new Error(`Unknown relative pseudo-class: '${pseudoName}'`);
      }
      foundElements = domElements.filter(relativePredicate);
    } else {
      // extra check is parser missed something
      throw new Error(`Unknown extended pseudo-class: '${pseudoName}'`);
    }
    return foundElements;
  };

  /**
   * Returns list of dom elements which is selected by RegularSelector value.
   *
   * @param domElements Array of DOM elements.
   * @param regularSelectorNode RegularSelector node.
   *
   * @returns Array of DOM elements.
   * @throws An error if RegularSelector has not value.
   */
  const getByFollowingRegularSelector = (domElements, regularSelectorNode) => {
    // array of arrays because of Array.map() later
    let foundElements = [];
    const value = getNodeValue(regularSelectorNode);
    if (value.startsWith(CHILD_COMBINATOR)) {
      // e.g. div:has(> img) > .banner
      foundElements = domElements.map(root => {
        const specifiedSelector = `${SCOPE_CSS_PSEUDO_CLASS}${value}`;
        return getByRegularSelector(regularSelectorNode, root, specifiedSelector);
      });
    } else if (value.startsWith(NEXT_SIBLING_COMBINATOR) || value.startsWith(SUBSEQUENT_SIBLING_COMBINATOR)) {
      // e.g. div:has(> img) + .banner
      // or   div:has(> img) ~ .banner
      foundElements = domElements.map(element => {
        const rootElement = element.parentElement;
        if (!rootElement) {
          // do not throw error if there in no parent for element
          // e.g. '*:contains(text)' selects `html` which has no parentElement
          return [];
        }
        const elementSelectorText = getElementSelectorDesc(element);
        const specifiedSelector = `${scopeDirectChildren}${elementSelectorText}${value}`;
        const selected = getByRegularSelector(regularSelectorNode, rootElement, specifiedSelector);
        return selected;
      });
    } else {
      // space-separated regular selector after extended one
      // e.g. div:has(> img) .banner
      foundElements = domElements.map(root => {
        const specifiedSelector = `${scopeAnyChildren}${getNodeValue(regularSelectorNode)}`;
        return getByRegularSelector(regularSelectorNode, root, specifiedSelector);
      });
    }
    // foundElements should be flattened
    // as getByRegularSelector() returns elements array, and Array.map() collects them to array
    return flatten(foundElements);
  };

  /**
   * Returns elements nodes for Selector node.
   * As far as any selector always starts with regular part,
   * it selects by RegularSelector first and checks found elements later.
   *
   * Relative pseudo-classes has it's own subtree so getElementsForSelectorNode is called recursively.
   *
   * 'specifiedSelector' is needed for :has(), :is(), and :not() pseudo-classes
   * as native querySelectorAll() does not select exact element descendants even if it is called on 'div'
   * e.g. ':scope' specification is needed for proper descendants selection for 'div:has(> img)'.
   * So we check `divNode.querySelectorAll(':scope > img').length > 0`.
   *
   * @param selectorNode Selector node.
   * @param root Root DOM element.
   * @param specifiedSelector Needed element specification.
   *
   * @returns Array of DOM elements.
   * @throws An error if there is no selectorNodeChild.
   */
  const getElementsForSelectorNode = (selectorNode, root, specifiedSelector) => {
    let selectedElements = [];
    let i = 0;
    while (i < selectorNode.children.length) {
      const selectorNodeChild = getItemByIndex(selectorNode.children, i, 'selectorNodeChild should be specified');
      if (i === 0) {
        // any selector always starts with regular selector
        selectedElements = getByRegularSelector(selectorNodeChild, root, specifiedSelector);
      } else if (isExtendedSelectorNode(selectorNodeChild)) {
        // filter previously selected elements by next selector nodes
        selectedElements = getByExtendedSelector(selectedElements, selectorNodeChild);
      } else if (isRegularSelectorNode(selectorNodeChild)) {
        selectedElements = getByFollowingRegularSelector(selectedElements, selectorNodeChild);
      }
      i += 1;
    }
    return selectedElements;
  };

  /**
   * Selects elements by ast.
   *
   * @param ast Ast of parsed selector.
   * @param doc Document.
   *
   * @returns Array of DOM elements.
   */
  const selectElementsByAst = function (ast) {
    let doc = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : document;
    const selectedElements = [];
    // ast root is SelectorList node;
    // it has Selector nodes as children which should be processed separately
    ast.children.forEach(selectorNode => {
      selectedElements.push(...getElementsForSelectorNode(selectorNode, doc));
    });
    // selectedElements should be flattened as it is array of arrays with elements
    const uniqueElements = [...new Set(flatten(selectedElements))];
    return uniqueElements;
  };

  /**
   * Class of ExtCssDocument is needed for caching.
   * For making cache related to each new instance of class, not global.
   */
  class ExtCssDocument {
    /**
     * Cache with selectors and their AST parsing results.
     */

    /**
     * Creates new ExtCssDocument and inits new `astCache`.
     */
    constructor() {
      this.astCache = new Map();
    }

    /**
     * Saves selector and it's ast to cache.
     *
     * @param selector Standard or extended selector.
     * @param ast Selector ast.
     */
    saveAstToCache(selector, ast) {
      this.astCache.set(selector, ast);
    }

    /**
     * Returns ast from cache for given selector.
     *
     * @param selector Standard or extended selector.
     *
     * @returns Previously parsed ast found in cache, or null if not found.
     */
    getAstFromCache(selector) {
      const cachedAst = this.astCache.get(selector) || null;
      return cachedAst;
    }

    /**
     * Returns selector ast:
     * - if cached ast exists — returns it;
     * - if no cached ast — saves newly parsed ast to cache and returns it.
     *
     * @param selector Standard or extended selector.
     *
     * @returns Ast for `selector`.
     */
    getSelectorAst(selector) {
      let ast = this.getAstFromCache(selector);
      if (!ast) {
        ast = parse$1(selector);
      }
      this.saveAstToCache(selector, ast);
      return ast;
    }

    /**
     * Selects elements by selector.
     *
     * @param selector Standard or extended selector.
     *
     * @returns Array of DOM elements.
     */
    querySelectorAll(selector) {
      const ast = this.getSelectorAst(selector);
      return selectElementsByAst(ast);
    }
  }
  const extCssDocument = new ExtCssDocument();

  /**
   * Checks the presence of :remove() pseudo-class and validates it while parsing the selector part of css rule.
   *
   * @param rawSelector Selector which may contain :remove() pseudo-class.
   *
   * @returns Parsed selector data with selector and styles.
   * @throws An error on invalid :remove() position.
   */
  const parseRemoveSelector = rawSelector => {
    /**
     * No error will be thrown on invalid selector as it will be validated later
     * so it's better to explicitly specify 'any' selector for :remove() pseudo-class by '*',
     * e.g. '.banner > *:remove()' instead of '.banner > :remove()'.
     */

    // ':remove()'
    // eslint-disable-next-line max-len
    const VALID_REMOVE_MARKER = `${COLON}${REMOVE_PSEUDO_MARKER}${BRACKETS.PARENTHESES.LEFT}${BRACKETS.PARENTHESES.RIGHT}`;
    // ':remove(' - needed for validation rules like 'div:remove(2)'
    const INVALID_REMOVE_MARKER = `${COLON}${REMOVE_PSEUDO_MARKER}${BRACKETS.PARENTHESES.LEFT}`;
    let selector;
    let shouldRemove = false;
    const firstIndex = rawSelector.indexOf(VALID_REMOVE_MARKER);
    if (firstIndex === 0) {
      // e.g. ':remove()'
      throw new Error(`${REMOVE_ERROR_PREFIX.NO_TARGET_SELECTOR}: '${rawSelector}'`);
    } else if (firstIndex > 0) {
      if (firstIndex !== rawSelector.lastIndexOf(VALID_REMOVE_MARKER)) {
        // rule with more than one :remove() pseudo-class is invalid
        // e.g. '.block:remove() > .banner:remove()'
        throw new Error(`${REMOVE_ERROR_PREFIX.MULTIPLE_USAGE}: '${rawSelector}'`);
      } else if (firstIndex + VALID_REMOVE_MARKER.length < rawSelector.length) {
        // remove pseudo-class should be last in the rule
        // e.g. '.block:remove():upward(2)'
        throw new Error(`${REMOVE_ERROR_PREFIX.INVALID_POSITION}: '${rawSelector}'`);
      } else {
        // valid :remove() pseudo-class position
        selector = rawSelector.substring(0, firstIndex);
        shouldRemove = true;
      }
    } else if (rawSelector.includes(INVALID_REMOVE_MARKER)) {
      // it is not valid if ':remove()' is absent in rule but just ':remove(' is present
      // e.g. 'div:remove(0)'
      throw new Error(`${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${rawSelector}'`);
    } else {
      // there is no :remove() pseudo-class is rule
      selector = rawSelector;
    }
    const stylesOfSelector = shouldRemove ? [{
      property: REMOVE_PSEUDO_MARKER,
      value: String(shouldRemove)
    }] : [];
    return {
      selector,
      stylesOfSelector
    };
  };

  /**
   * Converts array of `entries` to object.
   * Object.fromEntries() polyfill because it is not supported by old browsers, e.g. Chrome 55.
   * Only first two elements of `entries` array matter, other will be skipped silently.
   *
   * @see {@link https://caniuse.com/?search=Object.fromEntries}
   *
   * @param entries Array of pairs.
   *
   * @returns Object converted from `entries`.
   */
  const getObjectFromEntries = entries => {
    const object = {};
    entries.forEach(el => {
      const [key, value] = el;
      object[key] = value;
    });
    return object;
  };

  const DEBUG_PSEUDO_PROPERTY_KEY = 'debug';
  const REGEXP_DECLARATION_END = /[;}]/g;
  const REGEXP_DECLARATION_DIVIDER = /[;:}]/g;
  const REGEXP_NON_WHITESPACE = /\S/g;

  // ExtendedCss does not support at-rules
  // https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
  const AT_RULE_MARKER = '@';
  /**
   * Init value for rawRuleData.
   */
  const initRawRuleData = {
    selector: ''
  };

  /**
   * Resets rule data buffer to init value after rule successfully collected.
   *
   * @param context Stylesheet parser context.
   */
  const restoreRuleAcc = context => {
    context.rawRuleData = initRawRuleData;
  };

  /**
   * Parses cropped selector part found before `{` previously.
   *
   * @param context Stylesheet parser context.
   * @param extCssDoc Needed for caching of selector ast.
   *
   * @returns Parsed validation data for cropped part of stylesheet which may be a selector.
   * @throws An error on unsupported CSS features, e.g. at-rules.
   */
  const parseSelectorPart = (context, extCssDoc) => {
    let selector = context.selectorBuffer.trim();
    if (selector.startsWith(AT_RULE_MARKER)) {
      throw new Error(`At-rules are not supported: '${selector}'.`);
    }
    let removeSelectorData;
    try {
      removeSelectorData = parseRemoveSelector(selector);
    } catch (e) {
      // eslint-disable-line @typescript-eslint/no-explicit-any
      logger.error(e.message);
      throw new Error(`${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${selector}'`);
    }
    if (context.nextIndex === -1) {
      if (selector === removeSelectorData.selector) {
        // rule should have style or pseudo-class :remove()
        throw new Error(`${STYLESHEET_ERROR_PREFIX.NO_STYLE_OR_REMOVE}: '${context.cssToParse}'`);
      }
      // stop parsing as there is no style declaration and selector parsed fine
      context.cssToParse = '';
    }
    let stylesOfSelector = [];
    let success = false;
    let ast;
    try {
      selector = removeSelectorData.selector;
      stylesOfSelector = removeSelectorData.stylesOfSelector;
      // validate found selector by parsing it to ast
      // so if it is invalid error will be thrown
      ast = extCssDoc.getSelectorAst(selector);
      success = true;
    } catch (e) {
      // eslint-disable-line @typescript-eslint/no-explicit-any
      success = false;
    }
    if (context.nextIndex > 0) {
      // slice found valid selector part off
      // and parse rest of stylesheet later
      context.cssToParse = context.cssToParse.slice(context.nextIndex);
    }
    return {
      success,
      selector,
      ast,
      stylesOfSelector
    };
  };

  /**
   * Recursively parses style declaration string into `Style`s.
   *
   * @param context Stylesheet parser context.
   * @param styles Array of styles.
   *
   * @throws An error on invalid style declaration.
   * @returns A number index of the next `}` in `this.cssToParse`.
   */
  const parseUntilClosingBracket = (context, styles) => {
    // Expects ":", ";", and "}".
    REGEXP_DECLARATION_DIVIDER.lastIndex = context.nextIndex;
    let match = REGEXP_DECLARATION_DIVIDER.exec(context.cssToParse);
    if (match === null) {
      throw new Error(`${STYLESHEET_ERROR_PREFIX.INVALID_STYLE}: '${context.cssToParse}'`);
    }
    let matchPos = match.index;
    let matched = match[0];
    if (matched === BRACKETS.CURLY.RIGHT) {
      const declarationChunk = context.cssToParse.slice(context.nextIndex, matchPos);
      if (declarationChunk.trim().length === 0) {
        // empty style declaration
        // e.g. 'div { }'
        if (styles.length === 0) {
          throw new Error(`${STYLESHEET_ERROR_PREFIX.NO_STYLE}: '${context.cssToParse}'`);
        }
        // else valid style parsed before it
        // e.g. '{ display: none; }' -- position is after ';'
      } else {
        // closing curly bracket '}' is matched before colon ':'
        // trimmed declarationChunk is not a space, between ';' and '}',
        // e.g. 'visible }' in style '{ display: none; visible }' after part before ';' is parsed
        throw new Error(`${STYLESHEET_ERROR_PREFIX.INVALID_STYLE}: '${context.cssToParse}'`);
      }
      return matchPos;
    }
    if (matched === COLON) {
      const colonIndex = matchPos;
      // Expects ";" and "}".
      REGEXP_DECLARATION_END.lastIndex = colonIndex;
      match = REGEXP_DECLARATION_END.exec(context.cssToParse);
      if (match === null) {
        throw new Error(`${STYLESHEET_ERROR_PREFIX.UNCLOSED_STYLE}: '${context.cssToParse}'`);
      }
      matchPos = match.index;
      matched = match[0];
      // Populates the `styleMap` key-value map.
      const property = context.cssToParse.slice(context.nextIndex, colonIndex).trim();
      if (property.length === 0) {
        throw new Error(`${STYLESHEET_ERROR_PREFIX.NO_PROPERTY}: '${context.cssToParse}'`);
      }
      const value = context.cssToParse.slice(colonIndex + 1, matchPos).trim();
      if (value.length === 0) {
        throw new Error(`${STYLESHEET_ERROR_PREFIX.NO_VALUE}: '${context.cssToParse}'`);
      }
      styles.push({
        property,
        value
      });
      // finish style parsing if '}' is found
      // e.g. '{ display: none }' -- no ';' at the end of declaration
      if (matched === BRACKETS.CURLY.RIGHT) {
        return matchPos;
      }
    }
    // matchPos is the position of the next ';'
    // crop 'cssToParse' and re-run the loop
    context.cssToParse = context.cssToParse.slice(matchPos + 1);
    context.nextIndex = 0;
    return parseUntilClosingBracket(context, styles); // Should be a subject of tail-call optimization.
  };

  /**
   * Parses next style declaration part in stylesheet.
   *
   * @param context Stylesheet parser context.
   *
   * @returns Array of style data objects.
   */
  const parseNextStyle = context => {
    const styles = [];
    const styleEndPos = parseUntilClosingBracket(context, styles);

    // find next rule after the style declaration
    REGEXP_NON_WHITESPACE.lastIndex = styleEndPos + 1;
    const match = REGEXP_NON_WHITESPACE.exec(context.cssToParse);
    if (match === null) {
      context.cssToParse = '';
      return styles;
    }
    const matchPos = match.index;

    // cut out matched style declaration for previous selector
    context.cssToParse = context.cssToParse.slice(matchPos);
    return styles;
  };

  /**
   * Checks whether the 'remove' property positively set in styles
   * with only one positive value - 'true'.
   *
   * @param styles Array of styles.
   *
   * @returns True if there is 'remove' property with 'true' value in `styles`.
   */
  const isRemoveSetInStyles = styles => {
    return styles.some(s => {
      return s.property === REMOVE_PSEUDO_MARKER && s.value === PSEUDO_PROPERTY_POSITIVE_VALUE;
    });
  };

  /**
   * Returns valid 'debug' property value set in styles
   * where possible values are 'true' and 'global'.
   *
   * @param styles Array of styles.
   *
   * @returns Value of 'debug' property if it is set in `styles`,
   * or `undefined` if the property is not found.
   */
  const getDebugStyleValue = styles => {
    const debugStyle = styles.find(s => {
      return s.property === DEBUG_PSEUDO_PROPERTY_KEY;
    });
    return debugStyle === null || debugStyle === void 0 ? void 0 : debugStyle.value;
  };

  /**
   * Prepares final RuleData.
   *
   * @param selector String selector.
   * @param ast Parsed ast.
   * @param rawStyles Array of previously collected styles which may contain 'remove' and 'debug'.
   *
   * @returns Parsed ExtendedCss rule data.
   */
  const prepareRuleData = (selector, ast, rawStyles) => {
    const ruleData = {
      selector,
      ast
    };
    const debugValue = getDebugStyleValue(rawStyles);
    const shouldRemove = isRemoveSetInStyles(rawStyles);
    let styles = rawStyles;
    if (debugValue) {
      // get rid of 'debug' from styles
      styles = rawStyles.filter(s => s.property !== DEBUG_PSEUDO_PROPERTY_KEY);
      // and set it as separate property only if its value is valid
      // which is 'true' or 'global'
      if (debugValue === PSEUDO_PROPERTY_POSITIVE_VALUE || debugValue === DEBUG_PSEUDO_PROPERTY_GLOBAL_VALUE) {
        ruleData.debug = debugValue;
      }
    }
    if (shouldRemove) {
      // no other styles are needed to apply if 'remove' is set
      ruleData.style = {
        [REMOVE_PSEUDO_MARKER]: PSEUDO_PROPERTY_POSITIVE_VALUE
      };

      /**
       * 'content' property is needed for ExtCssConfiguration.beforeStyleApplied().
       *
       * @see {@link BeforeStyleAppliedCallback}
       */
      const contentStyle = styles.find(s => s.property === CONTENT_CSS_PROPERTY);
      if (contentStyle) {
        ruleData.style[CONTENT_CSS_PROPERTY] = contentStyle.value;
      }
    } else {
      // otherwise all styles should be applied.
      // every style property will be unique because of their converting into object
      if (styles.length > 0) {
        const stylesAsEntries = styles.map(style => {
          const {
            property,
            value
          } = style;
          return [property, value];
        });
        const preparedStyleData = getObjectFromEntries(stylesAsEntries);
        ruleData.style = preparedStyleData;
      }
    }
    return ruleData;
  };

  /**
   * Saves rules data for unique selectors.
   *
   * @param rawResults Previously collected results of parsing.
   * @param rawRuleData Parsed rule data.
   *
   * @throws An error if there is no rawRuleData.styles or rawRuleData.ast.
   */
  const saveToRawResults = (rawResults, rawRuleData) => {
    const {
      selector,
      ast,
      styles
    } = rawRuleData;
    if (!styles) {
      throw new Error(`No style declaration for selector: '${selector}'`);
    }
    if (!ast) {
      throw new Error(`No ast parsed for selector: '${selector}'`);
    }
    const storedRuleData = rawResults.get(selector);
    if (!storedRuleData) {
      rawResults.set(selector, {
        ast,
        styles
      });
    } else {
      storedRuleData.styles.push(...styles);
    }
  };

  /**
   * Parses stylesheet of rules into rules data objects (non-recursively):
   * 1. Iterates through stylesheet string.
   * 2. Finds first `{` which can be style declaration start or part of selector.
   * 3. Validates found string part via selector parser; and if:
   *  - it throws error — saves string part to buffer as part of selector,
   *    slice next stylesheet part to `{` [2] and validates again [3];
   *  - no error — saves found string part as selector and starts to parse styles (recursively).
   *
   * @param rawStylesheet Raw stylesheet as string.
   * @param extCssDoc ExtCssDocument which uses cache while selectors parsing.
   * @throws An error on unsupported CSS features, e.g. comments, or invalid stylesheet syntax.
   * @returns Array of rules data which contains:
   * - selector as string;
   * - ast to query elements by;
   * - map of styles to apply.
   */
  const parse = (rawStylesheet, extCssDoc) => {
    const stylesheet = rawStylesheet.trim();
    if (stylesheet.includes(`${SLASH}${ASTERISK}`) && stylesheet.includes(`${ASTERISK}${SLASH}`)) {
      throw new Error(`${STYLESHEET_ERROR_PREFIX.NO_COMMENT}: '${stylesheet}'`);
    }
    const context = {
      // any stylesheet should start with selector
      isSelector: true,
      // init value of parser position
      nextIndex: 0,
      // init value of cssToParse
      cssToParse: stylesheet,
      // buffer for collecting selector part
      selectorBuffer: '',
      // accumulator for rules
      rawRuleData: initRawRuleData
    };
    const rawResults = new Map();
    let selectorData;

    // context.cssToParse is going to be cropped while its parsing
    while (context.cssToParse) {
      if (context.isSelector) {
        // find index of first opening curly bracket
        // which may mean start of style part and end of selector one
        context.nextIndex = context.cssToParse.indexOf(BRACKETS.CURLY.LEFT);
        // rule should not start with style, selector is required
        // e.g. '{ display: none; }'
        if (context.selectorBuffer.length === 0 && context.nextIndex === 0) {
          throw new Error(`${STYLESHEET_ERROR_PREFIX.NO_SELECTOR}: '${context.cssToParse}'`);
        }
        if (context.nextIndex === -1) {
          // no style declaration in rule
          // but rule still may contain :remove() pseudo-class
          context.selectorBuffer = context.cssToParse;
        } else {
          // collect string parts before opening curly bracket
          // until valid selector collected
          context.selectorBuffer += context.cssToParse.slice(0, context.nextIndex);
        }
        selectorData = parseSelectorPart(context, extCssDoc);
        if (selectorData.success) {
          // selector successfully parsed
          context.rawRuleData.selector = selectorData.selector.trim();
          context.rawRuleData.ast = selectorData.ast;
          context.rawRuleData.styles = selectorData.stylesOfSelector;
          context.isSelector = false;
          // save rule data if there is no style declaration
          if (context.nextIndex === -1) {
            saveToRawResults(rawResults, context.rawRuleData);
            // clean up ruleContext
            restoreRuleAcc(context);
          } else {
            // skip the opening curly bracket at the start of style declaration part
            context.nextIndex = 1;
            context.selectorBuffer = '';
          }
        } else {
          // if selector was not successfully parsed parseSelectorPart(), continue stylesheet parsing:
          // save the found bracket to buffer and proceed to next loop iteration
          context.selectorBuffer += BRACKETS.CURLY.LEFT;
          // delete `{` from cssToParse
          context.cssToParse = context.cssToParse.slice(1);
        }
      } else {
        var _context$rawRuleData$;
        // style declaration should be parsed
        const parsedStyles = parseNextStyle(context);

        // styles can be parsed from selector part if it has :remove() pseudo-class
        // e.g. '.banner:remove() { debug: true; }'
        (_context$rawRuleData$ = context.rawRuleData.styles) === null || _context$rawRuleData$ === void 0 ? void 0 : _context$rawRuleData$.push(...parsedStyles);

        // save rule data to results
        saveToRawResults(rawResults, context.rawRuleData);
        context.nextIndex = 0;

        // clean up ruleContext
        restoreRuleAcc(context);

        // parse next rule selector after style successfully parsed
        context.isSelector = true;
      }
    }
    const results = [];
    rawResults.forEach((value, key) => {
      const selector = key;
      const {
        ast,
        styles: rawStyles
      } = value;
      results.push(prepareRuleData(selector, ast, rawStyles));
    });
    return results;
  };

  /**
   * Checks whether passed `arg` is number type.
   *
   * @param arg Value to check.
   *
   * @returns True if `arg` is number and not NaN.
   */
  const isNumber = arg => {
    return typeof arg === 'number' && !Number.isNaN(arg);
  };

  const isSupported = typeof window.requestAnimationFrame !== 'undefined';
  const timeout = isSupported ? requestAnimationFrame : window.setTimeout;
  const deleteTimeout = isSupported ? cancelAnimationFrame : clearTimeout;
  const perf = isSupported ? performance : Date;
  const DEFAULT_THROTTLE_DELAY_MS = 150;
  /**
   * The purpose of ThrottleWrapper is to throttle calls of the function
   * that applies ExtendedCss rules. The reasoning here is that the function calls
   * are triggered by MutationObserver and there may be many mutations in a short period of time.
   * We do not want to apply rules on every mutation so we use this helper to make sure
   * that there is only one call in the given amount of time.
   */
  class ThrottleWrapper {
    /**
     * The provided callback should be executed twice in this time frame:
     * very first time and not more often than throttleDelayMs for further executions.
     *
     * @see {@link ThrottleWrapper.run}
     */

    /**
     * Creates new ThrottleWrapper.
     *
     * @param context ExtendedCss context.
     * @param callback The callback.
     * @param throttleMs Throttle delay in ms.
     */
    constructor(context, callback, throttleMs) {
      this.context = context;
      this.callback = callback;
      this.throttleDelayMs = throttleMs || DEFAULT_THROTTLE_DELAY_MS;
      this.wrappedCb = this.wrappedCallback.bind(this);
    }

    /**
     * Wraps the callback (which supposed to be `applyRules`),
     * needed to update `lastRunTime` and clean previous timeouts for proper execution of the callback.
     *
     * @param timestamp Timestamp.
     */
    wrappedCallback(timestamp) {
      this.lastRunTime = isNumber(timestamp) ? timestamp : perf.now();
      // `timeoutId` can be requestAnimationFrame-related
      // so cancelAnimationFrame() as deleteTimeout() needs the arg to be defined
      if (this.timeoutId) {
        deleteTimeout(this.timeoutId);
        delete this.timeoutId;
      }
      clearTimeout(this.timerId);
      delete this.timerId;
      if (this.callback) {
        this.callback(this.context);
      }
    }

    /**
     * Indicates whether there is a scheduled callback.
     *
     * @returns True if scheduled callback exists.
     */
    hasPendingCallback() {
      return isNumber(this.timeoutId) || isNumber(this.timerId);
    }

    /**
     * Schedules the function which applies ExtendedCss rules before the next animation frame.
     *
     * Wraps function execution into `timeout` — requestAnimationFrame or setTimeout.
     * For the first time runs the function without any condition.
     * As it may be triggered by any mutation which may occur too ofter, we limit the function execution:
     * 1. If `elapsedTime` since last function execution is less then set `throttleDelayMs`,
     * next function call is hold till the end of throttle interval (subtracting `elapsed` from `throttleDelayMs`);
     * 2. Do nothing if triggered again but function call which is on hold has not yet started its execution.
     */
    run() {
      if (this.hasPendingCallback()) {
        // there is a pending execution scheduled
        return;
      }
      if (typeof this.lastRunTime !== 'undefined') {
        const elapsedTime = perf.now() - this.lastRunTime;
        if (elapsedTime < this.throttleDelayMs) {
          this.timerId = window.setTimeout(this.wrappedCb, this.throttleDelayMs - elapsedTime);
          return;
        }
      }
      this.timeoutId = timeout(this.wrappedCb);
    }

    /**
     * Returns timestamp for 'now'.
     *
     * @returns Timestamp.
     */
    static now() {
      return perf.now();
    }
  }

  const LAST_EVENT_TIMEOUT_MS = 10;
  const IGNORED_EVENTS = ['mouseover', 'mouseleave', 'mouseenter', 'mouseout'];
  const SUPPORTED_EVENTS = [
  // keyboard events
  'keydown', 'keypress', 'keyup',
  // mouse events
  'auxclick', 'click', 'contextmenu', 'dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseover', 'mouseout', 'mouseup', 'pointerlockchange', 'pointerlockerror', 'select', 'wheel'];

  // 'wheel' event makes scrolling in Safari twitchy
  // https://github.com/AdguardTeam/ExtendedCss/issues/120
  const SAFARI_PROBLEMATIC_EVENTS = ['wheel'];

  /**
   * We use EventTracker to track the event that is likely to cause the mutation.
   * The problem is that we cannot use `window.event` directly from the mutation observer call
   * as we're not in the event handler context anymore.
   */
  class EventTracker {
    /**
     * Creates new EventTracker.
     */
    constructor() {
      _defineProperty(this, "getLastEventType", () => this.lastEventType);
      _defineProperty(this, "getTimeSinceLastEvent", () => {
        if (!this.lastEventTime) {
          return null;
        }
        return Date.now() - this.lastEventTime;
      });
      this.trackedEvents = isSafariBrowser ? SUPPORTED_EVENTS.filter(event => !SAFARI_PROBLEMATIC_EVENTS.includes(event)) : SUPPORTED_EVENTS;
      this.trackedEvents.forEach(eventName => {
        document.documentElement.addEventListener(eventName, this.trackEvent, true);
      });
    }

    /**
     * Callback for event listener for events tracking.
     *
     * @param event Any event.
     */
    trackEvent(event) {
      this.lastEventType = event.type;
      this.lastEventTime = Date.now();
    }
    /**
     * Checks whether the last caught event should be ignored.
     *
     * @returns True if event should be ignored.
     */
    isIgnoredEventType() {
      const lastEventType = this.getLastEventType();
      const sinceLastEventTime = this.getTimeSinceLastEvent();
      return !!lastEventType && IGNORED_EVENTS.includes(lastEventType) && !!sinceLastEventTime && sinceLastEventTime < LAST_EVENT_TIMEOUT_MS;
    }

    /**
     * Stops event tracking by removing event listener.
     */
    stopTracking() {
      this.trackedEvents.forEach(eventName => {
        document.documentElement.removeEventListener(eventName, this.trackEvent, true);
      });
    }
  }

  const isEventListenerSupported = typeof window.addEventListener !== 'undefined';
  const observeDocument = (context, callback) => {
    // We are trying to limit the number of callback calls by not calling it on all kind of "hover" events.
    // The rationale behind this is that "hover" events often cause attributes modification,
    // but re-applying extCSS rules will be useless as these attribute changes are usually transient.
    const shouldIgnoreMutations = mutations => {
      // ignore if all mutations are about attributes changes
      return mutations.every(m => m.type === 'attributes');
    };
    if (natives.MutationObserver) {
      context.domMutationObserver = new natives.MutationObserver(mutations => {
        if (!mutations || mutations.length === 0) {
          return;
        }
        const eventTracker = new EventTracker();
        if (eventTracker.isIgnoredEventType() && shouldIgnoreMutations(mutations)) {
          return;
        }
        // save instance of EventTracker to context
        // for removing its event listeners on disconnectDocument() while mainDisconnect()
        context.eventTracker = eventTracker;
        callback();
      });
      context.domMutationObserver.observe(document, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['id', 'class']
      });
    } else if (isEventListenerSupported) {
      document.addEventListener('DOMNodeInserted', callback, false);
      document.addEventListener('DOMNodeRemoved', callback, false);
      document.addEventListener('DOMAttrModified', callback, false);
    }
  };
  const disconnectDocument = (context, callback) => {
    var _context$eventTracker;
    if (context.domMutationObserver) {
      context.domMutationObserver.disconnect();
    } else if (isEventListenerSupported) {
      document.removeEventListener('DOMNodeInserted', callback, false);
      document.removeEventListener('DOMNodeRemoved', callback, false);
      document.removeEventListener('DOMAttrModified', callback, false);
    }
    // clean up event listeners
    (_context$eventTracker = context.eventTracker) === null || _context$eventTracker === void 0 ? void 0 : _context$eventTracker.stopTracking();
  };
  const mainObserve = (context, mainCallback) => {
    if (context.isDomObserved) {
      return;
    }
    // handle dynamically added elements
    context.isDomObserved = true;
    observeDocument(context, mainCallback);
  };
  const mainDisconnect = (context, mainCallback) => {
    if (!context.isDomObserved) {
      return;
    }
    context.isDomObserved = false;
    disconnectDocument(context, mainCallback);
  };

  // added by tsurlfilter's CssHitsCounter
  const CONTENT_ATTR_PREFIX_REGEXP = /^("|')adguard.+?/;

  /**
   * Removes affectedElement.node from DOM.
   *
   * @param context ExtendedCss context.
   * @param affectedElement Affected element.
   */
  const removeElement = (context, affectedElement) => {
    const {
      node
    } = affectedElement;
    affectedElement.removed = true;
    const elementSelector = getElementSelectorPath(node);

    // check if the element has been already removed earlier
    const elementRemovalsCounter = context.removalsStatistic[elementSelector] || 0;

    // if removals attempts happened more than specified we do not try to remove node again
    if (elementRemovalsCounter > MAX_STYLE_PROTECTION_COUNT) {
      logger.error(`ExtendedCss: infinite loop protection for selector: '${elementSelector}'`);
      return;
    }
    if (node.parentElement) {
      node.parentElement.removeChild(node);
      context.removalsStatistic[elementSelector] = elementRemovalsCounter + 1;
    }
  };

  /**
   * Sets style to the specified DOM node.
   *
   * @param node DOM element.
   * @param style Style to set.
   */
  const setStyleToElement = (node, style) => {
    if (!(node instanceof HTMLElement)) {
      return;
    }
    Object.keys(style).forEach(prop => {
      // Apply this style only to existing properties
      // We cannot use hasOwnProperty here (does not work in FF)
      if (typeof node.style.getPropertyValue(prop.toString()) !== 'undefined') {
        let value = style[prop];
        if (!value) {
          return;
        }
        // do not apply 'content' style given by tsurlfilter
        // which is needed only for BeforeStyleAppliedCallback
        if (prop === CONTENT_CSS_PROPERTY && value.match(CONTENT_ATTR_PREFIX_REGEXP)) {
          return;
        }
        // First we should remove !important attribute (or it won't be applied')
        value = removeSuffix(value.trim(), '!important').trim();
        node.style.setProperty(prop, value, 'important');
      }
    });
  };

  /**
   * Applies style to the specified DOM node.
   *
   * @param context ExtendedCss context.
   * @param affectedElement Object containing DOM node and rule to be applied.
   *
   * @throws An error if affectedElement has no style to apply.
   */
  const applyStyle = (context, affectedElement) => {
    if (affectedElement.protectionObserver) {
      // style is already applied and protected by the observer
      return;
    }
    if (context.beforeStyleApplied) {
      affectedElement = context.beforeStyleApplied(affectedElement);
      if (!affectedElement) {
        return;
      }
    }
    const {
      node,
      rules
    } = affectedElement;
    for (let i = 0; i < rules.length; i += 1) {
      const rule = rules[i];
      const selector = rule === null || rule === void 0 ? void 0 : rule.selector;
      const style = rule === null || rule === void 0 ? void 0 : rule.style;
      const debug = rule === null || rule === void 0 ? void 0 : rule.debug;
      // rule may not have style to apply
      // e.g. 'div:has(> a) { debug: true }' -> means no style to apply, and enable debug mode
      if (style) {
        if (style[REMOVE_PSEUDO_MARKER] === PSEUDO_PROPERTY_POSITIVE_VALUE) {
          removeElement(context, affectedElement);
          return;
        }
        setStyleToElement(node, style);
      } else if (!debug) {
        // but rule should not have both style and debug properties
        throw new Error(`No style declaration in rule for selector: '${selector}'`);
      }
    }
  };

  /**
   * Reverts style for the affected object.
   *
   * @param affectedElement Affected element.
   */
  const revertStyle = affectedElement => {
    if (affectedElement.protectionObserver) {
      affectedElement.protectionObserver.disconnect();
    }
    affectedElement.node.style.cssText = affectedElement.originalStyle;
  };

  /**
   * ExtMutationObserver is a wrapper over regular MutationObserver with one additional function:
   * it keeps track of the number of times we called the "ProtectionCallback".
   *
   * We use an instance of this to monitor styles added by ExtendedCss
   * and to make sure these styles are recovered if the page script attempts to modify them.
   *
   * However, we want to avoid endless loops of modification if the page script repeatedly modifies the styles.
   * So we keep track of the number of calls and observe() makes a decision
   * whether to continue recovering the styles or not.
   */
  class ExtMutationObserver {
    /**
     * Extra property for keeping 'style fix counts'.
     */

    /**
     * Creates new ExtMutationObserver.
     *
     * @param protectionCallback Callback which execution should be counted.
     */
    constructor(protectionCallback) {
      this.styleProtectionCount = 0;
      this.observer = new natives.MutationObserver(mutations => {
        if (!mutations.length) {
          return;
        }
        this.styleProtectionCount += 1;
        protectionCallback(mutations, this);
      });
    }

    /**
     * Starts to observe target element,
     * prevents infinite loop of observing due to the limited number of times of callback runs.
     *
     * @param target Target to observe.
     * @param options Mutation observer options.
     */
    observe(target, options) {
      if (this.styleProtectionCount < MAX_STYLE_PROTECTION_COUNT) {
        this.observer.observe(target, options);
      } else {
        logger.error('ExtendedCss: infinite loop protection for style');
      }
    }

    /**
     * Stops ExtMutationObserver from observing any mutations.
     * Until the `observe()` is used again, `protectionCallback` will not be invoked.
     */
    disconnect() {
      this.observer.disconnect();
    }
  }

  const PROTECTION_OBSERVER_OPTIONS = {
    attributes: true,
    attributeOldValue: true,
    attributeFilter: ['style']
  };

  /**
   * Creates MutationObserver protection callback.
   *
   * @param styles Styles data object.
   *
   * @returns Callback for styles protection.
   */
  const createProtectionCallback = styles => {
    const protectionCallback = (mutations, extObserver) => {
      if (!mutations[0]) {
        return;
      }
      const {
        target
      } = mutations[0];
      extObserver.disconnect();
      styles.forEach(style => {
        setStyleToElement(target, style);
      });
      extObserver.observe(target, PROTECTION_OBSERVER_OPTIONS);
    };
    return protectionCallback;
  };

  /**
   * Sets up a MutationObserver which protects style attributes from changes.
   *
   * @param node DOM node.
   * @param rules Rule data objects.
   * @returns Mutation observer used to protect attribute or null if there's nothing to protect.
   */
  const protectStyleAttribute = (node, rules) => {
    if (!natives.MutationObserver) {
      return null;
    }
    const styles = [];
    rules.forEach(ruleData => {
      const {
        style
      } = ruleData;
      // some rules might have only debug property in style declaration
      // e.g. 'div:has(> a) { debug: true }' -> parsed to boolean `ruleData.debug`
      // so no style is fine, and here we should collect only valid styles to protect
      if (style) {
        styles.push(style);
      }
    });
    const protectionObserver = new ExtMutationObserver(createProtectionCallback(styles));
    protectionObserver.observe(node, PROTECTION_OBSERVER_OPTIONS);
    return protectionObserver;
  };

  const STATS_DECIMAL_DIGITS_COUNT = 4;
  /**
   * A helper class for applied rule stats.
   */
  class TimingStats {
    /**
     * Creates new TimingStats.
     */
    constructor() {
      this.appliesTimings = [];
      this.appliesCount = 0;
      this.timingsSum = 0;
      this.meanTiming = 0;
      this.squaredSum = 0;
      this.standardDeviation = 0;
    }

    /**
     * Observe target element and mark observer as active.
     *
     * @param elapsedTimeMs Time in ms.
     */
    push(elapsedTimeMs) {
      this.appliesTimings.push(elapsedTimeMs);
      this.appliesCount += 1;
      this.timingsSum += elapsedTimeMs;
      this.meanTiming = this.timingsSum / this.appliesCount;
      this.squaredSum += elapsedTimeMs * elapsedTimeMs;
      this.standardDeviation = Math.sqrt(this.squaredSum / this.appliesCount - Math.pow(this.meanTiming, 2));
    }
  }
  /**
   * Makes the timestamps more readable.
   *
   * @param timestamp Raw timestamp.
   *
   * @returns Fine-looking timestamps.
   */
  const beautifyTimingNumber = timestamp => {
    return Number(timestamp.toFixed(STATS_DECIMAL_DIGITS_COUNT));
  };

  /**
   * Improves timing stats readability.
   *
   * @param rawTimings Collected timings with raw timestamp.
   *
   * @returns Fine-looking timing stats.
   */
  const beautifyTimings = rawTimings => {
    return {
      appliesTimings: rawTimings.appliesTimings.map(t => beautifyTimingNumber(t)),
      appliesCount: beautifyTimingNumber(rawTimings.appliesCount),
      timingsSum: beautifyTimingNumber(rawTimings.timingsSum),
      meanTiming: beautifyTimingNumber(rawTimings.meanTiming),
      standardDeviation: beautifyTimingNumber(rawTimings.standardDeviation)
    };
  };

  /**
   * Prints timing information if debugging mode is enabled.
   *
   * @param context ExtendedCss context.
   */
  const printTimingInfo = context => {
    if (context.areTimingsPrinted) {
      return;
    }
    context.areTimingsPrinted = true;
    const timingsLogData = {};
    context.parsedRules.forEach(ruleData => {
      if (ruleData.timingStats) {
        const {
          selector,
          style,
          debug,
          matchedElements
        } = ruleData;
        // style declaration for some rules is parsed to debug property and no style to apply
        // e.g. 'div:has(> a) { debug: true }'
        if (!style && !debug) {
          throw new Error(`Rule should have style declaration for selector: '${selector}'`);
        }
        const selectorData = {
          selectorParsed: selector,
          timings: beautifyTimings(ruleData.timingStats)
        };
        // `ruleData.style` may contain `remove` pseudo-property
        // and make logs look better
        if (style && style[REMOVE_PSEUDO_MARKER] === PSEUDO_PROPERTY_POSITIVE_VALUE) {
          selectorData.removed = true;
          // no matchedElements for such case as they are removed after ExtendedCss applied
        } else {
          selectorData.styleApplied = style || null;
          selectorData.matchedElements = matchedElements;
        }
        timingsLogData[selector] = selectorData;
      }
    });
    if (Object.keys(timingsLogData).length === 0) {
      return;
    }
    // add location.href to the message to distinguish frames
    logger.info('[ExtendedCss] Timings in milliseconds for %o:\n%o', window.location.href, timingsLogData);
  };

  /**
   * Finds affectedElement object for the specified DOM node.
   *
   * @param affElements Array of affected elements — context.affectedElements.
   * @param domNode DOM node.
   * @returns Found affectedElement or undefined.
   */
  const findAffectedElement = (affElements, domNode) => {
    return affElements.find(affEl => affEl.node === domNode);
  };

  /**
   * Applies specified rule and returns list of elements affected.
   *
   * @param context ExtendedCss context.
   * @param ruleData Rule to apply.
   * @returns List of elements affected by the rule.
   */
  const applyRule = (context, ruleData) => {
    // debugging mode can be enabled in two ways:
    // 1. for separate rules - by `{ debug: true; }`
    // 2. for all rules simultaneously by:
    //   - `{ debug: global; }` in any rule
    //   - positive `debug` property in ExtCssConfiguration
    const isDebuggingMode = !!ruleData.debug || context.debug;
    let startTime;
    if (isDebuggingMode) {
      startTime = ThrottleWrapper.now();
    }
    const {
      ast
    } = ruleData;
    const nodes = selectElementsByAst(ast);
    nodes.forEach(node => {
      let affectedElement = findAffectedElement(context.affectedElements, node);
      if (affectedElement) {
        affectedElement.rules.push(ruleData);
        applyStyle(context, affectedElement);
      } else {
        // Applying style first time
        const originalStyle = node.style.cssText;
        affectedElement = {
          node,
          // affected DOM node
          rules: [ruleData],
          // rule to be applied
          originalStyle,
          // original node style
          protectionObserver: null // style attribute observer
        };

        applyStyle(context, affectedElement);
        context.affectedElements.push(affectedElement);
      }
    });
    if (isDebuggingMode && startTime) {
      const elapsedTimeMs = ThrottleWrapper.now() - startTime;
      if (!ruleData.timingStats) {
        ruleData.timingStats = new TimingStats();
      }
      ruleData.timingStats.push(elapsedTimeMs);
    }
    return nodes;
  };

  /**
   * Applies filtering rules.
   *
   * @param context ExtendedCss context.
   */
  const applyRules = context => {
    const newSelectedElements = [];
    // some rules could make call - selector.querySelectorAll() temporarily to change node id attribute
    // this caused MutationObserver to call recursively
    // https://github.com/AdguardTeam/ExtendedCss/issues/81
    mainDisconnect(context, context.mainCallback);
    context.parsedRules.forEach(ruleData => {
      const nodes = applyRule(context, ruleData);
      Array.prototype.push.apply(newSelectedElements, nodes);
      // save matched elements to ruleData as linked to applied rule
      // only for debugging purposes
      if (ruleData.debug) {
        ruleData.matchedElements = nodes;
      }
    });
    // Now revert styles for elements which are no more affected
    let affLength = context.affectedElements.length;
    // do nothing if there is no elements to process
    while (affLength) {
      const affectedElement = context.affectedElements[affLength - 1];
      if (!affectedElement) {
        break;
      }
      if (!newSelectedElements.includes(affectedElement.node)) {
        // Time to revert style
        revertStyle(affectedElement);
        context.affectedElements.splice(affLength - 1, 1);
      } else if (!affectedElement.removed) {
        // Add style protection observer
        // Protect "style" attribute from changes
        if (!affectedElement.protectionObserver) {
          affectedElement.protectionObserver = protectStyleAttribute(affectedElement.node, affectedElement.rules);
        }
      }
      affLength -= 1;
    }
    // After styles are applied we can start observe again
    mainObserve(context, context.mainCallback);
    printTimingInfo(context);
  };

  /**
   * Throttle timeout for ThrottleWrapper to execute applyRules().
   */
  const APPLY_RULES_DELAY = 150;

  /**
   * Result of selector validation.
   */

  /**
   * Main class of ExtendedCss lib.
   *
   * Parses css stylesheet with any selectors (passed to its argument as styleSheet),
   * and guarantee its applying as mutation observer is used to prevent the restyling of needed elements by other scripts.
   * This style protection is limited to 50 times to avoid infinite loop (MAX_STYLE_PROTECTION_COUNT).
   * Our own ThrottleWrapper is used for styles applying to avoid too often lib reactions on page mutations.
   *
   * Constructor creates the instance of class which should be run be `apply()` method to apply the rules,
   * and the applying can be stopped by `dispose()`.
   *
   * Can be used to select page elements by selector with `query()` method (similar to `Document.querySelectorAll()`),
   * which does not require instance creating.
   */
  class ExtendedCss {
    /**
     * Creates new ExtendedCss.
     *
     * @param configuration ExtendedCss configuration.
     */
    constructor(configuration) {
      if (!isBrowserSupported()) {
        throw new Error('Browser is not supported by ExtendedCss.');
      }
      if (!configuration) {
        throw new Error('ExtendedCss configuration should be provided.');
      }
      this.context = {
        beforeStyleApplied: configuration.beforeStyleApplied,
        debug: false,
        affectedElements: [],
        isDomObserved: false,
        removalsStatistic: {},
        parsedRules: parse(configuration.styleSheet, extCssDocument),
        mainCallback: () => {}
      };

      // true if set in configuration
      // or any rule in styleSheet has `debug: global`
      this.context.debug = configuration.debug || this.context.parsedRules.some(ruleData => {
        return ruleData.debug === DEBUG_PSEUDO_PROPERTY_GLOBAL_VALUE;
      });
      this.applyRulesScheduler = new ThrottleWrapper(this.context, applyRules, APPLY_RULES_DELAY);
      this.context.mainCallback = this.applyRulesScheduler.run.bind(this.applyRulesScheduler);
      if (this.context.beforeStyleApplied && typeof this.context.beforeStyleApplied !== 'function') {
        // eslint-disable-next-line max-len
        throw new Error(`Invalid configuration. Type of 'beforeStyleApplied' should be a function, received: '${typeof this.context.beforeStyleApplied}'`);
      }
      this.applyRulesCallbackListener = () => {
        applyRules(this.context);
      };
    }

    /**
     * Applies stylesheet rules on page.
     */
    apply() {
      applyRules(this.context);
      if (document.readyState !== 'complete') {
        document.addEventListener('DOMContentLoaded', this.applyRulesCallbackListener, false);
      }
    }

    /**
     * Disposes ExtendedCss and removes our styles from matched elements.
     */
    dispose() {
      mainDisconnect(this.context, this.context.mainCallback);
      this.context.affectedElements.forEach(el => {
        revertStyle(el);
      });
      document.removeEventListener('DOMContentLoaded', this.applyRulesCallbackListener, false);
    }

    /**
     * Exposed for testing purposes only.
     *
     * @returns Array of AffectedElement data objects.
     */
    getAffectedElements() {
      return this.context.affectedElements;
    }

    /**
     * Returns a list of the document's elements that match the specified selector.
     * Uses ExtCssDocument.querySelectorAll().
     *
     * @param selector Selector text.
     * @param [noTiming=true] If true — do not print the timings to the console.
     *
     * @throws An error if selector is not valid.
     * @returns A list of elements that match the selector.
     */
    static query(selector) {
      let noTiming = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
      if (typeof selector !== 'string') {
        throw new Error('Selector should be defined as a string.');
      }
      const start = ThrottleWrapper.now();
      try {
        return extCssDocument.querySelectorAll(selector);
      } finally {
        const end = ThrottleWrapper.now();
        if (!noTiming) {
          logger.info(`[ExtendedCss] Elapsed: ${Math.round((end - start) * 1000)} μs.`);
        }
      }
    }

    /**
     * Validates selector.
     *
     * @param inputSelector Selector text to validate.
     *
     * @returns Result of selector validation.
     */
    static validate(inputSelector) {
      try {
        // ExtendedCss in general supports :remove() in selector
        // but ExtendedCss.query() does not support it as it should be parsed by stylesheet parser.
        // so for validation we have to handle selectors with `:remove()` in it
        const {
          selector
        } = parseRemoveSelector(inputSelector);
        ExtendedCss.query(selector);
        return {
          ok: true,
          error: null
        };
      } catch (e) {
        const caughtErrorMessage = e instanceof Error ? e.message : e;
        // not valid input `selector` should be logged eventually
        const error = `Error: Invalid selector: '${inputSelector}' -- ${caughtErrorMessage}`;
        return {
          ok: false,
          error
        };
      }
    }
  }

  return ExtendedCss;

})();