extended-css

一个让用户可以使用扩展 CSS 选择器的库

当前为 2022-12-20 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/452263/1130140/extended-css.js

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