extended-css

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

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

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

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