extended-css

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

目前為 2022-12-04 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/452263/1124696/extended-css.js

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