بۇ قوليازمىنى بىۋاسىتە قاچىلاشقا بولمايدۇ. بۇ باشقا قوليازمىلارنىڭ ئىشلىتىشى ئۈچۈن تەمىنلەنگەن ئامبار بولۇپ، ئىشلىتىش ئۈچۈن مېتا كۆرسەتمىسىگە قىستۇرىدىغان كود: // @require https://update.greatest.deepsurf.us/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;
-
- })();