extended-css

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

بۇ قوليازمىنى بىۋاسىتە قاچىلاشقا بولمايدۇ. بۇ باشقا قوليازمىلارنىڭ ئىشلىتىشى ئۈچۈن تەمىنلەنگەن ئامبار بولۇپ، ئىشلىتىش ئۈچۈن مېتا كۆرسەتمىسىگە قىستۇرىدىغان كود: // @require https://update.greatest.deepsurf.us/scripts/452263/1135232/extended-css.js

  1. // ==UserScript==
  2. // @name extended-css
  3. // @name:zh-CN extended-css
  4. // @version 2.0.36
  5. // @namespace https://adguard.com/
  6. // @author AdguardTeam
  7. // @contributor AdguardTeam
  8. // @contributors AdguardTeam
  9. // @developer AdguardTeam
  10. // @copyright GPL-3.0
  11. // @license GPL-3.0
  12. // @description A javascript library that allows using extended CSS selectors (:has, :contains, etc)
  13. // @description:zh 一个让用户可以使用扩展 CSS 选择器的库
  14. // @description:zh-CN 一个让用户可以使用扩展 CSS 选择器的库
  15. // @description:zh_CN 一个让用户可以使用扩展 CSS 选择器的库
  16. // @homepage https://github.com/AdguardTeam/ExtendedCss
  17. // @homepageURL https://github.com/AdguardTeam/ExtendedCss
  18. // ==/UserScript==
  19. /**
  20. * @adguard/extended-css - v2.0.36 - Thu Jan 05 2023
  21. * https://github.com/AdguardTeam/ExtendedCss#homepage
  22. * Copyright (c) 2023 AdGuard. Licensed GPL-3.0
  23. */
  24. var ExtendedCss = (function () {
  25. 'use strict';
  26.  
  27. function _typeof(obj) {
  28. "@babel/helpers - typeof";
  29.  
  30. return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) {
  31. return typeof obj;
  32. } : function (obj) {
  33. return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
  34. }, _typeof(obj);
  35. }
  36.  
  37. function _toPrimitive(input, hint) {
  38. if (_typeof(input) !== "object" || input === null) return input;
  39. var prim = input[Symbol.toPrimitive];
  40. if (prim !== undefined) {
  41. var res = prim.call(input, hint || "default");
  42. if (_typeof(res) !== "object") return res;
  43. throw new TypeError("@@toPrimitive must return a primitive value.");
  44. }
  45. return (hint === "string" ? String : Number)(input);
  46. }
  47.  
  48. function _toPropertyKey(arg) {
  49. var key = _toPrimitive(arg, "string");
  50. return _typeof(key) === "symbol" ? key : String(key);
  51. }
  52.  
  53. function _defineProperty(obj, key, value) {
  54. key = _toPropertyKey(key);
  55. if (key in obj) {
  56. Object.defineProperty(obj, key, {
  57. value: value,
  58. enumerable: true,
  59. configurable: true,
  60. writable: true
  61. });
  62. } else {
  63. obj[key] = value;
  64. }
  65. return obj;
  66. }
  67.  
  68. let NodeType;
  69.  
  70. /**
  71. * Universal interface for all node types.
  72. */
  73. (function (NodeType) {
  74. NodeType["SelectorList"] = "SelectorList";
  75. NodeType["Selector"] = "Selector";
  76. NodeType["RegularSelector"] = "RegularSelector";
  77. NodeType["ExtendedSelector"] = "ExtendedSelector";
  78. NodeType["AbsolutePseudoClass"] = "AbsolutePseudoClass";
  79. NodeType["RelativePseudoClass"] = "RelativePseudoClass";
  80. })(NodeType || (NodeType = {}));
  81. /**
  82. * Class needed for creating ast nodes while selector parsing.
  83. * Used for SelectorList, Selector, ExtendedSelector.
  84. */
  85. class AnySelectorNode {
  86. /**
  87. * Creates new ast node.
  88. *
  89. * @param type Ast node type.
  90. */
  91. constructor(type) {
  92. _defineProperty(this, "children", []);
  93. this.type = type;
  94. }
  95.  
  96. /**
  97. * Adds child node to children array.
  98. *
  99. * @param child Ast node.
  100. */
  101. addChild(child) {
  102. this.children.push(child);
  103. }
  104. }
  105.  
  106. /**
  107. * Class needed for creating RegularSelector ast node while selector parsing.
  108. */
  109. class RegularSelectorNode extends AnySelectorNode {
  110. /**
  111. * Creates RegularSelector ast node.
  112. *
  113. * @param value Value of RegularSelector node.
  114. */
  115. constructor(value) {
  116. super(NodeType.RegularSelector);
  117. this.value = value;
  118. }
  119. }
  120.  
  121. /**
  122. * Class needed for creating RelativePseudoClass ast node while selector parsing.
  123. */
  124. class RelativePseudoClassNode extends AnySelectorNode {
  125. /**
  126. * Creates RegularSelector ast node.
  127. *
  128. * @param name Name of RelativePseudoClass node.
  129. */
  130. constructor(name) {
  131. super(NodeType.RelativePseudoClass);
  132. this.name = name;
  133. }
  134. }
  135.  
  136. /**
  137. * Class needed for creating AbsolutePseudoClass ast node while selector parsing.
  138. */
  139. class AbsolutePseudoClassNode extends AnySelectorNode {
  140. /**
  141. * Creates AbsolutePseudoClass ast node.
  142. *
  143. * @param name Name of AbsolutePseudoClass node.
  144. */
  145. constructor(name) {
  146. super(NodeType.AbsolutePseudoClass);
  147. _defineProperty(this, "value", '');
  148. this.name = name;
  149. }
  150. }
  151.  
  152. /* eslint-disable jsdoc/require-description-complete-sentence */
  153.  
  154. /**
  155. * Root node.
  156. *
  157. * SelectorList
  158. * : Selector
  159. * ...
  160. * ;
  161. */
  162.  
  163. /**
  164. * Selector node.
  165. *
  166. * Selector
  167. * : RegularSelector
  168. * | ExtendedSelector
  169. * ...
  170. * ;
  171. */
  172.  
  173. /**
  174. * Regular selector node.
  175. * It can be selected by querySelectorAll().
  176. *
  177. * RegularSelector
  178. * : type
  179. * : value
  180. * ;
  181. */
  182.  
  183. /**
  184. * Extended selector node.
  185. *
  186. * ExtendedSelector
  187. * : AbsolutePseudoClass
  188. * | RelativePseudoClass
  189. * ;
  190. */
  191.  
  192. /**
  193. * Absolute extended pseudo-class node,
  194. * i.e. none-selector args.
  195. *
  196. * AbsolutePseudoClass
  197. * : type
  198. * : name
  199. * : value
  200. * ;
  201. */
  202.  
  203. /**
  204. * Relative extended pseudo-class node
  205. * i.e. selector as arg.
  206. *
  207. * RelativePseudoClass
  208. * : type
  209. * : name
  210. * : SelectorList
  211. * ;
  212. */
  213.  
  214. //
  215. // ast example
  216. //
  217. // div.banner > div:has(span, p), a img.ad
  218. //
  219. // SelectorList - div.banner > div:has(span, p), a img.ad
  220. // Selector - div.banner > div:has(span, p)
  221. // RegularSelector - div.banner > div
  222. // ExtendedSelector - :has(span, p)
  223. // PseudoClassSelector - :has
  224. // SelectorList - span, p
  225. // Selector - span
  226. // RegularSelector - span
  227. // Selector - p
  228. // RegularSelector - p
  229. // Selector - a img.ad
  230. // RegularSelector - a img.ad
  231. //
  232.  
  233. const LEFT_SQUARE_BRACKET = '[';
  234. const RIGHT_SQUARE_BRACKET = ']';
  235. const LEFT_PARENTHESIS = '(';
  236. const RIGHT_PARENTHESIS = ')';
  237. const LEFT_CURLY_BRACKET = '{';
  238. const RIGHT_CURLY_BRACKET = '}';
  239. const BRACKETS = {
  240. SQUARE: {
  241. LEFT: LEFT_SQUARE_BRACKET,
  242. RIGHT: RIGHT_SQUARE_BRACKET
  243. },
  244. PARENTHESES: {
  245. LEFT: LEFT_PARENTHESIS,
  246. RIGHT: RIGHT_PARENTHESIS
  247. },
  248. CURLY: {
  249. LEFT: LEFT_CURLY_BRACKET,
  250. RIGHT: RIGHT_CURLY_BRACKET
  251. }
  252. };
  253. const SLASH = '/';
  254. const BACKSLASH = '\\';
  255. const SPACE = ' ';
  256. const COMMA = ',';
  257. const DOT = '.';
  258. const SEMICOLON = ';';
  259. const COLON = ':';
  260. const SINGLE_QUOTE = '\'';
  261. const DOUBLE_QUOTE = '"';
  262.  
  263. // do not consider hyphen `-` as separated mark
  264. // to avoid pseudo-class names splitting
  265. // e.g. 'matches-css' or 'if-not'
  266.  
  267. const CARET = '^';
  268. const DOLLAR_SIGN = '$';
  269. const EQUAL_SIGN = '=';
  270. const TAB = '\t';
  271. const CARRIAGE_RETURN = '\r';
  272. const LINE_FEED = '\n';
  273. const FORM_FEED = '\f';
  274. const WHITE_SPACE_CHARACTERS = [SPACE, TAB, CARRIAGE_RETURN, LINE_FEED, FORM_FEED];
  275.  
  276. // for universal selector and attributes
  277. const ASTERISK = '*';
  278. const ID_MARKER = '#';
  279. const CLASS_MARKER = DOT;
  280. const DESCENDANT_COMBINATOR = SPACE;
  281. const CHILD_COMBINATOR = '>';
  282. const NEXT_SIBLING_COMBINATOR = '+';
  283. const SUBSEQUENT_SIBLING_COMBINATOR = '~';
  284. const COMBINATORS = [DESCENDANT_COMBINATOR, CHILD_COMBINATOR, NEXT_SIBLING_COMBINATOR, SUBSEQUENT_SIBLING_COMBINATOR];
  285. 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];
  286.  
  287. // absolute:
  288. const CONTAINS_PSEUDO = 'contains';
  289. const HAS_TEXT_PSEUDO = 'has-text';
  290. const ABP_CONTAINS_PSEUDO = '-abp-contains';
  291. const MATCHES_CSS_PSEUDO = 'matches-css';
  292. const MATCHES_CSS_BEFORE_PSEUDO = 'matches-css-before';
  293. const MATCHES_CSS_AFTER_PSEUDO = 'matches-css-after';
  294. const MATCHES_ATTR_PSEUDO_CLASS_MARKER = 'matches-attr';
  295. const MATCHES_PROPERTY_PSEUDO_CLASS_MARKER = 'matches-property';
  296. const XPATH_PSEUDO_CLASS_MARKER = 'xpath';
  297. const NTH_ANCESTOR_PSEUDO_CLASS_MARKER = 'nth-ancestor';
  298. const CONTAINS_PSEUDO_NAMES = [CONTAINS_PSEUDO, HAS_TEXT_PSEUDO, ABP_CONTAINS_PSEUDO];
  299.  
  300. /**
  301. * Pseudo-class :upward() can get number or selector arg
  302. * and if the arg is selector it should be standard, not extended
  303. * so :upward pseudo-class is always absolute.
  304. */
  305. const UPWARD_PSEUDO_CLASS_MARKER = 'upward';
  306.  
  307. /**
  308. * Pseudo-class `:remove()` and pseudo-property `remove`
  309. * are used for element actions, not for element selecting.
  310. *
  311. * Selector text should not contain the pseudo-class
  312. * so selector parser should consider it as invalid
  313. * and both are handled by stylesheet parser.
  314. */
  315. const REMOVE_PSEUDO_MARKER = 'remove';
  316.  
  317. // relative:
  318. const HAS_PSEUDO_CLASS_MARKER = 'has';
  319. const ABP_HAS_PSEUDO_CLASS_MARKER = '-abp-has';
  320. const HAS_PSEUDO_CLASS_MARKERS = [HAS_PSEUDO_CLASS_MARKER, ABP_HAS_PSEUDO_CLASS_MARKER];
  321. const IS_PSEUDO_CLASS_MARKER = 'is';
  322. const NOT_PSEUDO_CLASS_MARKER = 'not';
  323. 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];
  324. const RELATIVE_PSEUDO_CLASSES = [...HAS_PSEUDO_CLASS_MARKERS, IS_PSEUDO_CLASS_MARKER, NOT_PSEUDO_CLASS_MARKER];
  325. const SUPPORTED_PSEUDO_CLASSES = [...ABSOLUTE_PSEUDO_CLASSES, ...RELATIVE_PSEUDO_CLASSES];
  326.  
  327. // these pseudo-classes should be part of RegularSelector value
  328. // if its arg does not contain extended selectors.
  329. // the ast will be checked after the selector is completely parsed
  330. const OPTIMIZATION_PSEUDO_CLASSES = [NOT_PSEUDO_CLASS_MARKER, IS_PSEUDO_CLASS_MARKER];
  331.  
  332. /**
  333. * ':scope' is used for extended pseudo-class :has(), if-not(), :is() and :not().
  334. */
  335. const SCOPE_CSS_PSEUDO_CLASS = ':scope';
  336.  
  337. /**
  338. * ':after' and ':before' are needed for :matches-css() pseudo-class
  339. * all other are needed for :has() limitation after regular pseudo-elements.
  340. *
  341. * @see {@link https://bugs.chromium.org/p/chromium/issues/detail?id=669058#c54} [case 3]
  342. */
  343. const REGULAR_PSEUDO_ELEMENTS = {
  344. AFTER: 'after',
  345. BACKDROP: 'backdrop',
  346. BEFORE: 'before',
  347. CUE: 'cue',
  348. CUE_REGION: 'cue-region',
  349. FIRST_LETTER: 'first-letter',
  350. FIRST_LINE: 'first-line',
  351. FILE_SELECTION_BUTTON: 'file-selector-button',
  352. GRAMMAR_ERROR: 'grammar-error',
  353. MARKER: 'marker',
  354. PART: 'part',
  355. PLACEHOLDER: 'placeholder',
  356. SELECTION: 'selection',
  357. SLOTTED: 'slotted',
  358. SPELLING_ERROR: 'spelling-error',
  359. TARGET_TEXT: 'target-text'
  360. };
  361. const CONTENT_CSS_PROPERTY = 'content';
  362. const PSEUDO_PROPERTY_POSITIVE_VALUE = 'true';
  363. const DEBUG_PSEUDO_PROPERTY_GLOBAL_VALUE = 'global';
  364. const NO_SELECTOR_ERROR_PREFIX = 'Selector should be defined before';
  365. const STYLESHEET_ERROR_PREFIX = {
  366. NO_STYLE: 'No style declaration at stylesheet part',
  367. NO_SELECTOR: `${NO_SELECTOR_ERROR_PREFIX} style declaration in stylesheet`,
  368. INVALID_STYLE: 'Invalid style declaration at stylesheet part',
  369. UNCLOSED_STYLE: 'Unclosed style declaration at stylesheet part',
  370. NO_PROPERTY: 'Missing style property in declaration at stylesheet part',
  371. NO_VALUE: 'Missing style value in declaration at stylesheet part',
  372. NO_STYLE_OR_REMOVE: 'Invalid stylesheet - no style declared or :remove() pseudo-class used',
  373. NO_COMMENT: 'Comments in stylesheet are not supported'
  374. };
  375. const REMOVE_ERROR_PREFIX = {
  376. INVALID_REMOVE: 'Invalid :remove() pseudo-class in selector',
  377. NO_TARGET_SELECTOR: `${NO_SELECTOR_ERROR_PREFIX} :remove() pseudo-class`,
  378. MULTIPLE_USAGE: 'Pseudo-class :remove() appears more than once in selector',
  379. INVALID_POSITION: 'Pseudo-class :remove() should be at the end of selector'
  380. };
  381. const MATCHING_ELEMENT_ERROR_PREFIX = 'Error while matching element';
  382. const MAX_STYLE_PROTECTION_COUNT = 50;
  383.  
  384. /**
  385. * Regexp that matches backward compatible syntaxes.
  386. */
  387. const REGEXP_VALID_OLD_SYNTAX = /\[-(?:ext)-([a-z-_]+)=(["'])((?:(?=(\\?))\4.)*?)\2\]/g;
  388.  
  389. /**
  390. * Marker for checking invalid selector after old-syntax normalizing by selector converter.
  391. */
  392. const INVALID_OLD_SYNTAX_MARKER = '[-ext-';
  393.  
  394. /**
  395. * Complex replacement function.
  396. * Undo quote escaping inside of an extended selector.
  397. *
  398. * @param match Whole matched string.
  399. * @param name Group 1.
  400. * @param quoteChar Group 2.
  401. * @param rawValue Group 3.
  402. *
  403. * @returns Converted string.
  404. */
  405. const evaluateMatch = (match, name, quoteChar, rawValue) => {
  406. // Unescape quotes
  407. const re = new RegExp(`([^\\\\]|^)\\\\${quoteChar}`, 'g');
  408. const value = rawValue.replace(re, `$1${quoteChar}`);
  409. return `:${name}(${value})`;
  410. };
  411.  
  412. // ':scope' pseudo may be at start of :has() argument
  413. // but ExtCssDocument.querySelectorAll() already use it for selecting exact element descendants
  414. const reScope = /\(:scope >/g;
  415. const SCOPE_REPLACER = '(>';
  416. const MATCHES_CSS_PSEUDO_ELEMENT_REGEXP = /(:matches-css)-(before|after)\(/g;
  417. const convertMatchesCss = (match, extendedPseudoClass, regularPseudoElement) => {
  418. // ':matches-css-before(' --> ':matches-css(before, '
  419. // ':matches-css-after(' --> ':matches-css(after, '
  420. return `${extendedPseudoClass}${BRACKETS.PARENTHESES.LEFT}${regularPseudoElement}${COMMA}`;
  421. };
  422.  
  423. /**
  424. * Handles old syntax and :scope inside :has().
  425. *
  426. * @param selector Trimmed selector to normalize.
  427. *
  428. * @returns Normalized selector.
  429. * @throws An error on invalid old extended syntax selector.
  430. */
  431. const normalize = selector => {
  432. const normalizedSelector = selector.replace(REGEXP_VALID_OLD_SYNTAX, evaluateMatch).replace(reScope, SCOPE_REPLACER).replace(MATCHES_CSS_PSEUDO_ELEMENT_REGEXP, convertMatchesCss);
  433.  
  434. // validate old syntax after normalizing
  435. // e.g. '[-ext-matches-css-before=\'content: /^[A-Z][a-z]'
  436. if (normalizedSelector.includes(INVALID_OLD_SYNTAX_MARKER)) {
  437. throw new Error(`Invalid extended-css old syntax selector: '${selector}'`);
  438. }
  439. return normalizedSelector;
  440. };
  441.  
  442. /**
  443. * Prepares the rawSelector before tokenization:
  444. * 1. Trims it.
  445. * 2. Converts old syntax `[-ext-pseudo-class="..."]` to new one `:pseudo-class(...)`.
  446. * 3. Handles :scope pseudo inside :has() pseudo-class arg.
  447. *
  448. * @param rawSelector Selector with no style declaration.
  449. * @returns Prepared selector with no style declaration.
  450. */
  451. const convert = rawSelector => {
  452. const trimmedSelector = rawSelector.trim();
  453. return normalize(trimmedSelector);
  454. };
  455.  
  456. let TokenType;
  457. (function (TokenType) {
  458. TokenType["Mark"] = "mark";
  459. TokenType["Word"] = "word";
  460. })(TokenType || (TokenType = {}));
  461. /**
  462. * Splits `input` string into tokens.
  463. *
  464. * @param input Input string to tokenize.
  465. * @param supportedMarks Array of supported marks to considered as `TokenType.Mark`;
  466. * all other will be considered as `TokenType.Word`.
  467. *
  468. * @returns Array of tokens.
  469. */
  470. const tokenize = (input, supportedMarks) => {
  471. // buffer is needed for words collecting while iterating
  472. let buffer = '';
  473. // result collection
  474. const tokens = [];
  475. const selectorSymbols = input.split('');
  476. // iterate through selector chars and collect tokens
  477. selectorSymbols.forEach((symbol, i) => {
  478. if (supportedMarks.includes(symbol)) {
  479. tokens.push({
  480. type: TokenType.Mark,
  481. value: symbol
  482. });
  483. return;
  484. }
  485. buffer += symbol;
  486. const nextSymbol = selectorSymbols[i + 1];
  487. // string end has been reached if nextSymbol is undefined
  488. if (!nextSymbol || supportedMarks.includes(nextSymbol)) {
  489. tokens.push({
  490. type: TokenType.Word,
  491. value: buffer
  492. });
  493. buffer = '';
  494. }
  495. });
  496. return tokens;
  497. };
  498.  
  499. /**
  500. * Prepares `rawSelector` and splits it into tokens.
  501. *
  502. * @param rawSelector Raw css selector.
  503. *
  504. * @returns Array of tokens supported for selector.
  505. */
  506. const tokenizeSelector = rawSelector => {
  507. const selector = convert(rawSelector);
  508. return tokenize(selector, SUPPORTED_SELECTOR_MARKS);
  509. };
  510.  
  511. /**
  512. * Splits `attribute` into tokens.
  513. *
  514. * @param attribute Input attribute.
  515. *
  516. * @returns Array of tokens supported for attribute.
  517. */
  518. const tokenizeAttribute = attribute => {
  519. // equal sigh `=` in attribute is considered as `TokenType.Mark`
  520. return tokenize(attribute, [...SUPPORTED_SELECTOR_MARKS, EQUAL_SIGN]);
  521. };
  522.  
  523. /**
  524. * Some browsers do not support Array.prototype.flat()
  525. * e.g. Opera 42 which is used for browserstack tests.
  526. *
  527. * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat}
  528. *
  529. * @param input Array needed to be flatten.
  530. *
  531. * @returns Flatten array.
  532. * @throws An error if array cannot be flatten.
  533. */
  534. const flatten = input => {
  535. const stack = [];
  536. input.forEach(el => stack.push(el));
  537. const res = [];
  538. while (stack.length) {
  539. // pop value from stack
  540. const next = stack.pop();
  541. if (!next) {
  542. throw new Error('Unable to make array flat');
  543. }
  544. if (Array.isArray(next)) {
  545. // push back array items, won't modify the original input
  546. next.forEach(el => stack.push(el));
  547. } else {
  548. res.push(next);
  549. }
  550. }
  551. // reverse to restore input order
  552. return res.reverse();
  553. };
  554.  
  555. /**
  556. * Returns first item from `array`.
  557. *
  558. * @param array Input array.
  559. *
  560. * @returns First array item, or `undefined` if there is no such item.
  561. */
  562. const getFirst = array => {
  563. return array[0];
  564. };
  565.  
  566. /**
  567. * Returns last item from array.
  568. *
  569. * @param array Input array.
  570. *
  571. * @returns Last array item, or `undefined` if there is no such item.
  572. */
  573. const getLast = array => {
  574. return array[array.length - 1];
  575. };
  576.  
  577. /**
  578. * Returns array item which is previous to the last one
  579. * e.g. for `[5, 6, 7, 8]` returns `7`.
  580. *
  581. * @param array Input array.
  582. *
  583. * @returns Previous to last array item, or `undefined` if there is no such item.
  584. */
  585. const getPrevToLast = array => {
  586. return array[array.length - 2];
  587. };
  588.  
  589. /**
  590. * Takes array of ast node `children` and returns the child by the `index`.
  591. *
  592. * @param array Array of ast node children.
  593. * @param index Index of needed child in the array.
  594. * @param errorMessage Optional error message to throw.
  595. *
  596. * @returns Array item at `index` position.
  597. * @throws An error if there is no child with specified `index` in array.
  598. */
  599. const getItemByIndex = (array, index, errorMessage) => {
  600. const indexChild = array[index];
  601. if (!indexChild) {
  602. throw new Error(errorMessage || `No array item found by index ${index}`);
  603. }
  604. return indexChild;
  605. };
  606.  
  607. const NO_REGULAR_SELECTOR_ERROR = 'At least one of Selector node children should be RegularSelector';
  608.  
  609. /**
  610. * Checks whether the type of `astNode` is SelectorList.
  611. *
  612. * @param astNode Ast node.
  613. *
  614. * @returns True if astNode.type === SelectorList.
  615. */
  616. const isSelectorListNode = astNode => {
  617. return (astNode === null || astNode === void 0 ? void 0 : astNode.type) === NodeType.SelectorList;
  618. };
  619.  
  620. /**
  621. * Checks whether the type of `astNode` is Selector.
  622. *
  623. * @param astNode Ast node.
  624. *
  625. * @returns True if astNode.type === Selector.
  626. */
  627. const isSelectorNode = astNode => {
  628. return (astNode === null || astNode === void 0 ? void 0 : astNode.type) === NodeType.Selector;
  629. };
  630.  
  631. /**
  632. * Checks whether the type of `astNode` is RegularSelector.
  633. *
  634. * @param astNode Ast node.
  635. *
  636. * @returns True if astNode.type === RegularSelector.
  637. */
  638. const isRegularSelectorNode = astNode => {
  639. return (astNode === null || astNode === void 0 ? void 0 : astNode.type) === NodeType.RegularSelector;
  640. };
  641.  
  642. /**
  643. * Checks whether the type of `astNode` is ExtendedSelector.
  644. *
  645. * @param astNode Ast node.
  646. *
  647. * @returns True if astNode.type === ExtendedSelector.
  648. */
  649. const isExtendedSelectorNode = astNode => {
  650. return astNode.type === NodeType.ExtendedSelector;
  651. };
  652.  
  653. /**
  654. * Checks whether the type of `astNode` is AbsolutePseudoClass.
  655. *
  656. * @param astNode Ast node.
  657. *
  658. * @returns True if astNode.type === AbsolutePseudoClass.
  659. */
  660. const isAbsolutePseudoClassNode = astNode => {
  661. return (astNode === null || astNode === void 0 ? void 0 : astNode.type) === NodeType.AbsolutePseudoClass;
  662. };
  663.  
  664. /**
  665. * Checks whether the type of `astNode` is RelativePseudoClass.
  666. *
  667. * @param astNode Ast node.
  668. *
  669. * @returns True if astNode.type === RelativePseudoClass.
  670. */
  671. const isRelativePseudoClassNode = astNode => {
  672. return (astNode === null || astNode === void 0 ? void 0 : astNode.type) === NodeType.RelativePseudoClass;
  673. };
  674.  
  675. /**
  676. * Returns name of `astNode`.
  677. *
  678. * @param astNode AbsolutePseudoClass or RelativePseudoClass node.
  679. *
  680. * @returns Name of `astNode`.
  681. * @throws An error on unsupported ast node or no name found.
  682. */
  683. const getNodeName = astNode => {
  684. if (astNode === null) {
  685. throw new Error('Ast node should be defined');
  686. }
  687. if (!isAbsolutePseudoClassNode(astNode) && !isRelativePseudoClassNode(astNode)) {
  688. throw new Error('Only AbsolutePseudoClass or RelativePseudoClass ast node can have a name');
  689. }
  690. if (!astNode.name) {
  691. throw new Error('Extended pseudo-class should have a name');
  692. }
  693. return astNode.name;
  694. };
  695.  
  696. /**
  697. * Returns value of `astNode`.
  698. *
  699. * @param astNode RegularSelector or AbsolutePseudoClass node.
  700. * @param errorMessage Optional error message if no value found.
  701. *
  702. * @returns Value of `astNode`.
  703. * @throws An error on unsupported ast node or no value found.
  704. */
  705. const getNodeValue = (astNode, errorMessage) => {
  706. if (astNode === null) {
  707. throw new Error('Ast node should be defined');
  708. }
  709. if (!isRegularSelectorNode(astNode) && !isAbsolutePseudoClassNode(astNode)) {
  710. throw new Error('Only RegularSelector ot AbsolutePseudoClass ast node can have a value');
  711. }
  712. if (!astNode.value) {
  713. throw new Error(errorMessage || 'Ast RegularSelector ot AbsolutePseudoClass node should have a value');
  714. }
  715. return astNode.value;
  716. };
  717.  
  718. /**
  719. * Returns only RegularSelector nodes from `children`.
  720. *
  721. * @param children Array of ast node children.
  722. *
  723. * @returns Array of RegularSelector nodes.
  724. */
  725. const getRegularSelectorNodes = children => {
  726. return children.filter(isRegularSelectorNode);
  727. };
  728.  
  729. /**
  730. * Returns the first RegularSelector node from `children`.
  731. *
  732. * @param children Array of ast node children.
  733. * @param errorMessage Optional error message if no value found.
  734. *
  735. * @returns Ast RegularSelector node.
  736. * @throws An error if no RegularSelector node found.
  737. */
  738. const getFirstRegularChild = (children, errorMessage) => {
  739. const regularSelectorNodes = getRegularSelectorNodes(children);
  740. const firstRegularSelectorNode = getFirst(regularSelectorNodes);
  741. if (!firstRegularSelectorNode) {
  742. throw new Error(errorMessage || NO_REGULAR_SELECTOR_ERROR);
  743. }
  744. return firstRegularSelectorNode;
  745. };
  746.  
  747. /**
  748. * Returns the last RegularSelector node from `children`.
  749. *
  750. * @param children Array of ast node children.
  751. *
  752. * @returns Ast RegularSelector node.
  753. * @throws An error if no RegularSelector node found.
  754. */
  755. const getLastRegularChild = children => {
  756. const regularSelectorNodes = getRegularSelectorNodes(children);
  757. const lastRegularSelectorNode = getLast(regularSelectorNodes);
  758. if (!lastRegularSelectorNode) {
  759. throw new Error(NO_REGULAR_SELECTOR_ERROR);
  760. }
  761. return lastRegularSelectorNode;
  762. };
  763.  
  764. /**
  765. * Returns the only child of `node`.
  766. *
  767. * @param node Ast node.
  768. * @param errorMessage Error message.
  769. *
  770. * @returns The only child of ast node.
  771. * @throws An error if none or more than one child found.
  772. */
  773. const getNodeOnlyChild = (node, errorMessage) => {
  774. if (node.children.length !== 1) {
  775. throw new Error(errorMessage);
  776. }
  777. const onlyChild = getFirst(node.children);
  778. if (!onlyChild) {
  779. throw new Error(errorMessage);
  780. }
  781. return onlyChild;
  782. };
  783.  
  784. /**
  785. * Takes ExtendedSelector node and returns its only child.
  786. *
  787. * @param extendedSelectorNode ExtendedSelector ast node.
  788. *
  789. * @returns AbsolutePseudoClass or RelativePseudoClass.
  790. * @throws An error if there is no specific pseudo-class ast node.
  791. */
  792. const getPseudoClassNode = extendedSelectorNode => {
  793. return getNodeOnlyChild(extendedSelectorNode, 'Extended selector should be specified');
  794. };
  795.  
  796. /**
  797. * Takes RelativePseudoClass node and returns its only child
  798. * which is relative SelectorList node.
  799. *
  800. * @param pseudoClassNode RelativePseudoClass.
  801. *
  802. * @returns Relative SelectorList node.
  803. * @throws An error if no selector list found.
  804. */
  805. const getRelativeSelectorListNode = pseudoClassNode => {
  806. if (!isRelativePseudoClassNode(pseudoClassNode)) {
  807. throw new Error('Only RelativePseudoClass node can have relative SelectorList node as child');
  808. }
  809. return getNodeOnlyChild(pseudoClassNode, `Missing arg for :${getNodeName(pseudoClassNode)}() pseudo-class`);
  810. };
  811.  
  812. const ATTRIBUTE_CASE_INSENSITIVE_FLAG = 'i';
  813.  
  814. /**
  815. * Limited list of available symbols before slash `/`
  816. * to check whether it is valid regexp pattern opening.
  817. */
  818. const POSSIBLE_MARKS_BEFORE_REGEXP = {
  819. COMMON: [
  820. // e.g. ':matches-attr(/data-/)'
  821. BRACKETS.PARENTHESES.LEFT,
  822. // e.g. `:matches-attr('/data-/')`
  823. SINGLE_QUOTE,
  824. // e.g. ':matches-attr("/data-/")'
  825. DOUBLE_QUOTE,
  826. // e.g. ':matches-attr(check=/data-v-/)'
  827. EQUAL_SIGN,
  828. // e.g. ':matches-property(inner./_test/=null)'
  829. DOT,
  830. // e.g. ':matches-css(height:/20px/)'
  831. COLON,
  832. // ':matches-css-after( content : /(\\d+\\s)*me/ )'
  833. SPACE],
  834. CONTAINS: [
  835. // e.g. ':contains(/text/)'
  836. BRACKETS.PARENTHESES.LEFT,
  837. // e.g. `:contains('/text/')`
  838. SINGLE_QUOTE,
  839. // e.g. ':contains("/text/")'
  840. DOUBLE_QUOTE]
  841. };
  842.  
  843. /**
  844. * Checks whether the passed token is supported extended pseudo-class.
  845. *
  846. * @param tokenValue Token value to check.
  847. *
  848. * @returns True if `tokenValue` is one of supported extended pseudo-class names.
  849. */
  850. const isSupportedPseudoClass = tokenValue => {
  851. return SUPPORTED_PSEUDO_CLASSES.includes(tokenValue);
  852. };
  853.  
  854. /**
  855. * Checks whether the passed pseudo-class `name` should be optimized,
  856. * i.e. :not() and :is().
  857. *
  858. * @param name Pseudo-class name.
  859. *
  860. * @returns True if `name` is one if pseudo-class which should be optimized.
  861. */
  862. const isOptimizationPseudoClass = name => {
  863. return OPTIMIZATION_PSEUDO_CLASSES.includes(name);
  864. };
  865.  
  866. /**
  867. * Checks whether next to "space" token is a continuation of regular selector being processed.
  868. *
  869. * @param nextTokenType Type of token next to current one.
  870. * @param nextTokenValue Value of token next to current one.
  871. *
  872. * @returns True if next token seems to be a part of current regular selector.
  873. */
  874. const doesRegularContinueAfterSpace = (nextTokenType, nextTokenValue) => {
  875. // regular selector does not continues after the current token
  876. if (!nextTokenType || !nextTokenValue) {
  877. return false;
  878. }
  879. return COMBINATORS.includes(nextTokenValue) || nextTokenType === TokenType.Word
  880. // e.g. '#main *:has(> .ad)'
  881. || nextTokenValue === ASTERISK || nextTokenValue === ID_MARKER || nextTokenValue === CLASS_MARKER
  882. // e.g. 'div :where(.content)'
  883. || nextTokenValue === COLON
  884. // e.g. "div[class*=' ']"
  885. || nextTokenValue === SINGLE_QUOTE
  886. // e.g. 'div[class*=" "]'
  887. || nextTokenValue === DOUBLE_QUOTE || nextTokenValue === BRACKETS.SQUARE.LEFT;
  888. };
  889.  
  890. /**
  891. * Checks whether the regexp pattern for pseudo-class arg starts.
  892. * Needed for `context.isRegexpOpen` flag.
  893. *
  894. * @param context Selector parser context.
  895. * @param prevTokenValue Value of previous token.
  896. * @param bufferNodeValue Value of bufferNode.
  897. *
  898. * @returns True if current token seems to be a start of regexp pseudo-class arg pattern.
  899. * @throws An error on invalid regexp pattern.
  900. */
  901. const isRegexpOpening = (context, prevTokenValue, bufferNodeValue) => {
  902. const lastExtendedPseudoClassName = getLast(context.extendedPseudoNamesStack);
  903. if (!lastExtendedPseudoClassName) {
  904. throw new Error('Regexp pattern allowed only in arg of extended pseudo-class');
  905. }
  906. // for regexp pattens the slash should not be escaped
  907. // const isRegexpPatternSlash = prevTokenValue !== BACKSLASH;
  908. // regexp pattern can be set as arg of pseudo-class
  909. // which means limited list of available symbols before slash `/`;
  910. // for :contains() pseudo-class regexp pattern should be at the beginning of arg
  911. if (CONTAINS_PSEUDO_NAMES.includes(lastExtendedPseudoClassName)) {
  912. return POSSIBLE_MARKS_BEFORE_REGEXP.CONTAINS.includes(prevTokenValue);
  913. }
  914. if (prevTokenValue === SLASH && lastExtendedPseudoClassName !== XPATH_PSEUDO_CLASS_MARKER) {
  915. const rawArgDesc = bufferNodeValue ? `in arg part: '${bufferNodeValue}'` : 'arg';
  916. throw new Error(`Invalid regexp pattern for :${lastExtendedPseudoClassName}() pseudo-class ${rawArgDesc}`);
  917. }
  918.  
  919. // for other pseudo-classes regexp pattern can be either the whole arg or its part
  920. return POSSIBLE_MARKS_BEFORE_REGEXP.COMMON.includes(prevTokenValue);
  921. };
  922.  
  923. /**
  924. * Checks whether the attribute starts.
  925. *
  926. * @param tokenValue Value of current token.
  927. * @param prevTokenValue Previous token value.
  928. *
  929. * @returns True if combination of current and previous token seems to be **a start** of attribute.
  930. */
  931. const isAttributeOpening = (tokenValue, prevTokenValue) => {
  932. return tokenValue === BRACKETS.SQUARE.LEFT && prevTokenValue !== BACKSLASH;
  933. };
  934.  
  935. /**
  936. * Checks whether the attribute ends.
  937. *
  938. * @param context Selector parser context.
  939. *
  940. * @returns True if combination of current and previous token seems to be **an end** of attribute.
  941. * @throws An error on invalid attribute.
  942. */
  943. const isAttributeClosing = context => {
  944. var _getPrevToLast;
  945. if (!context.isAttributeBracketsOpen) {
  946. return false;
  947. }
  948. // valid attributes may have extra spaces inside.
  949. // we get rid of them just to simplify the checking and they are skipped only here:
  950. // - spaces will be collected to the ast with spaces as they were declared is selector
  951. // - extra spaces in attribute are not relevant to attribute syntax validity
  952. // e.g. 'a[ title ]' is the same as 'a[title]'
  953. // 'div[style *= "MARGIN" i]' is the same as 'div[style*="MARGIN"i]'
  954. const noSpaceAttr = context.attributeBuffer.split(SPACE).join('');
  955. // tokenize the prepared attribute string
  956. const attrTokens = tokenizeAttribute(noSpaceAttr);
  957. const firstAttrToken = getFirst(attrTokens);
  958. const firstAttrTokenType = firstAttrToken === null || firstAttrToken === void 0 ? void 0 : firstAttrToken.type;
  959. const firstAttrTokenValue = firstAttrToken === null || firstAttrToken === void 0 ? void 0 : firstAttrToken.value;
  960. // signal an error on any mark-type token except backslash
  961. // e.g. '[="margin"]'
  962. if (firstAttrTokenType === TokenType.Mark
  963. // backslash is allowed at start of attribute
  964. // e.g. '[\\:data-service-slot]'
  965. && firstAttrTokenValue !== BACKSLASH) {
  966. // eslint-disable-next-line max-len
  967. throw new Error(`'[${context.attributeBuffer}]' is not a valid attribute due to '${firstAttrTokenValue}' at start of it`);
  968. }
  969. const lastAttrToken = getLast(attrTokens);
  970. const lastAttrTokenType = lastAttrToken === null || lastAttrToken === void 0 ? void 0 : lastAttrToken.type;
  971. const lastAttrTokenValue = lastAttrToken === null || lastAttrToken === void 0 ? void 0 : lastAttrToken.value;
  972. if (lastAttrTokenValue === EQUAL_SIGN) {
  973. // e.g. '[style=]'
  974. throw new Error(`'[${context.attributeBuffer}]' is not a valid attribute due to '${EQUAL_SIGN}'`);
  975. }
  976. const equalSignIndex = attrTokens.findIndex(token => {
  977. return token.type === TokenType.Mark && token.value === EQUAL_SIGN;
  978. });
  979. const prevToLastAttrTokenValue = (_getPrevToLast = getPrevToLast(attrTokens)) === null || _getPrevToLast === void 0 ? void 0 : _getPrevToLast.value;
  980. if (equalSignIndex === -1) {
  981. // if there is no '=' inside attribute,
  982. // it must be just attribute name which means the word-type token before closing bracket
  983. // e.g. 'div[style]'
  984. if (lastAttrTokenType === TokenType.Word) {
  985. return true;
  986. }
  987. return prevToLastAttrTokenValue === BACKSLASH
  988. // some weird attribute are valid too
  989. // e.g. '[class\\"ads-article\\"]'
  990. && (lastAttrTokenValue === DOUBLE_QUOTE
  991. // e.g. "[class\\'ads-article\\']"
  992. || lastAttrTokenValue === SINGLE_QUOTE);
  993. }
  994.  
  995. // get the value of token next to `=`
  996. const nextToEqualSignToken = getItemByIndex(attrTokens, equalSignIndex + 1);
  997. const nextToEqualSignTokenValue = nextToEqualSignToken.value;
  998. // check whether the attribute value wrapper in quotes
  999. const isAttrValueQuote = nextToEqualSignTokenValue === SINGLE_QUOTE || nextToEqualSignTokenValue === DOUBLE_QUOTE;
  1000.  
  1001. // for no quotes after `=` the last token before `]` should be a word-type one
  1002. // e.g. 'div[style*=margin]'
  1003. // 'div[style*=MARGIN i]'
  1004. if (!isAttrValueQuote) {
  1005. if (lastAttrTokenType === TokenType.Word) {
  1006. return true;
  1007. }
  1008. // otherwise signal an error
  1009. // e.g. 'table[style*=border: 0px"]'
  1010. throw new Error(`'[${context.attributeBuffer}]' is not a valid attribute`);
  1011. }
  1012.  
  1013. // otherwise if quotes for value are present
  1014. // the last token before `]` can still be word-type token
  1015. // e.g. 'div[style*="MARGIN" i]'
  1016. if (lastAttrTokenType === TokenType.Word && (lastAttrTokenValue === null || lastAttrTokenValue === void 0 ? void 0 : lastAttrTokenValue.toLocaleLowerCase()) === ATTRIBUTE_CASE_INSENSITIVE_FLAG) {
  1017. return prevToLastAttrTokenValue === nextToEqualSignTokenValue;
  1018. }
  1019.  
  1020. // eventually if there is quotes for attribute value and last token is not a word,
  1021. // the closing mark should be the same quote as opening one
  1022. return lastAttrTokenValue === nextToEqualSignTokenValue;
  1023. };
  1024.  
  1025. /**
  1026. * Checks whether the `tokenValue` is a whitespace character.
  1027. *
  1028. * @param tokenValue Token value.
  1029. *
  1030. * @returns True if `tokenValue` is a whitespace character.
  1031. */
  1032. const isWhiteSpaceChar = tokenValue => {
  1033. if (!tokenValue) {
  1034. return false;
  1035. }
  1036. return WHITE_SPACE_CHARACTERS.includes(tokenValue);
  1037. };
  1038.  
  1039. /**
  1040. * Checks whether the passed `str` is a name of supported absolute extended pseudo-class,
  1041. * e.g. :contains(), :matches-css() etc.
  1042. *
  1043. * @param str Token value to check.
  1044. *
  1045. * @returns True if `str` is one of absolute extended pseudo-class names.
  1046. */
  1047. const isAbsolutePseudoClass = str => {
  1048. return ABSOLUTE_PSEUDO_CLASSES.includes(str);
  1049. };
  1050.  
  1051. /**
  1052. * Checks whether the passed `str` is a name of supported relative extended pseudo-class,
  1053. * e.g. :has(), :not() etc.
  1054. *
  1055. * @param str Token value to check.
  1056. *
  1057. * @returns True if `str` is one of relative extended pseudo-class names.
  1058. */
  1059. const isRelativePseudoClass = str => {
  1060. return RELATIVE_PSEUDO_CLASSES.includes(str);
  1061. };
  1062.  
  1063. /**
  1064. * Returns the node which is being collected
  1065. * or null if there is no such one.
  1066. *
  1067. * @param context Selector parser context.
  1068. *
  1069. * @returns Buffer node or null.
  1070. */
  1071. const getBufferNode = context => {
  1072. if (context.pathToBufferNode.length === 0) {
  1073. return null;
  1074. }
  1075. // buffer node is always the last in the pathToBufferNode stack
  1076. return getLast(context.pathToBufferNode) || null;
  1077. };
  1078.  
  1079. /**
  1080. * Returns the parent node to the 'buffer node' — which is the one being collected —
  1081. * or null if there is no such one.
  1082. *
  1083. * @param context Selector parser context.
  1084. *
  1085. * @returns Parent node of buffer node or null.
  1086. */
  1087. const getBufferNodeParent = context => {
  1088. // at least two nodes should exist — the buffer node and its parent
  1089. // otherwise return null
  1090. if (context.pathToBufferNode.length < 2) {
  1091. return null;
  1092. }
  1093. // since the buffer node is always the last in the pathToBufferNode stack
  1094. // its parent is previous to it in the stack
  1095. return getPrevToLast(context.pathToBufferNode) || null;
  1096. };
  1097.  
  1098. /**
  1099. * Returns last RegularSelector ast node.
  1100. * Needed for parsing of the complex selector with extended pseudo-class inside it.
  1101. *
  1102. * @param context Selector parser context.
  1103. *
  1104. * @returns Ast RegularSelector node.
  1105. * @throws An error if:
  1106. * - bufferNode is absent;
  1107. * - type of bufferNode is unsupported;
  1108. * - no RegularSelector in bufferNode.
  1109. */
  1110. const getContextLastRegularSelectorNode = context => {
  1111. const bufferNode = getBufferNode(context);
  1112. if (!bufferNode) {
  1113. throw new Error('No bufferNode found');
  1114. }
  1115. if (!isSelectorNode(bufferNode)) {
  1116. throw new Error('Unsupported bufferNode type');
  1117. }
  1118. const lastRegularSelectorNode = getLastRegularChild(bufferNode.children);
  1119. context.pathToBufferNode.push(lastRegularSelectorNode);
  1120. return lastRegularSelectorNode;
  1121. };
  1122.  
  1123. /**
  1124. * Updates needed buffer node value while tokens iterating.
  1125. * For RegularSelector also collects token values to context.attributeBuffer
  1126. * for proper attribute parsing.
  1127. *
  1128. * @param context Selector parser context.
  1129. * @param tokenValue Value of current token.
  1130. *
  1131. * @throws An error if:
  1132. * - no bufferNode;
  1133. * - bufferNode.type is not RegularSelector or AbsolutePseudoClass.
  1134. */
  1135. const updateBufferNode = (context, tokenValue) => {
  1136. const bufferNode = getBufferNode(context);
  1137. if (bufferNode === null) {
  1138. throw new Error('No bufferNode to update');
  1139. }
  1140. if (isAbsolutePseudoClassNode(bufferNode)) {
  1141. bufferNode.value += tokenValue;
  1142. } else if (isRegularSelectorNode(bufferNode)) {
  1143. bufferNode.value += tokenValue;
  1144. if (context.isAttributeBracketsOpen) {
  1145. context.attributeBuffer += tokenValue;
  1146. }
  1147. } else {
  1148. // eslint-disable-next-line max-len
  1149. throw new Error(`${bufferNode.type} node cannot be updated. Only RegularSelector and AbsolutePseudoClass are supported`);
  1150. }
  1151. };
  1152.  
  1153. /**
  1154. * Adds SelectorList node to context.ast at the start of ast collecting.
  1155. *
  1156. * @param context Selector parser context.
  1157. */
  1158. const addSelectorListNode = context => {
  1159. const selectorListNode = new AnySelectorNode(NodeType.SelectorList);
  1160. context.ast = selectorListNode;
  1161. context.pathToBufferNode.push(selectorListNode);
  1162. };
  1163.  
  1164. /**
  1165. * Adds new node to buffer node children.
  1166. * New added node will be considered as buffer node after it.
  1167. *
  1168. * @param context Selector parser context.
  1169. * @param type Type of node to add.
  1170. * @param tokenValue Optional, defaults to `''`, value of processing token.
  1171. *
  1172. * @throws An error if no bufferNode.
  1173. */
  1174. const addAstNodeByType = function (context, type) {
  1175. let tokenValue = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
  1176. const bufferNode = getBufferNode(context);
  1177. if (bufferNode === null) {
  1178. throw new Error('No buffer node');
  1179. }
  1180. let node;
  1181. if (type === NodeType.RegularSelector) {
  1182. node = new RegularSelectorNode(tokenValue);
  1183. } else if (type === NodeType.AbsolutePseudoClass) {
  1184. node = new AbsolutePseudoClassNode(tokenValue);
  1185. } else if (type === NodeType.RelativePseudoClass) {
  1186. node = new RelativePseudoClassNode(tokenValue);
  1187. } else {
  1188. // SelectorList || Selector || ExtendedSelector
  1189. node = new AnySelectorNode(type);
  1190. }
  1191. bufferNode.addChild(node);
  1192. context.pathToBufferNode.push(node);
  1193. };
  1194.  
  1195. /**
  1196. * The very beginning of ast collecting.
  1197. *
  1198. * @param context Selector parser context.
  1199. * @param tokenValue Value of regular selector.
  1200. */
  1201. const initAst = (context, tokenValue) => {
  1202. addSelectorListNode(context);
  1203. addAstNodeByType(context, NodeType.Selector);
  1204. // RegularSelector node is always the first child of Selector node
  1205. addAstNodeByType(context, NodeType.RegularSelector, tokenValue);
  1206. };
  1207.  
  1208. /**
  1209. * Inits selector list subtree for relative extended pseudo-classes, e.g. :has(), :not().
  1210. *
  1211. * @param context Selector parser context.
  1212. * @param tokenValue Optional, defaults to `''`, value of inner regular selector.
  1213. */
  1214. const initRelativeSubtree = function (context) {
  1215. let tokenValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
  1216. addAstNodeByType(context, NodeType.SelectorList);
  1217. addAstNodeByType(context, NodeType.Selector);
  1218. addAstNodeByType(context, NodeType.RegularSelector, tokenValue);
  1219. };
  1220.  
  1221. /**
  1222. * Goes to closest parent specified by type.
  1223. * Actually updates path to buffer node for proper ast collecting of selectors while parsing.
  1224. *
  1225. * @param context Selector parser context.
  1226. * @param parentType Type of needed parent node in ast.
  1227. */
  1228. const upToClosest = (context, parentType) => {
  1229. for (let i = context.pathToBufferNode.length - 1; i >= 0; i -= 1) {
  1230. var _context$pathToBuffer;
  1231. if (((_context$pathToBuffer = context.pathToBufferNode[i]) === null || _context$pathToBuffer === void 0 ? void 0 : _context$pathToBuffer.type) === parentType) {
  1232. context.pathToBufferNode = context.pathToBufferNode.slice(0, i + 1);
  1233. break;
  1234. }
  1235. }
  1236. };
  1237.  
  1238. /**
  1239. * Returns needed buffer node updated due to complex selector parsing.
  1240. *
  1241. * @param context Selector parser context.
  1242. *
  1243. * @returns Ast node for following selector parsing.
  1244. * @throws An error if there is no upper SelectorNode is ast.
  1245. */
  1246. const getUpdatedBufferNode = context => {
  1247. // it may happen during the parsing of selector list
  1248. // which is an argument of relative pseudo-class
  1249. // e.g. '.banner:has(~span, ~p)'
  1250. // parser position is here ↑
  1251. // so if after the comma the buffer node type is SelectorList and parent type is RelativePseudoClass
  1252. // we should simply return the current buffer node
  1253. const bufferNode = getBufferNode(context);
  1254. if (bufferNode && isSelectorListNode(bufferNode) && isRelativePseudoClassNode(getBufferNodeParent(context))) {
  1255. return bufferNode;
  1256. }
  1257. upToClosest(context, NodeType.Selector);
  1258. const selectorNode = getBufferNode(context);
  1259. if (!selectorNode) {
  1260. throw new Error('No SelectorNode, impossible to continue selector parsing by ExtendedCss');
  1261. }
  1262. const lastSelectorNodeChild = getLast(selectorNode.children);
  1263. const hasExtended = lastSelectorNodeChild && isExtendedSelectorNode(lastSelectorNodeChild)
  1264. // parser position might be inside standard pseudo-class brackets which has space
  1265. // e.g. 'div:contains(/а/):nth-child(100n + 2)'
  1266. && context.standardPseudoBracketsStack.length === 0;
  1267. const supposedPseudoClassNode = hasExtended && getFirst(lastSelectorNodeChild.children);
  1268. let newNeededBufferNode = selectorNode;
  1269. if (supposedPseudoClassNode) {
  1270. // name of pseudo-class for last extended-node child for Selector node
  1271. const lastExtendedPseudoName = hasExtended && supposedPseudoClassNode.name;
  1272. const isLastExtendedNameRelative = lastExtendedPseudoName && isRelativePseudoClass(lastExtendedPseudoName);
  1273. const isLastExtendedNameAbsolute = lastExtendedPseudoName && isAbsolutePseudoClass(lastExtendedPseudoName);
  1274. const hasRelativeExtended = isLastExtendedNameRelative && context.extendedPseudoBracketsStack.length > 0 && context.extendedPseudoBracketsStack.length === context.extendedPseudoNamesStack.length;
  1275. const hasAbsoluteExtended = isLastExtendedNameAbsolute && lastExtendedPseudoName === getLast(context.extendedPseudoNamesStack);
  1276. if (hasRelativeExtended) {
  1277. // return relative selector node to update later
  1278. context.pathToBufferNode.push(lastSelectorNodeChild);
  1279. newNeededBufferNode = supposedPseudoClassNode;
  1280. } else if (hasAbsoluteExtended) {
  1281. // return absolute selector node to update later
  1282. context.pathToBufferNode.push(lastSelectorNodeChild);
  1283. newNeededBufferNode = supposedPseudoClassNode;
  1284. }
  1285. } else if (hasExtended) {
  1286. // return selector node to add new regular selector node later
  1287. newNeededBufferNode = selectorNode;
  1288. } else {
  1289. // otherwise return last regular selector node to update later
  1290. newNeededBufferNode = getContextLastRegularSelectorNode(context);
  1291. }
  1292. // update the path to buffer node properly
  1293. context.pathToBufferNode.push(newNeededBufferNode);
  1294. return newNeededBufferNode;
  1295. };
  1296.  
  1297. /**
  1298. * Checks values of few next tokens on colon token `:` and:
  1299. * - updates buffer node for following standard pseudo-class;
  1300. * - adds extended selector ast node for following extended pseudo-class;
  1301. * - validates some cases of `:remove()` and `:has()` usage.
  1302. *
  1303. * @param context Selector parser context.
  1304. * @param selector Selector.
  1305. * @param tokenValue Value of current token.
  1306. * @param nextTokenValue Value of token next to current one.
  1307. * @param nextToNextTokenValue Value of token next to next to current one.
  1308. *
  1309. * @throws An error on :remove() pseudo-class in selector
  1310. * or :has() inside regular pseudo limitation.
  1311. */
  1312. const handleNextTokenOnColon = (context, selector, tokenValue, nextTokenValue, nextToNextTokenValue) => {
  1313. if (!nextTokenValue) {
  1314. throw new Error(`Invalid colon ':' at the end of selector: '${selector}'`);
  1315. }
  1316. if (!isSupportedPseudoClass(nextTokenValue.toLowerCase())) {
  1317. if (nextTokenValue.toLowerCase() === REMOVE_PSEUDO_MARKER) {
  1318. // :remove() pseudo-class should be handled before
  1319. // as it is not about element selecting but actions with elements
  1320. // e.g. 'body > div:empty:remove()'
  1321. throw new Error(`${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${selector}'`);
  1322. }
  1323. // if following token is not an extended pseudo
  1324. // the colon should be collected to value of RegularSelector
  1325. // e.g. '.entry_text:nth-child(2)'
  1326. updateBufferNode(context, tokenValue);
  1327. // check the token after the pseudo and do balance parentheses later
  1328. // only if it is functional pseudo-class (standard with brackets, e.g. ':lang()').
  1329. // no brackets balance needed for such case,
  1330. // parser position is on first colon after the 'div':
  1331. // e.g. 'div:last-child:has(button.privacy-policy__btn)'
  1332. if (nextToNextTokenValue && nextToNextTokenValue === BRACKETS.PARENTHESES.LEFT
  1333. // no brackets balance needed for parentheses inside attribute value
  1334. // e.g. 'a[href="javascript:void(0)"]' <-- parser position is on colon `:`
  1335. // before `void` ↑
  1336. && !context.isAttributeBracketsOpen) {
  1337. context.standardPseudoNamesStack.push(nextTokenValue);
  1338. }
  1339. } else {
  1340. // it is supported extended pseudo-class.
  1341. // Disallow :has() inside the pseudos accepting only compound selectors
  1342. // https://bugs.chromium.org/p/chromium/issues/detail?id=669058#c54 [2]
  1343. if (HAS_PSEUDO_CLASS_MARKERS.includes(nextTokenValue) && context.standardPseudoNamesStack.length > 0) {
  1344. // eslint-disable-next-line max-len
  1345. throw new Error(`Usage of :${nextTokenValue}() pseudo-class is not allowed inside regular pseudo: '${getLast(context.standardPseudoNamesStack)}'`);
  1346. } else {
  1347. // stop RegularSelector value collecting
  1348. upToClosest(context, NodeType.Selector);
  1349. // add ExtendedSelector to Selector children
  1350. addAstNodeByType(context, NodeType.ExtendedSelector);
  1351. }
  1352. }
  1353. };
  1354.  
  1355. // limit applying of wildcard :is() and :not() pseudo-class only to html children
  1356. // e.g. ':is(.page, .main) > .banner' or '*:not(span):not(p)'
  1357. const IS_OR_NOT_PSEUDO_SELECTING_ROOT = `html ${ASTERISK}`;
  1358.  
  1359. /**
  1360. * Checks if there are any ExtendedSelector node in selector list.
  1361. *
  1362. * @param selectorList Ast SelectorList node.
  1363. *
  1364. * @returns True if `selectorList` has any inner ExtendedSelector node.
  1365. */
  1366. const hasExtendedSelector = selectorList => {
  1367. return selectorList.children.some(selectorNode => {
  1368. return selectorNode.children.some(selectorNodeChild => {
  1369. return isExtendedSelectorNode(selectorNodeChild);
  1370. });
  1371. });
  1372. };
  1373.  
  1374. /**
  1375. * Converts selector list of RegularSelector nodes to string.
  1376. *
  1377. * @param selectorList Ast SelectorList node.
  1378. *
  1379. * @returns String representation for selector list of regular selectors.
  1380. */
  1381. const selectorListOfRegularsToString = selectorList => {
  1382. // if there is no ExtendedSelector in relative SelectorList
  1383. // it means that each Selector node has single child — RegularSelector node
  1384. // and their values should be combined to string
  1385. const standardCssSelectors = selectorList.children.map(selectorNode => {
  1386. const selectorOnlyChild = getNodeOnlyChild(selectorNode, 'Ast Selector node should have RegularSelector node');
  1387. return getNodeValue(selectorOnlyChild);
  1388. });
  1389. return standardCssSelectors.join(`${COMMA}${SPACE}`);
  1390. };
  1391.  
  1392. /**
  1393. * Updates children of `node` replacing them with `newChildren`.
  1394. * Important: modifies input `node` which is passed by reference.
  1395. *
  1396. * @param node Ast node to update.
  1397. * @param newChildren Array of new children for ast node.
  1398. *
  1399. * @returns Updated ast node.
  1400. */
  1401. const updateNodeChildren = (node, newChildren) => {
  1402. node.children = newChildren;
  1403. return node;
  1404. };
  1405.  
  1406. /**
  1407. * Recursively checks whether the ExtendedSelector node should be optimized.
  1408. * It has to be recursive because RelativePseudoClass has inner SelectorList node.
  1409. *
  1410. * @param currExtendedSelectorNode Ast ExtendedSelector node.
  1411. *
  1412. * @returns True is ExtendedSelector should be optimized.
  1413. */
  1414. const shouldOptimizeExtendedSelector = currExtendedSelectorNode => {
  1415. if (currExtendedSelectorNode === null) {
  1416. return false;
  1417. }
  1418. const extendedPseudoClassNode = getPseudoClassNode(currExtendedSelectorNode);
  1419. const pseudoName = getNodeName(extendedPseudoClassNode);
  1420. if (isAbsolutePseudoClass(pseudoName)) {
  1421. return false;
  1422. }
  1423. const relativeSelectorList = getRelativeSelectorListNode(extendedPseudoClassNode);
  1424. const innerSelectorNodes = relativeSelectorList.children;
  1425. // simple checking for standard selectors in arg of :not() or :is() pseudo-class
  1426. // e.g. 'div > *:is(div, a, span)'
  1427. if (isOptimizationPseudoClass(pseudoName)) {
  1428. const areAllSelectorNodeChildrenRegular = innerSelectorNodes.every(selectorNode => {
  1429. try {
  1430. const selectorOnlyChild = getNodeOnlyChild(selectorNode, 'Selector node should have RegularSelector');
  1431. // it means that the only child is RegularSelector and it can be optimized
  1432. return isRegularSelectorNode(selectorOnlyChild);
  1433. } catch (e) {
  1434. return false;
  1435. }
  1436. });
  1437. if (areAllSelectorNodeChildrenRegular) {
  1438. return true;
  1439. }
  1440. }
  1441. // for other extended pseudo-classes than :not() and :is()
  1442. return innerSelectorNodes.some(selectorNode => {
  1443. return selectorNode.children.some(selectorNodeChild => {
  1444. if (!isExtendedSelectorNode(selectorNodeChild)) {
  1445. return false;
  1446. }
  1447. // check inner ExtendedSelector recursively
  1448. // e.g. 'div:has(*:not(.header))'
  1449. return shouldOptimizeExtendedSelector(selectorNodeChild);
  1450. });
  1451. });
  1452. };
  1453.  
  1454. /**
  1455. * Returns optimized ExtendedSelector node if it can be optimized
  1456. * or null if ExtendedSelector is fully optimized while function execution
  1457. * which means that value of `prevRegularSelectorNode` is updated.
  1458. *
  1459. * @param currExtendedSelectorNode Current ExtendedSelector node to optimize.
  1460. * @param prevRegularSelectorNode Previous RegularSelector node.
  1461. *
  1462. * @returns Ast node or null.
  1463. */
  1464. const getOptimizedExtendedSelector = (currExtendedSelectorNode, prevRegularSelectorNode) => {
  1465. if (!currExtendedSelectorNode) {
  1466. return null;
  1467. }
  1468. const extendedPseudoClassNode = getPseudoClassNode(currExtendedSelectorNode);
  1469. const relativeSelectorList = getRelativeSelectorListNode(extendedPseudoClassNode);
  1470. const hasInnerExtendedSelector = hasExtendedSelector(relativeSelectorList);
  1471. if (!hasInnerExtendedSelector) {
  1472. // if there is no extended selectors for :not() or :is()
  1473. // e.g. 'div:not(.content, .main)'
  1474. const relativeSelectorListStr = selectorListOfRegularsToString(relativeSelectorList);
  1475. const pseudoName = getNodeName(extendedPseudoClassNode);
  1476. // eslint-disable-next-line max-len
  1477. const optimizedExtendedStr = `${COLON}${pseudoName}${BRACKETS.PARENTHESES.LEFT}${relativeSelectorListStr}${BRACKETS.PARENTHESES.RIGHT}`;
  1478. prevRegularSelectorNode.value = `${getNodeValue(prevRegularSelectorNode)}${optimizedExtendedStr}`;
  1479. return null;
  1480. }
  1481.  
  1482. // eslint-disable-next-line @typescript-eslint/no-use-before-define
  1483. const optimizedRelativeSelectorList = optimizeSelectorListNode(relativeSelectorList);
  1484. const optimizedExtendedPseudoClassNode = updateNodeChildren(extendedPseudoClassNode, [optimizedRelativeSelectorList]);
  1485. return updateNodeChildren(currExtendedSelectorNode, [optimizedExtendedPseudoClassNode]);
  1486. };
  1487.  
  1488. /**
  1489. * Combines values of `previous` and `current` RegularSelector nodes.
  1490. * It may happen during the optimization when ExtendedSelector between RegularSelector node was optimized.
  1491. *
  1492. * @param current Current RegularSelector node.
  1493. * @param previous Previous RegularSelector node.
  1494. */
  1495. const optimizeCurrentRegularSelector = (current, previous) => {
  1496. previous.value = `${getNodeValue(previous)}${SPACE}${getNodeValue(current)}`;
  1497. };
  1498.  
  1499. /**
  1500. * Optimizes ast Selector node.
  1501. *
  1502. * @param selectorNode Ast Selector node.
  1503. *
  1504. * @returns Optimized ast node.
  1505. * @throws An error while collecting optimized nodes.
  1506. */
  1507. const optimizeSelectorNode = selectorNode => {
  1508. // non-optimized list of SelectorNode children
  1509. const rawSelectorNodeChildren = selectorNode.children;
  1510. // for collecting optimized children list
  1511. const optimizedChildrenList = [];
  1512. let currentIndex = 0;
  1513. // iterate through all children in non-optimized ast Selector node
  1514. while (currentIndex < rawSelectorNodeChildren.length) {
  1515. const currentChild = getItemByIndex(rawSelectorNodeChildren, currentIndex, 'currentChild should be specified');
  1516. // no need to optimize the very first child which is always RegularSelector node
  1517. if (currentIndex === 0) {
  1518. optimizedChildrenList.push(currentChild);
  1519. } else {
  1520. const prevRegularChild = getLastRegularChild(optimizedChildrenList);
  1521. if (isExtendedSelectorNode(currentChild)) {
  1522. // start checking with point is null
  1523. let optimizedExtendedSelector = null;
  1524. // check whether the optimization is needed
  1525. let isOptimizationNeeded = shouldOptimizeExtendedSelector(currentChild);
  1526. // update optimizedExtendedSelector so it can be optimized recursively
  1527. // i.e. `getOptimizedExtendedSelector(optimizedExtendedSelector)` below
  1528. optimizedExtendedSelector = currentChild;
  1529. while (isOptimizationNeeded) {
  1530. // recursively optimize ExtendedSelector until no optimization needed
  1531. // e.g. div > *:is(.banner:not(.block))
  1532. optimizedExtendedSelector = getOptimizedExtendedSelector(optimizedExtendedSelector, prevRegularChild);
  1533. isOptimizationNeeded = shouldOptimizeExtendedSelector(optimizedExtendedSelector);
  1534. }
  1535. // if it was simple :not() of :is() with standard selector arg
  1536. // e.g. 'div:not([class][id])'
  1537. // or '.main > *:is([data-loaded], .banner)'
  1538. // after the optimization the ExtendedSelector node become part of RegularSelector
  1539. // so nothing to save eventually
  1540. // otherwise the optimized ExtendedSelector should be saved
  1541. // e.g. 'div:has(:not([class]))'
  1542. if (optimizedExtendedSelector !== null) {
  1543. optimizedChildrenList.push(optimizedExtendedSelector);
  1544. // if optimization is not needed
  1545. const optimizedPseudoClass = getPseudoClassNode(optimizedExtendedSelector);
  1546. const optimizedPseudoName = getNodeName(optimizedPseudoClass);
  1547. // parent element checking is used to apply :is() and :not() pseudo-classes as extended.
  1548. // as there is no parentNode for root element (html)
  1549. // so element selection should be limited to it's children
  1550. // e.g. '*:is(:has(.page))' -> 'html *:is(has(.page))'
  1551. // or '*:not(:has(span))' -> 'html *:not(:has(span))'
  1552. if (getNodeValue(prevRegularChild) === ASTERISK && isOptimizationPseudoClass(optimizedPseudoName)) {
  1553. prevRegularChild.value = IS_OR_NOT_PSEUDO_SELECTING_ROOT;
  1554. }
  1555. }
  1556. } else if (isRegularSelectorNode(currentChild)) {
  1557. // in non-optimized ast, RegularSelector node may follow ExtendedSelector which should be optimized
  1558. // for example, for 'div:not(.content) > .banner' schematically it looks like
  1559. // non-optimized ast: [
  1560. // 1. RegularSelector: 'div'
  1561. // 2. ExtendedSelector: 'not(.content)'
  1562. // 3. RegularSelector: '> .banner'
  1563. // ]
  1564. // which after the ExtendedSelector looks like
  1565. // partly optimized ast: [
  1566. // 1. RegularSelector: 'div:not(.content)'
  1567. // 2. RegularSelector: '> .banner'
  1568. // ]
  1569. // so second RegularSelector value should be combined with first one
  1570. // optimized ast: [
  1571. // 1. RegularSelector: 'div:not(.content) > .banner'
  1572. // ]
  1573. // here we check **children of selectorNode** after previous optimization if it was
  1574. const lastOptimizedChild = getLast(optimizedChildrenList) || null;
  1575. if (isRegularSelectorNode(lastOptimizedChild)) {
  1576. optimizeCurrentRegularSelector(currentChild, prevRegularChild);
  1577. }
  1578. }
  1579. }
  1580. currentIndex += 1;
  1581. }
  1582. return updateNodeChildren(selectorNode, optimizedChildrenList);
  1583. };
  1584.  
  1585. /**
  1586. * Optimizes ast SelectorList node.
  1587. *
  1588. * @param selectorListNode SelectorList node.
  1589. *
  1590. * @returns Optimized ast node.
  1591. */
  1592. const optimizeSelectorListNode = selectorListNode => {
  1593. return updateNodeChildren(selectorListNode, selectorListNode.children.map(s => optimizeSelectorNode(s)));
  1594. };
  1595.  
  1596. /**
  1597. * Optimizes ast:
  1598. * If arg of :not() and :is() pseudo-classes does not contain extended selectors,
  1599. * native Document.querySelectorAll() can be used to query elements.
  1600. * It means that ExtendedSelector ast nodes can be removed
  1601. * and value of relevant RegularSelector node should be updated accordingly.
  1602. *
  1603. * @param ast Non-optimized ast.
  1604. *
  1605. * @returns Optimized ast.
  1606. */
  1607. const optimizeAst = ast => {
  1608. // ast is basically the selector list of selectors
  1609. return optimizeSelectorListNode(ast);
  1610. };
  1611.  
  1612. // limit applying of :xpath() pseudo-class to 'any' element
  1613. // https://github.com/AdguardTeam/ExtendedCss/issues/115
  1614. const XPATH_PSEUDO_SELECTING_ROOT = 'body';
  1615. const NO_WHITESPACE_ERROR_PREFIX = 'No white space is allowed before or after extended pseudo-class name in selector';
  1616.  
  1617. /**
  1618. * Parses selector into ast for following element selection.
  1619. *
  1620. * @param selector Selector to parse.
  1621. *
  1622. * @returns Parsed ast.
  1623. * @throws An error on invalid selector.
  1624. */
  1625. const parse$1 = selector => {
  1626. const tokens = tokenizeSelector(selector);
  1627. const context = {
  1628. ast: null,
  1629. pathToBufferNode: [],
  1630. extendedPseudoNamesStack: [],
  1631. extendedPseudoBracketsStack: [],
  1632. standardPseudoNamesStack: [],
  1633. standardPseudoBracketsStack: [],
  1634. isAttributeBracketsOpen: false,
  1635. attributeBuffer: '',
  1636. isRegexpOpen: false,
  1637. shouldOptimize: false
  1638. };
  1639. let i = 0;
  1640. while (i < tokens.length) {
  1641. const token = tokens[i];
  1642. if (!token) {
  1643. break;
  1644. }
  1645. // Token to process
  1646. const {
  1647. type: tokenType,
  1648. value: tokenValue
  1649. } = token;
  1650.  
  1651. // needed for SPACE and COLON tokens checking
  1652. const nextToken = tokens[i + 1];
  1653. const nextTokenType = nextToken === null || nextToken === void 0 ? void 0 : nextToken.type;
  1654. const nextTokenValue = nextToken === null || nextToken === void 0 ? void 0 : nextToken.value;
  1655.  
  1656. // needed for limitations
  1657. // - :not() and :is() root element
  1658. // - :has() usage
  1659. // - white space before and after pseudo-class name
  1660. const nextToNextToken = tokens[i + 2];
  1661. const nextToNextTokenValue = nextToNextToken === null || nextToNextToken === void 0 ? void 0 : nextToNextToken.value;
  1662.  
  1663. // needed for COLON token checking for none-specified regular selector before extended one
  1664. // e.g. 'p, :hover'
  1665. // or '.banner, :contains(ads)'
  1666. const previousToken = tokens[i - 1];
  1667. const prevTokenType = previousToken === null || previousToken === void 0 ? void 0 : previousToken.type;
  1668. const prevTokenValue = previousToken === null || previousToken === void 0 ? void 0 : previousToken.value;
  1669.  
  1670. // needed for proper parsing of regexp pattern arg
  1671. // e.g. ':matches-css(background-image: /^url\(https:\/\/example\.org\//)'
  1672. const previousToPreviousToken = tokens[i - 2];
  1673. const prevToPrevTokenValue = previousToPreviousToken === null || previousToPreviousToken === void 0 ? void 0 : previousToPreviousToken.value;
  1674. let bufferNode = getBufferNode(context);
  1675. switch (tokenType) {
  1676. case TokenType.Word:
  1677. if (bufferNode === null) {
  1678. // there is no buffer node only in one case — no ast collecting has been started
  1679. initAst(context, tokenValue);
  1680. } else if (isSelectorListNode(bufferNode)) {
  1681. // add new selector to selector list
  1682. addAstNodeByType(context, NodeType.Selector);
  1683. addAstNodeByType(context, NodeType.RegularSelector, tokenValue);
  1684. } else if (isRegularSelectorNode(bufferNode)) {
  1685. updateBufferNode(context, tokenValue);
  1686. } else if (isExtendedSelectorNode(bufferNode)) {
  1687. // No white space is allowed between the name of extended pseudo-class
  1688. // and its opening parenthesis
  1689. // https://www.w3.org/TR/selectors-4/#pseudo-classes
  1690. // e.g. 'span:contains (text)'
  1691. if (isWhiteSpaceChar(nextTokenValue) && nextToNextTokenValue === BRACKETS.PARENTHESES.LEFT) {
  1692. throw new Error(`${NO_WHITESPACE_ERROR_PREFIX}: '${selector}'`);
  1693. }
  1694. const lowerCaseTokenValue = tokenValue.toLowerCase();
  1695. // save pseudo-class name for brackets balance checking
  1696. context.extendedPseudoNamesStack.push(lowerCaseTokenValue);
  1697. // extended pseudo-class name are parsed in lower case
  1698. // as they should be case-insensitive
  1699. // https://www.w3.org/TR/selectors-4/#pseudo-classes
  1700. if (isAbsolutePseudoClass(lowerCaseTokenValue)) {
  1701. addAstNodeByType(context, NodeType.AbsolutePseudoClass, lowerCaseTokenValue);
  1702. } else {
  1703. // if it is not absolute pseudo-class, it must be relative one
  1704. // add RelativePseudoClass with tokenValue as pseudo-class name to ExtendedSelector children
  1705. addAstNodeByType(context, NodeType.RelativePseudoClass, lowerCaseTokenValue);
  1706. // for :not() and :is() pseudo-classes parsed ast should be optimized later
  1707. if (isOptimizationPseudoClass(lowerCaseTokenValue)) {
  1708. context.shouldOptimize = true;
  1709. }
  1710. }
  1711. } else if (isAbsolutePseudoClassNode(bufferNode)) {
  1712. // collect absolute pseudo-class arg
  1713. updateBufferNode(context, tokenValue);
  1714. } else if (isRelativePseudoClassNode(bufferNode)) {
  1715. initRelativeSubtree(context, tokenValue);
  1716. }
  1717. break;
  1718. case TokenType.Mark:
  1719. switch (tokenValue) {
  1720. case COMMA:
  1721. if (!bufferNode || typeof bufferNode !== 'undefined' && !nextTokenValue) {
  1722. // consider the selector is invalid if there is no bufferNode yet (e.g. ', a')
  1723. // or there is nothing after the comma while bufferNode is defined (e.g. 'div, ')
  1724. throw new Error(`'${selector}' is not a valid selector`);
  1725. } else if (isRegularSelectorNode(bufferNode)) {
  1726. if (context.isAttributeBracketsOpen) {
  1727. // the comma might be inside element attribute value
  1728. // e.g. 'div[data-comma="0,1"]'
  1729. updateBufferNode(context, tokenValue);
  1730. } else {
  1731. // new Selector should be collected to upper SelectorList
  1732. upToClosest(context, NodeType.SelectorList);
  1733. }
  1734. } else if (isAbsolutePseudoClassNode(bufferNode)) {
  1735. // the comma inside arg of absolute extended pseudo
  1736. // e.g. 'div:xpath(//h3[contains(text(),"Share it!")]/..)'
  1737. updateBufferNode(context, tokenValue);
  1738. } else if (isSelectorNode(bufferNode)) {
  1739. // new Selector should be collected to upper SelectorList
  1740. // if parser position is on Selector node
  1741. upToClosest(context, NodeType.SelectorList);
  1742. }
  1743. break;
  1744. case SPACE:
  1745. // it might be complex selector with extended pseudo-class inside it
  1746. // and the space is between that complex selector and following regular selector
  1747. // parser position is on ` ` before `span` now:
  1748. // e.g. 'div:has(img).banner span'
  1749. // so we need to check whether the new ast node should be added (example above)
  1750. // or previous regular selector node should be updated
  1751. if (isRegularSelectorNode(bufferNode)
  1752. // no need to update the buffer node if attribute value is being parsed
  1753. // e.g. 'div:not([id])[style="position: absolute; z-index: 10000;"]'
  1754. // parser position inside attribute ↑
  1755. && !context.isAttributeBracketsOpen) {
  1756. bufferNode = getUpdatedBufferNode(context);
  1757. }
  1758. if (isRegularSelectorNode(bufferNode)) {
  1759. // standard selectors with white space between colon and name of pseudo
  1760. // are invalid for native document.querySelectorAll() anyway,
  1761. // so throwing the error here is better
  1762. // than proper parsing of invalid selector and passing it further.
  1763. // first of all do not check attributes
  1764. // e.g. div[style="text-align: center"]
  1765. if (!context.isAttributeBracketsOpen
  1766. // check the space after the colon and before the pseudo
  1767. // e.g. '.block: nth-child(2)
  1768. && (prevTokenValue === COLON && nextTokenType === TokenType.Word
  1769. // or after the pseudo and before the opening parenthesis
  1770. // e.g. '.block:nth-child (2)
  1771. || prevTokenType === TokenType.Word && nextTokenValue === BRACKETS.PARENTHESES.LEFT)) {
  1772. throw new Error(`'${selector}' is not a valid selector`);
  1773. }
  1774. // collect current tokenValue to value of RegularSelector
  1775. // if it is the last token or standard selector continues after the space.
  1776. // otherwise it will be skipped
  1777. if (!nextTokenValue || doesRegularContinueAfterSpace(nextTokenType, nextTokenValue)
  1778. // we also should collect space inside attribute value
  1779. // e.g. `[onclick^="window.open ('https://example.com/share?url="]`
  1780. // parser position ↑
  1781. || context.isAttributeBracketsOpen) {
  1782. updateBufferNode(context, tokenValue);
  1783. }
  1784. }
  1785. if (isAbsolutePseudoClassNode(bufferNode)) {
  1786. // space inside extended pseudo-class arg
  1787. // e.g. 'span:contains(some text)'
  1788. updateBufferNode(context, tokenValue);
  1789. }
  1790. if (isRelativePseudoClassNode(bufferNode)) {
  1791. // init with empty value RegularSelector
  1792. // as the space is not needed for selector value
  1793. // e.g. 'p:not( .content )'
  1794. initRelativeSubtree(context);
  1795. }
  1796. if (isSelectorNode(bufferNode)) {
  1797. // do NOT add RegularSelector if parser position on space BEFORE the comma in selector list
  1798. // e.g. '.block:has(> img) , .banner)'
  1799. if (doesRegularContinueAfterSpace(nextTokenType, nextTokenValue)) {
  1800. // regular selector might be after the extended one.
  1801. // extra space before combinator or selector should not be collected
  1802. // e.g. '.banner:upward(2) .block'
  1803. // '.banner:upward(2) > .block'
  1804. // so no tokenValue passed to addAnySelectorNode()
  1805. addAstNodeByType(context, NodeType.RegularSelector);
  1806. }
  1807. }
  1808. break;
  1809. case DESCENDANT_COMBINATOR:
  1810. case CHILD_COMBINATOR:
  1811. case NEXT_SIBLING_COMBINATOR:
  1812. case SUBSEQUENT_SIBLING_COMBINATOR:
  1813. case SEMICOLON:
  1814. case SLASH:
  1815. case BACKSLASH:
  1816. case SINGLE_QUOTE:
  1817. case DOUBLE_QUOTE:
  1818. case CARET:
  1819. case DOLLAR_SIGN:
  1820. case BRACKETS.CURLY.LEFT:
  1821. case BRACKETS.CURLY.RIGHT:
  1822. case ASTERISK:
  1823. case ID_MARKER:
  1824. case CLASS_MARKER:
  1825. case BRACKETS.SQUARE.LEFT:
  1826. // it might be complex selector with extended pseudo-class inside it
  1827. // and the space is between that complex selector and following regular selector
  1828. // e.g. 'div:has(img).banner' // parser position is on `.` before `banner` now
  1829. // 'div:has(img)[attr]' // parser position is on `[` before `attr` now
  1830. // so we need to check whether the new ast node should be added (example above)
  1831. // or previous regular selector node should be updated
  1832. if (COMBINATORS.includes(tokenValue)) {
  1833. if (bufferNode === null) {
  1834. // cases where combinator at very beginning of a selector
  1835. // e.g. '> div'
  1836. // or '~ .banner'
  1837. // or even '+js(overlay-buster)' which not a selector at all
  1838. // but may be validated by FilterCompiler so error message should be appropriate
  1839. throw new Error(`'${selector}' is not a valid selector`);
  1840. }
  1841. bufferNode = getUpdatedBufferNode(context);
  1842. }
  1843. if (bufferNode === null) {
  1844. // no ast collecting has been started
  1845. // e.g. '.banner > p'
  1846. // or '#top > div.ad'
  1847. // or '[class][style][attr]'
  1848. // or '*:not(span)'
  1849. initAst(context, tokenValue);
  1850. if (isAttributeOpening(tokenValue, prevTokenValue)) {
  1851. // e.g. '[class^="banner-"]'
  1852. context.isAttributeBracketsOpen = true;
  1853. }
  1854. } else if (isRegularSelectorNode(bufferNode)) {
  1855. // collect the mark to the value of RegularSelector node
  1856. updateBufferNode(context, tokenValue);
  1857. if (isAttributeOpening(tokenValue, prevTokenValue)) {
  1858. // needed for proper handling element attribute value with comma
  1859. // e.g. 'div[data-comma="0,1"]'
  1860. context.isAttributeBracketsOpen = true;
  1861. }
  1862. } else if (isAbsolutePseudoClassNode(bufferNode)) {
  1863. // collect the mark to the arg of AbsolutePseudoClass node
  1864. updateBufferNode(context, tokenValue);
  1865. // 'isRegexpOpen' flag is needed for brackets balancing inside extended pseudo-class arg
  1866. if (tokenValue === SLASH && context.extendedPseudoNamesStack.length > 0) {
  1867. if (prevTokenValue === SLASH && prevToPrevTokenValue === BACKSLASH) {
  1868. // it may be specific url regexp pattern in arg of pseudo-class
  1869. // e.g. ':matches-css(background-image: /^url\(https:\/\/example\.org\//)'
  1870. // parser position is on final slash before `)` ↑
  1871. context.isRegexpOpen = false;
  1872. } else if (prevTokenValue && prevTokenValue !== BACKSLASH) {
  1873. if (isRegexpOpening(context, prevTokenValue, getNodeValue(bufferNode))) {
  1874. context.isRegexpOpen = !context.isRegexpOpen;
  1875. } else {
  1876. // otherwise force `isRegexpOpen` flag to `false`
  1877. context.isRegexpOpen = false;
  1878. }
  1879. }
  1880. }
  1881. } else if (isRelativePseudoClassNode(bufferNode)) {
  1882. // add SelectorList to children of RelativePseudoClass node
  1883. initRelativeSubtree(context, tokenValue);
  1884. if (isAttributeOpening(tokenValue, prevTokenValue)) {
  1885. // besides of creating the relative subtree
  1886. // opening square bracket means start of attribute
  1887. // e.g. 'div:not([class="content"])'
  1888. // 'div:not([href*="window.print()"])'
  1889. context.isAttributeBracketsOpen = true;
  1890. }
  1891. } else if (isSelectorNode(bufferNode)) {
  1892. // after the extended pseudo closing parentheses
  1893. // parser position is on Selector node
  1894. // and regular selector can be after the extended one
  1895. // e.g. '.banner:upward(2)> .block'
  1896. // or '.inner:nth-ancestor(1)~ .banner'
  1897. if (COMBINATORS.includes(tokenValue)) {
  1898. addAstNodeByType(context, NodeType.RegularSelector, tokenValue);
  1899. } else if (!context.isRegexpOpen) {
  1900. // it might be complex selector with extended pseudo-class inside it.
  1901. // parser position is on `.` now:
  1902. // e.g. 'div:has(img).banner'
  1903. // so we need to get last regular selector node and update its value
  1904. bufferNode = getContextLastRegularSelectorNode(context);
  1905. updateBufferNode(context, tokenValue);
  1906. if (isAttributeOpening(tokenValue, prevTokenValue)) {
  1907. // handle attribute in compound selector after extended pseudo-class
  1908. // e.g. 'div:not(.top)[style="z-index: 10000;"]'
  1909. // parser position ↑
  1910. context.isAttributeBracketsOpen = true;
  1911. }
  1912. }
  1913. } else if (isSelectorListNode(bufferNode)) {
  1914. // add Selector to SelectorList
  1915. addAstNodeByType(context, NodeType.Selector);
  1916. // and RegularSelector as it is always the first child of Selector
  1917. addAstNodeByType(context, NodeType.RegularSelector, tokenValue);
  1918. if (isAttributeOpening(tokenValue, prevTokenValue)) {
  1919. // handle simple attribute selector in selector list
  1920. // e.g. '.banner, [class^="ad-"]'
  1921. context.isAttributeBracketsOpen = true;
  1922. }
  1923. }
  1924. break;
  1925. case BRACKETS.SQUARE.RIGHT:
  1926. if (isRegularSelectorNode(bufferNode)) {
  1927. // unescaped `]` in regular selector allowed only inside attribute value
  1928. if (!context.isAttributeBracketsOpen && prevTokenValue !== BACKSLASH) {
  1929. // e.g. 'div]'
  1930. // eslint-disable-next-line max-len
  1931. throw new Error(`'${selector}' is not a valid selector due to '${tokenValue}' after '${getNodeValue(bufferNode)}'`);
  1932. }
  1933. // needed for proper parsing regular selectors after the attributes with comma
  1934. // e.g. 'div[data-comma="0,1"] > img'
  1935. if (isAttributeClosing(context)) {
  1936. context.isAttributeBracketsOpen = false;
  1937. // reset attribute buffer on closing `]`
  1938. context.attributeBuffer = '';
  1939. }
  1940. // collect the bracket to the value of RegularSelector node
  1941. updateBufferNode(context, tokenValue);
  1942. }
  1943. if (isAbsolutePseudoClassNode(bufferNode)) {
  1944. // :xpath() expended pseudo-class arg might contain square bracket
  1945. // so it should be collected
  1946. // e.g. 'div:xpath(//h3[contains(text(),"Share it!")]/..)'
  1947. updateBufferNode(context, tokenValue);
  1948. }
  1949. break;
  1950. case COLON:
  1951. // No white space is allowed between the colon and the following name of the pseudo-class
  1952. // https://www.w3.org/TR/selectors-4/#pseudo-classes
  1953. // e.g. 'span: contains(text)'
  1954. if (isWhiteSpaceChar(nextTokenValue) && nextToNextTokenValue && SUPPORTED_PSEUDO_CLASSES.includes(nextToNextTokenValue)) {
  1955. throw new Error(`${NO_WHITESPACE_ERROR_PREFIX}: '${selector}'`);
  1956. }
  1957. if (bufferNode === null) {
  1958. // no ast collecting has been started
  1959. if (nextTokenValue === XPATH_PSEUDO_CLASS_MARKER) {
  1960. // limit applying of "naked" :xpath pseudo-class
  1961. // https://github.com/AdguardTeam/ExtendedCss/issues/115
  1962. initAst(context, XPATH_PSEUDO_SELECTING_ROOT);
  1963. } else if (nextTokenValue === UPWARD_PSEUDO_CLASS_MARKER || nextTokenValue === NTH_ANCESTOR_PSEUDO_CLASS_MARKER) {
  1964. // selector should be specified before :nth-ancestor() or :upward()
  1965. // e.g. ':nth-ancestor(3)'
  1966. // or ':upward(span)'
  1967. throw new Error(`${NO_SELECTOR_ERROR_PREFIX} :${nextTokenValue}() pseudo-class`);
  1968. } else {
  1969. // make it more obvious if selector starts with pseudo with no tag specified
  1970. // e.g. ':has(a)' -> '*:has(a)'
  1971. // or ':empty' -> '*:empty'
  1972. initAst(context, ASTERISK);
  1973. }
  1974.  
  1975. // bufferNode should be updated for following checking
  1976. bufferNode = getBufferNode(context);
  1977. }
  1978. if (isSelectorListNode(bufferNode)) {
  1979. // bufferNode is SelectorList after comma has been parsed.
  1980. // parser position is on colon now:
  1981. // e.g. 'img,:not(.content)'
  1982. addAstNodeByType(context, NodeType.Selector);
  1983. // add empty value RegularSelector anyway as any selector should start with it
  1984. // and check previous token on the next step
  1985. addAstNodeByType(context, NodeType.RegularSelector);
  1986. // bufferNode should be updated for following checking
  1987. bufferNode = getBufferNode(context);
  1988. }
  1989. if (isRegularSelectorNode(bufferNode)) {
  1990. // it can be extended or standard pseudo
  1991. // e.g. '#share, :contains(share it)'
  1992. // or 'div,:hover'
  1993. // of 'div:has(+:contains(text))' // position is after '+'
  1994. if (prevTokenValue && COMBINATORS.includes(prevTokenValue) || prevTokenValue === COMMA) {
  1995. // case with colon at the start of string - e.g. ':contains(text)'
  1996. // is covered by 'bufferNode === null' above at start of COLON checking
  1997. updateBufferNode(context, ASTERISK);
  1998. }
  1999. handleNextTokenOnColon(context, selector, tokenValue, nextTokenValue, nextToNextTokenValue);
  2000. }
  2001. if (isSelectorNode(bufferNode)) {
  2002. // e.g. 'div:contains(text):'
  2003. if (!nextTokenValue) {
  2004. throw new Error(`Invalid colon ':' at the end of selector: '${selector}'`);
  2005. }
  2006. // after the extended pseudo closing parentheses
  2007. // parser position is on Selector node
  2008. // and there is might be another extended selector.
  2009. // parser position is on colon before 'upward':
  2010. // e.g. 'p:contains(PR):upward(2)'
  2011. if (isSupportedPseudoClass(nextTokenValue.toLowerCase())) {
  2012. // if supported extended pseudo-class is next to colon
  2013. // add ExtendedSelector to Selector children
  2014. addAstNodeByType(context, NodeType.ExtendedSelector);
  2015. } else if (nextTokenValue.toLowerCase() === REMOVE_PSEUDO_MARKER) {
  2016. // :remove() pseudo-class should be handled before
  2017. // as it is not about element selecting but actions with elements
  2018. // e.g. '#banner:upward(2):remove()'
  2019. throw new Error(`${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${selector}'`);
  2020. } else {
  2021. // otherwise it is standard pseudo after extended pseudo-class in complex selector
  2022. // and colon should be collected to value of previous RegularSelector
  2023. // e.g. 'body *:not(input)::selection'
  2024. // 'input:matches-css(padding: 10):checked'
  2025. bufferNode = getContextLastRegularSelectorNode(context);
  2026. handleNextTokenOnColon(context, selector, tokenValue, nextTokenType, nextToNextTokenValue);
  2027. }
  2028. }
  2029. if (isAbsolutePseudoClassNode(bufferNode)) {
  2030. // :xpath() pseudo-class should be the last of extended pseudo-classes
  2031. if (getNodeName(bufferNode) === XPATH_PSEUDO_CLASS_MARKER && nextTokenValue && SUPPORTED_PSEUDO_CLASSES.includes(nextTokenValue) && nextToNextTokenValue === BRACKETS.PARENTHESES.LEFT) {
  2032. throw new Error(`:xpath() pseudo-class should be the last in selector: '${selector}'`);
  2033. }
  2034. // collecting arg for absolute pseudo-class
  2035. // e.g. 'div:matches-css(width:400px)'
  2036. updateBufferNode(context, tokenValue);
  2037. }
  2038. if (isRelativePseudoClassNode(bufferNode)) {
  2039. if (!nextTokenValue) {
  2040. // e.g. 'div:has(:'
  2041. throw new Error(`Invalid pseudo-class arg at the end of selector: '${selector}'`);
  2042. }
  2043. // make it more obvious if selector starts with pseudo with no tag specified
  2044. // parser position is on colon inside :has() arg
  2045. // e.g. 'div:has(:contains(text))'
  2046. // or 'div:not(:empty)'
  2047. initRelativeSubtree(context, ASTERISK);
  2048. if (!isSupportedPseudoClass(nextTokenValue.toLowerCase())) {
  2049. // collect the colon to value of RegularSelector
  2050. // e.g. 'div:not(:empty)'
  2051. updateBufferNode(context, tokenValue);
  2052. // parentheses should be balanced only for functional pseudo-classes
  2053. // e.g. '.yellow:not(:nth-child(3))'
  2054. if (nextToNextTokenValue === BRACKETS.PARENTHESES.LEFT) {
  2055. context.standardPseudoNamesStack.push(nextTokenValue);
  2056. }
  2057. } else {
  2058. // add ExtendedSelector to Selector children
  2059. // e.g. 'div:has(:contains(text))'
  2060. upToClosest(context, NodeType.Selector);
  2061. addAstNodeByType(context, NodeType.ExtendedSelector);
  2062. }
  2063. }
  2064. break;
  2065. case BRACKETS.PARENTHESES.LEFT:
  2066. // start of pseudo-class arg
  2067. if (isAbsolutePseudoClassNode(bufferNode)) {
  2068. // no brackets balancing needed inside
  2069. // 1. :xpath() extended pseudo-class arg
  2070. // 2. regexp arg for other extended pseudo-classes
  2071. if (getNodeName(bufferNode) !== XPATH_PSEUDO_CLASS_MARKER && context.isRegexpOpen) {
  2072. // if the parentheses is escaped it should be part of regexp
  2073. // collect it to arg of AbsolutePseudoClass
  2074. // e.g. 'div:matches-css(background-image: /^url\\("data:image\\/gif;base64.+/)'
  2075. updateBufferNode(context, tokenValue);
  2076. } else {
  2077. // otherwise brackets should be balanced
  2078. // e.g. 'div:xpath(//h3[contains(text(),"Share it!")]/..)'
  2079. context.extendedPseudoBracketsStack.push(tokenValue);
  2080. // eslint-disable-next-line max-len
  2081. if (context.extendedPseudoBracketsStack.length > context.extendedPseudoNamesStack.length) {
  2082. updateBufferNode(context, tokenValue);
  2083. }
  2084. }
  2085. }
  2086. if (isRegularSelectorNode(bufferNode)) {
  2087. // continue RegularSelector value collecting for standard pseudo-classes
  2088. // e.g. '.banner:where(div)'
  2089. if (context.standardPseudoNamesStack.length > 0) {
  2090. updateBufferNode(context, tokenValue);
  2091. context.standardPseudoBracketsStack.push(tokenValue);
  2092. }
  2093. // parentheses inside attribute value should be part of RegularSelector value
  2094. // e.g. 'div:not([href*="window.print()"])' <-- parser position
  2095. // is on the `(` after `print` ↑
  2096. if (context.isAttributeBracketsOpen) {
  2097. updateBufferNode(context, tokenValue);
  2098. }
  2099. }
  2100. if (isRelativePseudoClassNode(bufferNode)) {
  2101. // save opening bracket for balancing
  2102. // e.g. 'div:not()' // position is on `(`
  2103. context.extendedPseudoBracketsStack.push(tokenValue);
  2104. }
  2105. break;
  2106. case BRACKETS.PARENTHESES.RIGHT:
  2107. if (isAbsolutePseudoClassNode(bufferNode)) {
  2108. // no brackets balancing needed inside
  2109. // 1. :xpath() extended pseudo-class arg
  2110. // 2. regexp arg for other extended pseudo-classes
  2111. if (getNodeName(bufferNode) !== XPATH_PSEUDO_CLASS_MARKER && context.isRegexpOpen) {
  2112. // if closing bracket is part of regexp
  2113. // simply save it to pseudo-class arg
  2114. updateBufferNode(context, tokenValue);
  2115. } else {
  2116. // remove stacked open parentheses for brackets balance
  2117. // e.g. 'h3:contains((Ads))'
  2118. // or 'div:xpath(//h3[contains(text(),"Share it!")]/..)'
  2119. context.extendedPseudoBracketsStack.pop();
  2120. if (getNodeName(bufferNode) !== XPATH_PSEUDO_CLASS_MARKER) {
  2121. // for all other absolute pseudo-classes except :xpath()
  2122. // remove stacked name of extended pseudo-class
  2123. context.extendedPseudoNamesStack.pop();
  2124. // eslint-disable-next-line max-len
  2125. if (context.extendedPseudoBracketsStack.length > context.extendedPseudoNamesStack.length) {
  2126. // if brackets stack is not empty yet,
  2127. // save tokenValue to arg of AbsolutePseudoClass
  2128. // parser position on first closing bracket after 'Ads':
  2129. // e.g. 'h3:contains((Ads))'
  2130. updateBufferNode(context, tokenValue);
  2131. } else if (context.extendedPseudoBracketsStack.length >= 0 && context.extendedPseudoNamesStack.length >= 0) {
  2132. // assume it is combined extended pseudo-classes
  2133. // parser position on first closing bracket after 'advert':
  2134. // e.g. 'div:has(.banner, :contains(advert))'
  2135. upToClosest(context, NodeType.Selector);
  2136. }
  2137. } else {
  2138. // for :xpath()
  2139. // eslint-disable-next-line max-len
  2140. if (context.extendedPseudoBracketsStack.length < context.extendedPseudoNamesStack.length) {
  2141. // remove stacked name of extended pseudo-class
  2142. // if there are less brackets than pseudo-class names
  2143. // with means last removes bracket was closing for pseudo-class
  2144. context.extendedPseudoNamesStack.pop();
  2145. } else {
  2146. // otherwise the bracket is part of arg
  2147. updateBufferNode(context, tokenValue);
  2148. }
  2149. }
  2150. }
  2151. }
  2152. if (isRegularSelectorNode(bufferNode)) {
  2153. if (context.isAttributeBracketsOpen) {
  2154. // parentheses inside attribute value should be part of RegularSelector value
  2155. // e.g. 'div:not([href*="window.print()"])' <-- parser position
  2156. // is on the `)` after `print(` ↑
  2157. updateBufferNode(context, tokenValue);
  2158. } else if (context.standardPseudoNamesStack.length > 0 && context.standardPseudoBracketsStack.length > 0) {
  2159. // standard pseudo-class was processing.
  2160. // collect the closing bracket to value of RegularSelector
  2161. // parser position is on bracket after 'class' now:
  2162. // e.g. 'div:where(.class)'
  2163. updateBufferNode(context, tokenValue);
  2164. // remove bracket and pseudo name from stacks
  2165. context.standardPseudoBracketsStack.pop();
  2166. const lastStandardPseudo = context.standardPseudoNamesStack.pop();
  2167. if (!lastStandardPseudo) {
  2168. // standard pseudo should be in standardPseudoNamesStack
  2169. // as related to standardPseudoBracketsStack
  2170. throw new Error(`Parsing error. Invalid selector: ${selector}`);
  2171. }
  2172. // Disallow :has() after regular pseudo-elements
  2173. // https://bugs.chromium.org/p/chromium/issues/detail?id=669058#c54 [3]
  2174. if (Object.values(REGULAR_PSEUDO_ELEMENTS).includes(lastStandardPseudo)
  2175. // check token which is next to closing parentheses and token after it
  2176. // parser position is on bracket after 'foo' now:
  2177. // e.g. '::part(foo):has(.a)'
  2178. && nextTokenValue === COLON && nextToNextTokenValue && HAS_PSEUDO_CLASS_MARKERS.includes(nextToNextTokenValue)) {
  2179. // eslint-disable-next-line max-len
  2180. throw new Error(`Usage of :${nextToNextTokenValue}() pseudo-class is not allowed after any regular pseudo-element: '${lastStandardPseudo}'`);
  2181. }
  2182. } else {
  2183. // extended pseudo-class was processing.
  2184. // e.g. 'div:has(h3)'
  2185. // remove bracket and pseudo name from stacks
  2186. context.extendedPseudoBracketsStack.pop();
  2187. context.extendedPseudoNamesStack.pop();
  2188. upToClosest(context, NodeType.ExtendedSelector);
  2189. // go to upper selector for possible selector continuation after extended pseudo-class
  2190. // e.g. 'div:has(h3) > img'
  2191. upToClosest(context, NodeType.Selector);
  2192. }
  2193. }
  2194. if (isSelectorNode(bufferNode)) {
  2195. // after inner extended pseudo-class bufferNode is Selector.
  2196. // parser position is on last bracket now:
  2197. // e.g. 'div:has(.banner, :contains(ads))'
  2198. context.extendedPseudoBracketsStack.pop();
  2199. context.extendedPseudoNamesStack.pop();
  2200. upToClosest(context, NodeType.ExtendedSelector);
  2201. upToClosest(context, NodeType.Selector);
  2202. }
  2203. if (isRelativePseudoClassNode(bufferNode)) {
  2204. // save opening bracket for balancing
  2205. // e.g. 'div:not()' // position is on `)`
  2206. // context.extendedPseudoBracketsStack.push(tokenValue);
  2207. if (context.extendedPseudoNamesStack.length > 0 && context.extendedPseudoBracketsStack.length > 0) {
  2208. context.extendedPseudoBracketsStack.pop();
  2209. context.extendedPseudoNamesStack.pop();
  2210. }
  2211. }
  2212. break;
  2213. case LINE_FEED:
  2214. case FORM_FEED:
  2215. case CARRIAGE_RETURN:
  2216. // such characters at start and end of selector should be trimmed
  2217. // so is there is one them among tokens, it is not valid selector
  2218. throw new Error(`'${selector}' is not a valid selector`);
  2219. case TAB:
  2220. // allow tab only inside attribute value
  2221. // as there are such valid rules in filter lists
  2222. // e.g. 'div[style^="margin-right: auto; text-align: left;',
  2223. // parser position ↑
  2224. if (isRegularSelectorNode(bufferNode) && context.isAttributeBracketsOpen) {
  2225. updateBufferNode(context, tokenValue);
  2226. } else {
  2227. // otherwise not valid
  2228. throw new Error(`'${selector}' is not a valid selector`);
  2229. }
  2230. }
  2231. break;
  2232. // no default statement for Marks as they are limited to SUPPORTED_SELECTOR_MARKS
  2233. // and all other symbol combinations are tokenized as Word
  2234. // so error for invalid Word will be thrown later while element selecting by parsed ast
  2235. default:
  2236. throw new Error(`Unknown type of token: '${tokenValue}'`);
  2237. }
  2238. i += 1;
  2239. }
  2240. if (context.ast === null) {
  2241. throw new Error(`'${selector}' is not a valid selector`);
  2242. }
  2243. if (context.extendedPseudoNamesStack.length > 0 || context.extendedPseudoBracketsStack.length > 0) {
  2244. // eslint-disable-next-line max-len
  2245. throw new Error(`Unbalanced brackets for extended pseudo-class: '${getLast(context.extendedPseudoNamesStack)}'`);
  2246. }
  2247. if (context.isAttributeBracketsOpen) {
  2248. throw new Error(`Unbalanced attribute brackets in selector: '${selector}'`);
  2249. }
  2250. return context.shouldOptimize ? optimizeAst(context.ast) : context.ast;
  2251. };
  2252.  
  2253. const natives = {
  2254. MutationObserver: window.MutationObserver || window.WebKitMutationObserver
  2255. };
  2256.  
  2257. /**
  2258. * As soon as possible stores native Node textContent getter to be used for contains pseudo-class
  2259. * because elements' 'textContent' and 'innerText' properties might be mocked.
  2260. *
  2261. * @see {@link https://github.com/AdguardTeam/ExtendedCss/issues/127}
  2262. */
  2263. const nodeTextContentGetter = (() => {
  2264. var _Object$getOwnPropert;
  2265. const nativeNode = window.Node || Node;
  2266. return (_Object$getOwnPropert = Object.getOwnPropertyDescriptor(nativeNode.prototype, 'textContent')) === null || _Object$getOwnPropert === void 0 ? void 0 : _Object$getOwnPropert.get;
  2267. })();
  2268.  
  2269. /**
  2270. * Returns textContent of passed domElement.
  2271. *
  2272. * @param domElement DOM element.
  2273. *
  2274. * @returns DOM element textContent.
  2275. */
  2276. const getNodeTextContent = domElement => {
  2277. return (nodeTextContentGetter === null || nodeTextContentGetter === void 0 ? void 0 : nodeTextContentGetter.apply(domElement)) || '';
  2278. };
  2279.  
  2280. /**
  2281. * Returns element selector text based on it's tagName and attributes.
  2282. *
  2283. * @param element DOM element.
  2284. *
  2285. * @returns String representation of `element`.
  2286. */
  2287. const getElementSelectorDesc = element => {
  2288. let selectorText = element.tagName.toLowerCase();
  2289. selectorText += Array.from(element.attributes).map(attr => {
  2290. return `[${attr.name}="${element.getAttribute(attr.name)}"]`;
  2291. }).join('');
  2292. return selectorText;
  2293. };
  2294.  
  2295. /**
  2296. * Returns path to a DOM element as a selector string.
  2297. *
  2298. * @param inputEl Input element.
  2299. *
  2300. * @returns String path to a DOM element.
  2301. * @throws An error if `inputEl` in not instance of `Element`.
  2302. */
  2303. const getElementSelectorPath = inputEl => {
  2304. if (!(inputEl instanceof Element)) {
  2305. throw new Error('Function received argument with wrong type');
  2306. }
  2307. let el;
  2308. el = inputEl;
  2309. const path = [];
  2310. // we need to check '!!el' first because it is possible
  2311. // that some ancestor of the inputEl was removed before it
  2312. while (!!el && el.nodeType === Node.ELEMENT_NODE) {
  2313. let selector = el.nodeName.toLowerCase();
  2314. if (el.id && typeof el.id === 'string') {
  2315. selector += `#${el.id}`;
  2316. path.unshift(selector);
  2317. break;
  2318. }
  2319. let sibling = el;
  2320. let nth = 1;
  2321. while (sibling.previousElementSibling) {
  2322. sibling = sibling.previousElementSibling;
  2323. if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName.toLowerCase() === selector) {
  2324. nth += 1;
  2325. }
  2326. }
  2327. if (nth !== 1) {
  2328. selector += `:nth-of-type(${nth})`;
  2329. }
  2330. path.unshift(selector);
  2331. el = el.parentElement;
  2332. }
  2333. return path.join(' > ');
  2334. };
  2335.  
  2336. /**
  2337. * Checks whether the element is instance of HTMLElement.
  2338. *
  2339. * @param element Element to check.
  2340. *
  2341. * @returns True if `element` is HTMLElement.
  2342. */
  2343. const isHtmlElement = element => {
  2344. return element instanceof HTMLElement;
  2345. };
  2346.  
  2347. /**
  2348. * Takes `element` and returns its parent element.
  2349. *
  2350. * @param element Element.
  2351. * @param errorMessage Optional error message to throw.
  2352. *
  2353. * @returns Parent of `element`.
  2354. * @throws An error if element has no parent element.
  2355. */
  2356. const getParent = (element, errorMessage) => {
  2357. const {
  2358. parentElement
  2359. } = element;
  2360. if (!parentElement) {
  2361. throw new Error(errorMessage || 'Element does no have parent element');
  2362. }
  2363. return parentElement;
  2364. };
  2365.  
  2366. const logger = {
  2367. /**
  2368. * Safe console.error version.
  2369. */
  2370. error: typeof console !== 'undefined' && console.error && console.error.bind ? console.error.bind(window.console) : console.error,
  2371. /**
  2372. * Safe console.info version.
  2373. */
  2374. info: typeof console !== 'undefined' && console.info && console.info.bind ? console.info.bind(window.console) : console.info
  2375. };
  2376.  
  2377. /**
  2378. * Returns string without suffix.
  2379. *
  2380. * @param str Input string.
  2381. * @param suffix Needed to remove.
  2382. *
  2383. * @returns String without suffix.
  2384. */
  2385. const removeSuffix = (str, suffix) => {
  2386. const index = str.indexOf(suffix, str.length - suffix.length);
  2387. if (index >= 0) {
  2388. return str.substring(0, index);
  2389. }
  2390. return str;
  2391. };
  2392.  
  2393. /**
  2394. * Replaces all `pattern`s with `replacement` in `input` string.
  2395. * String.replaceAll() polyfill because it is not supported by old browsers, e.g. Chrome 55.
  2396. *
  2397. * @see {@link https://caniuse.com/?search=String.replaceAll}
  2398. *
  2399. * @param input Input string to process.
  2400. * @param pattern Find in the input string.
  2401. * @param replacement Replace the pattern with.
  2402. *
  2403. * @returns Modified string.
  2404. */
  2405. const replaceAll = (input, pattern, replacement) => {
  2406. if (!input) {
  2407. return input;
  2408. }
  2409. return input.split(pattern).join(replacement);
  2410. };
  2411.  
  2412. /**
  2413. * Converts string pattern to regular expression.
  2414. *
  2415. * @param str String to convert.
  2416. *
  2417. * @returns Regular expression converted from pattern `str`.
  2418. */
  2419. const toRegExp = str => {
  2420. if (str.startsWith(SLASH) && str.endsWith(SLASH)) {
  2421. return new RegExp(str.slice(1, -1));
  2422. }
  2423. const escaped = str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  2424. return new RegExp(escaped);
  2425. };
  2426.  
  2427. /**
  2428. * Converts any simple type value to string type,
  2429. * e.g. `undefined` -> `'undefined'`.
  2430. *
  2431. * @param value Any type value.
  2432. *
  2433. * @returns String representation of `value`.
  2434. */
  2435. const convertTypeIntoString = value => {
  2436. let output;
  2437. switch (value) {
  2438. case undefined:
  2439. output = 'undefined';
  2440. break;
  2441. case null:
  2442. output = 'null';
  2443. break;
  2444. default:
  2445. output = value.toString();
  2446. }
  2447. return output;
  2448. };
  2449.  
  2450. /**
  2451. * Converts instance of string value into other simple types,
  2452. * e.g. `'null'` -> `null`, `'true'` -> `true`.
  2453. *
  2454. * @param value String-type value.
  2455. *
  2456. * @returns Its own type representation of string-type `value`.
  2457. */
  2458. const convertTypeFromString = value => {
  2459. const numValue = Number(value);
  2460. let output;
  2461. if (!Number.isNaN(numValue)) {
  2462. output = numValue;
  2463. } else {
  2464. switch (value) {
  2465. case 'undefined':
  2466. output = undefined;
  2467. break;
  2468. case 'null':
  2469. output = null;
  2470. break;
  2471. case 'true':
  2472. output = true;
  2473. break;
  2474. case 'false':
  2475. output = false;
  2476. break;
  2477. default:
  2478. output = value;
  2479. }
  2480. }
  2481. return output;
  2482. };
  2483.  
  2484. var BrowserName;
  2485. (function (BrowserName) {
  2486. BrowserName["Chrome"] = "Chrome";
  2487. BrowserName["Firefox"] = "Firefox";
  2488. BrowserName["Edge"] = "Edg";
  2489. BrowserName["Opera"] = "Opera";
  2490. BrowserName["Safari"] = "Safari";
  2491. BrowserName["HeadlessChrome"] = "HeadlessChrome";
  2492. })(BrowserName || (BrowserName = {}));
  2493. const CHROMIUM_BRAND_NAME = 'Chromium';
  2494. const GOOGLE_CHROME_BRAND_NAME = 'Google Chrome';
  2495.  
  2496. /**
  2497. * Simple check for Safari browser.
  2498. */
  2499. const isSafariBrowser = navigator.vendor === 'Apple Computer, Inc.';
  2500. const SUPPORTED_BROWSERS_DATA = {
  2501. [BrowserName.Chrome]: {
  2502. // avoid Chromium-based Edge browser
  2503. // 'EdgA' for android version
  2504. MASK: /\s(Chrome)\/(\d+)\..+\s(?!.*(Edg|EdgA)\/)/,
  2505. MIN_VERSION: 88
  2506. },
  2507. [BrowserName.Firefox]: {
  2508. MASK: /\s(Firefox)\/(\d+)\./,
  2509. MIN_VERSION: 84
  2510. },
  2511. [BrowserName.Edge]: {
  2512. MASK: /\s(Edg)\/(\d+)\./,
  2513. MIN_VERSION: 88
  2514. },
  2515. [BrowserName.Opera]: {
  2516. MASK: /\s(OPR)\/(\d+)\./,
  2517. MIN_VERSION: 80
  2518. },
  2519. [BrowserName.Safari]: {
  2520. MASK: /\sVersion\/(\d{2}\.\d)(.+\s|\s)(Safari)\//,
  2521. MIN_VERSION: 14
  2522. },
  2523. [BrowserName.HeadlessChrome]: {
  2524. // support headless Chrome used by puppeteer
  2525. MASK: /\s(HeadlessChrome)\/(\d+)\..+\s(?!.*Edg\/)/,
  2526. // version should be the same as for BrowserName.Chrome
  2527. MIN_VERSION: 88
  2528. }
  2529. };
  2530.  
  2531. /**
  2532. * Returns chromium brand object or null if not supported.
  2533. * Chromium because of all browsers based on it should be supported as well
  2534. * and it is universal way to check it.
  2535. *
  2536. * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/brands}
  2537. *
  2538. * @param uaDataBrands Array of user agent brand information.
  2539. *
  2540. * @returns Chromium brand data object or null if it is not supported.
  2541. */
  2542. const getChromiumBrand = uaDataBrands => {
  2543. if (!uaDataBrands) {
  2544. return null;
  2545. }
  2546. // for chromium-based browsers
  2547. const chromiumBrand = uaDataBrands.find(brandData => {
  2548. return brandData.brand === CHROMIUM_BRAND_NAME || brandData.brand === GOOGLE_CHROME_BRAND_NAME;
  2549. });
  2550. return chromiumBrand || null;
  2551. };
  2552. /**
  2553. * Parses userAgent string and returns the data object for supported browsers;
  2554. * otherwise returns null.
  2555. *
  2556. * @param userAgent User agent to parse.
  2557. *
  2558. * @returns Parsed userAgent data object if browser is supported, otherwise null.
  2559. */
  2560. const parseUserAgent = userAgent => {
  2561. let browserName;
  2562. let currentVersion;
  2563. const browserNames = Object.values(BrowserName);
  2564. for (let i = 0; i < browserNames.length; i += 1) {
  2565. let match = null;
  2566. const name = browserNames[i];
  2567. if (name) {
  2568. var _SUPPORTED_BROWSERS_D;
  2569. match = (_SUPPORTED_BROWSERS_D = SUPPORTED_BROWSERS_DATA[name]) === null || _SUPPORTED_BROWSERS_D === void 0 ? void 0 : _SUPPORTED_BROWSERS_D.MASK.exec(userAgent);
  2570. }
  2571. if (match) {
  2572. // for safari browser the order is different because of regexp
  2573. if (match[3] === browserNames[i]) {
  2574. browserName = match[3];
  2575. currentVersion = Number(match[1]);
  2576. } else {
  2577. // for others first is name and second is version
  2578. browserName = match[1];
  2579. currentVersion = Number(match[2]);
  2580. }
  2581. if (!browserName || !currentVersion) {
  2582. return null;
  2583. }
  2584. return {
  2585. browserName,
  2586. currentVersion
  2587. };
  2588. }
  2589. }
  2590. return null;
  2591. };
  2592.  
  2593. /**
  2594. * Returns info about browser.
  2595. *
  2596. * @param userAgent User agent of browser.
  2597. * @param uaDataBrands Array of user agent brand information if supported by browser.
  2598. *
  2599. * @returns Data object if browser is supported, otherwise null.
  2600. */
  2601. const getBrowserInfoAsSupported = (userAgent, uaDataBrands) => {
  2602. const brandData = getChromiumBrand(uaDataBrands);
  2603. if (!brandData) {
  2604. const uaInfo = parseUserAgent(userAgent);
  2605. if (!uaInfo) {
  2606. return null;
  2607. }
  2608. const {
  2609. browserName,
  2610. currentVersion
  2611. } = uaInfo;
  2612. return {
  2613. browserName,
  2614. currentVersion
  2615. };
  2616. }
  2617.  
  2618. // if navigator.userAgentData is supported
  2619. const {
  2620. brand,
  2621. version
  2622. } = brandData;
  2623. // handle chromium-based browsers
  2624. const browserName = brand === CHROMIUM_BRAND_NAME || brand === GOOGLE_CHROME_BRAND_NAME ? BrowserName.Chrome : brand;
  2625. return {
  2626. browserName,
  2627. currentVersion: Number(version)
  2628. };
  2629. };
  2630.  
  2631. /**
  2632. * Checks whether the browser userAgent and userAgentData.brands is supported.
  2633. *
  2634. * @param userAgent User agent of browser.
  2635. * @param uaDataBrands Array of user agent brand information if supported by browser.
  2636. *
  2637. * @returns True if browser is supported.
  2638. */
  2639. const isUserAgentSupported = (userAgent, uaDataBrands) => {
  2640. var _SUPPORTED_BROWSERS_D2;
  2641. // do not support Internet Explorer
  2642. if (userAgent.includes('MSIE') || userAgent.includes('Trident/')) {
  2643. return false;
  2644. }
  2645.  
  2646. // for local testing purposes
  2647. if (userAgent.includes('jsdom')) {
  2648. return true;
  2649. }
  2650. const browserData = getBrowserInfoAsSupported(userAgent, uaDataBrands);
  2651. if (!browserData) {
  2652. return false;
  2653. }
  2654. const {
  2655. browserName,
  2656. currentVersion
  2657. } = browserData;
  2658. if (!browserName || !currentVersion) {
  2659. return false;
  2660. }
  2661. const minVersion = (_SUPPORTED_BROWSERS_D2 = SUPPORTED_BROWSERS_DATA[browserName]) === null || _SUPPORTED_BROWSERS_D2 === void 0 ? void 0 : _SUPPORTED_BROWSERS_D2.MIN_VERSION;
  2662. if (!minVersion) {
  2663. return false;
  2664. }
  2665. return currentVersion >= minVersion;
  2666. };
  2667.  
  2668. /**
  2669. * Checks whether the current browser is supported.
  2670. *
  2671. * @returns True if *current* browser is supported.
  2672. */
  2673. const isBrowserSupported = () => {
  2674. var _navigator$userAgentD;
  2675. return isUserAgentSupported(navigator.userAgent, (_navigator$userAgentD = navigator.userAgentData) === null || _navigator$userAgentD === void 0 ? void 0 : _navigator$userAgentD.brands);
  2676. };
  2677.  
  2678. var CssProperty;
  2679. (function (CssProperty) {
  2680. CssProperty["Background"] = "background";
  2681. CssProperty["BackgroundImage"] = "background-image";
  2682. CssProperty["Content"] = "content";
  2683. CssProperty["Opacity"] = "opacity";
  2684. })(CssProperty || (CssProperty = {}));
  2685. const REGEXP_ANY_SYMBOL = '.*';
  2686. const REGEXP_WITH_FLAGS_REGEXP = /^\s*\/.*\/[gmisuy]*\s*$/;
  2687. /**
  2688. * Removes quotes for specified content value.
  2689. *
  2690. * For example, content style declaration with `::before` can be set as '-' (e.g. unordered list)
  2691. * which displayed as simple dash `-` with no quotes.
  2692. * But CSSStyleDeclaration.getPropertyValue('content') will return value
  2693. * wrapped into quotes, e.g. '"-"', which should be removed
  2694. * because filters maintainers does not use any quotes in real rules.
  2695. *
  2696. * @param str Input string.
  2697. *
  2698. * @returns String with no quotes for content value.
  2699. */
  2700. const removeContentQuotes = str => {
  2701. return str.replace(/^(["'])([\s\S]*)\1$/, '$2');
  2702. };
  2703.  
  2704. /**
  2705. * Adds quotes for specified background url value.
  2706. *
  2707. * If background-image is specified **without** quotes:
  2708. * e.g. 'background: url(data:image/gif;base64,R0lGODlhAQA7)'.
  2709. *
  2710. * CSSStyleDeclaration.getPropertyValue('background-image') may return value **with** quotes:
  2711. * e.g. 'background: url("data:image/gif;base64,R0lGODlhAQA7")'.
  2712. *
  2713. * So we add quotes for compatibility since filters maintainers might use quotes in real rules.
  2714. *
  2715. * @param str Input string.
  2716. *
  2717. * @returns String with unified quotes for background url value.
  2718. */
  2719. const addUrlPropertyQuotes = str => {
  2720. if (!str.includes('url("')) {
  2721. const re = /url\((.*?)\)/g;
  2722. return str.replace(re, 'url("$1")');
  2723. }
  2724. return str;
  2725. };
  2726.  
  2727. /**
  2728. * Adds quotes to url arg for consistent property value matching.
  2729. */
  2730. const addUrlQuotesTo = {
  2731. regexpArg: str => {
  2732. // e.g. /^url\\([a-z]{4}:[a-z]{5}/
  2733. // or /^url\\(data\\:\\image\\/gif;base64.+/
  2734. const re = /(\^)?url(\\)?\\\((\w|\[\w)/g;
  2735. return str.replace(re, '$1url$2\\(\\"?$3');
  2736. },
  2737. noneRegexpArg: addUrlPropertyQuotes
  2738. };
  2739.  
  2740. /**
  2741. * Escapes regular expression string.
  2742. *
  2743. * @see {@link https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/regexp}
  2744. *
  2745. * @param str Input string.
  2746. *
  2747. * @returns Escaped regular expression string.
  2748. */
  2749. const escapeRegExp = str => {
  2750. // should be escaped . * + ? ^ $ { } ( ) | [ ] / \
  2751. // except of * | ^
  2752. const specials = ['.', '+', '?', '$', '{', '}', '(', ')', '[', ']', '\\', '/'];
  2753. const specialsRegex = new RegExp(`[${specials.join('\\')}]`, 'g');
  2754. return str.replace(specialsRegex, '\\$&');
  2755. };
  2756.  
  2757. /**
  2758. * Converts :matches-css() arg property value match to regexp.
  2759. *
  2760. * @param rawValue Style match value pattern.
  2761. *
  2762. * @returns Arg of :matches-css() converted to regular expression.
  2763. */
  2764. const convertStyleMatchValueToRegexp = rawValue => {
  2765. let value;
  2766. if (rawValue.startsWith(SLASH) && rawValue.endsWith(SLASH)) {
  2767. // For regex patterns double quotes `"` and backslashes `\` should be escaped
  2768. value = addUrlQuotesTo.regexpArg(rawValue);
  2769. value = value.slice(1, -1);
  2770. } else {
  2771. // For non-regex patterns parentheses `(` `)` and square brackets `[` `]`
  2772. // should be unescaped, because their escaping in filter rules is required
  2773. value = addUrlQuotesTo.noneRegexpArg(rawValue);
  2774. value = value.replace(/\\([\\()[\]"])/g, '$1');
  2775. value = escapeRegExp(value);
  2776. // e.g. div:matches-css(background-image: url(data:*))
  2777. value = replaceAll(value, ASTERISK, REGEXP_ANY_SYMBOL);
  2778. }
  2779. return new RegExp(value, 'i');
  2780. };
  2781.  
  2782. /**
  2783. * Makes some properties values compatible.
  2784. *
  2785. * @param propertyName Name of style property.
  2786. * @param propertyValue Value of style property.
  2787. *
  2788. * @returns Normalized values for some CSS properties.
  2789. */
  2790. const normalizePropertyValue = (propertyName, propertyValue) => {
  2791. let normalized = '';
  2792. switch (propertyName) {
  2793. case CssProperty.Background:
  2794. case CssProperty.BackgroundImage:
  2795. // sometimes url property does not have quotes
  2796. // so we add them for consistent matching
  2797. normalized = addUrlPropertyQuotes(propertyValue);
  2798. break;
  2799. case CssProperty.Content:
  2800. normalized = removeContentQuotes(propertyValue);
  2801. break;
  2802. case CssProperty.Opacity:
  2803. // https://bugs.webkit.org/show_bug.cgi?id=93445
  2804. normalized = isSafariBrowser ? (Math.round(parseFloat(propertyValue) * 100) / 100).toString() : propertyValue;
  2805. break;
  2806. default:
  2807. normalized = propertyValue;
  2808. }
  2809. return normalized;
  2810. };
  2811.  
  2812. /**
  2813. * Returns domElement style property value
  2814. * by css property name and standard pseudo-element.
  2815. *
  2816. * @param domElement DOM element.
  2817. * @param propertyName CSS property name.
  2818. * @param regularPseudoElement Standard pseudo-element — '::before', '::after' etc.
  2819. *
  2820. * @returns String containing the value of a specified CSS property.
  2821. */
  2822. const getComputedStylePropertyValue = (domElement, propertyName, regularPseudoElement) => {
  2823. const style = window.getComputedStyle(domElement, regularPseudoElement);
  2824. const propertyValue = style.getPropertyValue(propertyName);
  2825. return normalizePropertyValue(propertyName, propertyValue);
  2826. };
  2827. /**
  2828. * Parses arg of absolute pseudo-class into 'name' and 'value' if set.
  2829. *
  2830. * Used for :matches-css() - with COLON as separator,
  2831. * for :matches-attr() and :matches-property() - with EQUAL_SIGN as separator.
  2832. *
  2833. * @param pseudoArg Arg of pseudo-class.
  2834. * @param separator Divider symbol.
  2835. *
  2836. * @returns Parsed 'matches' pseudo-class arg data.
  2837. */
  2838. const getPseudoArgData = (pseudoArg, separator) => {
  2839. const index = pseudoArg.indexOf(separator);
  2840. let name;
  2841. let value;
  2842. if (index > -1) {
  2843. name = pseudoArg.substring(0, index).trim();
  2844. value = pseudoArg.substring(index + 1).trim();
  2845. } else {
  2846. name = pseudoArg;
  2847. }
  2848. return {
  2849. name,
  2850. value
  2851. };
  2852. };
  2853. /**
  2854. * Parses :matches-css() pseudo-class arg
  2855. * where regular pseudo-element can be a part of arg
  2856. * e.g. 'div:matches-css(before, color: rgb(255, 255, 255))' <-- obsolete `:matches-css-before()`.
  2857. *
  2858. * @param pseudoName Pseudo-class name.
  2859. * @param rawArg Pseudo-class arg.
  2860. *
  2861. * @returns Parsed :matches-css() pseudo-class arg data.
  2862. * @throws An error on invalid `rawArg`.
  2863. */
  2864. const parseStyleMatchArg = (pseudoName, rawArg) => {
  2865. const {
  2866. name,
  2867. value
  2868. } = getPseudoArgData(rawArg, COMMA);
  2869. let regularPseudoElement = name;
  2870. let styleMatchArg = value;
  2871.  
  2872. // check whether the string part before the separator is valid regular pseudo-element,
  2873. // otherwise `regularPseudoElement` is null, and `styleMatchArg` is rawArg
  2874. if (!Object.values(REGULAR_PSEUDO_ELEMENTS).includes(name)) {
  2875. regularPseudoElement = null;
  2876. styleMatchArg = rawArg;
  2877. }
  2878. if (!styleMatchArg) {
  2879. throw new Error(`Required style property argument part is missing in :${pseudoName}() arg: '${rawArg}'`);
  2880. }
  2881.  
  2882. // if regularPseudoElement is not `null`
  2883. if (regularPseudoElement) {
  2884. // pseudo-element should have two colon marks for Window.getComputedStyle() due to the syntax:
  2885. // https://www.w3.org/TR/selectors-4/#pseudo-element-syntax
  2886. // ':matches-css(before, content: ads)' ->> '::before'
  2887. regularPseudoElement = `${COLON}${COLON}${regularPseudoElement}`;
  2888. }
  2889. return {
  2890. regularPseudoElement,
  2891. styleMatchArg
  2892. };
  2893. };
  2894.  
  2895. /**
  2896. * Checks whether the domElement is matched by :matches-css() arg.
  2897. *
  2898. * @param argsData Pseudo-class name, arg, and dom element to check.
  2899. *
  2900. @returns True if DOM element is matched.
  2901. * @throws An error on invalid pseudo-class arg.
  2902. */
  2903. const isStyleMatched = argsData => {
  2904. const {
  2905. pseudoName,
  2906. pseudoArg,
  2907. domElement
  2908. } = argsData;
  2909. const {
  2910. regularPseudoElement,
  2911. styleMatchArg
  2912. } = parseStyleMatchArg(pseudoName, pseudoArg);
  2913. const {
  2914. name: matchName,
  2915. value: matchValue
  2916. } = getPseudoArgData(styleMatchArg, COLON);
  2917. if (!matchName || !matchValue) {
  2918. throw new Error(`Required property name or value is missing in :${pseudoName}() arg: '${styleMatchArg}'`);
  2919. }
  2920. let valueRegexp;
  2921. try {
  2922. valueRegexp = convertStyleMatchValueToRegexp(matchValue);
  2923. } catch (e) {
  2924. logger.error(e);
  2925. throw new Error(`Invalid argument of :${pseudoName}() pseudo-class: '${styleMatchArg}'`);
  2926. }
  2927. const value = getComputedStylePropertyValue(domElement, matchName, regularPseudoElement);
  2928. return valueRegexp && valueRegexp.test(value);
  2929. };
  2930.  
  2931. /**
  2932. * Validates string arg for :matches-attr() and :matches-property().
  2933. *
  2934. * @param arg Pseudo-class arg.
  2935. *
  2936. * @returns True if 'matches' pseudo-class string arg is valid.
  2937. */
  2938. const validateStrMatcherArg = arg => {
  2939. if (arg.includes(SLASH)) {
  2940. return false;
  2941. }
  2942. if (!/^[\w-]+$/.test(arg)) {
  2943. return false;
  2944. }
  2945. return true;
  2946. };
  2947.  
  2948. /**
  2949. * Returns valid arg for :matches-attr() and :matcher-property().
  2950. *
  2951. * @param rawArg Arg pattern.
  2952. * @param [isWildcardAllowed=false] Flag for wildcard (`*`) using as pseudo-class arg.
  2953. *
  2954. * @returns Valid arg for :matches-attr() and :matcher-property().
  2955. * @throws An error on invalid `rawArg`.
  2956. */
  2957. const getValidMatcherArg = function (rawArg) {
  2958. let isWildcardAllowed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
  2959. // if rawArg is missing for pseudo-class
  2960. // e.g. :matches-attr()
  2961. // error will be thrown before getValidMatcherArg() is called:
  2962. // name or arg is missing in AbsolutePseudoClass
  2963.  
  2964. let arg;
  2965. if (rawArg.length > 1 && rawArg.startsWith(DOUBLE_QUOTE) && rawArg.endsWith(DOUBLE_QUOTE)) {
  2966. rawArg = rawArg.slice(1, -1);
  2967. }
  2968. if (rawArg === '') {
  2969. // e.g. :matches-property("")
  2970. throw new Error('Argument should be specified. Empty arg is invalid.');
  2971. }
  2972. if (rawArg.startsWith(SLASH) && rawArg.endsWith(SLASH)) {
  2973. // e.g. :matches-property("//")
  2974. if (rawArg.length > 2) {
  2975. arg = toRegExp(rawArg);
  2976. } else {
  2977. throw new Error(`Invalid regexp: '${rawArg}'`);
  2978. }
  2979. } else if (rawArg.includes(ASTERISK)) {
  2980. if (rawArg === ASTERISK && !isWildcardAllowed) {
  2981. // e.g. :matches-attr(*)
  2982. throw new Error(`Argument should be more specific than ${rawArg}`);
  2983. }
  2984. arg = replaceAll(rawArg, ASTERISK, REGEXP_ANY_SYMBOL);
  2985. arg = new RegExp(arg);
  2986. } else {
  2987. if (!validateStrMatcherArg(rawArg)) {
  2988. throw new Error(`Invalid argument: '${rawArg}'`);
  2989. }
  2990. arg = rawArg;
  2991. }
  2992. return arg;
  2993. };
  2994. /**
  2995. * Parses pseudo-class argument and returns parsed data.
  2996. *
  2997. * @param pseudoName Extended pseudo-class name.
  2998. * @param pseudoArg Extended pseudo-class argument.
  2999. *
  3000. * @returns Parsed pseudo-class argument data.
  3001. * @throws An error if attribute name is missing in pseudo-class arg.
  3002. */
  3003. const getRawMatchingData = (pseudoName, pseudoArg) => {
  3004. const {
  3005. name: rawName,
  3006. value: rawValue
  3007. } = getPseudoArgData(pseudoArg, EQUAL_SIGN);
  3008. if (!rawName) {
  3009. throw new Error(`Required attribute name is missing in :${pseudoName} arg: ${pseudoArg}`);
  3010. }
  3011. return {
  3012. rawName,
  3013. rawValue
  3014. };
  3015. };
  3016.  
  3017. /**
  3018. * Checks whether the domElement is matched by :matches-attr() arg.
  3019. *
  3020. * @param argsData Pseudo-class name, arg, and dom element to check.
  3021. *
  3022. @returns True if DOM element is matched.
  3023. * @throws An error on invalid arg of pseudo-class.
  3024. */
  3025. const isAttributeMatched = argsData => {
  3026. const {
  3027. pseudoName,
  3028. pseudoArg,
  3029. domElement
  3030. } = argsData;
  3031. const elementAttributes = domElement.attributes;
  3032. // no match if dom element has no attributes
  3033. if (elementAttributes.length === 0) {
  3034. return false;
  3035. }
  3036. const {
  3037. rawName: rawAttrName,
  3038. rawValue: rawAttrValue
  3039. } = getRawMatchingData(pseudoName, pseudoArg);
  3040. let attrNameMatch;
  3041. try {
  3042. attrNameMatch = getValidMatcherArg(rawAttrName);
  3043. } catch (e) {
  3044. // eslint-disable-line @typescript-eslint/no-explicit-any
  3045. logger.error(e);
  3046. throw new SyntaxError(e.message);
  3047. }
  3048. let isMatched = false;
  3049. let i = 0;
  3050. while (i < elementAttributes.length && !isMatched) {
  3051. const attr = elementAttributes[i];
  3052. if (!attr) {
  3053. break;
  3054. }
  3055. const isNameMatched = attrNameMatch instanceof RegExp ? attrNameMatch.test(attr.name) : attrNameMatch === attr.name;
  3056. if (!rawAttrValue) {
  3057. // for rules with no attribute value specified
  3058. // e.g. :matches-attr("/regex/") or :matches-attr("attr-name")
  3059. isMatched = isNameMatched;
  3060. } else {
  3061. let attrValueMatch;
  3062. try {
  3063. attrValueMatch = getValidMatcherArg(rawAttrValue);
  3064. } catch (e) {
  3065. // eslint-disable-line @typescript-eslint/no-explicit-any
  3066. logger.error(e);
  3067. throw new SyntaxError(e.message);
  3068. }
  3069. const isValueMatched = attrValueMatch instanceof RegExp ? attrValueMatch.test(attr.value) : attrValueMatch === attr.value;
  3070. isMatched = isNameMatched && isValueMatched;
  3071. }
  3072. i += 1;
  3073. }
  3074. return isMatched;
  3075. };
  3076.  
  3077. /**
  3078. * Parses raw :matches-property() arg which may be chain of properties.
  3079. *
  3080. * @param input Argument of :matches-property().
  3081. *
  3082. * @returns Arg of :matches-property() as array of strings or regular expressions.
  3083. * @throws An error on invalid chain.
  3084. */
  3085. const parseRawPropChain = input => {
  3086. if (input.length > 1 && input.startsWith(DOUBLE_QUOTE) && input.endsWith(DOUBLE_QUOTE)) {
  3087. input = input.slice(1, -1);
  3088. }
  3089. const chainChunks = input.split(DOT);
  3090. const chainPatterns = [];
  3091. let patternBuffer = '';
  3092. let isRegexpPattern = false;
  3093. let i = 0;
  3094. while (i < chainChunks.length) {
  3095. const chunk = getItemByIndex(chainChunks, i, `Invalid pseudo-class arg: '${input}'`);
  3096. if (chunk.startsWith(SLASH) && chunk.endsWith(SLASH) && chunk.length > 2) {
  3097. // regexp pattern with no dot in it, e.g. /propName/
  3098. chainPatterns.push(chunk);
  3099. } else if (chunk.startsWith(SLASH)) {
  3100. // if chunk is a start of regexp pattern
  3101. isRegexpPattern = true;
  3102. patternBuffer += chunk;
  3103. } else if (chunk.endsWith(SLASH)) {
  3104. isRegexpPattern = false;
  3105. // restore dot removed while splitting
  3106. // e.g. testProp./.{1,5}/
  3107. patternBuffer += `.${chunk}`;
  3108. chainPatterns.push(patternBuffer);
  3109. patternBuffer = '';
  3110. } else {
  3111. // if there are few dots in regexp pattern
  3112. // so chunk might be in the middle of it
  3113. if (isRegexpPattern) {
  3114. patternBuffer += chunk;
  3115. } else {
  3116. // otherwise it is string pattern
  3117. chainPatterns.push(chunk);
  3118. }
  3119. }
  3120. i += 1;
  3121. }
  3122. if (patternBuffer.length > 0) {
  3123. throw new Error(`Invalid regexp property pattern '${input}'`);
  3124. }
  3125. const chainMatchPatterns = chainPatterns.map(pattern => {
  3126. if (pattern.length === 0) {
  3127. // e.g. '.prop.id' or 'nested..test'
  3128. throw new Error(`Empty pattern '${pattern}' is invalid in chain '${input}'`);
  3129. }
  3130. let validPattern;
  3131. try {
  3132. validPattern = getValidMatcherArg(pattern, true);
  3133. } catch (e) {
  3134. logger.error(e);
  3135. throw new Error(`Invalid property pattern '${pattern}' in property chain '${input}'`);
  3136. }
  3137. return validPattern;
  3138. });
  3139. return chainMatchPatterns;
  3140. };
  3141. /**
  3142. * Checks if the property exists in the base object (recursively).
  3143. *
  3144. * @param base Element to check.
  3145. * @param chain Array of objects - parsed string property chain.
  3146. * @param [output=[]] Result acc.
  3147. *
  3148. * @returns Array of parsed data — representation of `base`-related `chain`.
  3149. */
  3150. const filterRootsByRegexpChain = function (base, chain) {
  3151. let output = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
  3152. const tempProp = getFirst(chain);
  3153. if (chain.length === 1) {
  3154. let key;
  3155. for (key in base) {
  3156. if (tempProp instanceof RegExp) {
  3157. if (tempProp.test(key)) {
  3158. output.push({
  3159. base,
  3160. prop: key,
  3161. value: base[key]
  3162. });
  3163. }
  3164. } else if (tempProp === key) {
  3165. output.push({
  3166. base,
  3167. prop: tempProp,
  3168. value: base[key]
  3169. });
  3170. }
  3171. }
  3172. return output;
  3173. }
  3174.  
  3175. // if there is a regexp prop in input chain
  3176. // e.g. 'unit./^ad.+/.src' for 'unit.ad-1gf2.src unit.ad-fgd34.src'),
  3177. // every base keys should be tested by regexp and it can be more that one results
  3178. if (tempProp instanceof RegExp) {
  3179. const nextProp = chain.slice(1);
  3180. const baseKeys = [];
  3181. for (const key in base) {
  3182. if (tempProp.test(key)) {
  3183. baseKeys.push(key);
  3184. }
  3185. }
  3186. baseKeys.forEach(key => {
  3187. var _Object$getOwnPropert;
  3188. const item = (_Object$getOwnPropert = Object.getOwnPropertyDescriptor(base, key)) === null || _Object$getOwnPropert === void 0 ? void 0 : _Object$getOwnPropert.value;
  3189. filterRootsByRegexpChain(item, nextProp, output);
  3190. });
  3191. }
  3192. if (base && typeof tempProp === 'string') {
  3193. var _Object$getOwnPropert2;
  3194. const nextBase = (_Object$getOwnPropert2 = Object.getOwnPropertyDescriptor(base, tempProp)) === null || _Object$getOwnPropert2 === void 0 ? void 0 : _Object$getOwnPropert2.value;
  3195. chain = chain.slice(1);
  3196. if (nextBase !== undefined) {
  3197. filterRootsByRegexpChain(nextBase, chain, output);
  3198. }
  3199. }
  3200. return output;
  3201. };
  3202.  
  3203. /**
  3204. * Checks whether the domElement is matched by :matches-property() arg.
  3205. *
  3206. * @param argsData Pseudo-class name, arg, and dom element to check.
  3207. *
  3208. @returns True if DOM element is matched.
  3209. * @throws An error on invalid prop in chain.
  3210. */
  3211. const isPropertyMatched = argsData => {
  3212. const {
  3213. pseudoName,
  3214. pseudoArg,
  3215. domElement
  3216. } = argsData;
  3217. const {
  3218. rawName: rawPropertyName,
  3219. rawValue: rawPropertyValue
  3220. } = getRawMatchingData(pseudoName, pseudoArg);
  3221.  
  3222. // chained property name cannot include '/' or '.'
  3223. // so regex prop names with such escaped characters are invalid
  3224. if (rawPropertyName.includes('\\/') || rawPropertyName.includes('\\.')) {
  3225. throw new Error(`Invalid :${pseudoName} name pattern: ${rawPropertyName}`);
  3226. }
  3227. let propChainMatches;
  3228. try {
  3229. propChainMatches = parseRawPropChain(rawPropertyName);
  3230. } catch (e) {
  3231. // eslint-disable-line @typescript-eslint/no-explicit-any
  3232. logger.error(e);
  3233. throw new SyntaxError(e.message);
  3234. }
  3235. const ownerObjArr = filterRootsByRegexpChain(domElement, propChainMatches);
  3236. if (ownerObjArr.length === 0) {
  3237. return false;
  3238. }
  3239. let isMatched = true;
  3240. if (rawPropertyValue) {
  3241. let propValueMatch;
  3242. try {
  3243. propValueMatch = getValidMatcherArg(rawPropertyValue);
  3244. } catch (e) {
  3245. // eslint-disable-line @typescript-eslint/no-explicit-any
  3246. logger.error(e);
  3247. throw new SyntaxError(e.message);
  3248. }
  3249. if (propValueMatch) {
  3250. for (let i = 0; i < ownerObjArr.length; i += 1) {
  3251. var _ownerObjArr$i;
  3252. const realValue = (_ownerObjArr$i = ownerObjArr[i]) === null || _ownerObjArr$i === void 0 ? void 0 : _ownerObjArr$i.value;
  3253. if (propValueMatch instanceof RegExp) {
  3254. isMatched = propValueMatch.test(convertTypeIntoString(realValue));
  3255. } else {
  3256. // handle 'null' and 'undefined' property values set as string
  3257. if (realValue === 'null' || realValue === 'undefined') {
  3258. isMatched = propValueMatch === realValue;
  3259. break;
  3260. }
  3261. isMatched = convertTypeFromString(propValueMatch) === realValue;
  3262. }
  3263. if (isMatched) {
  3264. break;
  3265. }
  3266. }
  3267. }
  3268. }
  3269. return isMatched;
  3270. };
  3271.  
  3272. /**
  3273. * Checks whether the textContent is matched by :contains arg.
  3274. *
  3275. * @param argsData Pseudo-class name, arg, and dom element to check.
  3276. *
  3277. @returns True if DOM element is matched.
  3278. * @throws An error on invalid arg of pseudo-class.
  3279. */
  3280. const isTextMatched = argsData => {
  3281. const {
  3282. pseudoName,
  3283. pseudoArg,
  3284. domElement
  3285. } = argsData;
  3286. const textContent = getNodeTextContent(domElement);
  3287. let isTextContentMatched;
  3288. let pseudoArgToMatch = pseudoArg;
  3289. if (pseudoArgToMatch.startsWith(SLASH) && REGEXP_WITH_FLAGS_REGEXP.test(pseudoArgToMatch)) {
  3290. // regexp arg
  3291. const flagsIndex = pseudoArgToMatch.lastIndexOf('/');
  3292. const flagsStr = pseudoArgToMatch.substring(flagsIndex + 1);
  3293. pseudoArgToMatch = pseudoArgToMatch.substring(0, flagsIndex + 1).slice(1, -1).replace(/\\([\\"])/g, '$1');
  3294. let regex;
  3295. try {
  3296. regex = new RegExp(pseudoArgToMatch, flagsStr);
  3297. } catch (e) {
  3298. throw new Error(`Invalid argument of :${pseudoName}() pseudo-class: ${pseudoArg}`);
  3299. }
  3300. isTextContentMatched = regex.test(textContent);
  3301. } else {
  3302. // none-regexp arg
  3303. pseudoArgToMatch = pseudoArgToMatch.replace(/\\([\\()[\]"])/g, '$1');
  3304. isTextContentMatched = textContent.includes(pseudoArgToMatch);
  3305. }
  3306. return isTextContentMatched;
  3307. };
  3308.  
  3309. /**
  3310. * Validates number arg for :nth-ancestor() and :upward() pseudo-classes.
  3311. *
  3312. * @param rawArg Raw arg of pseudo-class.
  3313. * @param pseudoName Pseudo-class name.
  3314. *
  3315. * @returns Valid number arg for :nth-ancestor() and :upward().
  3316. * @throws An error on invalid `rawArg`.
  3317. */
  3318. const getValidNumberAncestorArg = (rawArg, pseudoName) => {
  3319. const deep = Number(rawArg);
  3320. if (Number.isNaN(deep) || deep < 1 || deep >= 256) {
  3321. throw new Error(`Invalid argument of :${pseudoName} pseudo-class: '${rawArg}'`);
  3322. }
  3323. return deep;
  3324. };
  3325.  
  3326. /**
  3327. * Returns nth ancestor by 'deep' number arg OR undefined if ancestor range limit exceeded.
  3328. *
  3329. * @param domElement DOM element to find ancestor for.
  3330. * @param nth Depth up to needed ancestor.
  3331. * @param pseudoName Pseudo-class name.
  3332. *
  3333. * @returns Ancestor element found in DOM, or null if not found.
  3334. * @throws An error on invalid `nth` arg.
  3335. */
  3336. const getNthAncestor = (domElement, nth, pseudoName) => {
  3337. let ancestor = null;
  3338. let i = 0;
  3339. while (i < nth) {
  3340. ancestor = domElement.parentElement;
  3341. if (!ancestor) {
  3342. throw new Error(`Out of DOM: Argument of :${pseudoName}() pseudo-class is too big '${nth}'.`);
  3343. }
  3344. domElement = ancestor;
  3345. i += 1;
  3346. }
  3347. return ancestor;
  3348. };
  3349.  
  3350. /**
  3351. * Validates standard CSS selector.
  3352. *
  3353. * @param selector Standard selector.
  3354. *
  3355. * @returns True if standard CSS selector is valid.
  3356. */
  3357. const validateStandardSelector = selector => {
  3358. let isValid;
  3359. try {
  3360. document.querySelectorAll(selector);
  3361. isValid = true;
  3362. } catch (e) {
  3363. isValid = false;
  3364. }
  3365. return isValid;
  3366. };
  3367.  
  3368. /**
  3369. * Wrapper to run matcher `callback` with `args`
  3370. * and throw error with `errorMessage` if `callback` run fails.
  3371. *
  3372. * @param callback Matcher callback.
  3373. * @param argsData Args needed for matcher callback.
  3374. * @param errorMessage Error message.
  3375. *
  3376. * @returns True if `callback` returns true.
  3377. * @throws An error if `callback` fails.
  3378. */
  3379. const matcherWrapper = (callback, argsData, errorMessage) => {
  3380. let isMatched;
  3381. try {
  3382. isMatched = callback(argsData);
  3383. } catch (e) {
  3384. logger.error(e);
  3385. throw new Error(errorMessage);
  3386. }
  3387. return isMatched;
  3388. };
  3389.  
  3390. /**
  3391. * Generates common error message to throw while matching element `propDesc`.
  3392. *
  3393. * @param propDesc Text to describe what element 'prop' pseudo-class is trying to match.
  3394. * @param pseudoName Pseudo-class name.
  3395. * @param pseudoArg Pseudo-class arg.
  3396. *
  3397. * @returns Generated error message string.
  3398. */
  3399. const getAbsolutePseudoError = (propDesc, pseudoName, pseudoArg) => {
  3400. // eslint-disable-next-line max-len
  3401. return `${MATCHING_ELEMENT_ERROR_PREFIX} ${propDesc}, may be invalid :${pseudoName}() pseudo-class arg: '${pseudoArg}'`;
  3402. };
  3403.  
  3404. /**
  3405. * Checks whether the domElement is matched by absolute extended pseudo-class argument.
  3406. *
  3407. * @param domElement Page element.
  3408. * @param pseudoName Pseudo-class name.
  3409. * @param pseudoArg Pseudo-class arg.
  3410. *
  3411. * @returns True if `domElement` is matched by absolute pseudo-class.
  3412. * @throws An error on unknown absolute pseudo-class.
  3413. */
  3414. const isMatchedByAbsolutePseudo = (domElement, pseudoName, pseudoArg) => {
  3415. let argsData;
  3416. let errorMessage;
  3417. let callback;
  3418. switch (pseudoName) {
  3419. case CONTAINS_PSEUDO:
  3420. case HAS_TEXT_PSEUDO:
  3421. case ABP_CONTAINS_PSEUDO:
  3422. callback = isTextMatched;
  3423. argsData = {
  3424. pseudoName,
  3425. pseudoArg,
  3426. domElement
  3427. };
  3428. errorMessage = getAbsolutePseudoError('text content', pseudoName, pseudoArg);
  3429. break;
  3430. case MATCHES_CSS_PSEUDO:
  3431. case MATCHES_CSS_AFTER_PSEUDO:
  3432. case MATCHES_CSS_BEFORE_PSEUDO:
  3433. callback = isStyleMatched;
  3434. argsData = {
  3435. pseudoName,
  3436. pseudoArg,
  3437. domElement
  3438. };
  3439. errorMessage = getAbsolutePseudoError('style', pseudoName, pseudoArg);
  3440. break;
  3441. case MATCHES_ATTR_PSEUDO_CLASS_MARKER:
  3442. callback = isAttributeMatched;
  3443. argsData = {
  3444. domElement,
  3445. pseudoName,
  3446. pseudoArg
  3447. };
  3448. errorMessage = getAbsolutePseudoError('attributes', pseudoName, pseudoArg);
  3449. break;
  3450. case MATCHES_PROPERTY_PSEUDO_CLASS_MARKER:
  3451. callback = isPropertyMatched;
  3452. argsData = {
  3453. domElement,
  3454. pseudoName,
  3455. pseudoArg
  3456. };
  3457. errorMessage = getAbsolutePseudoError('properties', pseudoName, pseudoArg);
  3458. break;
  3459. default:
  3460. throw new Error(`Unknown absolute pseudo-class :${pseudoName}()`);
  3461. }
  3462. return matcherWrapper(callback, argsData, errorMessage);
  3463. };
  3464. const findByAbsolutePseudoPseudo = {
  3465. /**
  3466. * Returns list of nth ancestors relative to every dom node from domElements list.
  3467. *
  3468. * @param domElements DOM elements.
  3469. * @param rawPseudoArg Number arg of :nth-ancestor() or :upward() pseudo-class.
  3470. * @param pseudoName Pseudo-class name.
  3471. *
  3472. * @returns Array of ancestor DOM elements.
  3473. */
  3474. nthAncestor: (domElements, rawPseudoArg, pseudoName) => {
  3475. const deep = getValidNumberAncestorArg(rawPseudoArg, pseudoName);
  3476. const ancestors = domElements.map(domElement => {
  3477. let ancestor = null;
  3478. try {
  3479. ancestor = getNthAncestor(domElement, deep, pseudoName);
  3480. } catch (e) {
  3481. logger.error(e);
  3482. }
  3483. return ancestor;
  3484. }).filter(isHtmlElement);
  3485. return ancestors;
  3486. },
  3487. /**
  3488. * Returns list of elements by xpath expression, evaluated on every dom node from domElements list.
  3489. *
  3490. * @param domElements DOM elements.
  3491. * @param rawPseudoArg Arg of :xpath() pseudo-class.
  3492. *
  3493. * @returns Array of DOM elements matched by xpath expression.
  3494. */
  3495. xpath: (domElements, rawPseudoArg) => {
  3496. const foundElements = domElements.map(domElement => {
  3497. const result = [];
  3498. let xpathResult;
  3499. try {
  3500. xpathResult = document.evaluate(rawPseudoArg, domElement, null, window.XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
  3501. } catch (e) {
  3502. logger.error(e);
  3503. throw new Error(`Invalid argument of :xpath pseudo-class: '${rawPseudoArg}'`);
  3504. }
  3505. let node = xpathResult.iterateNext();
  3506. while (node) {
  3507. if (isHtmlElement(node)) {
  3508. result.push(node);
  3509. }
  3510. node = xpathResult.iterateNext();
  3511. }
  3512. return result;
  3513. });
  3514. return flatten(foundElements);
  3515. },
  3516. /**
  3517. * Returns list of closest ancestors relative to every dom node from domElements list.
  3518. *
  3519. * @param domElements DOM elements.
  3520. * @param rawPseudoArg Standard selector arg of :upward() pseudo-class.
  3521. *
  3522. * @returns Array of closest ancestor DOM elements.
  3523. * @throws An error if `rawPseudoArg` is not a valid standard selector.
  3524. */
  3525. upward: (domElements, rawPseudoArg) => {
  3526. if (!validateStandardSelector(rawPseudoArg)) {
  3527. throw new Error(`Invalid argument of :upward pseudo-class: '${rawPseudoArg}'`);
  3528. }
  3529. const closestAncestors = domElements.map(domElement => {
  3530. // closest to parent element should be found
  3531. // otherwise `.base:upward(.base)` will return itself too, not only ancestor
  3532. const parent = domElement.parentElement;
  3533. if (!parent) {
  3534. return null;
  3535. }
  3536. return parent.closest(rawPseudoArg);
  3537. }).filter(isHtmlElement);
  3538. return closestAncestors;
  3539. }
  3540. };
  3541.  
  3542. /**
  3543. * Calculated selector text which is needed to :has(), :is() and :not() pseudo-classes.
  3544. * Contains calculated part (depends on the processed element)
  3545. * and value of RegularSelector which is next to selector by.
  3546. *
  3547. * Native Document.querySelectorAll() does not select exact descendant elements
  3548. * but match all page elements satisfying the selector,
  3549. * so extra specification is needed for proper descendants selection
  3550. * e.g. 'div:has(> img)'.
  3551. *
  3552. * Its calculation depends on extended selector.
  3553. */
  3554.  
  3555. /**
  3556. * Combined `:scope` pseudo-class and **child** combinator — `:scope>`.
  3557. */
  3558. const scopeDirectChildren = `${SCOPE_CSS_PSEUDO_CLASS}${CHILD_COMBINATOR}`;
  3559.  
  3560. /**
  3561. * Combined `:scope` pseudo-class and **descendant** combinator — `:scope `.
  3562. */
  3563. const scopeAnyChildren = `${SCOPE_CSS_PSEUDO_CLASS}${DESCENDANT_COMBINATOR}`;
  3564.  
  3565. /**
  3566. * Interface for relative pseudo-class helpers args.
  3567. */
  3568.  
  3569. /**
  3570. * Returns the first of RegularSelector child node for `selectorNode`.
  3571. *
  3572. * @param selectorNode Ast Selector node.
  3573. * @param pseudoName Name of relative pseudo-class.
  3574. *
  3575. * @returns Ast RegularSelector node.
  3576. */
  3577. const getFirstInnerRegularChild = (selectorNode, pseudoName) => {
  3578. return getFirstRegularChild(selectorNode.children, `RegularSelector is missing for :${pseudoName}() pseudo-class`);
  3579. };
  3580.  
  3581. // TODO: fix for <forgiving-relative-selector-list>
  3582. // https://github.com/AdguardTeam/ExtendedCss/issues/154
  3583. /**
  3584. * Checks whether the element has all relative elements specified by pseudo-class arg.
  3585. * Used for :has() pseudo-class.
  3586. *
  3587. * @param argsData Relative pseudo-class helpers args data.
  3588. *
  3589. * @returns True if **all selectors** from argsData.relativeSelectorList is **matched** for argsData.element.
  3590. */
  3591. const hasRelativesBySelectorList = argsData => {
  3592. const {
  3593. element,
  3594. relativeSelectorList,
  3595. pseudoName
  3596. } = argsData;
  3597. return relativeSelectorList.children
  3598. // Array.every() is used here as each Selector node from SelectorList should exist on page
  3599. .every(selectorNode => {
  3600. // selectorList.children always starts with regular selector as any selector generally
  3601. const relativeRegularSelector = getFirstInnerRegularChild(selectorNode, pseudoName);
  3602. let specifiedSelector = '';
  3603. let rootElement = null;
  3604. const regularSelector = getNodeValue(relativeRegularSelector);
  3605. if (regularSelector.startsWith(NEXT_SIBLING_COMBINATOR) || regularSelector.startsWith(SUBSEQUENT_SIBLING_COMBINATOR)) {
  3606. /**
  3607. * For matching the element by "element:has(+ next-sibling)" and "element:has(~ sibling)"
  3608. * we check whether the element's parentElement has specific direct child combination,
  3609. * e.g. 'h1:has(+ .share)' -> `h1Node.parentElement.querySelectorAll(':scope > h1 + .share')`.
  3610. *
  3611. * @see {@link https://www.w3.org/TR/selectors-4/#relational}
  3612. */
  3613. rootElement = element.parentElement;
  3614. const elementSelectorText = getElementSelectorDesc(element);
  3615. specifiedSelector = `${scopeDirectChildren}${elementSelectorText}${regularSelector}`;
  3616. } else if (regularSelector === ASTERISK) {
  3617. /**
  3618. * :scope specification is needed for proper descendants selection
  3619. * as native element.querySelectorAll() does not select exact element descendants
  3620. * e.g. 'a:has(> img)' -> `aNode.querySelectorAll(':scope > img')`.
  3621. *
  3622. * For 'any selector' as arg of relative simplicity should be set for all inner elements
  3623. * e.g. 'div:has(*)' -> `divNode.querySelectorAll(':scope *')`
  3624. * which means empty div with no child element.
  3625. */
  3626. rootElement = element;
  3627. specifiedSelector = `${scopeAnyChildren}${ASTERISK}`;
  3628. } else {
  3629. /**
  3630. * As it described above, inner elements should be found using `:scope` pseudo-class
  3631. * e.g. 'a:has(> img)' -> `aNode.querySelectorAll(':scope > img')`
  3632. * OR '.block(div > span)' -> `blockClassNode.querySelectorAll(':scope div > span')`.
  3633. */
  3634. specifiedSelector = `${scopeAnyChildren}${regularSelector}`;
  3635. rootElement = element;
  3636. }
  3637. if (!rootElement) {
  3638. throw new Error(`Selection by :${pseudoName}() pseudo-class is not possible`);
  3639. }
  3640. let relativeElements;
  3641. try {
  3642. // eslint-disable-next-line @typescript-eslint/no-use-before-define
  3643. relativeElements = getElementsForSelectorNode(selectorNode, rootElement, specifiedSelector);
  3644. } catch (e) {
  3645. logger.error(e);
  3646. // fail for invalid selector
  3647. throw new Error(`Invalid selector for :${pseudoName}() pseudo-class: '${regularSelector}'`);
  3648. }
  3649. return relativeElements.length > 0;
  3650. });
  3651. };
  3652.  
  3653. /**
  3654. * Checks whether the element is an any element specified by pseudo-class arg.
  3655. * Used for :is() pseudo-class.
  3656. *
  3657. * @param argsData Relative pseudo-class helpers args data.
  3658. *
  3659. * @returns True if **any selector** from argsData.relativeSelectorList is **matched** for argsData.element.
  3660. */
  3661. const isAnyElementBySelectorList = argsData => {
  3662. const {
  3663. element,
  3664. relativeSelectorList,
  3665. pseudoName
  3666. } = argsData;
  3667. return relativeSelectorList.children
  3668. // Array.some() is used here as any selector from selector list should exist on page
  3669. .some(selectorNode => {
  3670. // selectorList.children always starts with regular selector
  3671. const relativeRegularSelector = getFirstInnerRegularChild(selectorNode, pseudoName);
  3672.  
  3673. /**
  3674. * For checking the element by 'div:is(.banner)'
  3675. * we check whether the element's parentElement has any specific direct child.
  3676. */
  3677. const rootElement = getParent(element, `Selection by :${pseudoName}() pseudo-class is not possible`);
  3678.  
  3679. /**
  3680. * So we calculate the element "description" by it's tagname and attributes for targeting
  3681. * and use it to specify the selection
  3682. * e.g. `div:is(.banner)` --> `divNode.parentElement.querySelectorAll(':scope > .banner')`.
  3683. */
  3684. const specifiedSelector = `${scopeDirectChildren}${getNodeValue(relativeRegularSelector)}`;
  3685. let anyElements;
  3686. try {
  3687. // eslint-disable-next-line @typescript-eslint/no-use-before-define
  3688. anyElements = getElementsForSelectorNode(selectorNode, rootElement, specifiedSelector);
  3689. } catch (e) {
  3690. // do not fail on invalid selectors for :is()
  3691. return false;
  3692. }
  3693.  
  3694. // TODO: figure out how to handle complex selectors with extended pseudo-classes
  3695. // (check readme - extended-css-is-limitations)
  3696. // because `element` and `anyElements` may be from different DOM levels
  3697. return anyElements.includes(element);
  3698. });
  3699. };
  3700.  
  3701. /**
  3702. * Checks whether the element is not an element specified by pseudo-class arg.
  3703. * Used for :not() pseudo-class.
  3704. *
  3705. * @param argsData Relative pseudo-class helpers args data.
  3706. *
  3707. * @returns True if **any selector** from argsData.relativeSelectorList is **not matched** for argsData.element.
  3708. */
  3709. const notElementBySelectorList = argsData => {
  3710. const {
  3711. element,
  3712. relativeSelectorList,
  3713. pseudoName
  3714. } = argsData;
  3715. return relativeSelectorList.children
  3716. // Array.every() is used here as element should not be selected by any selector from selector list
  3717. .every(selectorNode => {
  3718. // selectorList.children always starts with regular selector
  3719. const relativeRegularSelector = getFirstInnerRegularChild(selectorNode, pseudoName);
  3720.  
  3721. /**
  3722. * For checking the element by 'div:not([data="content"])
  3723. * we check whether the element's parentElement has any specific direct child.
  3724. */
  3725. const rootElement = getParent(element, `Selection by :${pseudoName}() pseudo-class is not possible`);
  3726.  
  3727. /**
  3728. * So we calculate the element "description" by it's tagname and attributes for targeting
  3729. * and use it to specify the selection
  3730. * e.g. `div:not(.banner)` --> `divNode.parentElement.querySelectorAll(':scope > .banner')`.
  3731. */
  3732. const specifiedSelector = `${scopeDirectChildren}${getNodeValue(relativeRegularSelector)}`;
  3733. let anyElements;
  3734. try {
  3735. // eslint-disable-next-line @typescript-eslint/no-use-before-define
  3736. anyElements = getElementsForSelectorNode(selectorNode, rootElement, specifiedSelector);
  3737. } catch (e) {
  3738. // fail on invalid selectors for :not()
  3739. logger.error(e);
  3740. // eslint-disable-next-line max-len
  3741. throw new Error(`Invalid selector for :${pseudoName}() pseudo-class: '${getNodeValue(relativeRegularSelector)}'`);
  3742. }
  3743.  
  3744. // TODO: figure out how to handle up-looking pseudo-classes inside :not()
  3745. // (check readme - extended-css-not-limitations)
  3746. // because `element` and `anyElements` may be from different DOM levels
  3747. return !anyElements.includes(element);
  3748. });
  3749. };
  3750.  
  3751. /**
  3752. * Selects dom elements by value of RegularSelector.
  3753. *
  3754. * @param regularSelectorNode RegularSelector node.
  3755. * @param root Root DOM element.
  3756. * @param specifiedSelector @see {@link SpecifiedSelector}.
  3757. *
  3758. * @returns Array of DOM elements.
  3759. * @throws An error if RegularSelector node value is an invalid selector.
  3760. */
  3761. const getByRegularSelector = (regularSelectorNode, root, specifiedSelector) => {
  3762. const selectorText = specifiedSelector ? specifiedSelector : getNodeValue(regularSelectorNode);
  3763. let selectedElements = [];
  3764. try {
  3765. selectedElements = Array.from(root.querySelectorAll(selectorText));
  3766. } catch (e) {
  3767. // eslint-disable-line @typescript-eslint/no-explicit-any
  3768. throw new Error(`Error: unable to select by '${selectorText}' ${e.message}`);
  3769. }
  3770. return selectedElements;
  3771. };
  3772.  
  3773. /**
  3774. * Returns list of dom elements filtered or selected by ExtendedSelector node.
  3775. *
  3776. * @param domElements Array of DOM elements.
  3777. * @param extendedSelectorNode ExtendedSelector node.
  3778. *
  3779. * @returns Array of DOM elements.
  3780. * @throws An error on unknown pseudo-class,
  3781. * absent or invalid arg of extended pseudo-class, etc.
  3782. */
  3783. const getByExtendedSelector = (domElements, extendedSelectorNode) => {
  3784. let foundElements = [];
  3785. const extendedPseudoClassNode = getPseudoClassNode(extendedSelectorNode);
  3786. const pseudoName = getNodeName(extendedPseudoClassNode);
  3787. if (isAbsolutePseudoClass(pseudoName)) {
  3788. // absolute extended pseudo-classes should have an argument
  3789. const absolutePseudoArg = getNodeValue(extendedPseudoClassNode, `Missing arg for :${pseudoName}() pseudo-class`);
  3790. if (pseudoName === NTH_ANCESTOR_PSEUDO_CLASS_MARKER) {
  3791. // :nth-ancestor()
  3792. foundElements = findByAbsolutePseudoPseudo.nthAncestor(domElements, absolutePseudoArg, pseudoName);
  3793. } else if (pseudoName === XPATH_PSEUDO_CLASS_MARKER) {
  3794. // :xpath()
  3795. try {
  3796. document.createExpression(absolutePseudoArg, null);
  3797. } catch (e) {
  3798. throw new Error(`Invalid argument of :${pseudoName}() pseudo-class: '${absolutePseudoArg}'`);
  3799. }
  3800. foundElements = findByAbsolutePseudoPseudo.xpath(domElements, absolutePseudoArg);
  3801. } else if (pseudoName === UPWARD_PSEUDO_CLASS_MARKER) {
  3802. // :upward()
  3803. if (Number.isNaN(Number(absolutePseudoArg))) {
  3804. // so arg is selector, not a number
  3805. foundElements = findByAbsolutePseudoPseudo.upward(domElements, absolutePseudoArg);
  3806. } else {
  3807. foundElements = findByAbsolutePseudoPseudo.nthAncestor(domElements, absolutePseudoArg, pseudoName);
  3808. }
  3809. } else {
  3810. // all other absolute extended pseudo-classes
  3811. // e.g. contains, matches-attr, etc.
  3812. foundElements = domElements.filter(element => {
  3813. return isMatchedByAbsolutePseudo(element, pseudoName, absolutePseudoArg);
  3814. });
  3815. }
  3816. } else if (isRelativePseudoClass(pseudoName)) {
  3817. const relativeSelectorList = getRelativeSelectorListNode(extendedPseudoClassNode);
  3818. let relativePredicate;
  3819. switch (pseudoName) {
  3820. case HAS_PSEUDO_CLASS_MARKER:
  3821. case ABP_HAS_PSEUDO_CLASS_MARKER:
  3822. relativePredicate = element => hasRelativesBySelectorList({
  3823. element,
  3824. relativeSelectorList,
  3825. pseudoName
  3826. });
  3827. break;
  3828. case IS_PSEUDO_CLASS_MARKER:
  3829. relativePredicate = element => isAnyElementBySelectorList({
  3830. element,
  3831. relativeSelectorList,
  3832. pseudoName
  3833. });
  3834. break;
  3835. case NOT_PSEUDO_CLASS_MARKER:
  3836. relativePredicate = element => notElementBySelectorList({
  3837. element,
  3838. relativeSelectorList,
  3839. pseudoName
  3840. });
  3841. break;
  3842. default:
  3843. throw new Error(`Unknown relative pseudo-class: '${pseudoName}'`);
  3844. }
  3845. foundElements = domElements.filter(relativePredicate);
  3846. } else {
  3847. // extra check is parser missed something
  3848. throw new Error(`Unknown extended pseudo-class: '${pseudoName}'`);
  3849. }
  3850. return foundElements;
  3851. };
  3852.  
  3853. /**
  3854. * Returns list of dom elements which is selected by RegularSelector value.
  3855. *
  3856. * @param domElements Array of DOM elements.
  3857. * @param regularSelectorNode RegularSelector node.
  3858. *
  3859. * @returns Array of DOM elements.
  3860. * @throws An error if RegularSelector has not value.
  3861. */
  3862. const getByFollowingRegularSelector = (domElements, regularSelectorNode) => {
  3863. // array of arrays because of Array.map() later
  3864. let foundElements = [];
  3865. const value = getNodeValue(regularSelectorNode);
  3866. if (value.startsWith(CHILD_COMBINATOR)) {
  3867. // e.g. div:has(> img) > .banner
  3868. foundElements = domElements.map(root => {
  3869. const specifiedSelector = `${SCOPE_CSS_PSEUDO_CLASS}${value}`;
  3870. return getByRegularSelector(regularSelectorNode, root, specifiedSelector);
  3871. });
  3872. } else if (value.startsWith(NEXT_SIBLING_COMBINATOR) || value.startsWith(SUBSEQUENT_SIBLING_COMBINATOR)) {
  3873. // e.g. div:has(> img) + .banner
  3874. // or div:has(> img) ~ .banner
  3875. foundElements = domElements.map(element => {
  3876. const rootElement = element.parentElement;
  3877. if (!rootElement) {
  3878. // do not throw error if there in no parent for element
  3879. // e.g. '*:contains(text)' selects `html` which has no parentElement
  3880. return [];
  3881. }
  3882. const elementSelectorText = getElementSelectorDesc(element);
  3883. const specifiedSelector = `${scopeDirectChildren}${elementSelectorText}${value}`;
  3884. const selected = getByRegularSelector(regularSelectorNode, rootElement, specifiedSelector);
  3885. return selected;
  3886. });
  3887. } else {
  3888. // space-separated regular selector after extended one
  3889. // e.g. div:has(> img) .banner
  3890. foundElements = domElements.map(root => {
  3891. const specifiedSelector = `${scopeAnyChildren}${getNodeValue(regularSelectorNode)}`;
  3892. return getByRegularSelector(regularSelectorNode, root, specifiedSelector);
  3893. });
  3894. }
  3895. // foundElements should be flattened
  3896. // as getByRegularSelector() returns elements array, and Array.map() collects them to array
  3897. return flatten(foundElements);
  3898. };
  3899.  
  3900. /**
  3901. * Returns elements nodes for Selector node.
  3902. * As far as any selector always starts with regular part,
  3903. * it selects by RegularSelector first and checks found elements later.
  3904. *
  3905. * Relative pseudo-classes has it's own subtree so getElementsForSelectorNode is called recursively.
  3906. *
  3907. * 'specifiedSelector' is needed for :has(), :is(), and :not() pseudo-classes
  3908. * as native querySelectorAll() does not select exact element descendants even if it is called on 'div'
  3909. * e.g. ':scope' specification is needed for proper descendants selection for 'div:has(> img)'.
  3910. * So we check `divNode.querySelectorAll(':scope > img').length > 0`.
  3911. *
  3912. * @param selectorNode Selector node.
  3913. * @param root Root DOM element.
  3914. * @param specifiedSelector Needed element specification.
  3915. *
  3916. * @returns Array of DOM elements.
  3917. * @throws An error if there is no selectorNodeChild.
  3918. */
  3919. const getElementsForSelectorNode = (selectorNode, root, specifiedSelector) => {
  3920. let selectedElements = [];
  3921. let i = 0;
  3922. while (i < selectorNode.children.length) {
  3923. const selectorNodeChild = getItemByIndex(selectorNode.children, i, 'selectorNodeChild should be specified');
  3924. if (i === 0) {
  3925. // any selector always starts with regular selector
  3926. selectedElements = getByRegularSelector(selectorNodeChild, root, specifiedSelector);
  3927. } else if (isExtendedSelectorNode(selectorNodeChild)) {
  3928. // filter previously selected elements by next selector nodes
  3929. selectedElements = getByExtendedSelector(selectedElements, selectorNodeChild);
  3930. } else if (isRegularSelectorNode(selectorNodeChild)) {
  3931. selectedElements = getByFollowingRegularSelector(selectedElements, selectorNodeChild);
  3932. }
  3933. i += 1;
  3934. }
  3935. return selectedElements;
  3936. };
  3937.  
  3938. /**
  3939. * Selects elements by ast.
  3940. *
  3941. * @param ast Ast of parsed selector.
  3942. * @param doc Document.
  3943. *
  3944. * @returns Array of DOM elements.
  3945. */
  3946. const selectElementsByAst = function (ast) {
  3947. let doc = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : document;
  3948. const selectedElements = [];
  3949. // ast root is SelectorList node;
  3950. // it has Selector nodes as children which should be processed separately
  3951. ast.children.forEach(selectorNode => {
  3952. selectedElements.push(...getElementsForSelectorNode(selectorNode, doc));
  3953. });
  3954. // selectedElements should be flattened as it is array of arrays with elements
  3955. const uniqueElements = [...new Set(flatten(selectedElements))];
  3956. return uniqueElements;
  3957. };
  3958.  
  3959. /**
  3960. * Class of ExtCssDocument is needed for caching.
  3961. * For making cache related to each new instance of class, not global.
  3962. */
  3963. class ExtCssDocument {
  3964. /**
  3965. * Cache with selectors and their AST parsing results.
  3966. */
  3967.  
  3968. /**
  3969. * Creates new ExtCssDocument and inits new `astCache`.
  3970. */
  3971. constructor() {
  3972. this.astCache = new Map();
  3973. }
  3974.  
  3975. /**
  3976. * Saves selector and it's ast to cache.
  3977. *
  3978. * @param selector Standard or extended selector.
  3979. * @param ast Selector ast.
  3980. */
  3981. saveAstToCache(selector, ast) {
  3982. this.astCache.set(selector, ast);
  3983. }
  3984.  
  3985. /**
  3986. * Returns ast from cache for given selector.
  3987. *
  3988. * @param selector Standard or extended selector.
  3989. *
  3990. * @returns Previously parsed ast found in cache, or null if not found.
  3991. */
  3992. getAstFromCache(selector) {
  3993. const cachedAst = this.astCache.get(selector) || null;
  3994. return cachedAst;
  3995. }
  3996.  
  3997. /**
  3998. * Returns selector ast:
  3999. * - if cached ast exists — returns it;
  4000. * - if no cached ast — saves newly parsed ast to cache and returns it.
  4001. *
  4002. * @param selector Standard or extended selector.
  4003. *
  4004. * @returns Ast for `selector`.
  4005. */
  4006. getSelectorAst(selector) {
  4007. let ast = this.getAstFromCache(selector);
  4008. if (!ast) {
  4009. ast = parse$1(selector);
  4010. }
  4011. this.saveAstToCache(selector, ast);
  4012. return ast;
  4013. }
  4014.  
  4015. /**
  4016. * Selects elements by selector.
  4017. *
  4018. * @param selector Standard or extended selector.
  4019. *
  4020. * @returns Array of DOM elements.
  4021. */
  4022. querySelectorAll(selector) {
  4023. const ast = this.getSelectorAst(selector);
  4024. return selectElementsByAst(ast);
  4025. }
  4026. }
  4027. const extCssDocument = new ExtCssDocument();
  4028.  
  4029. /**
  4030. * Checks the presence of :remove() pseudo-class and validates it while parsing the selector part of css rule.
  4031. *
  4032. * @param rawSelector Selector which may contain :remove() pseudo-class.
  4033. *
  4034. * @returns Parsed selector data with selector and styles.
  4035. * @throws An error on invalid :remove() position.
  4036. */
  4037. const parseRemoveSelector = rawSelector => {
  4038. /**
  4039. * No error will be thrown on invalid selector as it will be validated later
  4040. * so it's better to explicitly specify 'any' selector for :remove() pseudo-class by '*',
  4041. * e.g. '.banner > *:remove()' instead of '.banner > :remove()'.
  4042. */
  4043.  
  4044. // ':remove()'
  4045. // eslint-disable-next-line max-len
  4046. const VALID_REMOVE_MARKER = `${COLON}${REMOVE_PSEUDO_MARKER}${BRACKETS.PARENTHESES.LEFT}${BRACKETS.PARENTHESES.RIGHT}`;
  4047. // ':remove(' - needed for validation rules like 'div:remove(2)'
  4048. const INVALID_REMOVE_MARKER = `${COLON}${REMOVE_PSEUDO_MARKER}${BRACKETS.PARENTHESES.LEFT}`;
  4049. let selector;
  4050. let shouldRemove = false;
  4051. const firstIndex = rawSelector.indexOf(VALID_REMOVE_MARKER);
  4052. if (firstIndex === 0) {
  4053. // e.g. ':remove()'
  4054. throw new Error(`${REMOVE_ERROR_PREFIX.NO_TARGET_SELECTOR}: '${rawSelector}'`);
  4055. } else if (firstIndex > 0) {
  4056. if (firstIndex !== rawSelector.lastIndexOf(VALID_REMOVE_MARKER)) {
  4057. // rule with more than one :remove() pseudo-class is invalid
  4058. // e.g. '.block:remove() > .banner:remove()'
  4059. throw new Error(`${REMOVE_ERROR_PREFIX.MULTIPLE_USAGE}: '${rawSelector}'`);
  4060. } else if (firstIndex + VALID_REMOVE_MARKER.length < rawSelector.length) {
  4061. // remove pseudo-class should be last in the rule
  4062. // e.g. '.block:remove():upward(2)'
  4063. throw new Error(`${REMOVE_ERROR_PREFIX.INVALID_POSITION}: '${rawSelector}'`);
  4064. } else {
  4065. // valid :remove() pseudo-class position
  4066. selector = rawSelector.substring(0, firstIndex);
  4067. shouldRemove = true;
  4068. }
  4069. } else if (rawSelector.includes(INVALID_REMOVE_MARKER)) {
  4070. // it is not valid if ':remove()' is absent in rule but just ':remove(' is present
  4071. // e.g. 'div:remove(0)'
  4072. throw new Error(`${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${rawSelector}'`);
  4073. } else {
  4074. // there is no :remove() pseudo-class is rule
  4075. selector = rawSelector;
  4076. }
  4077. const stylesOfSelector = shouldRemove ? [{
  4078. property: REMOVE_PSEUDO_MARKER,
  4079. value: String(shouldRemove)
  4080. }] : [];
  4081. return {
  4082. selector,
  4083. stylesOfSelector
  4084. };
  4085. };
  4086.  
  4087. /**
  4088. * Converts array of `entries` to object.
  4089. * Object.fromEntries() polyfill because it is not supported by old browsers, e.g. Chrome 55.
  4090. * Only first two elements of `entries` array matter, other will be skipped silently.
  4091. *
  4092. * @see {@link https://caniuse.com/?search=Object.fromEntries}
  4093. *
  4094. * @param entries Array of pairs.
  4095. *
  4096. * @returns Object converted from `entries`.
  4097. */
  4098. const getObjectFromEntries = entries => {
  4099. const object = {};
  4100. entries.forEach(el => {
  4101. const [key, value] = el;
  4102. object[key] = value;
  4103. });
  4104. return object;
  4105. };
  4106.  
  4107. const DEBUG_PSEUDO_PROPERTY_KEY = 'debug';
  4108. const REGEXP_DECLARATION_END = /[;}]/g;
  4109. const REGEXP_DECLARATION_DIVIDER = /[;:}]/g;
  4110. const REGEXP_NON_WHITESPACE = /\S/g;
  4111.  
  4112. // ExtendedCss does not support at-rules
  4113. // https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
  4114. const AT_RULE_MARKER = '@';
  4115. /**
  4116. * Init value for rawRuleData.
  4117. */
  4118. const initRawRuleData = {
  4119. selector: ''
  4120. };
  4121.  
  4122. /**
  4123. * Resets rule data buffer to init value after rule successfully collected.
  4124. *
  4125. * @param context Stylesheet parser context.
  4126. */
  4127. const restoreRuleAcc = context => {
  4128. context.rawRuleData = initRawRuleData;
  4129. };
  4130.  
  4131. /**
  4132. * Parses cropped selector part found before `{` previously.
  4133. *
  4134. * @param context Stylesheet parser context.
  4135. * @param extCssDoc Needed for caching of selector ast.
  4136. *
  4137. * @returns Parsed validation data for cropped part of stylesheet which may be a selector.
  4138. * @throws An error on unsupported CSS features, e.g. at-rules.
  4139. */
  4140. const parseSelectorPart = (context, extCssDoc) => {
  4141. let selector = context.selectorBuffer.trim();
  4142. if (selector.startsWith(AT_RULE_MARKER)) {
  4143. throw new Error(`At-rules are not supported: '${selector}'.`);
  4144. }
  4145. let removeSelectorData;
  4146. try {
  4147. removeSelectorData = parseRemoveSelector(selector);
  4148. } catch (e) {
  4149. // eslint-disable-line @typescript-eslint/no-explicit-any
  4150. logger.error(e.message);
  4151. throw new Error(`${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${selector}'`);
  4152. }
  4153. if (context.nextIndex === -1) {
  4154. if (selector === removeSelectorData.selector) {
  4155. // rule should have style or pseudo-class :remove()
  4156. throw new Error(`${STYLESHEET_ERROR_PREFIX.NO_STYLE_OR_REMOVE}: '${context.cssToParse}'`);
  4157. }
  4158. // stop parsing as there is no style declaration and selector parsed fine
  4159. context.cssToParse = '';
  4160. }
  4161. let stylesOfSelector = [];
  4162. let success = false;
  4163. let ast;
  4164. try {
  4165. selector = removeSelectorData.selector;
  4166. stylesOfSelector = removeSelectorData.stylesOfSelector;
  4167. // validate found selector by parsing it to ast
  4168. // so if it is invalid error will be thrown
  4169. ast = extCssDoc.getSelectorAst(selector);
  4170. success = true;
  4171. } catch (e) {
  4172. // eslint-disable-line @typescript-eslint/no-explicit-any
  4173. success = false;
  4174. }
  4175. if (context.nextIndex > 0) {
  4176. // slice found valid selector part off
  4177. // and parse rest of stylesheet later
  4178. context.cssToParse = context.cssToParse.slice(context.nextIndex);
  4179. }
  4180. return {
  4181. success,
  4182. selector,
  4183. ast,
  4184. stylesOfSelector
  4185. };
  4186. };
  4187.  
  4188. /**
  4189. * Recursively parses style declaration string into `Style`s.
  4190. *
  4191. * @param context Stylesheet parser context.
  4192. * @param styles Array of styles.
  4193. *
  4194. * @throws An error on invalid style declaration.
  4195. * @returns A number index of the next `}` in `this.cssToParse`.
  4196. */
  4197. const parseUntilClosingBracket = (context, styles) => {
  4198. // Expects ":", ";", and "}".
  4199. REGEXP_DECLARATION_DIVIDER.lastIndex = context.nextIndex;
  4200. let match = REGEXP_DECLARATION_DIVIDER.exec(context.cssToParse);
  4201. if (match === null) {
  4202. throw new Error(`${STYLESHEET_ERROR_PREFIX.INVALID_STYLE}: '${context.cssToParse}'`);
  4203. }
  4204. let matchPos = match.index;
  4205. let matched = match[0];
  4206. if (matched === BRACKETS.CURLY.RIGHT) {
  4207. const declarationChunk = context.cssToParse.slice(context.nextIndex, matchPos);
  4208. if (declarationChunk.trim().length === 0) {
  4209. // empty style declaration
  4210. // e.g. 'div { }'
  4211. if (styles.length === 0) {
  4212. throw new Error(`${STYLESHEET_ERROR_PREFIX.NO_STYLE}: '${context.cssToParse}'`);
  4213. }
  4214. // else valid style parsed before it
  4215. // e.g. '{ display: none; }' -- position is after ';'
  4216. } else {
  4217. // closing curly bracket '}' is matched before colon ':'
  4218. // trimmed declarationChunk is not a space, between ';' and '}',
  4219. // e.g. 'visible }' in style '{ display: none; visible }' after part before ';' is parsed
  4220. throw new Error(`${STYLESHEET_ERROR_PREFIX.INVALID_STYLE}: '${context.cssToParse}'`);
  4221. }
  4222. return matchPos;
  4223. }
  4224. if (matched === COLON) {
  4225. const colonIndex = matchPos;
  4226. // Expects ";" and "}".
  4227. REGEXP_DECLARATION_END.lastIndex = colonIndex;
  4228. match = REGEXP_DECLARATION_END.exec(context.cssToParse);
  4229. if (match === null) {
  4230. throw new Error(`${STYLESHEET_ERROR_PREFIX.UNCLOSED_STYLE}: '${context.cssToParse}'`);
  4231. }
  4232. matchPos = match.index;
  4233. matched = match[0];
  4234. // Populates the `styleMap` key-value map.
  4235. const property = context.cssToParse.slice(context.nextIndex, colonIndex).trim();
  4236. if (property.length === 0) {
  4237. throw new Error(`${STYLESHEET_ERROR_PREFIX.NO_PROPERTY}: '${context.cssToParse}'`);
  4238. }
  4239. const value = context.cssToParse.slice(colonIndex + 1, matchPos).trim();
  4240. if (value.length === 0) {
  4241. throw new Error(`${STYLESHEET_ERROR_PREFIX.NO_VALUE}: '${context.cssToParse}'`);
  4242. }
  4243. styles.push({
  4244. property,
  4245. value
  4246. });
  4247. // finish style parsing if '}' is found
  4248. // e.g. '{ display: none }' -- no ';' at the end of declaration
  4249. if (matched === BRACKETS.CURLY.RIGHT) {
  4250. return matchPos;
  4251. }
  4252. }
  4253. // matchPos is the position of the next ';'
  4254. // crop 'cssToParse' and re-run the loop
  4255. context.cssToParse = context.cssToParse.slice(matchPos + 1);
  4256. context.nextIndex = 0;
  4257. return parseUntilClosingBracket(context, styles); // Should be a subject of tail-call optimization.
  4258. };
  4259.  
  4260. /**
  4261. * Parses next style declaration part in stylesheet.
  4262. *
  4263. * @param context Stylesheet parser context.
  4264. *
  4265. * @returns Array of style data objects.
  4266. */
  4267. const parseNextStyle = context => {
  4268. const styles = [];
  4269. const styleEndPos = parseUntilClosingBracket(context, styles);
  4270.  
  4271. // find next rule after the style declaration
  4272. REGEXP_NON_WHITESPACE.lastIndex = styleEndPos + 1;
  4273. const match = REGEXP_NON_WHITESPACE.exec(context.cssToParse);
  4274. if (match === null) {
  4275. context.cssToParse = '';
  4276. return styles;
  4277. }
  4278. const matchPos = match.index;
  4279.  
  4280. // cut out matched style declaration for previous selector
  4281. context.cssToParse = context.cssToParse.slice(matchPos);
  4282. return styles;
  4283. };
  4284.  
  4285. /**
  4286. * Checks whether the 'remove' property positively set in styles
  4287. * with only one positive value - 'true'.
  4288. *
  4289. * @param styles Array of styles.
  4290. *
  4291. * @returns True if there is 'remove' property with 'true' value in `styles`.
  4292. */
  4293. const isRemoveSetInStyles = styles => {
  4294. return styles.some(s => {
  4295. return s.property === REMOVE_PSEUDO_MARKER && s.value === PSEUDO_PROPERTY_POSITIVE_VALUE;
  4296. });
  4297. };
  4298.  
  4299. /**
  4300. * Returns valid 'debug' property value set in styles
  4301. * where possible values are 'true' and 'global'.
  4302. *
  4303. * @param styles Array of styles.
  4304. *
  4305. * @returns Value of 'debug' property if it is set in `styles`,
  4306. * or `undefined` if the property is not found.
  4307. */
  4308. const getDebugStyleValue = styles => {
  4309. const debugStyle = styles.find(s => {
  4310. return s.property === DEBUG_PSEUDO_PROPERTY_KEY;
  4311. });
  4312. return debugStyle === null || debugStyle === void 0 ? void 0 : debugStyle.value;
  4313. };
  4314.  
  4315. /**
  4316. * Prepares final RuleData.
  4317. *
  4318. * @param selector String selector.
  4319. * @param ast Parsed ast.
  4320. * @param rawStyles Array of previously collected styles which may contain 'remove' and 'debug'.
  4321. *
  4322. * @returns Parsed ExtendedCss rule data.
  4323. */
  4324. const prepareRuleData = (selector, ast, rawStyles) => {
  4325. const ruleData = {
  4326. selector,
  4327. ast
  4328. };
  4329. const debugValue = getDebugStyleValue(rawStyles);
  4330. const shouldRemove = isRemoveSetInStyles(rawStyles);
  4331. let styles = rawStyles;
  4332. if (debugValue) {
  4333. // get rid of 'debug' from styles
  4334. styles = rawStyles.filter(s => s.property !== DEBUG_PSEUDO_PROPERTY_KEY);
  4335. // and set it as separate property only if its value is valid
  4336. // which is 'true' or 'global'
  4337. if (debugValue === PSEUDO_PROPERTY_POSITIVE_VALUE || debugValue === DEBUG_PSEUDO_PROPERTY_GLOBAL_VALUE) {
  4338. ruleData.debug = debugValue;
  4339. }
  4340. }
  4341. if (shouldRemove) {
  4342. // no other styles are needed to apply if 'remove' is set
  4343. ruleData.style = {
  4344. [REMOVE_PSEUDO_MARKER]: PSEUDO_PROPERTY_POSITIVE_VALUE
  4345. };
  4346.  
  4347. /**
  4348. * 'content' property is needed for ExtCssConfiguration.beforeStyleApplied().
  4349. *
  4350. * @see {@link BeforeStyleAppliedCallback}
  4351. */
  4352. const contentStyle = styles.find(s => s.property === CONTENT_CSS_PROPERTY);
  4353. if (contentStyle) {
  4354. ruleData.style[CONTENT_CSS_PROPERTY] = contentStyle.value;
  4355. }
  4356. } else {
  4357. // otherwise all styles should be applied.
  4358. // every style property will be unique because of their converting into object
  4359. if (styles.length > 0) {
  4360. const stylesAsEntries = styles.map(style => {
  4361. const {
  4362. property,
  4363. value
  4364. } = style;
  4365. return [property, value];
  4366. });
  4367. const preparedStyleData = getObjectFromEntries(stylesAsEntries);
  4368. ruleData.style = preparedStyleData;
  4369. }
  4370. }
  4371. return ruleData;
  4372. };
  4373.  
  4374. /**
  4375. * Saves rules data for unique selectors.
  4376. *
  4377. * @param rawResults Previously collected results of parsing.
  4378. * @param rawRuleData Parsed rule data.
  4379. *
  4380. * @throws An error if there is no rawRuleData.styles or rawRuleData.ast.
  4381. */
  4382. const saveToRawResults = (rawResults, rawRuleData) => {
  4383. const {
  4384. selector,
  4385. ast,
  4386. styles
  4387. } = rawRuleData;
  4388. if (!styles) {
  4389. throw new Error(`No style declaration for selector: '${selector}'`);
  4390. }
  4391. if (!ast) {
  4392. throw new Error(`No ast parsed for selector: '${selector}'`);
  4393. }
  4394. const storedRuleData = rawResults.get(selector);
  4395. if (!storedRuleData) {
  4396. rawResults.set(selector, {
  4397. ast,
  4398. styles
  4399. });
  4400. } else {
  4401. storedRuleData.styles.push(...styles);
  4402. }
  4403. };
  4404.  
  4405. /**
  4406. * Parses stylesheet of rules into rules data objects (non-recursively):
  4407. * 1. Iterates through stylesheet string.
  4408. * 2. Finds first `{` which can be style declaration start or part of selector.
  4409. * 3. Validates found string part via selector parser; and if:
  4410. * - it throws error — saves string part to buffer as part of selector,
  4411. * slice next stylesheet part to `{` [2] and validates again [3];
  4412. * - no error — saves found string part as selector and starts to parse styles (recursively).
  4413. *
  4414. * @param rawStylesheet Raw stylesheet as string.
  4415. * @param extCssDoc ExtCssDocument which uses cache while selectors parsing.
  4416. * @throws An error on unsupported CSS features, e.g. comments, or invalid stylesheet syntax.
  4417. * @returns Array of rules data which contains:
  4418. * - selector as string;
  4419. * - ast to query elements by;
  4420. * - map of styles to apply.
  4421. */
  4422. const parse = (rawStylesheet, extCssDoc) => {
  4423. const stylesheet = rawStylesheet.trim();
  4424. if (stylesheet.includes(`${SLASH}${ASTERISK}`) && stylesheet.includes(`${ASTERISK}${SLASH}`)) {
  4425. throw new Error(`${STYLESHEET_ERROR_PREFIX.NO_COMMENT}: '${stylesheet}'`);
  4426. }
  4427. const context = {
  4428. // any stylesheet should start with selector
  4429. isSelector: true,
  4430. // init value of parser position
  4431. nextIndex: 0,
  4432. // init value of cssToParse
  4433. cssToParse: stylesheet,
  4434. // buffer for collecting selector part
  4435. selectorBuffer: '',
  4436. // accumulator for rules
  4437. rawRuleData: initRawRuleData
  4438. };
  4439. const rawResults = new Map();
  4440. let selectorData;
  4441.  
  4442. // context.cssToParse is going to be cropped while its parsing
  4443. while (context.cssToParse) {
  4444. if (context.isSelector) {
  4445. // find index of first opening curly bracket
  4446. // which may mean start of style part and end of selector one
  4447. context.nextIndex = context.cssToParse.indexOf(BRACKETS.CURLY.LEFT);
  4448. // rule should not start with style, selector is required
  4449. // e.g. '{ display: none; }'
  4450. if (context.selectorBuffer.length === 0 && context.nextIndex === 0) {
  4451. throw new Error(`${STYLESHEET_ERROR_PREFIX.NO_SELECTOR}: '${context.cssToParse}'`);
  4452. }
  4453. if (context.nextIndex === -1) {
  4454. // no style declaration in rule
  4455. // but rule still may contain :remove() pseudo-class
  4456. context.selectorBuffer = context.cssToParse;
  4457. } else {
  4458. // collect string parts before opening curly bracket
  4459. // until valid selector collected
  4460. context.selectorBuffer += context.cssToParse.slice(0, context.nextIndex);
  4461. }
  4462. selectorData = parseSelectorPart(context, extCssDoc);
  4463. if (selectorData.success) {
  4464. // selector successfully parsed
  4465. context.rawRuleData.selector = selectorData.selector.trim();
  4466. context.rawRuleData.ast = selectorData.ast;
  4467. context.rawRuleData.styles = selectorData.stylesOfSelector;
  4468. context.isSelector = false;
  4469. // save rule data if there is no style declaration
  4470. if (context.nextIndex === -1) {
  4471. saveToRawResults(rawResults, context.rawRuleData);
  4472. // clean up ruleContext
  4473. restoreRuleAcc(context);
  4474. } else {
  4475. // skip the opening curly bracket at the start of style declaration part
  4476. context.nextIndex = 1;
  4477. context.selectorBuffer = '';
  4478. }
  4479. } else {
  4480. // if selector was not successfully parsed parseSelectorPart(), continue stylesheet parsing:
  4481. // save the found bracket to buffer and proceed to next loop iteration
  4482. context.selectorBuffer += BRACKETS.CURLY.LEFT;
  4483. // delete `{` from cssToParse
  4484. context.cssToParse = context.cssToParse.slice(1);
  4485. }
  4486. } else {
  4487. var _context$rawRuleData$;
  4488. // style declaration should be parsed
  4489. const parsedStyles = parseNextStyle(context);
  4490.  
  4491. // styles can be parsed from selector part if it has :remove() pseudo-class
  4492. // e.g. '.banner:remove() { debug: true; }'
  4493. (_context$rawRuleData$ = context.rawRuleData.styles) === null || _context$rawRuleData$ === void 0 ? void 0 : _context$rawRuleData$.push(...parsedStyles);
  4494.  
  4495. // save rule data to results
  4496. saveToRawResults(rawResults, context.rawRuleData);
  4497. context.nextIndex = 0;
  4498.  
  4499. // clean up ruleContext
  4500. restoreRuleAcc(context);
  4501.  
  4502. // parse next rule selector after style successfully parsed
  4503. context.isSelector = true;
  4504. }
  4505. }
  4506. const results = [];
  4507. rawResults.forEach((value, key) => {
  4508. const selector = key;
  4509. const {
  4510. ast,
  4511. styles: rawStyles
  4512. } = value;
  4513. results.push(prepareRuleData(selector, ast, rawStyles));
  4514. });
  4515. return results;
  4516. };
  4517.  
  4518. /**
  4519. * Checks whether passed `arg` is number type.
  4520. *
  4521. * @param arg Value to check.
  4522. *
  4523. * @returns True if `arg` is number and not NaN.
  4524. */
  4525. const isNumber = arg => {
  4526. return typeof arg === 'number' && !Number.isNaN(arg);
  4527. };
  4528.  
  4529. const isSupported = typeof window.requestAnimationFrame !== 'undefined';
  4530. const timeout = isSupported ? requestAnimationFrame : window.setTimeout;
  4531. const deleteTimeout = isSupported ? cancelAnimationFrame : clearTimeout;
  4532. const perf = isSupported ? performance : Date;
  4533. const DEFAULT_THROTTLE_DELAY_MS = 150;
  4534. /**
  4535. * The purpose of ThrottleWrapper is to throttle calls of the function
  4536. * that applies ExtendedCss rules. The reasoning here is that the function calls
  4537. * are triggered by MutationObserver and there may be many mutations in a short period of time.
  4538. * We do not want to apply rules on every mutation so we use this helper to make sure
  4539. * that there is only one call in the given amount of time.
  4540. */
  4541. class ThrottleWrapper {
  4542. /**
  4543. * The provided callback should be executed twice in this time frame:
  4544. * very first time and not more often than throttleDelayMs for further executions.
  4545. *
  4546. * @see {@link ThrottleWrapper.run}
  4547. */
  4548.  
  4549. /**
  4550. * Creates new ThrottleWrapper.
  4551. *
  4552. * @param context ExtendedCss context.
  4553. * @param callback The callback.
  4554. * @param throttleMs Throttle delay in ms.
  4555. */
  4556. constructor(context, callback, throttleMs) {
  4557. this.context = context;
  4558. this.callback = callback;
  4559. this.throttleDelayMs = throttleMs || DEFAULT_THROTTLE_DELAY_MS;
  4560. this.wrappedCb = this.wrappedCallback.bind(this);
  4561. }
  4562.  
  4563. /**
  4564. * Wraps the callback (which supposed to be `applyRules`),
  4565. * needed to update `lastRunTime` and clean previous timeouts for proper execution of the callback.
  4566. *
  4567. * @param timestamp Timestamp.
  4568. */
  4569. wrappedCallback(timestamp) {
  4570. this.lastRunTime = isNumber(timestamp) ? timestamp : perf.now();
  4571. // `timeoutId` can be requestAnimationFrame-related
  4572. // so cancelAnimationFrame() as deleteTimeout() needs the arg to be defined
  4573. if (this.timeoutId) {
  4574. deleteTimeout(this.timeoutId);
  4575. delete this.timeoutId;
  4576. }
  4577. clearTimeout(this.timerId);
  4578. delete this.timerId;
  4579. if (this.callback) {
  4580. this.callback(this.context);
  4581. }
  4582. }
  4583.  
  4584. /**
  4585. * Indicates whether there is a scheduled callback.
  4586. *
  4587. * @returns True if scheduled callback exists.
  4588. */
  4589. hasPendingCallback() {
  4590. return isNumber(this.timeoutId) || isNumber(this.timerId);
  4591. }
  4592.  
  4593. /**
  4594. * Schedules the function which applies ExtendedCss rules before the next animation frame.
  4595. *
  4596. * Wraps function execution into `timeout` — requestAnimationFrame or setTimeout.
  4597. * For the first time runs the function without any condition.
  4598. * As it may be triggered by any mutation which may occur too ofter, we limit the function execution:
  4599. * 1. If `elapsedTime` since last function execution is less then set `throttleDelayMs`,
  4600. * next function call is hold till the end of throttle interval (subtracting `elapsed` from `throttleDelayMs`);
  4601. * 2. Do nothing if triggered again but function call which is on hold has not yet started its execution.
  4602. */
  4603. run() {
  4604. if (this.hasPendingCallback()) {
  4605. // there is a pending execution scheduled
  4606. return;
  4607. }
  4608. if (typeof this.lastRunTime !== 'undefined') {
  4609. const elapsedTime = perf.now() - this.lastRunTime;
  4610. if (elapsedTime < this.throttleDelayMs) {
  4611. this.timerId = window.setTimeout(this.wrappedCb, this.throttleDelayMs - elapsedTime);
  4612. return;
  4613. }
  4614. }
  4615. this.timeoutId = timeout(this.wrappedCb);
  4616. }
  4617.  
  4618. /**
  4619. * Returns timestamp for 'now'.
  4620. *
  4621. * @returns Timestamp.
  4622. */
  4623. static now() {
  4624. return perf.now();
  4625. }
  4626. }
  4627.  
  4628. const LAST_EVENT_TIMEOUT_MS = 10;
  4629. const IGNORED_EVENTS = ['mouseover', 'mouseleave', 'mouseenter', 'mouseout'];
  4630. const SUPPORTED_EVENTS = [
  4631. // keyboard events
  4632. 'keydown', 'keypress', 'keyup',
  4633. // mouse events
  4634. 'auxclick', 'click', 'contextmenu', 'dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseover', 'mouseout', 'mouseup', 'pointerlockchange', 'pointerlockerror', 'select', 'wheel'];
  4635.  
  4636. // 'wheel' event makes scrolling in Safari twitchy
  4637. // https://github.com/AdguardTeam/ExtendedCss/issues/120
  4638. const SAFARI_PROBLEMATIC_EVENTS = ['wheel'];
  4639.  
  4640. /**
  4641. * We use EventTracker to track the event that is likely to cause the mutation.
  4642. * The problem is that we cannot use `window.event` directly from the mutation observer call
  4643. * as we're not in the event handler context anymore.
  4644. */
  4645. class EventTracker {
  4646. /**
  4647. * Creates new EventTracker.
  4648. */
  4649. constructor() {
  4650. _defineProperty(this, "getLastEventType", () => this.lastEventType);
  4651. _defineProperty(this, "getTimeSinceLastEvent", () => {
  4652. if (!this.lastEventTime) {
  4653. return null;
  4654. }
  4655. return Date.now() - this.lastEventTime;
  4656. });
  4657. this.trackedEvents = isSafariBrowser ? SUPPORTED_EVENTS.filter(event => !SAFARI_PROBLEMATIC_EVENTS.includes(event)) : SUPPORTED_EVENTS;
  4658. this.trackedEvents.forEach(eventName => {
  4659. document.documentElement.addEventListener(eventName, this.trackEvent, true);
  4660. });
  4661. }
  4662.  
  4663. /**
  4664. * Callback for event listener for events tracking.
  4665. *
  4666. * @param event Any event.
  4667. */
  4668. trackEvent(event) {
  4669. this.lastEventType = event.type;
  4670. this.lastEventTime = Date.now();
  4671. }
  4672. /**
  4673. * Checks whether the last caught event should be ignored.
  4674. *
  4675. * @returns True if event should be ignored.
  4676. */
  4677. isIgnoredEventType() {
  4678. const lastEventType = this.getLastEventType();
  4679. const sinceLastEventTime = this.getTimeSinceLastEvent();
  4680. return !!lastEventType && IGNORED_EVENTS.includes(lastEventType) && !!sinceLastEventTime && sinceLastEventTime < LAST_EVENT_TIMEOUT_MS;
  4681. }
  4682.  
  4683. /**
  4684. * Stops event tracking by removing event listener.
  4685. */
  4686. stopTracking() {
  4687. this.trackedEvents.forEach(eventName => {
  4688. document.documentElement.removeEventListener(eventName, this.trackEvent, true);
  4689. });
  4690. }
  4691. }
  4692.  
  4693. const isEventListenerSupported = typeof window.addEventListener !== 'undefined';
  4694. const observeDocument = (context, callback) => {
  4695. // We are trying to limit the number of callback calls by not calling it on all kind of "hover" events.
  4696. // The rationale behind this is that "hover" events often cause attributes modification,
  4697. // but re-applying extCSS rules will be useless as these attribute changes are usually transient.
  4698. const shouldIgnoreMutations = mutations => {
  4699. // ignore if all mutations are about attributes changes
  4700. return mutations.every(m => m.type === 'attributes');
  4701. };
  4702. if (natives.MutationObserver) {
  4703. context.domMutationObserver = new natives.MutationObserver(mutations => {
  4704. if (!mutations || mutations.length === 0) {
  4705. return;
  4706. }
  4707. const eventTracker = new EventTracker();
  4708. if (eventTracker.isIgnoredEventType() && shouldIgnoreMutations(mutations)) {
  4709. return;
  4710. }
  4711. // save instance of EventTracker to context
  4712. // for removing its event listeners on disconnectDocument() while mainDisconnect()
  4713. context.eventTracker = eventTracker;
  4714. callback();
  4715. });
  4716. context.domMutationObserver.observe(document, {
  4717. childList: true,
  4718. subtree: true,
  4719. attributes: true,
  4720. attributeFilter: ['id', 'class']
  4721. });
  4722. } else if (isEventListenerSupported) {
  4723. document.addEventListener('DOMNodeInserted', callback, false);
  4724. document.addEventListener('DOMNodeRemoved', callback, false);
  4725. document.addEventListener('DOMAttrModified', callback, false);
  4726. }
  4727. };
  4728. const disconnectDocument = (context, callback) => {
  4729. var _context$eventTracker;
  4730. if (context.domMutationObserver) {
  4731. context.domMutationObserver.disconnect();
  4732. } else if (isEventListenerSupported) {
  4733. document.removeEventListener('DOMNodeInserted', callback, false);
  4734. document.removeEventListener('DOMNodeRemoved', callback, false);
  4735. document.removeEventListener('DOMAttrModified', callback, false);
  4736. }
  4737. // clean up event listeners
  4738. (_context$eventTracker = context.eventTracker) === null || _context$eventTracker === void 0 ? void 0 : _context$eventTracker.stopTracking();
  4739. };
  4740. const mainObserve = (context, mainCallback) => {
  4741. if (context.isDomObserved) {
  4742. return;
  4743. }
  4744. // handle dynamically added elements
  4745. context.isDomObserved = true;
  4746. observeDocument(context, mainCallback);
  4747. };
  4748. const mainDisconnect = (context, mainCallback) => {
  4749. if (!context.isDomObserved) {
  4750. return;
  4751. }
  4752. context.isDomObserved = false;
  4753. disconnectDocument(context, mainCallback);
  4754. };
  4755.  
  4756. // added by tsurlfilter's CssHitsCounter
  4757. const CONTENT_ATTR_PREFIX_REGEXP = /^("|')adguard.+?/;
  4758.  
  4759. /**
  4760. * Removes affectedElement.node from DOM.
  4761. *
  4762. * @param context ExtendedCss context.
  4763. * @param affectedElement Affected element.
  4764. */
  4765. const removeElement = (context, affectedElement) => {
  4766. const {
  4767. node
  4768. } = affectedElement;
  4769. affectedElement.removed = true;
  4770. const elementSelector = getElementSelectorPath(node);
  4771.  
  4772. // check if the element has been already removed earlier
  4773. const elementRemovalsCounter = context.removalsStatistic[elementSelector] || 0;
  4774.  
  4775. // if removals attempts happened more than specified we do not try to remove node again
  4776. if (elementRemovalsCounter > MAX_STYLE_PROTECTION_COUNT) {
  4777. logger.error(`ExtendedCss: infinite loop protection for selector: '${elementSelector}'`);
  4778. return;
  4779. }
  4780. if (node.parentElement) {
  4781. node.parentElement.removeChild(node);
  4782. context.removalsStatistic[elementSelector] = elementRemovalsCounter + 1;
  4783. }
  4784. };
  4785.  
  4786. /**
  4787. * Sets style to the specified DOM node.
  4788. *
  4789. * @param node DOM element.
  4790. * @param style Style to set.
  4791. */
  4792. const setStyleToElement = (node, style) => {
  4793. if (!(node instanceof HTMLElement)) {
  4794. return;
  4795. }
  4796. Object.keys(style).forEach(prop => {
  4797. // Apply this style only to existing properties
  4798. // We cannot use hasOwnProperty here (does not work in FF)
  4799. if (typeof node.style.getPropertyValue(prop.toString()) !== 'undefined') {
  4800. let value = style[prop];
  4801. if (!value) {
  4802. return;
  4803. }
  4804. // do not apply 'content' style given by tsurlfilter
  4805. // which is needed only for BeforeStyleAppliedCallback
  4806. if (prop === CONTENT_CSS_PROPERTY && value.match(CONTENT_ATTR_PREFIX_REGEXP)) {
  4807. return;
  4808. }
  4809. // First we should remove !important attribute (or it won't be applied')
  4810. value = removeSuffix(value.trim(), '!important').trim();
  4811. node.style.setProperty(prop, value, 'important');
  4812. }
  4813. });
  4814. };
  4815.  
  4816. /**
  4817. * Applies style to the specified DOM node.
  4818. *
  4819. * @param context ExtendedCss context.
  4820. * @param affectedElement Object containing DOM node and rule to be applied.
  4821. *
  4822. * @throws An error if affectedElement has no style to apply.
  4823. */
  4824. const applyStyle = (context, affectedElement) => {
  4825. if (affectedElement.protectionObserver) {
  4826. // style is already applied and protected by the observer
  4827. return;
  4828. }
  4829. if (context.beforeStyleApplied) {
  4830. affectedElement = context.beforeStyleApplied(affectedElement);
  4831. if (!affectedElement) {
  4832. return;
  4833. }
  4834. }
  4835. const {
  4836. node,
  4837. rules
  4838. } = affectedElement;
  4839. for (let i = 0; i < rules.length; i += 1) {
  4840. const rule = rules[i];
  4841. const selector = rule === null || rule === void 0 ? void 0 : rule.selector;
  4842. const style = rule === null || rule === void 0 ? void 0 : rule.style;
  4843. const debug = rule === null || rule === void 0 ? void 0 : rule.debug;
  4844. // rule may not have style to apply
  4845. // e.g. 'div:has(> a) { debug: true }' -> means no style to apply, and enable debug mode
  4846. if (style) {
  4847. if (style[REMOVE_PSEUDO_MARKER] === PSEUDO_PROPERTY_POSITIVE_VALUE) {
  4848. removeElement(context, affectedElement);
  4849. return;
  4850. }
  4851. setStyleToElement(node, style);
  4852. } else if (!debug) {
  4853. // but rule should not have both style and debug properties
  4854. throw new Error(`No style declaration in rule for selector: '${selector}'`);
  4855. }
  4856. }
  4857. };
  4858.  
  4859. /**
  4860. * Reverts style for the affected object.
  4861. *
  4862. * @param affectedElement Affected element.
  4863. */
  4864. const revertStyle = affectedElement => {
  4865. if (affectedElement.protectionObserver) {
  4866. affectedElement.protectionObserver.disconnect();
  4867. }
  4868. affectedElement.node.style.cssText = affectedElement.originalStyle;
  4869. };
  4870.  
  4871. /**
  4872. * ExtMutationObserver is a wrapper over regular MutationObserver with one additional function:
  4873. * it keeps track of the number of times we called the "ProtectionCallback".
  4874. *
  4875. * We use an instance of this to monitor styles added by ExtendedCss
  4876. * and to make sure these styles are recovered if the page script attempts to modify them.
  4877. *
  4878. * However, we want to avoid endless loops of modification if the page script repeatedly modifies the styles.
  4879. * So we keep track of the number of calls and observe() makes a decision
  4880. * whether to continue recovering the styles or not.
  4881. */
  4882. class ExtMutationObserver {
  4883. /**
  4884. * Extra property for keeping 'style fix counts'.
  4885. */
  4886.  
  4887. /**
  4888. * Creates new ExtMutationObserver.
  4889. *
  4890. * @param protectionCallback Callback which execution should be counted.
  4891. */
  4892. constructor(protectionCallback) {
  4893. this.styleProtectionCount = 0;
  4894. this.observer = new natives.MutationObserver(mutations => {
  4895. if (!mutations.length) {
  4896. return;
  4897. }
  4898. this.styleProtectionCount += 1;
  4899. protectionCallback(mutations, this);
  4900. });
  4901. }
  4902.  
  4903. /**
  4904. * Starts to observe target element,
  4905. * prevents infinite loop of observing due to the limited number of times of callback runs.
  4906. *
  4907. * @param target Target to observe.
  4908. * @param options Mutation observer options.
  4909. */
  4910. observe(target, options) {
  4911. if (this.styleProtectionCount < MAX_STYLE_PROTECTION_COUNT) {
  4912. this.observer.observe(target, options);
  4913. } else {
  4914. logger.error('ExtendedCss: infinite loop protection for style');
  4915. }
  4916. }
  4917.  
  4918. /**
  4919. * Stops ExtMutationObserver from observing any mutations.
  4920. * Until the `observe()` is used again, `protectionCallback` will not be invoked.
  4921. */
  4922. disconnect() {
  4923. this.observer.disconnect();
  4924. }
  4925. }
  4926.  
  4927. const PROTECTION_OBSERVER_OPTIONS = {
  4928. attributes: true,
  4929. attributeOldValue: true,
  4930. attributeFilter: ['style']
  4931. };
  4932.  
  4933. /**
  4934. * Creates MutationObserver protection callback.
  4935. *
  4936. * @param styles Styles data object.
  4937. *
  4938. * @returns Callback for styles protection.
  4939. */
  4940. const createProtectionCallback = styles => {
  4941. const protectionCallback = (mutations, extObserver) => {
  4942. if (!mutations[0]) {
  4943. return;
  4944. }
  4945. const {
  4946. target
  4947. } = mutations[0];
  4948. extObserver.disconnect();
  4949. styles.forEach(style => {
  4950. setStyleToElement(target, style);
  4951. });
  4952. extObserver.observe(target, PROTECTION_OBSERVER_OPTIONS);
  4953. };
  4954. return protectionCallback;
  4955. };
  4956.  
  4957. /**
  4958. * Sets up a MutationObserver which protects style attributes from changes.
  4959. *
  4960. * @param node DOM node.
  4961. * @param rules Rule data objects.
  4962. * @returns Mutation observer used to protect attribute or null if there's nothing to protect.
  4963. */
  4964. const protectStyleAttribute = (node, rules) => {
  4965. if (!natives.MutationObserver) {
  4966. return null;
  4967. }
  4968. const styles = [];
  4969. rules.forEach(ruleData => {
  4970. const {
  4971. style
  4972. } = ruleData;
  4973. // some rules might have only debug property in style declaration
  4974. // e.g. 'div:has(> a) { debug: true }' -> parsed to boolean `ruleData.debug`
  4975. // so no style is fine, and here we should collect only valid styles to protect
  4976. if (style) {
  4977. styles.push(style);
  4978. }
  4979. });
  4980. const protectionObserver = new ExtMutationObserver(createProtectionCallback(styles));
  4981. protectionObserver.observe(node, PROTECTION_OBSERVER_OPTIONS);
  4982. return protectionObserver;
  4983. };
  4984.  
  4985. const STATS_DECIMAL_DIGITS_COUNT = 4;
  4986. /**
  4987. * A helper class for applied rule stats.
  4988. */
  4989. class TimingStats {
  4990. /**
  4991. * Creates new TimingStats.
  4992. */
  4993. constructor() {
  4994. this.appliesTimings = [];
  4995. this.appliesCount = 0;
  4996. this.timingsSum = 0;
  4997. this.meanTiming = 0;
  4998. this.squaredSum = 0;
  4999. this.standardDeviation = 0;
  5000. }
  5001.  
  5002. /**
  5003. * Observe target element and mark observer as active.
  5004. *
  5005. * @param elapsedTimeMs Time in ms.
  5006. */
  5007. push(elapsedTimeMs) {
  5008. this.appliesTimings.push(elapsedTimeMs);
  5009. this.appliesCount += 1;
  5010. this.timingsSum += elapsedTimeMs;
  5011. this.meanTiming = this.timingsSum / this.appliesCount;
  5012. this.squaredSum += elapsedTimeMs * elapsedTimeMs;
  5013. this.standardDeviation = Math.sqrt(this.squaredSum / this.appliesCount - Math.pow(this.meanTiming, 2));
  5014. }
  5015. }
  5016. /**
  5017. * Makes the timestamps more readable.
  5018. *
  5019. * @param timestamp Raw timestamp.
  5020. *
  5021. * @returns Fine-looking timestamps.
  5022. */
  5023. const beautifyTimingNumber = timestamp => {
  5024. return Number(timestamp.toFixed(STATS_DECIMAL_DIGITS_COUNT));
  5025. };
  5026.  
  5027. /**
  5028. * Improves timing stats readability.
  5029. *
  5030. * @param rawTimings Collected timings with raw timestamp.
  5031. *
  5032. * @returns Fine-looking timing stats.
  5033. */
  5034. const beautifyTimings = rawTimings => {
  5035. return {
  5036. appliesTimings: rawTimings.appliesTimings.map(t => beautifyTimingNumber(t)),
  5037. appliesCount: beautifyTimingNumber(rawTimings.appliesCount),
  5038. timingsSum: beautifyTimingNumber(rawTimings.timingsSum),
  5039. meanTiming: beautifyTimingNumber(rawTimings.meanTiming),
  5040. standardDeviation: beautifyTimingNumber(rawTimings.standardDeviation)
  5041. };
  5042. };
  5043.  
  5044. /**
  5045. * Prints timing information if debugging mode is enabled.
  5046. *
  5047. * @param context ExtendedCss context.
  5048. */
  5049. const printTimingInfo = context => {
  5050. if (context.areTimingsPrinted) {
  5051. return;
  5052. }
  5053. context.areTimingsPrinted = true;
  5054. const timingsLogData = {};
  5055. context.parsedRules.forEach(ruleData => {
  5056. if (ruleData.timingStats) {
  5057. const {
  5058. selector,
  5059. style,
  5060. debug,
  5061. matchedElements
  5062. } = ruleData;
  5063. // style declaration for some rules is parsed to debug property and no style to apply
  5064. // e.g. 'div:has(> a) { debug: true }'
  5065. if (!style && !debug) {
  5066. throw new Error(`Rule should have style declaration for selector: '${selector}'`);
  5067. }
  5068. const selectorData = {
  5069. selectorParsed: selector,
  5070. timings: beautifyTimings(ruleData.timingStats)
  5071. };
  5072. // `ruleData.style` may contain `remove` pseudo-property
  5073. // and make logs look better
  5074. if (style && style[REMOVE_PSEUDO_MARKER] === PSEUDO_PROPERTY_POSITIVE_VALUE) {
  5075. selectorData.removed = true;
  5076. // no matchedElements for such case as they are removed after ExtendedCss applied
  5077. } else {
  5078. selectorData.styleApplied = style || null;
  5079. selectorData.matchedElements = matchedElements;
  5080. }
  5081. timingsLogData[selector] = selectorData;
  5082. }
  5083. });
  5084. if (Object.keys(timingsLogData).length === 0) {
  5085. return;
  5086. }
  5087. // add location.href to the message to distinguish frames
  5088. logger.info('[ExtendedCss] Timings in milliseconds for %o:\n%o', window.location.href, timingsLogData);
  5089. };
  5090.  
  5091. /**
  5092. * Finds affectedElement object for the specified DOM node.
  5093. *
  5094. * @param affElements Array of affected elements — context.affectedElements.
  5095. * @param domNode DOM node.
  5096. * @returns Found affectedElement or undefined.
  5097. */
  5098. const findAffectedElement = (affElements, domNode) => {
  5099. return affElements.find(affEl => affEl.node === domNode);
  5100. };
  5101.  
  5102. /**
  5103. * Applies specified rule and returns list of elements affected.
  5104. *
  5105. * @param context ExtendedCss context.
  5106. * @param ruleData Rule to apply.
  5107. * @returns List of elements affected by the rule.
  5108. */
  5109. const applyRule = (context, ruleData) => {
  5110. // debugging mode can be enabled in two ways:
  5111. // 1. for separate rules - by `{ debug: true; }`
  5112. // 2. for all rules simultaneously by:
  5113. // - `{ debug: global; }` in any rule
  5114. // - positive `debug` property in ExtCssConfiguration
  5115. const isDebuggingMode = !!ruleData.debug || context.debug;
  5116. let startTime;
  5117. if (isDebuggingMode) {
  5118. startTime = ThrottleWrapper.now();
  5119. }
  5120. const {
  5121. ast
  5122. } = ruleData;
  5123. const nodes = selectElementsByAst(ast);
  5124. nodes.forEach(node => {
  5125. let affectedElement = findAffectedElement(context.affectedElements, node);
  5126. if (affectedElement) {
  5127. affectedElement.rules.push(ruleData);
  5128. applyStyle(context, affectedElement);
  5129. } else {
  5130. // Applying style first time
  5131. const originalStyle = node.style.cssText;
  5132. affectedElement = {
  5133. node,
  5134. // affected DOM node
  5135. rules: [ruleData],
  5136. // rule to be applied
  5137. originalStyle,
  5138. // original node style
  5139. protectionObserver: null // style attribute observer
  5140. };
  5141.  
  5142. applyStyle(context, affectedElement);
  5143. context.affectedElements.push(affectedElement);
  5144. }
  5145. });
  5146. if (isDebuggingMode && startTime) {
  5147. const elapsedTimeMs = ThrottleWrapper.now() - startTime;
  5148. if (!ruleData.timingStats) {
  5149. ruleData.timingStats = new TimingStats();
  5150. }
  5151. ruleData.timingStats.push(elapsedTimeMs);
  5152. }
  5153. return nodes;
  5154. };
  5155.  
  5156. /**
  5157. * Applies filtering rules.
  5158. *
  5159. * @param context ExtendedCss context.
  5160. */
  5161. const applyRules = context => {
  5162. const newSelectedElements = [];
  5163. // some rules could make call - selector.querySelectorAll() temporarily to change node id attribute
  5164. // this caused MutationObserver to call recursively
  5165. // https://github.com/AdguardTeam/ExtendedCss/issues/81
  5166. mainDisconnect(context, context.mainCallback);
  5167. context.parsedRules.forEach(ruleData => {
  5168. const nodes = applyRule(context, ruleData);
  5169. Array.prototype.push.apply(newSelectedElements, nodes);
  5170. // save matched elements to ruleData as linked to applied rule
  5171. // only for debugging purposes
  5172. if (ruleData.debug) {
  5173. ruleData.matchedElements = nodes;
  5174. }
  5175. });
  5176. // Now revert styles for elements which are no more affected
  5177. let affLength = context.affectedElements.length;
  5178. // do nothing if there is no elements to process
  5179. while (affLength) {
  5180. const affectedElement = context.affectedElements[affLength - 1];
  5181. if (!affectedElement) {
  5182. break;
  5183. }
  5184. if (!newSelectedElements.includes(affectedElement.node)) {
  5185. // Time to revert style
  5186. revertStyle(affectedElement);
  5187. context.affectedElements.splice(affLength - 1, 1);
  5188. } else if (!affectedElement.removed) {
  5189. // Add style protection observer
  5190. // Protect "style" attribute from changes
  5191. if (!affectedElement.protectionObserver) {
  5192. affectedElement.protectionObserver = protectStyleAttribute(affectedElement.node, affectedElement.rules);
  5193. }
  5194. }
  5195. affLength -= 1;
  5196. }
  5197. // After styles are applied we can start observe again
  5198. mainObserve(context, context.mainCallback);
  5199. printTimingInfo(context);
  5200. };
  5201.  
  5202. /**
  5203. * Throttle timeout for ThrottleWrapper to execute applyRules().
  5204. */
  5205. const APPLY_RULES_DELAY = 150;
  5206.  
  5207. /**
  5208. * Result of selector validation.
  5209. */
  5210.  
  5211. /**
  5212. * Main class of ExtendedCss lib.
  5213. *
  5214. * Parses css stylesheet with any selectors (passed to its argument as styleSheet),
  5215. * and guarantee its applying as mutation observer is used to prevent the restyling of needed elements by other scripts.
  5216. * This style protection is limited to 50 times to avoid infinite loop (MAX_STYLE_PROTECTION_COUNT).
  5217. * Our own ThrottleWrapper is used for styles applying to avoid too often lib reactions on page mutations.
  5218. *
  5219. * Constructor creates the instance of class which should be run be `apply()` method to apply the rules,
  5220. * and the applying can be stopped by `dispose()`.
  5221. *
  5222. * Can be used to select page elements by selector with `query()` method (similar to `Document.querySelectorAll()`),
  5223. * which does not require instance creating.
  5224. */
  5225. class ExtendedCss {
  5226. /**
  5227. * Creates new ExtendedCss.
  5228. *
  5229. * @param configuration ExtendedCss configuration.
  5230. */
  5231. constructor(configuration) {
  5232. if (!isBrowserSupported()) {
  5233. throw new Error('Browser is not supported by ExtendedCss.');
  5234. }
  5235. if (!configuration) {
  5236. throw new Error('ExtendedCss configuration should be provided.');
  5237. }
  5238. this.context = {
  5239. beforeStyleApplied: configuration.beforeStyleApplied,
  5240. debug: false,
  5241. affectedElements: [],
  5242. isDomObserved: false,
  5243. removalsStatistic: {},
  5244. parsedRules: parse(configuration.styleSheet, extCssDocument),
  5245. mainCallback: () => {}
  5246. };
  5247.  
  5248. // true if set in configuration
  5249. // or any rule in styleSheet has `debug: global`
  5250. this.context.debug = configuration.debug || this.context.parsedRules.some(ruleData => {
  5251. return ruleData.debug === DEBUG_PSEUDO_PROPERTY_GLOBAL_VALUE;
  5252. });
  5253. this.applyRulesScheduler = new ThrottleWrapper(this.context, applyRules, APPLY_RULES_DELAY);
  5254. this.context.mainCallback = this.applyRulesScheduler.run.bind(this.applyRulesScheduler);
  5255. if (this.context.beforeStyleApplied && typeof this.context.beforeStyleApplied !== 'function') {
  5256. // eslint-disable-next-line max-len
  5257. throw new Error(`Invalid configuration. Type of 'beforeStyleApplied' should be a function, received: '${typeof this.context.beforeStyleApplied}'`);
  5258. }
  5259. this.applyRulesCallbackListener = () => {
  5260. applyRules(this.context);
  5261. };
  5262. }
  5263.  
  5264. /**
  5265. * Applies stylesheet rules on page.
  5266. */
  5267. apply() {
  5268. applyRules(this.context);
  5269. if (document.readyState !== 'complete') {
  5270. document.addEventListener('DOMContentLoaded', this.applyRulesCallbackListener, false);
  5271. }
  5272. }
  5273.  
  5274. /**
  5275. * Disposes ExtendedCss and removes our styles from matched elements.
  5276. */
  5277. dispose() {
  5278. mainDisconnect(this.context, this.context.mainCallback);
  5279. this.context.affectedElements.forEach(el => {
  5280. revertStyle(el);
  5281. });
  5282. document.removeEventListener('DOMContentLoaded', this.applyRulesCallbackListener, false);
  5283. }
  5284.  
  5285. /**
  5286. * Exposed for testing purposes only.
  5287. *
  5288. * @returns Array of AffectedElement data objects.
  5289. */
  5290. getAffectedElements() {
  5291. return this.context.affectedElements;
  5292. }
  5293.  
  5294. /**
  5295. * Returns a list of the document's elements that match the specified selector.
  5296. * Uses ExtCssDocument.querySelectorAll().
  5297. *
  5298. * @param selector Selector text.
  5299. * @param [noTiming=true] If true — do not print the timings to the console.
  5300. *
  5301. * @throws An error if selector is not valid.
  5302. * @returns A list of elements that match the selector.
  5303. */
  5304. static query(selector) {
  5305. let noTiming = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
  5306. if (typeof selector !== 'string') {
  5307. throw new Error('Selector should be defined as a string.');
  5308. }
  5309. const start = ThrottleWrapper.now();
  5310. try {
  5311. return extCssDocument.querySelectorAll(selector);
  5312. } finally {
  5313. const end = ThrottleWrapper.now();
  5314. if (!noTiming) {
  5315. logger.info(`[ExtendedCss] Elapsed: ${Math.round((end - start) * 1000)} μs.`);
  5316. }
  5317. }
  5318. }
  5319.  
  5320. /**
  5321. * Validates selector.
  5322. *
  5323. * @param inputSelector Selector text to validate.
  5324. *
  5325. * @returns Result of selector validation.
  5326. */
  5327. static validate(inputSelector) {
  5328. try {
  5329. // ExtendedCss in general supports :remove() in selector
  5330. // but ExtendedCss.query() does not support it as it should be parsed by stylesheet parser.
  5331. // so for validation we have to handle selectors with `:remove()` in it
  5332. const {
  5333. selector
  5334. } = parseRemoveSelector(inputSelector);
  5335. ExtendedCss.query(selector);
  5336. return {
  5337. ok: true,
  5338. error: null
  5339. };
  5340. } catch (e) {
  5341. const caughtErrorMessage = e instanceof Error ? e.message : e;
  5342. // not valid input `selector` should be logged eventually
  5343. const error = `Error: Invalid selector: '${inputSelector}' -- ${caughtErrorMessage}`;
  5344. return {
  5345. ok: false,
  5346. error
  5347. };
  5348. }
  5349. }
  5350. }
  5351.  
  5352. return ExtendedCss;
  5353.  
  5354. })();