AdBlock Script for WebView

Parse ABP Cosmetic rules to CSS and apply it.

目前為 2024-03-21 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name AdBlock Script for WebView
  3. // @name:zh-CN 套壳油猴的广告拦截脚本
  4. // @author Lemon399
  5. // @version 2.8.4
  6. // @description Parse ABP Cosmetic rules to CSS and apply it.
  7. // @description:zh-CN 将 ABP 中的元素隐藏规则转换为 CSS 使用
  8. // @resource jiekouAD https://slink.ltd/https://raw.githubusercontent.com/damengzhu/banad/main/jiekouAD.txt
  9. // @resource CSSRule https://slink.ltd/https://raw.githubusercontent.com/damengzhu/abpmerge/main/CSSRule.txt
  10. // @match http://*/*
  11. // @match https://*/*
  12. // @run-at document-start
  13. // @grant unsafeWindow
  14. // @grant GM_registerMenuCommand
  15. // @grant GM.registerMenuCommand
  16. // @grant GM_unregisterMenuCommand
  17. // @grant GM.unregisterMenuCommand
  18. // @grant GM_getValue
  19. // @grant GM.getValue
  20. // @grant GM_deleteValue
  21. // @grant GM.deleteValue
  22. // @grant GM_setValue
  23. // @grant GM.setValue
  24. // @grant GM_addStyle
  25. // @grant GM.addStyle
  26. // @grant GM_xmlhttpRequest
  27. // @grant GM.xmlHttpRequest
  28. // @grant GM_getResourceText
  29. // @grant GM.getResourceText
  30. // @grant GM_download
  31. // @grant GM.download
  32. // @grant GM_listValues
  33. // @grant GM.listValues
  34. // @namespace https://lemon399-bitbucket-io.vercel.app/
  35. // @source https://gitee.com/lemon399/tampermonkey-cli/tree/master/projects/abp_parse
  36. // @source https://bitbucket.org/lemon399/tampermonkey-cli/src/master/projects/abp_parse/
  37. // @connect slink.ltd
  38. // @copyright GPL-3.0
  39. // @license GPL-3.0
  40. // ==/UserScript==
  41.  
  42. /* ==UserConfig==
  43. 配置:
  44. rules:
  45. title: 自定义规则
  46. description: 添加自定义的 ABP 隐藏规则
  47. type: textarea
  48. rows: 10
  49. default: |-
  50. ! 不支持的规则和开头为 ! 的行会忽略
  51. !
  52. ! 由于语法限制,此处规则中
  53. ! 一个反斜杠需要改成两个,像这样 \\
  54. css:
  55. title: 隐藏 CSS
  56. description: 隐藏广告使用的 CSS
  57. type: textarea
  58. rows: 7
  59. default: |-
  60. {
  61. display: none !important;
  62. width: 0 !important;
  63. height: 0 !important;
  64. }
  65. timeout:
  66. title: 规则下载超时
  67. description: 更新规则时,规则下载超时时间
  68. type: number
  69. default: 10000
  70. min: 0
  71. unit: 毫秒
  72. headTimeout:
  73. title: 获取规则信息超时
  74. description: 更新规则时,获取规则信息 (HEAD 请求) 超时时间
  75. type: number
  76. default: 2000
  77. min: 0
  78. unit: 毫秒
  79. tryCount:
  80. title: CSS 注入尝试次数
  81. description: 某些框架会重建页面,需要多次注入,只有检测到 CSS 不存在时才会尝试再次注入
  82. type: number
  83. default: 5
  84. min: 0
  85. unit: 次
  86. tryTimeout:
  87. title: CSS 注入尝试间隔
  88. description: 两次注入尝试的间隔时间
  89. type: number
  90. default: 500
  91. min: 100
  92. unit: 毫秒
  93. autoCleanSize:
  94. title: 自动清空预存
  95. description: 预存超过此大小自动清空,0 关闭
  96. type: number
  97. default: 0
  98. min: 0
  99. unit: 字节
  100. ==/UserConfig== */
  101.  
  102. /* eslint-disable no-redeclare, no-unused-vars, require-yield, no-prototype-builtins */
  103. /* global GM_info, GM, unsafeWindow, GM_registerMenuCommand, GM_unregisterMenuCommand, GM_getValue, GM_deleteValue, GM_setValue, GM_addStyle, GM_xmlhttpRequest, GM_getResourceText, GM_download, GM_listValues */
  104.  
  105. (function () {
  106. "use strict";
  107.  
  108. const $presets = {
  109. userConfig: {
  110. rules: `
  111. ! 不支持的规则和开头为 ! 的行会忽略
  112. !
  113. ! 由于语法限制,此处规则中
  114. ! 一个反斜杠需要改成两个,像这样 \\
  115.  
  116. `,
  117. css: `{
  118. display: none !important;
  119. width: 0 !important;
  120. height: 0 !important;
  121. }`,
  122. timeout: 10000,
  123. headTimeout: 2000,
  124. tryCount: 5,
  125. tryTimeout: 500,
  126. autoCleanSize: 0,
  127. },
  128. onlineRules: [
  129. {
  130. 标识: `jiekouAD`,
  131. 地址: `https://slink.ltd/https://raw.githubusercontent.com/damengzhu/banad/main/jiekouAD.txt`,
  132. 在线更新: true,
  133. 筛选后存储: true,
  134. },
  135. {
  136. 标识: `CSSRule`,
  137. 地址: `https://slink.ltd/https://raw.githubusercontent.com/damengzhu/abpmerge/main/CSSRule.txt`,
  138. 在线更新: true,
  139. 筛选后存储: false,
  140. },
  141. ],
  142. };
  143.  
  144. let $listeners = [],
  145. $hasStorEvListener = false;
  146.  
  147. const $polyfills = {
  148. // 以下 polyfills 修改自 NullMonkey,使用 MPL-2.0 发布
  149. /* This Source Code Form is subject to the terms of the
  150. * Mozilla Public License, v. 2.0. If a copy of the MPL
  151. * was not distributed with this file, You can obtain
  152. * one at https://mozilla.org/MPL/2.0/. */
  153. GM_info:
  154. typeof GM_info == "object"
  155. ? GM_info
  156. : {
  157. script: {
  158. author: "Lemon399",
  159. copyright: "GPL-3.0",
  160. description: "Parse ABP Cosmetic rules to CSS and apply it.",
  161. downloadURL: null,
  162. excludes: [],
  163. excludeMatches: [],
  164. grant: [
  165. "unsafeWindow",
  166. "GM_registerMenuCommand",
  167. "GM.registerMenuCommand",
  168. "GM_unregisterMenuCommand",
  169. "GM.unregisterMenuCommand",
  170. "GM_getValue",
  171. "GM.getValue",
  172. "GM_deleteValue",
  173. "GM.deleteValue",
  174. "GM_setValue",
  175. "GM.setValue",
  176. "GM_addStyle",
  177. "GM.addStyle",
  178. "GM_xmlhttpRequest",
  179. "GM.xmlHttpRequest",
  180. "GM_getResourceText",
  181. "GM.getResourceText",
  182. "GM_download",
  183. "GM.download",
  184. "GM_listValues",
  185. "GM.listValues",
  186. ],
  187. homepage: null,
  188. icon: null,
  189. icon64: null,
  190. includes: [],
  191. matches: ["http://*/*", "https://*/*"],
  192. name: "AdBlock Script for WebView",
  193. namespace: "https://lemon399-bitbucket-io.vercel.app/",
  194. noframes: false,
  195. "run-at": "document-start",
  196. resources: [
  197. {
  198. name: "jiekouAD",
  199. url: "https://slink.ltd/https://raw.githubusercontent.com/damengzhu/banad/main/jiekouAD.txt",
  200. },
  201. {
  202. name: "CSSRule",
  203. url: "https://slink.ltd/https://raw.githubusercontent.com/damengzhu/abpmerge/main/CSSRule.txt",
  204. },
  205. ],
  206. supportURL: null,
  207. unwrap: false,
  208. updateURL: null,
  209. version: "2.8.4",
  210. webRequest: null,
  211. },
  212. scriptWillUpdate: false,
  213. },
  214. parseValue: function parseValue(stored) {
  215. const value =
  216. typeof stored == "string" &&
  217. stored.startsWith("[") &&
  218. stored.endsWith("]")
  219. ? JSON.parse(stored)[0]
  220. : void 0;
  221. return value === "__$NaN"
  222. ? NaN
  223. : value === "__$UdF"
  224. ? undefined
  225. : value === "__$FnT"
  226. ? Infinity
  227. : value === "__$XnT"
  228. ? -Infinity
  229. : value;
  230. },
  231. unsafeWindow: typeof unsafeWindow == "object" ? unsafeWindow : window,
  232. GM_registerMenuCommand:
  233. typeof GM_registerMenuCommand == "function"
  234. ? GM_registerMenuCommand
  235. : void 0,
  236. GM_unregisterMenuCommand:
  237. typeof GM_unregisterMenuCommand == "function"
  238. ? GM_unregisterMenuCommand
  239. : void 0,
  240. GM_getValue:
  241. typeof GM_getValue == "function"
  242. ? GM_getValue
  243. : function DM_getValue(key, defaultValue) {
  244. const stor = window.localStorage.getItem(
  245. "$DMValue$AdBlock Script for WebView$" + key,
  246. );
  247. return typeof stor == "string" &&
  248. stor.startsWith("[") &&
  249. stor.endsWith("]")
  250. ? $polyfills.parseValue(stor)
  251. : defaultValue;
  252. },
  253. GM_deleteValue:
  254. typeof GM_deleteValue == "function"
  255. ? GM_deleteValue
  256. : function DM_deleteValue(key) {
  257. $listeners.forEach((listenerArray, id) => {
  258. if (listenerArray[0] === key) {
  259. const oldValue = $polyfills.GM_getValue(key);
  260. listenerArray[1].call(
  261. {
  262. id,
  263. key,
  264. cb: listenerArray[1],
  265. },
  266. key,
  267. oldValue,
  268. undefined,
  269. false,
  270. );
  271. }
  272. });
  273. window.localStorage.removeItem(
  274. "$DMValue$AdBlock Script for WebView$" + key,
  275. );
  276. },
  277. GM_setValue:
  278. typeof GM_setValue == "function"
  279. ? GM_setValue
  280. : function DM_setValue(key, value) {
  281. const packed = JSON.stringify([
  282. typeof value == "function"
  283. ? value.toString()
  284. : typeof value == "number" && isNaN(value)
  285. ? "__$NaN"
  286. : typeof value == "number" && !isFinite(value)
  287. ? value > 0
  288. ? "__$FnT"
  289. : "__$XnT"
  290. : typeof value == "undefined"
  291. ? "__$UdF"
  292. : typeof value == "bigint"
  293. ? value.toString()
  294. : value,
  295. ]);
  296. $listeners.forEach((listenerArray, id) => {
  297. if (listenerArray[0] === key) {
  298. const oldValue = $polyfills.GM_getValue(key);
  299. listenerArray[1].call(
  300. {
  301. id,
  302. key,
  303. cb: listenerArray[1],
  304. },
  305. key,
  306. oldValue,
  307. value,
  308. false,
  309. );
  310. }
  311. });
  312. window.localStorage.setItem(
  313. "$DMValue$AdBlock Script for WebView$" + key,
  314. packed,
  315. );
  316. },
  317. GM_addStyle:
  318. typeof GM_addStyle == "function"
  319. ? GM_addStyle
  320. : function DM_addStyle(css) {
  321. const styleEl = document.createElement("style");
  322. styleEl.innerText = css;
  323. (
  324. document.head ||
  325. document.body ||
  326. document.documentElement
  327. ).appendChild(styleEl);
  328. return styleEl;
  329. },
  330. GM_xmlhttpRequest:
  331. typeof GM_xmlhttpRequest == "function" ? GM_xmlhttpRequest : void 0,
  332. GM_getResourceText:
  333. typeof GM_getResourceText == "function" ? GM_getResourceText : void 0,
  334. GM_download:
  335. // 以下浏览器的 GM_download 不支持 blob: 需要使用 Polyfill
  336. typeof GM_download == "function" &&
  337. // X 浏览器
  338. !GM_download.toString().includes("mbrowser.GM_download") &&
  339. // Via 浏览器
  340. !GM_download.toString().includes("via_gm.download") &&
  341. // MDM 浏览器
  342. !(
  343. typeof window.moe == "object" &&
  344. typeof window.moe.download == "function"
  345. ) &&
  346. // 海阔世界 / 嗅觉
  347. !GM_download.toString().includes("window.open") &&
  348. // Rains 浏览器
  349. !Array.isArray(GM_download.toString().match(/load\(\) {};$/))
  350. ? GM_download
  351. : function DM_download(objOrURL, filename) {
  352. var _a;
  353. const linkEl = document.createElement("a");
  354. if (typeof objOrURL == "object") {
  355. linkEl.href = objOrURL.url;
  356. linkEl.download = objOrURL.name;
  357. linkEl.onclick =
  358. (_a = objOrURL.onload) !== null && _a !== void 0 ? _a : null;
  359. } else {
  360. linkEl.href = objOrURL;
  361. linkEl.download =
  362. filename !== null && filename !== void 0 ? filename : "";
  363. }
  364. linkEl.style.cssText = "position:absolute;top:-100%";
  365. document.body.appendChild(linkEl);
  366. setTimeout(() => {
  367. linkEl.click();
  368. linkEl.remove();
  369. }, 0);
  370. return {
  371. abort: () => void 0,
  372. };
  373. },
  374. GM_listValues:
  375. typeof GM_listValues == "function"
  376. ? GM_listValues
  377. : function DM_listValues() {
  378. const keysArray = [];
  379. for (let i = 0; i < window.localStorage.length; i++) {
  380. const key = window.localStorage.key(i);
  381. if (
  382. key === null || key === void 0
  383. ? void 0
  384. : key.startsWith("$DMValue$AdBlock Script for WebView$")
  385. ) {
  386. keysArray.push(
  387. key.replace("$DMValue$AdBlock Script for WebView$", ""),
  388. );
  389. }
  390. }
  391. return keysArray;
  392. },
  393. GM:
  394. typeof GM == "object"
  395. ? GM
  396. : {
  397. getValue: function DM_getValue4(...args) {
  398. return Promise.resolve($polyfills.GM_getValue(...args));
  399. },
  400. deleteValue: function DM_deleteValue4(...args) {
  401. return Promise.resolve($polyfills.GM_deleteValue(...args));
  402. },
  403. setValue: function DM_setValue4(...args) {
  404. return Promise.resolve($polyfills.GM_setValue(...args));
  405. },
  406. addStyle: function DM_addStyle4(...args) {
  407. return Promise.resolve($polyfills.GM_addStyle(...args));
  408. },
  409. download: function DM_download4(...args) {
  410. return Promise.resolve($polyfills.GM_download(...args));
  411. },
  412. listValues: function DM_listValues4(...args) {
  413. return Promise.resolve($polyfills.GM_listValues(...args));
  414. },
  415. },
  416. // polyfills 结束
  417. };
  418.  
  419. (function (tm) {
  420. "use strict";
  421.  
  422. /******************************************************************************
  423. Copyright (c) Microsoft Corporation.
  424. Permission to use, copy, modify, and/or distribute this software for any
  425. purpose with or without fee is hereby granted.
  426. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
  427. REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
  428. AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
  429. INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
  430. LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
  431. OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
  432. PERFORMANCE OF THIS SOFTWARE.
  433. ***************************************************************************** */
  434. /* global Reflect, Promise, SuppressedError, Symbol */
  435. function __awaiter(thisArg, _arguments, P, generator) {
  436. function adopt(value) {
  437. return value instanceof P
  438. ? value
  439. : new P(function (resolve) {
  440. resolve(value);
  441. });
  442. }
  443. return new (P || (P = Promise))(function (resolve, reject) {
  444. function fulfilled(value) {
  445. try {
  446. step(generator.next(value));
  447. } catch (e) {
  448. reject(e);
  449. }
  450. }
  451. function rejected(value) {
  452. try {
  453. step(generator["throw"](value));
  454. } catch (e) {
  455. reject(e);
  456. }
  457. }
  458. function step(result) {
  459. result.done
  460. ? resolve(result.value)
  461. : adopt(result.value).then(fulfilled, rejected);
  462. }
  463. step((generator = generator.apply(thisArg, _arguments || [])).next());
  464. });
  465. }
  466. typeof SuppressedError === "function"
  467. ? SuppressedError
  468. : function (error, suppressed, message) {
  469. var e = new Error(message);
  470. return (
  471. (e.name = "SuppressedError"),
  472. (e.error = error),
  473. (e.suppressed = suppressed),
  474. e
  475. );
  476. };
  477. const rules = [];
  478. {
  479. rules.push(
  480. {
  481. 标识: "jiekouAD",
  482. 地址: "https://slink.ltd/https://raw.githubusercontent.com/damengzhu/banad/main/jiekouAD.txt",
  483. 在线更新: !!1,
  484. 筛选后存储: !!1,
  485. },
  486. {
  487. 标识: "CSSRule",
  488. 地址: "https://slink.ltd/https://raw.githubusercontent.com/damengzhu/abpmerge/main/CSSRule.txt",
  489. 在线更新: !!1,
  490. 筛选后存储: !!0,
  491. },
  492. );
  493. }
  494. const presets = {
  495. userConfig: {
  496. rules: `
  497. ! 不支持的规则和开头为 ! 的行会忽略
  498. !
  499. ! 由于语法限制,此处规则中
  500. ! 一个反斜杠需要改成两个,像这样 \\
  501.  
  502. `,
  503. css: `{
  504. display: none !important;
  505. width: 0 !important;
  506. height: 0 !important;
  507. }`,
  508. timeout: 10000,
  509. // 规则下载超时
  510. headTimeout: 2000,
  511. // 获取规则信息超时
  512. tryCount: 5,
  513. // CSS 注入尝试次数
  514. tryTimeout: 500,
  515. // CSS 注入尝试间隔
  516. autoCleanSize: 0, // 预存超过此大小自动清空,0 关闭
  517. },
  518. onlineRules: rules,
  519. };
  520. const data = {
  521. isFrame: tm.unsafeWindow.self !== tm.unsafeWindow.top,
  522. isClean: false,
  523. disabled: false,
  524. saved: false,
  525. update: true,
  526. updating: false,
  527. alertLog: false,
  528. receivedRules: "",
  529. customRules: presets.userConfig.rules,
  530. allRules: "",
  531. genHideCss: "",
  532. genExtraCss: "",
  533. spcHideCss: "",
  534. spcExtraCss: "",
  535. bRules: [],
  536. appliedLevel: 0,
  537. appliedCount: 0,
  538. mutex: "__lemon__abp__parser__$__",
  539. preset: presets.userConfig.css,
  540. timeout: presets.userConfig.timeout,
  541. headTimeout: presets.userConfig.headTimeout,
  542. tryCount: presets.userConfig.tryCount,
  543. tryTimeout: presets.userConfig.tryTimeout,
  544. autoCleanSize: presets.userConfig.autoCleanSize,
  545. };
  546. const defaultValues = {
  547. get black() {
  548. return "";
  549. },
  550. get rules() {
  551. return {};
  552. },
  553. get css() {
  554. return {
  555. needUpdate: true,
  556. genHideCss: "",
  557. genExtraCss: "",
  558. spcHideCss: "",
  559. spcExtraCss: "",
  560. };
  561. },
  562. get time() {
  563. return "0/0/0 0:0:0";
  564. },
  565. get etags() {
  566. return {};
  567. },
  568. get brules() {
  569. return [];
  570. },
  571. get hash() {
  572. return "";
  573. },
  574. };
  575. const values = {
  576. black(value) {
  577. return __awaiter(this, void 0, void 0, function* () {
  578. if (typeof value == "undefined") {
  579. const arrStr = yield gmValue(
  580. "get",
  581. false,
  582. "ajs_disabled_domains",
  583. defaultValues.black,
  584. );
  585. return typeof arrStr == "string" && arrStr.length > 0
  586. ? arrStr.split(",")
  587. : [];
  588. } else {
  589. return yield gmValue(
  590. "set",
  591. false,
  592. "ajs_disabled_domains",
  593. value === null ? null : value.join(),
  594. );
  595. }
  596. });
  597. },
  598. rules(value) {
  599. return __awaiter(this, void 0, void 0, function* () {
  600. return typeof value == "undefined"
  601. ? yield gmValue(
  602. "get",
  603. true,
  604. "ajs_saved_abprules",
  605. defaultValues.rules,
  606. )
  607. : yield gmValue("set", true, "ajs_saved_abprules", value);
  608. });
  609. },
  610. css(value_1) {
  611. return __awaiter(
  612. this,
  613. arguments,
  614. void 0,
  615. function* (value, host = location.hostname) {
  616. return typeof value == "undefined"
  617. ? yield gmValue(
  618. "get",
  619. true,
  620. `ajs_saved_styles_${host}`,
  621. defaultValues.css,
  622. )
  623. : yield gmValue("set", true, `ajs_saved_styles_${host}`, value);
  624. },
  625. );
  626. },
  627. time(value) {
  628. return __awaiter(this, void 0, void 0, function* () {
  629. return typeof value == "undefined"
  630. ? yield gmValue("get", false, "ajs_rules_ver", defaultValues.time)
  631. : yield gmValue("set", false, "ajs_rules_ver", value);
  632. });
  633. },
  634. etags(value) {
  635. return __awaiter(this, void 0, void 0, function* () {
  636. return typeof value == "undefined"
  637. ? yield gmValue(
  638. "get",
  639. true,
  640. "ajs_rules_etags",
  641. defaultValues.etags,
  642. )
  643. : yield gmValue("set", true, "ajs_rules_etags", value);
  644. });
  645. },
  646. brules(value) {
  647. return __awaiter(this, void 0, void 0, function* () {
  648. return typeof value == "undefined"
  649. ? yield gmValue(
  650. "get",
  651. true,
  652. "ajs_modifier_rules",
  653. defaultValues.brules,
  654. )
  655. : yield gmValue("set", true, "ajs_modifier_rules", value);
  656. });
  657. },
  658. hash(value) {
  659. return __awaiter(this, void 0, void 0, function* () {
  660. return typeof value == "undefined"
  661. ? yield gmValue(
  662. "get",
  663. false,
  664. "ajs_custom_hash",
  665. defaultValues.hash,
  666. )
  667. : yield gmValue("set", false, "ajs_custom_hash", value);
  668. });
  669. },
  670. },
  671. menus = {
  672. /** 禁用拦截菜单 */
  673. disable: {
  674. id: void 0,
  675. text() {
  676. return Promise.resolve(
  677. `在此域名${data.disabled ? "启用" : "禁用"}拦截`,
  678. );
  679. },
  680. },
  681. /** 更新规则菜单 */
  682. update: {
  683. id: void 0,
  684. text() {
  685. return __awaiter(this, void 0, void 0, function* () {
  686. const time = yield values.time();
  687. return data.updating
  688. ? "正在更新..."
  689. : `点击更新 ${(time === null || time === void 0 ? void 0 : time.slice(0, 1)) === "0" ? "未知时间" : time}`;
  690. });
  691. },
  692. },
  693. /** 清空规则菜单 */
  694. count: {
  695. id: void 0,
  696. text() {
  697. return __awaiter(this, void 0, void 0, function* () {
  698. var _a;
  699. const abpRules =
  700. (_a = yield values.rules()) !== null && _a !== void 0
  701. ? _a
  702. : defaultValues.rules;
  703. let ruleCount = 0;
  704. Object.values(abpRules).forEach((rules) => {
  705. ruleCount += rules.split("\n").length;
  706. });
  707. return data.isClean
  708. ? `已清空,点击${data.disabled ? "刷新网页" : "重新加载规则"}`
  709. : `点击清空,存储规则 ${ruleCount} 预存 ${(yield getCssLength()).join()}`;
  710. });
  711. },
  712. },
  713. /** 导出报告菜单 */
  714. export: {
  715. id: void 0,
  716. text() {
  717. let cssCount = "";
  718. if (!data.disabled) {
  719. if ((data.appliedLevel & 1) == 0) {
  720. cssCount += data.genHideCss + data.genExtraCss;
  721. }
  722. if ((data.appliedLevel & 2) == 0) {
  723. cssCount += data.spcHideCss + data.spcExtraCss;
  724. }
  725. }
  726. return Promise.resolve(
  727. `下载统计报告 ${data.saved ? `CSS ${cssCount.split("*/").length - 1}` : `规则 ${data.appliedCount} / ${data.allRules.split("\n").length}`}`,
  728. );
  729. },
  730. },
  731. };
  732. /**
  733. * 选择合适的 油猴/模拟 接口
  734. * @param {(Function | undefined)} gm1 GM_xxx
  735. * @param {(Function | undefined)} gm4 GM.xxx
  736. * @returns {(Function | undefined)} 合适的接口 或者 undefined
  737. */
  738. function gmChooser(gm1, gm4) {
  739. const gm1dm =
  740. gm1 === null || gm1 === void 0 ? void 0 : gm1.name.startsWith("DM_");
  741. const gm4dm =
  742. gm4 === null || gm4 === void 0 ? void 0 : gm4.name.startsWith("DM_");
  743. if (gm1 && gm4) {
  744. if (gm1dm !== gm4dm) {
  745. return gm1dm ? gm4 : gm1;
  746. } else {
  747. return gm1;
  748. }
  749. } else {
  750. return gm1 ? gm1 : gm4 ? gm4 : void 0;
  751. }
  752. }
  753. /**
  754. * 添加/删除/替换 油猴脚本菜单项
  755. * @async
  756. * @param {string} name 菜单项的 key
  757. * @param {Function} cb 点击菜单项回调,不指定即删除菜单项
  758. * @returns {Promise.<void>}
  759. */
  760. function gmMenu(name, cb) {
  761. return __awaiter(this, void 0, void 0, function* () {
  762. var _a;
  763. const id =
  764. (_a = menus[name].id) !== null && _a !== void 0 ? _a : void 0;
  765. const gmr = gmChooser(
  766. tm.GM_registerMenuCommand,
  767. tm.GM === null || tm.GM === void 0
  768. ? void 0
  769. : tm.GM.registerMenuCommand,
  770. );
  771. const gmu = gmChooser(
  772. tm.GM_unregisterMenuCommand,
  773. tm.GM === null || tm.GM === void 0
  774. ? void 0
  775. : tm.GM.unregisterMenuCommand,
  776. );
  777. if (typeof gmr != "function" || data.isFrame) return;
  778. if (typeof id != "undefined" && typeof gmu == "function") {
  779. menus[name].id = void 0;
  780. yield gmu(id);
  781. }
  782. if (typeof cb == "function") {
  783. menus[name].id = yield gmr(yield menus[name].text(), cb);
  784. }
  785. return;
  786. });
  787. }
  788. function gmValue(action, json, key, value) {
  789. return __awaiter(this, void 0, void 0, function* () {
  790. var _a, _b, _c, _d;
  791. switch (action) {
  792. case "get":
  793. try {
  794. let v =
  795. (_a = gmChooser(
  796. tm.GM_getValue,
  797. tm.GM === null || tm.GM === void 0 ? void 0 : tm.GM.getValue,
  798. )) === null || _a === void 0
  799. ? void 0
  800. : _a(key, json ? JSON.stringify(value) : value);
  801. v = v instanceof Promise ? yield v : v;
  802. return json && typeof v == "string" ? JSON.parse(v) : v;
  803. } catch (error) {
  804. return value;
  805. }
  806. case "set":
  807. try {
  808. return value === null || value === void 0
  809. ? (_b = gmChooser(
  810. tm.GM_deleteValue,
  811. tm.GM === null || tm.GM === void 0
  812. ? void 0
  813. : tm.GM.deleteValue,
  814. )) === null || _b === void 0
  815. ? void 0
  816. : _b(key)
  817. : (_c = gmChooser(
  818. tm.GM_setValue,
  819. tm.GM === null || tm.GM === void 0
  820. ? void 0
  821. : tm.GM.setValue,
  822. )) === null || _c === void 0
  823. ? void 0
  824. : _c(key, json ? JSON.stringify(value) : value);
  825. } catch (error) {
  826. return Promise.reject(
  827. (_d = gmChooser(
  828. tm.GM_deleteValue,
  829. tm.GM === null || tm.GM === void 0
  830. ? void 0
  831. : tm.GM.deleteValue,
  832. )) === null || _d === void 0
  833. ? void 0
  834. : _d(key),
  835. );
  836. }
  837. }
  838. });
  839. }
  840. /**
  841. * 获取脚本猫用户配置,非脚本猫返回默认值
  842. * @async
  843. * @param {string} prop 用户配置项 key
  844. * @returns {Promise.<*>} 用户配置项的值
  845. */
  846. function getUserConfig(prop) {
  847. return __awaiter(this, void 0, void 0, function* () {
  848. var _a;
  849. return (_a = yield gmValue("get", false, `配置.${prop}`)) !== null &&
  850. _a !== void 0
  851. ? _a
  852. : presets.userConfig[prop];
  853. });
  854. }
  855. /**
  856. * 可靠的向页面注入 CSS,失败自动重试
  857. * @async
  858. * @param {string} css 需要注入的 CSS
  859. * @param {number} [pass=0] 当前重试次数,留空
  860. * @returns {Promise.<void>} 返回空的 Promise
  861. */
  862. function addStyle(css_1) {
  863. return __awaiter(this, arguments, void 0, function* (css, pass = 0) {
  864. var _a;
  865. if (pass >= data.tryCount) return;
  866. const el = yield (_a = gmChooser(
  867. tm.GM_addStyle,
  868. tm.GM === null || tm.GM === void 0 ? void 0 : tm.GM.addStyle,
  869. )) === null || _a === void 0
  870. ? void 0
  871. : _a(css);
  872. if (typeof el == "object") {
  873. if (!document.documentElement.contains(el)) {
  874. setTimeout(() => {
  875. addStyle(css, pass + 1);
  876. }, data.tryTimeout);
  877. }
  878. }
  879. return;
  880. });
  881. }
  882. /**
  883. * 异步 GM_xmlhttpRequest 封装
  884. * @async
  885. * @param details GM_xmlhttpRequest 的 details
  886. * @returns 返回 Promise
  887. *
  888. * 成功 resolve GM_xmlhttpRequest onload Response
  889. *
  890. * 失败 reject 如下对象
  891. * ```ts
  892. * type XhrError = {
  893. * error: "noxhr" | "abort" | "error" | "timeout" | "Via timeout";
  894. * resp?: Response;
  895. * }
  896. * ```
  897. */
  898. function promiseXhr(details) {
  899. return __awaiter(this, void 0, void 0, function* () {
  900. const timeout =
  901. details.method === "HEAD" ? data.headTimeout : data.timeout;
  902. let loaded = false;
  903. const gmXhr = gmChooser(
  904. tm.GM_xmlhttpRequest,
  905. tm.GM === null || tm.GM === void 0 ? void 0 : tm.GM.xmlHttpRequest,
  906. );
  907. if (typeof gmXhr != "function") {
  908. return Promise.reject({
  909. error: "noxhr",
  910. });
  911. }
  912. return yield new Promise((resolve, reject) => {
  913. gmXhr(
  914. Object.assign(
  915. {
  916. onload(e) {
  917. loaded = true;
  918. resolve(e);
  919. },
  920. onabort(e) {
  921. loaded = true;
  922. reject({
  923. error: "abort",
  924. resp: e,
  925. });
  926. },
  927. onerror(e) {
  928. loaded = true;
  929. reject({
  930. error: "error",
  931. resp: e,
  932. });
  933. },
  934. ontimeout(e) {
  935. loaded = true;
  936. reject({
  937. error: "timeout",
  938. resp: e,
  939. });
  940. },
  941. onreadystatechange(e) {
  942. // Via 浏览器超时中断,不给成功状态...
  943. if (
  944. (e === null || e === void 0 ? void 0 : e.readyState) === 3
  945. ) {
  946. setTimeout(() => {
  947. if (!loaded) {
  948. reject({
  949. error: "Via timeout",
  950. resp: e,
  951. });
  952. }
  953. }, timeout);
  954. }
  955. },
  956. timeout,
  957. },
  958. details,
  959. ),
  960. );
  961. });
  962. });
  963. }
  964. /**
  965. * GM_getResourceText 代理
  966. * @async
  967. * @param {string} key `@resource` 的 key
  968. * @returns {(?string | undefined)} GM_getResourceText 的返回,不支持的返回 undefined
  969. */
  970. function getRuleFromResource(key) {
  971. return __awaiter(this, void 0, void 0, function* () {
  972. var _a;
  973. try {
  974. return yield (_a = gmChooser(
  975. tm.GM_getResourceText,
  976. tm.GM === null || tm.GM === void 0 ? void 0 : tm.GM.getResourceText,
  977. )) === null || _a === void 0
  978. ? void 0
  979. : _a(key);
  980. } catch (error) {
  981. return null;
  982. }
  983. });
  984. }
  985. /**
  986. * 保证只运行一次
  987. * @async
  988. * @param {string} key 互斥字符串
  989. * @param {Function} func 运行函数
  990. * @returns {Promise.<*>} Promise,重复运行或失败 reject
  991. */
  992. function runOnce(key, func) {
  993. return __awaiter(this, void 0, void 0, function* () {
  994. if (key in tm.unsafeWindow) {
  995. return yield Promise.reject(tm.unsafeWindow[key]);
  996. }
  997. tm.unsafeWindow[key] = true;
  998. return yield func();
  999. });
  1000. }
  1001. /**
  1002. * GM_download 代理
  1003. * @param {string} url 下载 url
  1004. * @param {string} name 文件名
  1005. */
  1006. function downUrl(url, name) {
  1007. tm.GM_download === null || tm.GM_download === void 0
  1008. ? void 0
  1009. : tm.GM_download({
  1010. url,
  1011. name,
  1012. });
  1013. }
  1014. /**
  1015. * 检查域名是否有预存,不指定域名则返回有预存的域名数组
  1016. * @async
  1017. * @param {?string} host 域名
  1018. * @returns {Promise.<(boolean | string[])>} 域名是否有预存 / 有预存的域名数组
  1019. */
  1020. function getSavedHosts(host) {
  1021. return __awaiter(this, void 0, void 0, function* () {
  1022. var _a, _b;
  1023. const keys =
  1024. (_b = yield (_a = gmChooser(
  1025. tm.GM_listValues,
  1026. tm.GM === null || tm.GM === void 0 ? void 0 : tm.GM.listValues,
  1027. )) === null || _a === void 0
  1028. ? void 0
  1029. : _a()) !== null && _b !== void 0
  1030. ? _b
  1031. : [];
  1032. const domains = (
  1033. Array.isArray(keys)
  1034. ? keys
  1035. : // Rains
  1036. Object.keys(keys)
  1037. )
  1038. .filter((key) => key.startsWith("ajs_saved_styles_"))
  1039. .map((key) => key.replace("ajs_saved_styles_", ""));
  1040. return host ? domains.includes(host) : domains;
  1041. });
  1042. }
  1043. /**
  1044. * 获取篡改猴脚本注释
  1045. * @returns {string} 脚本注释,不支持返回空字符串
  1046. */
  1047. function getComment() {
  1048. var _a, _b, _c;
  1049. return (_c =
  1050. (_b =
  1051. (_a = tm.GM_info.script) === null || _a === void 0
  1052. ? void 0
  1053. : _a.options) === null || _b === void 0
  1054. ? void 0
  1055. : _b.comment) !== null && _c !== void 0
  1056. ? _c
  1057. : "";
  1058. }
  1059. /**
  1060. * 获取预存域名数量和预存总大小
  1061. * @async
  1062. * @returns {Promise.<number[]>} [预存域名数量, 预存总大小]
  1063. */
  1064. function getCssLength() {
  1065. return __awaiter(this, void 0, void 0, function* () {
  1066. const savedHosts = yield getSavedHosts();
  1067. let savedChars = 0;
  1068. for (let i = 0; i < savedHosts.length; i++) {
  1069. const co = yield values.css(void 0, savedHosts[i]);
  1070. savedChars += JSON.stringify(co).length;
  1071. }
  1072. return [savedHosts.length, savedChars];
  1073. });
  1074. }
  1075.  
  1076. /**
  1077. * @adguard/extended-css - v2.0.56 - Tue Nov 28 2023
  1078. * https://github.com/AdguardTeam/ExtendedCss#homepage
  1079. * Copyright (c) 2023 AdGuard. Licensed GPL-3.0
  1080. */
  1081. function _defineProperty(obj, key, value) {
  1082. if (key in obj) {
  1083. Object.defineProperty(obj, key, {
  1084. value: value,
  1085. enumerable: true,
  1086. configurable: true,
  1087. writable: true,
  1088. });
  1089. } else {
  1090. obj[key] = value;
  1091. }
  1092. return obj;
  1093. }
  1094. /**
  1095. * Possible ast node types.
  1096. *
  1097. * IMPORTANT: it is used as 'const' instead of 'enum' to avoid side effects
  1098. * during ExtendedCss import into other libraries.
  1099. */
  1100. const NODE = {
  1101. SELECTOR_LIST: "SelectorList",
  1102. SELECTOR: "Selector",
  1103. REGULAR_SELECTOR: "RegularSelector",
  1104. EXTENDED_SELECTOR: "ExtendedSelector",
  1105. ABSOLUTE_PSEUDO_CLASS: "AbsolutePseudoClass",
  1106. RELATIVE_PSEUDO_CLASS: "RelativePseudoClass",
  1107. };
  1108. /**
  1109. * Class needed for creating ast nodes while selector parsing.
  1110. * Used for SelectorList, Selector, ExtendedSelector.
  1111. */
  1112. class AnySelectorNode {
  1113. /**
  1114. * Creates new ast node.
  1115. *
  1116. * @param type Ast node type.
  1117. */
  1118. constructor(type) {
  1119. _defineProperty(this, "children", []);
  1120. this.type = type;
  1121. }
  1122. /**
  1123. * Adds child node to children array.
  1124. *
  1125. * @param child Ast node.
  1126. */
  1127. addChild(child) {
  1128. this.children.push(child);
  1129. }
  1130. }
  1131. /**
  1132. * Class needed for creating RegularSelector ast node while selector parsing.
  1133. */
  1134. class RegularSelectorNode extends AnySelectorNode {
  1135. /**
  1136. * Creates RegularSelector ast node.
  1137. *
  1138. * @param value Value of RegularSelector node.
  1139. */
  1140. constructor(value) {
  1141. super(NODE.REGULAR_SELECTOR);
  1142. this.value = value;
  1143. }
  1144. }
  1145. /**
  1146. * Class needed for creating RelativePseudoClass ast node while selector parsing.
  1147. */
  1148. class RelativePseudoClassNode extends AnySelectorNode {
  1149. /**
  1150. * Creates RegularSelector ast node.
  1151. *
  1152. * @param name Name of RelativePseudoClass node.
  1153. */
  1154. constructor(name) {
  1155. super(NODE.RELATIVE_PSEUDO_CLASS);
  1156. this.name = name;
  1157. }
  1158. }
  1159. /**
  1160. * Class needed for creating AbsolutePseudoClass ast node while selector parsing.
  1161. */
  1162. class AbsolutePseudoClassNode extends AnySelectorNode {
  1163. /**
  1164. * Creates AbsolutePseudoClass ast node.
  1165. *
  1166. * @param name Name of AbsolutePseudoClass node.
  1167. */
  1168. constructor(name) {
  1169. super(NODE.ABSOLUTE_PSEUDO_CLASS);
  1170. _defineProperty(this, "value", "");
  1171. this.name = name;
  1172. }
  1173. }
  1174. const LEFT_SQUARE_BRACKET = "[";
  1175. const RIGHT_SQUARE_BRACKET = "]";
  1176. const LEFT_PARENTHESIS = "(";
  1177. const RIGHT_PARENTHESIS = ")";
  1178. const LEFT_CURLY_BRACKET = "{";
  1179. const RIGHT_CURLY_BRACKET = "}";
  1180. const BRACKET = {
  1181. SQUARE: {
  1182. LEFT: LEFT_SQUARE_BRACKET,
  1183. RIGHT: RIGHT_SQUARE_BRACKET,
  1184. },
  1185. PARENTHESES: {
  1186. LEFT: LEFT_PARENTHESIS,
  1187. RIGHT: RIGHT_PARENTHESIS,
  1188. },
  1189. CURLY: {
  1190. LEFT: LEFT_CURLY_BRACKET,
  1191. RIGHT: RIGHT_CURLY_BRACKET,
  1192. },
  1193. };
  1194. const SLASH = "/";
  1195. const BACKSLASH = "\\";
  1196. const SPACE = " ";
  1197. const COMMA = ",";
  1198. const DOT = ".";
  1199. const SEMICOLON = ";";
  1200. const COLON = ":";
  1201. const SINGLE_QUOTE = "'";
  1202. const DOUBLE_QUOTE = '"'; // do not consider hyphen `-` as separated mark
  1203. // to avoid pseudo-class names splitting
  1204. // e.g. 'matches-css' or 'if-not'
  1205. const CARET = "^";
  1206. const DOLLAR_SIGN = "$";
  1207. const EQUAL_SIGN = "=";
  1208. const TAB = "\t";
  1209. const CARRIAGE_RETURN = "\r";
  1210. const LINE_FEED = "\n";
  1211. const FORM_FEED = "\f";
  1212. const WHITE_SPACE_CHARACTERS = [
  1213. SPACE,
  1214. TAB,
  1215. CARRIAGE_RETURN,
  1216. LINE_FEED,
  1217. FORM_FEED,
  1218. ]; // for universal selector and attributes
  1219. const ASTERISK = "*";
  1220. const ID_MARKER = "#";
  1221. const CLASS_MARKER = DOT;
  1222. const DESCENDANT_COMBINATOR = SPACE;
  1223. const CHILD_COMBINATOR = ">";
  1224. const NEXT_SIBLING_COMBINATOR = "+";
  1225. const SUBSEQUENT_SIBLING_COMBINATOR = "~";
  1226. const COMBINATORS = [
  1227. DESCENDANT_COMBINATOR,
  1228. CHILD_COMBINATOR,
  1229. NEXT_SIBLING_COMBINATOR,
  1230. SUBSEQUENT_SIBLING_COMBINATOR,
  1231. ];
  1232. const SUPPORTED_SELECTOR_MARKS = [
  1233. LEFT_SQUARE_BRACKET,
  1234. RIGHT_SQUARE_BRACKET,
  1235. LEFT_PARENTHESIS,
  1236. RIGHT_PARENTHESIS,
  1237. LEFT_CURLY_BRACKET,
  1238. RIGHT_CURLY_BRACKET,
  1239. SLASH,
  1240. BACKSLASH,
  1241. SEMICOLON,
  1242. COLON,
  1243. COMMA,
  1244. SINGLE_QUOTE,
  1245. DOUBLE_QUOTE,
  1246. CARET,
  1247. DOLLAR_SIGN,
  1248. ASTERISK,
  1249. ID_MARKER,
  1250. CLASS_MARKER,
  1251. DESCENDANT_COMBINATOR,
  1252. CHILD_COMBINATOR,
  1253. NEXT_SIBLING_COMBINATOR,
  1254. SUBSEQUENT_SIBLING_COMBINATOR,
  1255. TAB,
  1256. CARRIAGE_RETURN,
  1257. LINE_FEED,
  1258. FORM_FEED,
  1259. ];
  1260. const SUPPORTED_STYLE_DECLARATION_MARKS = [
  1261. // divider between property and value in declaration
  1262. COLON,
  1263. // divider between declarations
  1264. SEMICOLON,
  1265. // sometimes is needed for value wrapping
  1266. // e.g. 'content: "-"'
  1267. SINGLE_QUOTE,
  1268. DOUBLE_QUOTE,
  1269. // needed for quote escaping inside the same-type quotes
  1270. BACKSLASH,
  1271. // whitespaces
  1272. SPACE,
  1273. TAB,
  1274. CARRIAGE_RETURN,
  1275. LINE_FEED,
  1276. FORM_FEED,
  1277. ]; // absolute:
  1278. const CONTAINS_PSEUDO = "contains";
  1279. const HAS_TEXT_PSEUDO = "has-text";
  1280. const ABP_CONTAINS_PSEUDO = "-abp-contains";
  1281. const MATCHES_CSS_PSEUDO = "matches-css";
  1282. const MATCHES_CSS_BEFORE_PSEUDO = "matches-css-before";
  1283. const MATCHES_CSS_AFTER_PSEUDO = "matches-css-after";
  1284. const MATCHES_ATTR_PSEUDO_CLASS_MARKER = "matches-attr";
  1285. const MATCHES_PROPERTY_PSEUDO_CLASS_MARKER = "matches-property";
  1286. const XPATH_PSEUDO_CLASS_MARKER = "xpath";
  1287. const NTH_ANCESTOR_PSEUDO_CLASS_MARKER = "nth-ancestor";
  1288. const CONTAINS_PSEUDO_NAMES = [
  1289. CONTAINS_PSEUDO,
  1290. HAS_TEXT_PSEUDO,
  1291. ABP_CONTAINS_PSEUDO,
  1292. ];
  1293. /**
  1294. * Pseudo-class :upward() can get number or selector arg
  1295. * and if the arg is selector it should be standard, not extended
  1296. * so :upward pseudo-class is always absolute.
  1297. */
  1298. const UPWARD_PSEUDO_CLASS_MARKER = "upward";
  1299. /**
  1300. * Pseudo-class `:remove()` and pseudo-property `remove`
  1301. * are used for element actions, not for element selecting.
  1302. *
  1303. * Selector text should not contain the pseudo-class
  1304. * so selector parser should consider it as invalid
  1305. * and both are handled by stylesheet parser.
  1306. */
  1307. const REMOVE_PSEUDO_MARKER = "remove"; // relative:
  1308. const HAS_PSEUDO_CLASS_MARKER = "has";
  1309. const ABP_HAS_PSEUDO_CLASS_MARKER = "-abp-has";
  1310. const HAS_PSEUDO_CLASS_MARKERS = [
  1311. HAS_PSEUDO_CLASS_MARKER,
  1312. ABP_HAS_PSEUDO_CLASS_MARKER,
  1313. ];
  1314. const IS_PSEUDO_CLASS_MARKER = "is";
  1315. const NOT_PSEUDO_CLASS_MARKER = "not";
  1316. const ABSOLUTE_PSEUDO_CLASSES = [
  1317. CONTAINS_PSEUDO,
  1318. HAS_TEXT_PSEUDO,
  1319. ABP_CONTAINS_PSEUDO,
  1320. MATCHES_CSS_PSEUDO,
  1321. MATCHES_CSS_BEFORE_PSEUDO,
  1322. MATCHES_CSS_AFTER_PSEUDO,
  1323. MATCHES_ATTR_PSEUDO_CLASS_MARKER,
  1324. MATCHES_PROPERTY_PSEUDO_CLASS_MARKER,
  1325. XPATH_PSEUDO_CLASS_MARKER,
  1326. NTH_ANCESTOR_PSEUDO_CLASS_MARKER,
  1327. UPWARD_PSEUDO_CLASS_MARKER,
  1328. ];
  1329. const RELATIVE_PSEUDO_CLASSES = [
  1330. ...HAS_PSEUDO_CLASS_MARKERS,
  1331. IS_PSEUDO_CLASS_MARKER,
  1332. NOT_PSEUDO_CLASS_MARKER,
  1333. ];
  1334. const SUPPORTED_PSEUDO_CLASSES = [
  1335. ...ABSOLUTE_PSEUDO_CLASSES,
  1336. ...RELATIVE_PSEUDO_CLASSES,
  1337. ]; // these pseudo-classes should be part of RegularSelector value
  1338. // if its arg does not contain extended selectors.
  1339. // the ast will be checked after the selector is completely parsed
  1340. const OPTIMIZATION_PSEUDO_CLASSES = [
  1341. NOT_PSEUDO_CLASS_MARKER,
  1342. IS_PSEUDO_CLASS_MARKER,
  1343. ];
  1344. /**
  1345. * ':scope' is used for extended pseudo-class :has(), if-not(), :is() and :not().
  1346. */
  1347. const SCOPE_CSS_PSEUDO_CLASS = ":scope";
  1348. /**
  1349. * ':after' and ':before' are needed for :matches-css() pseudo-class
  1350. * all other are needed for :has() limitation after regular pseudo-elements.
  1351. *
  1352. * @see {@link https://bugs.chromium.org/p/chromium/issues/detail?id=669058#c54} [case 3]
  1353. */
  1354. const REGULAR_PSEUDO_ELEMENTS = {
  1355. AFTER: "after",
  1356. BACKDROP: "backdrop",
  1357. BEFORE: "before",
  1358. CUE: "cue",
  1359. CUE_REGION: "cue-region",
  1360. FIRST_LETTER: "first-letter",
  1361. FIRST_LINE: "first-line",
  1362. FILE_SELECTION_BUTTON: "file-selector-button",
  1363. GRAMMAR_ERROR: "grammar-error",
  1364. MARKER: "marker",
  1365. PART: "part",
  1366. PLACEHOLDER: "placeholder",
  1367. SELECTION: "selection",
  1368. SLOTTED: "slotted",
  1369. SPELLING_ERROR: "spelling-error",
  1370. TARGET_TEXT: "target-text",
  1371. }; // ExtendedCss does not support at-rules
  1372. // https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
  1373. const AT_RULE_MARKER = "@";
  1374. const CONTENT_CSS_PROPERTY = "content";
  1375. const PSEUDO_PROPERTY_POSITIVE_VALUE = "true";
  1376. const DEBUG_PSEUDO_PROPERTY_GLOBAL_VALUE = "global";
  1377. const NO_SELECTOR_ERROR_PREFIX = "Selector should be defined";
  1378. const STYLE_ERROR_PREFIX = {
  1379. NO_STYLE: "No style declaration found",
  1380. NO_SELECTOR: `${NO_SELECTOR_ERROR_PREFIX} before style declaration in stylesheet`,
  1381. INVALID_STYLE: "Invalid style declaration",
  1382. UNCLOSED_STYLE: "Unclosed style declaration",
  1383. NO_PROPERTY: "Missing style property in declaration",
  1384. NO_VALUE: "Missing style value in declaration",
  1385. NO_STYLE_OR_REMOVE:
  1386. "Style should be declared or :remove() pseudo-class should used",
  1387. NO_COMMENT: "Comments are not supported",
  1388. };
  1389. const NO_AT_RULE_ERROR_PREFIX = "At-rules are not supported";
  1390. const REMOVE_ERROR_PREFIX = {
  1391. INVALID_REMOVE: "Invalid :remove() pseudo-class in selector",
  1392. NO_TARGET_SELECTOR: `${NO_SELECTOR_ERROR_PREFIX} before :remove() pseudo-class`,
  1393. MULTIPLE_USAGE:
  1394. "Pseudo-class :remove() appears more than once in selector",
  1395. INVALID_POSITION:
  1396. "Pseudo-class :remove() should be at the end of selector",
  1397. };
  1398. const MATCHING_ELEMENT_ERROR_PREFIX = "Error while matching element";
  1399. const MAX_STYLE_PROTECTION_COUNT = 50;
  1400. /**
  1401. * Regexp that matches backward compatible syntaxes.
  1402. */
  1403. const REGEXP_VALID_OLD_SYNTAX =
  1404. /\[-(?:ext)-([a-z-_]+)=(["'])((?:(?=(\\?))\4.)*?)\2\]/g;
  1405. /**
  1406. * Marker for checking invalid selector after old-syntax normalizing by selector converter.
  1407. */
  1408. const INVALID_OLD_SYNTAX_MARKER = "[-ext-";
  1409. /**
  1410. * Complex replacement function.
  1411. * Undo quote escaping inside of an extended selector.
  1412. *
  1413. * @param match Whole matched string.
  1414. * @param name Group 1.
  1415. * @param quoteChar Group 2.
  1416. * @param rawValue Group 3.
  1417. *
  1418. * @returns Converted string.
  1419. */
  1420. const evaluateMatch = (match, name, quoteChar, rawValue) => {
  1421. // Unescape quotes
  1422. const re = new RegExp(`([^\\\\]|^)\\\\${quoteChar}`, "g");
  1423. const value = rawValue.replace(re, `$1${quoteChar}`);
  1424. return `:${name}(${value})`;
  1425. }; // ':scope' pseudo may be at start of :has() argument
  1426. // but ExtCssDocument.querySelectorAll() already use it for selecting exact element descendants
  1427. const SCOPE_MARKER_REGEXP = /\(:scope >/g;
  1428. const SCOPE_REPLACER = "(>";
  1429. const MATCHES_CSS_PSEUDO_ELEMENT_REGEXP =
  1430. /(:matches-css)-(before|after)\(/g;
  1431. const convertMatchesCss = (
  1432. match,
  1433. extendedPseudoClass,
  1434. regularPseudoElement,
  1435. ) => {
  1436. // ':matches-css-before(' --> ':matches-css(before, '
  1437. // ':matches-css-after(' --> ':matches-css(after, '
  1438. return `${extendedPseudoClass}${BRACKET.PARENTHESES.LEFT}${regularPseudoElement}${COMMA}`;
  1439. };
  1440. /**
  1441. * Handles old syntax and :scope inside :has().
  1442. *
  1443. * @param selector Trimmed selector to normalize.
  1444. *
  1445. * @returns Normalized selector.
  1446. * @throws An error on invalid old extended syntax selector.
  1447. */
  1448. const normalize = (selector) => {
  1449. const normalizedSelector = selector
  1450. .replace(REGEXP_VALID_OLD_SYNTAX, evaluateMatch)
  1451. .replace(SCOPE_MARKER_REGEXP, SCOPE_REPLACER)
  1452. .replace(MATCHES_CSS_PSEUDO_ELEMENT_REGEXP, convertMatchesCss); // validate old syntax after normalizing
  1453. // e.g. '[-ext-matches-css-before=\'content: /^[A-Z][a-z]'
  1454. if (normalizedSelector.includes(INVALID_OLD_SYNTAX_MARKER)) {
  1455. throw new Error(
  1456. `Invalid extended-css old syntax selector: '${selector}'`,
  1457. );
  1458. }
  1459. return normalizedSelector;
  1460. };
  1461. /**
  1462. * Prepares the rawSelector before tokenization:
  1463. * 1. Trims it.
  1464. * 2. Converts old syntax `[-ext-pseudo-class="..."]` to new one `:pseudo-class(...)`.
  1465. * 3. Handles :scope pseudo inside :has() pseudo-class arg.
  1466. *
  1467. * @param rawSelector Selector with no style declaration.
  1468. * @returns Prepared selector with no style declaration.
  1469. */
  1470. const convert = (rawSelector) => {
  1471. const trimmedSelector = rawSelector.trim();
  1472. return normalize(trimmedSelector);
  1473. };
  1474. /**
  1475. * Possible token types.
  1476. *
  1477. * IMPORTANT: it is used as 'const' instead of 'enum' to avoid side effects
  1478. * during ExtendedCss import into other libraries.
  1479. */
  1480. const TOKEN_TYPE = {
  1481. MARK: "mark",
  1482. WORD: "word",
  1483. };
  1484. /**
  1485. * Splits `input` string into tokens.
  1486. *
  1487. * @param input Input string to tokenize.
  1488. * @param supportedMarks Array of supported marks to considered as `TOKEN_TYPE.MARK`;
  1489. * all other will be considered as `TOKEN_TYPE.WORD`.
  1490. *
  1491. * @returns Array of tokens.
  1492. */
  1493. const tokenize = (input, supportedMarks) => {
  1494. // buffer is needed for words collecting while iterating
  1495. let wordBuffer = ""; // result collection
  1496. const tokens = [];
  1497. const selectorSymbols = input.split(""); // iterate through selector chars and collect tokens
  1498. selectorSymbols.forEach((symbol) => {
  1499. if (supportedMarks.includes(symbol)) {
  1500. // if anything was collected to the buffer before
  1501. if (wordBuffer.length > 0) {
  1502. // now it is time to stop buffer collecting and save is as "word"
  1503. tokens.push({
  1504. type: TOKEN_TYPE.WORD,
  1505. value: wordBuffer,
  1506. }); // reset the buffer
  1507. wordBuffer = "";
  1508. } // save current symbol as "mark"
  1509. tokens.push({
  1510. type: TOKEN_TYPE.MARK,
  1511. value: symbol,
  1512. });
  1513. return;
  1514. } // otherwise collect symbol to the buffer
  1515. wordBuffer += symbol;
  1516. }); // save the last collected word
  1517. if (wordBuffer.length > 0) {
  1518. tokens.push({
  1519. type: TOKEN_TYPE.WORD,
  1520. value: wordBuffer,
  1521. });
  1522. }
  1523. return tokens;
  1524. };
  1525. /**
  1526. * Prepares `rawSelector` and splits it into tokens.
  1527. *
  1528. * @param rawSelector Raw css selector.
  1529. *
  1530. * @returns Array of tokens supported for selector.
  1531. */
  1532. const tokenizeSelector = (rawSelector) => {
  1533. const selector = convert(rawSelector);
  1534. return tokenize(selector, SUPPORTED_SELECTOR_MARKS);
  1535. };
  1536. /**
  1537. * Splits `attribute` into tokens.
  1538. *
  1539. * @param attribute Input attribute.
  1540. *
  1541. * @returns Array of tokens supported for attribute.
  1542. */
  1543. const tokenizeAttribute = (attribute) => {
  1544. // equal sigh `=` in attribute is considered as `TOKEN_TYPE.MARK`
  1545. return tokenize(attribute, [...SUPPORTED_SELECTOR_MARKS, EQUAL_SIGN]);
  1546. };
  1547. /**
  1548. * Some browsers do not support Array.prototype.flat()
  1549. * e.g. Opera 42 which is used for browserstack tests.
  1550. *
  1551. * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat}
  1552. *
  1553. * @param input Array needed to be flatten.
  1554. *
  1555. * @returns Flatten array.
  1556. * @throws An error if array cannot be flatten.
  1557. */
  1558. const flatten = (input) => {
  1559. const stack = [];
  1560. input.forEach((el) => stack.push(el));
  1561. const res = [];
  1562. while (stack.length) {
  1563. // pop value from stack
  1564. const next = stack.pop();
  1565. if (!next) {
  1566. throw new Error("Unable to make array flat");
  1567. }
  1568. if (Array.isArray(next)) {
  1569. // push back array items, won't modify the original input
  1570. next.forEach((el) => stack.push(el));
  1571. } else {
  1572. res.push(next);
  1573. }
  1574. } // reverse to restore input order
  1575. return res.reverse();
  1576. };
  1577. /**
  1578. * Returns first item from `array`.
  1579. *
  1580. * @param array Input array.
  1581. *
  1582. * @returns First array item, or `undefined` if there is no such item.
  1583. */
  1584. const getFirst = (array) => {
  1585. return array[0];
  1586. };
  1587. /**
  1588. * Returns last item from array.
  1589. *
  1590. * @param array Input array.
  1591. *
  1592. * @returns Last array item, or `undefined` if there is no such item.
  1593. */
  1594. const getLast = (array) => {
  1595. return array[array.length - 1];
  1596. };
  1597. /**
  1598. * Returns array item which is previous to the last one
  1599. * e.g. for `[5, 6, 7, 8]` returns `7`.
  1600. *
  1601. * @param array Input array.
  1602. *
  1603. * @returns Previous to last array item, or `undefined` if there is no such item.
  1604. */
  1605. const getPrevToLast = (array) => {
  1606. return array[array.length - 2];
  1607. };
  1608. /**
  1609. * Takes array of ast node `children` and returns the child by the `index`.
  1610. *
  1611. * @param array Array of ast node children.
  1612. * @param index Index of needed child in the array.
  1613. * @param errorMessage Optional error message to throw.
  1614. *
  1615. * @returns Array item at `index` position.
  1616. * @throws An error if there is no child with specified `index` in array.
  1617. */
  1618. const getItemByIndex = (array, index, errorMessage) => {
  1619. const indexChild = array[index];
  1620. if (!indexChild) {
  1621. throw new Error(
  1622. errorMessage || `No array item found by index ${index}`,
  1623. );
  1624. }
  1625. return indexChild;
  1626. };
  1627. const NO_REGULAR_SELECTOR_ERROR =
  1628. "At least one of Selector node children should be RegularSelector";
  1629. /**
  1630. * Checks whether the type of `astNode` is SelectorList.
  1631. *
  1632. * @param astNode Ast node.
  1633. *
  1634. * @returns True if astNode.type === SelectorList.
  1635. */
  1636. const isSelectorListNode = (astNode) => {
  1637. return (
  1638. (astNode === null || astNode === void 0 ? void 0 : astNode.type) ===
  1639. NODE.SELECTOR_LIST
  1640. );
  1641. };
  1642. /**
  1643. * Checks whether the type of `astNode` is Selector.
  1644. *
  1645. * @param astNode Ast node.
  1646. *
  1647. * @returns True if astNode.type === Selector.
  1648. */
  1649. const isSelectorNode = (astNode) => {
  1650. return (
  1651. (astNode === null || astNode === void 0 ? void 0 : astNode.type) ===
  1652. NODE.SELECTOR
  1653. );
  1654. };
  1655. /**
  1656. * Checks whether the type of `astNode` is RegularSelector.
  1657. *
  1658. * @param astNode Ast node.
  1659. *
  1660. * @returns True if astNode.type === RegularSelector.
  1661. */
  1662. const isRegularSelectorNode = (astNode) => {
  1663. return (
  1664. (astNode === null || astNode === void 0 ? void 0 : astNode.type) ===
  1665. NODE.REGULAR_SELECTOR
  1666. );
  1667. };
  1668. /**
  1669. * Checks whether the type of `astNode` is ExtendedSelector.
  1670. *
  1671. * @param astNode Ast node.
  1672. *
  1673. * @returns True if astNode.type === ExtendedSelector.
  1674. */
  1675. const isExtendedSelectorNode = (astNode) => {
  1676. return astNode.type === NODE.EXTENDED_SELECTOR;
  1677. };
  1678. /**
  1679. * Checks whether the type of `astNode` is AbsolutePseudoClass.
  1680. *
  1681. * @param astNode Ast node.
  1682. *
  1683. * @returns True if astNode.type === AbsolutePseudoClass.
  1684. */
  1685. const isAbsolutePseudoClassNode = (astNode) => {
  1686. return (
  1687. (astNode === null || astNode === void 0 ? void 0 : astNode.type) ===
  1688. NODE.ABSOLUTE_PSEUDO_CLASS
  1689. );
  1690. };
  1691. /**
  1692. * Checks whether the type of `astNode` is RelativePseudoClass.
  1693. *
  1694. * @param astNode Ast node.
  1695. *
  1696. * @returns True if astNode.type === RelativePseudoClass.
  1697. */
  1698. const isRelativePseudoClassNode = (astNode) => {
  1699. return (
  1700. (astNode === null || astNode === void 0 ? void 0 : astNode.type) ===
  1701. NODE.RELATIVE_PSEUDO_CLASS
  1702. );
  1703. };
  1704. /**
  1705. * Returns name of `astNode`.
  1706. *
  1707. * @param astNode AbsolutePseudoClass or RelativePseudoClass node.
  1708. *
  1709. * @returns Name of `astNode`.
  1710. * @throws An error on unsupported ast node or no name found.
  1711. */
  1712. const getNodeName = (astNode) => {
  1713. if (astNode === null) {
  1714. throw new Error("Ast node should be defined");
  1715. }
  1716. if (
  1717. !isAbsolutePseudoClassNode(astNode) &&
  1718. !isRelativePseudoClassNode(astNode)
  1719. ) {
  1720. throw new Error(
  1721. "Only AbsolutePseudoClass or RelativePseudoClass ast node can have a name",
  1722. );
  1723. }
  1724. if (!astNode.name) {
  1725. throw new Error("Extended pseudo-class should have a name");
  1726. }
  1727. return astNode.name;
  1728. };
  1729. /**
  1730. * Returns value of `astNode`.
  1731. *
  1732. * @param astNode RegularSelector or AbsolutePseudoClass node.
  1733. * @param errorMessage Optional error message if no value found.
  1734. *
  1735. * @returns Value of `astNode`.
  1736. * @throws An error on unsupported ast node or no value found.
  1737. */
  1738. const getNodeValue = (astNode, errorMessage) => {
  1739. if (astNode === null) {
  1740. throw new Error("Ast node should be defined");
  1741. }
  1742. if (
  1743. !isRegularSelectorNode(astNode) &&
  1744. !isAbsolutePseudoClassNode(astNode)
  1745. ) {
  1746. throw new Error(
  1747. "Only RegularSelector ot AbsolutePseudoClass ast node can have a value",
  1748. );
  1749. }
  1750. if (!astNode.value) {
  1751. throw new Error(
  1752. errorMessage ||
  1753. "Ast RegularSelector ot AbsolutePseudoClass node should have a value",
  1754. );
  1755. }
  1756. return astNode.value;
  1757. };
  1758. /**
  1759. * Returns only RegularSelector nodes from `children`.
  1760. *
  1761. * @param children Array of ast node children.
  1762. *
  1763. * @returns Array of RegularSelector nodes.
  1764. */
  1765. const getRegularSelectorNodes = (children) => {
  1766. return children.filter(isRegularSelectorNode);
  1767. };
  1768. /**
  1769. * Returns the first RegularSelector node from `children`.
  1770. *
  1771. * @param children Array of ast node children.
  1772. * @param errorMessage Optional error message if no value found.
  1773. *
  1774. * @returns Ast RegularSelector node.
  1775. * @throws An error if no RegularSelector node found.
  1776. */
  1777. const getFirstRegularChild = (children, errorMessage) => {
  1778. const regularSelectorNodes = getRegularSelectorNodes(children);
  1779. const firstRegularSelectorNode = getFirst(regularSelectorNodes);
  1780. if (!firstRegularSelectorNode) {
  1781. throw new Error(errorMessage || NO_REGULAR_SELECTOR_ERROR);
  1782. }
  1783. return firstRegularSelectorNode;
  1784. };
  1785. /**
  1786. * Returns the last RegularSelector node from `children`.
  1787. *
  1788. * @param children Array of ast node children.
  1789. *
  1790. * @returns Ast RegularSelector node.
  1791. * @throws An error if no RegularSelector node found.
  1792. */
  1793. const getLastRegularChild = (children) => {
  1794. const regularSelectorNodes = getRegularSelectorNodes(children);
  1795. const lastRegularSelectorNode = getLast(regularSelectorNodes);
  1796. if (!lastRegularSelectorNode) {
  1797. throw new Error(NO_REGULAR_SELECTOR_ERROR);
  1798. }
  1799. return lastRegularSelectorNode;
  1800. };
  1801. /**
  1802. * Returns the only child of `node`.
  1803. *
  1804. * @param node Ast node.
  1805. * @param errorMessage Error message.
  1806. *
  1807. * @returns The only child of ast node.
  1808. * @throws An error if none or more than one child found.
  1809. */
  1810. const getNodeOnlyChild = (node, errorMessage) => {
  1811. if (node.children.length !== 1) {
  1812. throw new Error(errorMessage);
  1813. }
  1814. const onlyChild = getFirst(node.children);
  1815. if (!onlyChild) {
  1816. throw new Error(errorMessage);
  1817. }
  1818. return onlyChild;
  1819. };
  1820. /**
  1821. * Takes ExtendedSelector node and returns its only child.
  1822. *
  1823. * @param extendedSelectorNode ExtendedSelector ast node.
  1824. *
  1825. * @returns AbsolutePseudoClass or RelativePseudoClass.
  1826. * @throws An error if there is no specific pseudo-class ast node.
  1827. */
  1828. const getPseudoClassNode = (extendedSelectorNode) => {
  1829. return getNodeOnlyChild(
  1830. extendedSelectorNode,
  1831. "Extended selector should be specified",
  1832. );
  1833. };
  1834. /**
  1835. * Takes RelativePseudoClass node and returns its only child
  1836. * which is relative SelectorList node.
  1837. *
  1838. * @param pseudoClassNode RelativePseudoClass.
  1839. *
  1840. * @returns Relative SelectorList node.
  1841. * @throws An error if no selector list found.
  1842. */
  1843. const getRelativeSelectorListNode = (pseudoClassNode) => {
  1844. if (!isRelativePseudoClassNode(pseudoClassNode)) {
  1845. throw new Error(
  1846. "Only RelativePseudoClass node can have relative SelectorList node as child",
  1847. );
  1848. }
  1849. return getNodeOnlyChild(
  1850. pseudoClassNode,
  1851. `Missing arg for :${getNodeName(pseudoClassNode)}() pseudo-class`,
  1852. );
  1853. };
  1854. const ATTRIBUTE_CASE_INSENSITIVE_FLAG = "i";
  1855. /**
  1856. * Limited list of available symbols before slash `/`
  1857. * to check whether it is valid regexp pattern opening.
  1858. */
  1859. const POSSIBLE_MARKS_BEFORE_REGEXP = {
  1860. COMMON: [
  1861. // e.g. ':matches-attr(/data-/)'
  1862. BRACKET.PARENTHESES.LEFT,
  1863. // e.g. `:matches-attr('/data-/')`
  1864. SINGLE_QUOTE,
  1865. // e.g. ':matches-attr("/data-/")'
  1866. DOUBLE_QUOTE,
  1867. // e.g. ':matches-attr(check=/data-v-/)'
  1868. EQUAL_SIGN,
  1869. // e.g. ':matches-property(inner./_test/=null)'
  1870. DOT,
  1871. // e.g. ':matches-css(height:/20px/)'
  1872. COLON,
  1873. // ':matches-css-after( content : /(\\d+\\s)*me/ )'
  1874. SPACE,
  1875. ],
  1876. CONTAINS: [
  1877. // e.g. ':contains(/text/)'
  1878. BRACKET.PARENTHESES.LEFT,
  1879. // e.g. `:contains('/text/')`
  1880. SINGLE_QUOTE,
  1881. // e.g. ':contains("/text/")'
  1882. DOUBLE_QUOTE,
  1883. ],
  1884. };
  1885. /**
  1886. * Checks whether the passed token is supported extended pseudo-class.
  1887. *
  1888. * @param tokenValue Token value to check.
  1889. *
  1890. * @returns True if `tokenValue` is one of supported extended pseudo-class names.
  1891. */
  1892. const isSupportedPseudoClass = (tokenValue) => {
  1893. return SUPPORTED_PSEUDO_CLASSES.includes(tokenValue);
  1894. };
  1895. /**
  1896. * Checks whether the passed pseudo-class `name` should be optimized,
  1897. * i.e. :not() and :is().
  1898. *
  1899. * @param name Pseudo-class name.
  1900. *
  1901. * @returns True if `name` is one if pseudo-class which should be optimized.
  1902. */
  1903. const isOptimizationPseudoClass = (name) => {
  1904. return OPTIMIZATION_PSEUDO_CLASSES.includes(name);
  1905. };
  1906. /**
  1907. * Checks whether next to "space" token is a continuation of regular selector being processed.
  1908. *
  1909. * @param nextTokenType Type of token next to current one.
  1910. * @param nextTokenValue Value of token next to current one.
  1911. *
  1912. * @returns True if next token seems to be a part of current regular selector.
  1913. */
  1914. const doesRegularContinueAfterSpace = (nextTokenType, nextTokenValue) => {
  1915. // regular selector does not continues after the current token
  1916. if (!nextTokenType || !nextTokenValue) {
  1917. return false;
  1918. }
  1919. return (
  1920. COMBINATORS.includes(nextTokenValue) ||
  1921. nextTokenType === TOKEN_TYPE.WORD || // e.g. '#main *:has(> .ad)'
  1922. nextTokenValue === ASTERISK ||
  1923. nextTokenValue === ID_MARKER ||
  1924. nextTokenValue === CLASS_MARKER || // e.g. 'div :where(.content)'
  1925. nextTokenValue === COLON || // e.g. "div[class*=' ']"
  1926. nextTokenValue === SINGLE_QUOTE || // e.g. 'div[class*=" "]'
  1927. nextTokenValue === DOUBLE_QUOTE ||
  1928. nextTokenValue === BRACKET.SQUARE.LEFT
  1929. );
  1930. };
  1931. /**
  1932. * Checks whether the regexp pattern for pseudo-class arg starts.
  1933. * Needed for `context.isRegexpOpen` flag.
  1934. *
  1935. * @param context Selector parser context.
  1936. * @param prevTokenValue Value of previous token.
  1937. * @param bufferNodeValue Value of bufferNode.
  1938. *
  1939. * @returns True if current token seems to be a start of regexp pseudo-class arg pattern.
  1940. * @throws An error on invalid regexp pattern.
  1941. */
  1942. const isRegexpOpening = (context, prevTokenValue, bufferNodeValue) => {
  1943. const lastExtendedPseudoClassName = getLast(
  1944. context.extendedPseudoNamesStack,
  1945. );
  1946. if (!lastExtendedPseudoClassName) {
  1947. throw new Error(
  1948. "Regexp pattern allowed only in arg of extended pseudo-class",
  1949. );
  1950. } // for regexp pattens the slash should not be escaped
  1951. // const isRegexpPatternSlash = prevTokenValue !== BACKSLASH;
  1952. // regexp pattern can be set as arg of pseudo-class
  1953. // which means limited list of available symbols before slash `/`;
  1954. // for :contains() pseudo-class regexp pattern should be at the beginning of arg
  1955. if (CONTAINS_PSEUDO_NAMES.includes(lastExtendedPseudoClassName)) {
  1956. return POSSIBLE_MARKS_BEFORE_REGEXP.CONTAINS.includes(prevTokenValue);
  1957. }
  1958. if (
  1959. prevTokenValue === SLASH &&
  1960. lastExtendedPseudoClassName !== XPATH_PSEUDO_CLASS_MARKER
  1961. ) {
  1962. const rawArgDesc = bufferNodeValue
  1963. ? `in arg part: '${bufferNodeValue}'`
  1964. : "arg";
  1965. throw new Error(
  1966. `Invalid regexp pattern for :${lastExtendedPseudoClassName}() pseudo-class ${rawArgDesc}`,
  1967. );
  1968. } // for other pseudo-classes regexp pattern can be either the whole arg or its part
  1969. return POSSIBLE_MARKS_BEFORE_REGEXP.COMMON.includes(prevTokenValue);
  1970. };
  1971. /**
  1972. * Checks whether the attribute starts.
  1973. *
  1974. * @param tokenValue Value of current token.
  1975. * @param prevTokenValue Previous token value.
  1976. *
  1977. * @returns True if combination of current and previous token seems to be **a start** of attribute.
  1978. */
  1979. const isAttributeOpening = (tokenValue, prevTokenValue) => {
  1980. return tokenValue === BRACKET.SQUARE.LEFT && prevTokenValue !== BACKSLASH;
  1981. };
  1982. /**
  1983. * Checks whether the attribute ends.
  1984. *
  1985. * @param context Selector parser context.
  1986. *
  1987. * @returns True if combination of current and previous token seems to be **an end** of attribute.
  1988. * @throws An error on invalid attribute.
  1989. */
  1990. const isAttributeClosing = (context) => {
  1991. var _getPrevToLast;
  1992. if (!context.isAttributeBracketsOpen) {
  1993. return false;
  1994. } // valid attributes may have extra spaces inside.
  1995. // we get rid of them just to simplify the checking and they are skipped only here:
  1996. // - spaces will be collected to the ast with spaces as they were declared is selector
  1997. // - extra spaces in attribute are not relevant to attribute syntax validity
  1998. // e.g. 'a[ title ]' is the same as 'a[title]'
  1999. // 'div[style *= "MARGIN" i]' is the same as 'div[style*="MARGIN"i]'
  2000. const noSpaceAttr = context.attributeBuffer.split(SPACE).join(""); // tokenize the prepared attribute string
  2001. const attrTokens = tokenizeAttribute(noSpaceAttr);
  2002. const firstAttrToken = getFirst(attrTokens);
  2003. const firstAttrTokenType =
  2004. firstAttrToken === null || firstAttrToken === void 0
  2005. ? void 0
  2006. : firstAttrToken.type;
  2007. const firstAttrTokenValue =
  2008. firstAttrToken === null || firstAttrToken === void 0
  2009. ? void 0
  2010. : firstAttrToken.value; // signal an error on any mark-type token except backslash
  2011. // e.g. '[="margin"]'
  2012. if (
  2013. firstAttrTokenType === TOKEN_TYPE.MARK && // backslash is allowed at start of attribute
  2014. // e.g. '[\\:data-service-slot]'
  2015. firstAttrTokenValue !== BACKSLASH
  2016. ) {
  2017. // eslint-disable-next-line max-len
  2018. throw new Error(
  2019. `'[${context.attributeBuffer}]' is not a valid attribute due to '${firstAttrTokenValue}' at start of it`,
  2020. );
  2021. }
  2022. const lastAttrToken = getLast(attrTokens);
  2023. const lastAttrTokenType =
  2024. lastAttrToken === null || lastAttrToken === void 0
  2025. ? void 0
  2026. : lastAttrToken.type;
  2027. const lastAttrTokenValue =
  2028. lastAttrToken === null || lastAttrToken === void 0
  2029. ? void 0
  2030. : lastAttrToken.value;
  2031. if (lastAttrTokenValue === EQUAL_SIGN) {
  2032. // e.g. '[style=]'
  2033. throw new Error(
  2034. `'[${context.attributeBuffer}]' is not a valid attribute due to '${EQUAL_SIGN}'`,
  2035. );
  2036. }
  2037. const equalSignIndex = attrTokens.findIndex((token) => {
  2038. return token.type === TOKEN_TYPE.MARK && token.value === EQUAL_SIGN;
  2039. });
  2040. const prevToLastAttrTokenValue =
  2041. (_getPrevToLast = getPrevToLast(attrTokens)) === null ||
  2042. _getPrevToLast === void 0
  2043. ? void 0
  2044. : _getPrevToLast.value;
  2045. if (equalSignIndex === -1) {
  2046. // if there is no '=' inside attribute,
  2047. // it must be just attribute name which means the word-type token before closing bracket
  2048. // e.g. 'div[style]'
  2049. if (lastAttrTokenType === TOKEN_TYPE.WORD) {
  2050. return true;
  2051. }
  2052. return (
  2053. prevToLastAttrTokenValue === BACKSLASH && // some weird attribute are valid too
  2054. // e.g. '[class\\"ads-article\\"]'
  2055. (lastAttrTokenValue === DOUBLE_QUOTE || // e.g. "[class\\'ads-article\\']"
  2056. lastAttrTokenValue === SINGLE_QUOTE)
  2057. );
  2058. } // get the value of token next to `=`
  2059. const nextToEqualSignToken = getItemByIndex(
  2060. attrTokens,
  2061. equalSignIndex + 1,
  2062. );
  2063. const nextToEqualSignTokenValue = nextToEqualSignToken.value; // check whether the attribute value wrapper in quotes
  2064. const isAttrValueQuote =
  2065. nextToEqualSignTokenValue === SINGLE_QUOTE ||
  2066. nextToEqualSignTokenValue === DOUBLE_QUOTE; // for no quotes after `=` the last token before `]` should be a word-type one
  2067. // e.g. 'div[style*=margin]'
  2068. // 'div[style*=MARGIN i]'
  2069. if (!isAttrValueQuote) {
  2070. if (lastAttrTokenType === TOKEN_TYPE.WORD) {
  2071. return true;
  2072. } // otherwise signal an error
  2073. // e.g. 'table[style*=border: 0px"]'
  2074. throw new Error(
  2075. `'[${context.attributeBuffer}]' is not a valid attribute`,
  2076. );
  2077. } // otherwise if quotes for value are present
  2078. // the last token before `]` can still be word-type token
  2079. // e.g. 'div[style*="MARGIN" i]'
  2080. if (
  2081. lastAttrTokenType === TOKEN_TYPE.WORD &&
  2082. (lastAttrTokenValue === null || lastAttrTokenValue === void 0
  2083. ? void 0
  2084. : lastAttrTokenValue.toLocaleLowerCase()) ===
  2085. ATTRIBUTE_CASE_INSENSITIVE_FLAG
  2086. ) {
  2087. return prevToLastAttrTokenValue === nextToEqualSignTokenValue;
  2088. } // eventually if there is quotes for attribute value and last token is not a word,
  2089. // the closing mark should be the same quote as opening one
  2090. return lastAttrTokenValue === nextToEqualSignTokenValue;
  2091. };
  2092. /**
  2093. * Checks whether the `tokenValue` is a whitespace character.
  2094. *
  2095. * @param tokenValue Token value.
  2096. *
  2097. * @returns True if `tokenValue` is a whitespace character.
  2098. */
  2099. const isWhiteSpaceChar = (tokenValue) => {
  2100. if (!tokenValue) {
  2101. return false;
  2102. }
  2103. return WHITE_SPACE_CHARACTERS.includes(tokenValue);
  2104. };
  2105. /**
  2106. * Checks whether the passed `str` is a name of supported absolute extended pseudo-class,
  2107. * e.g. :contains(), :matches-css() etc.
  2108. *
  2109. * @param str Token value to check.
  2110. *
  2111. * @returns True if `str` is one of absolute extended pseudo-class names.
  2112. */
  2113. const isAbsolutePseudoClass = (str) => {
  2114. return ABSOLUTE_PSEUDO_CLASSES.includes(str);
  2115. };
  2116. /**
  2117. * Checks whether the passed `str` is a name of supported relative extended pseudo-class,
  2118. * e.g. :has(), :not() etc.
  2119. *
  2120. * @param str Token value to check.
  2121. *
  2122. * @returns True if `str` is one of relative extended pseudo-class names.
  2123. */
  2124. const isRelativePseudoClass = (str) => {
  2125. return RELATIVE_PSEUDO_CLASSES.includes(str);
  2126. };
  2127. /**
  2128. * Returns the node which is being collected
  2129. * or null if there is no such one.
  2130. *
  2131. * @param context Selector parser context.
  2132. *
  2133. * @returns Buffer node or null.
  2134. */
  2135. const getBufferNode = (context) => {
  2136. if (context.pathToBufferNode.length === 0) {
  2137. return null;
  2138. } // buffer node is always the last in the pathToBufferNode stack
  2139. return getLast(context.pathToBufferNode) || null;
  2140. };
  2141. /**
  2142. * Returns the parent node to the 'buffer node' — which is the one being collected —
  2143. * or null if there is no such one.
  2144. *
  2145. * @param context Selector parser context.
  2146. *
  2147. * @returns Parent node of buffer node or null.
  2148. */
  2149. const getBufferNodeParent = (context) => {
  2150. // at least two nodes should exist — the buffer node and its parent
  2151. // otherwise return null
  2152. if (context.pathToBufferNode.length < 2) {
  2153. return null;
  2154. } // since the buffer node is always the last in the pathToBufferNode stack
  2155. // its parent is previous to it in the stack
  2156. return getPrevToLast(context.pathToBufferNode) || null;
  2157. };
  2158. /**
  2159. * Returns last RegularSelector ast node.
  2160. * Needed for parsing of the complex selector with extended pseudo-class inside it.
  2161. *
  2162. * @param context Selector parser context.
  2163. *
  2164. * @returns Ast RegularSelector node.
  2165. * @throws An error if:
  2166. * - bufferNode is absent;
  2167. * - type of bufferNode is unsupported;
  2168. * - no RegularSelector in bufferNode.
  2169. */
  2170. const getContextLastRegularSelectorNode = (context) => {
  2171. const bufferNode = getBufferNode(context);
  2172. if (!bufferNode) {
  2173. throw new Error("No bufferNode found");
  2174. }
  2175. if (!isSelectorNode(bufferNode)) {
  2176. throw new Error("Unsupported bufferNode type");
  2177. }
  2178. const lastRegularSelectorNode = getLastRegularChild(bufferNode.children);
  2179. context.pathToBufferNode.push(lastRegularSelectorNode);
  2180. return lastRegularSelectorNode;
  2181. };
  2182. /**
  2183. * Updates needed buffer node value while tokens iterating.
  2184. * For RegularSelector also collects token values to context.attributeBuffer
  2185. * for proper attribute parsing.
  2186. *
  2187. * @param context Selector parser context.
  2188. * @param tokenValue Value of current token.
  2189. *
  2190. * @throws An error if:
  2191. * - no bufferNode;
  2192. * - bufferNode.type is not RegularSelector or AbsolutePseudoClass.
  2193. */
  2194. const updateBufferNode = (context, tokenValue) => {
  2195. const bufferNode = getBufferNode(context);
  2196. if (bufferNode === null) {
  2197. throw new Error("No bufferNode to update");
  2198. }
  2199. if (isAbsolutePseudoClassNode(bufferNode)) {
  2200. bufferNode.value += tokenValue;
  2201. } else if (isRegularSelectorNode(bufferNode)) {
  2202. bufferNode.value += tokenValue;
  2203. if (context.isAttributeBracketsOpen) {
  2204. context.attributeBuffer += tokenValue;
  2205. }
  2206. } else {
  2207. // eslint-disable-next-line max-len
  2208. throw new Error(
  2209. `${bufferNode.type} node cannot be updated. Only RegularSelector and AbsolutePseudoClass are supported`,
  2210. );
  2211. }
  2212. };
  2213. /**
  2214. * Adds SelectorList node to context.ast at the start of ast collecting.
  2215. *
  2216. * @param context Selector parser context.
  2217. */
  2218. const addSelectorListNode = (context) => {
  2219. const selectorListNode = new AnySelectorNode(NODE.SELECTOR_LIST);
  2220. context.ast = selectorListNode;
  2221. context.pathToBufferNode.push(selectorListNode);
  2222. };
  2223. /**
  2224. * Adds new node to buffer node children.
  2225. * New added node will be considered as buffer node after it.
  2226. *
  2227. * @param context Selector parser context.
  2228. * @param type Type of node to add.
  2229. * @param tokenValue Optional, defaults to `''`, value of processing token.
  2230. *
  2231. * @throws An error if no bufferNode.
  2232. */
  2233. const addAstNodeByType = function (context, type) {
  2234. let tokenValue =
  2235. arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "";
  2236. const bufferNode = getBufferNode(context);
  2237. if (bufferNode === null) {
  2238. throw new Error("No buffer node");
  2239. }
  2240. let node;
  2241. if (type === NODE.REGULAR_SELECTOR) {
  2242. node = new RegularSelectorNode(tokenValue);
  2243. } else if (type === NODE.ABSOLUTE_PSEUDO_CLASS) {
  2244. node = new AbsolutePseudoClassNode(tokenValue);
  2245. } else if (type === NODE.RELATIVE_PSEUDO_CLASS) {
  2246. node = new RelativePseudoClassNode(tokenValue);
  2247. } else {
  2248. // SelectorList || Selector || ExtendedSelector
  2249. node = new AnySelectorNode(type);
  2250. }
  2251. bufferNode.addChild(node);
  2252. context.pathToBufferNode.push(node);
  2253. };
  2254. /**
  2255. * The very beginning of ast collecting.
  2256. *
  2257. * @param context Selector parser context.
  2258. * @param tokenValue Value of regular selector.
  2259. */
  2260. const initAst = (context, tokenValue) => {
  2261. addSelectorListNode(context);
  2262. addAstNodeByType(context, NODE.SELECTOR); // RegularSelector node is always the first child of Selector node
  2263. addAstNodeByType(context, NODE.REGULAR_SELECTOR, tokenValue);
  2264. };
  2265. /**
  2266. * Inits selector list subtree for relative extended pseudo-classes, e.g. :has(), :not().
  2267. *
  2268. * @param context Selector parser context.
  2269. * @param tokenValue Optional, defaults to `''`, value of inner regular selector.
  2270. */
  2271. const initRelativeSubtree = function (context) {
  2272. let tokenValue =
  2273. arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "";
  2274. addAstNodeByType(context, NODE.SELECTOR_LIST);
  2275. addAstNodeByType(context, NODE.SELECTOR);
  2276. addAstNodeByType(context, NODE.REGULAR_SELECTOR, tokenValue);
  2277. };
  2278. /**
  2279. * Goes to closest parent specified by type.
  2280. * Actually updates path to buffer node for proper ast collecting of selectors while parsing.
  2281. *
  2282. * @param context Selector parser context.
  2283. * @param parentType Type of needed parent node in ast.
  2284. */
  2285. const upToClosest = (context, parentType) => {
  2286. for (let i = context.pathToBufferNode.length - 1; i >= 0; i -= 1) {
  2287. var _context$pathToBuffer;
  2288. if (
  2289. ((_context$pathToBuffer = context.pathToBufferNode[i]) === null ||
  2290. _context$pathToBuffer === void 0
  2291. ? void 0
  2292. : _context$pathToBuffer.type) === parentType
  2293. ) {
  2294. context.pathToBufferNode = context.pathToBufferNode.slice(0, i + 1);
  2295. break;
  2296. }
  2297. }
  2298. };
  2299. /**
  2300. * Returns needed buffer node updated due to complex selector parsing.
  2301. *
  2302. * @param context Selector parser context.
  2303. *
  2304. * @returns Ast node for following selector parsing.
  2305. * @throws An error if there is no upper SelectorNode is ast.
  2306. */
  2307. const getUpdatedBufferNode = (context) => {
  2308. // it may happen during the parsing of selector list
  2309. // which is an argument of relative pseudo-class
  2310. // e.g. '.banner:has(~span, ~p)'
  2311. // parser position is here ↑
  2312. // so if after the comma the buffer node type is SelectorList and parent type is RelativePseudoClass
  2313. // we should simply return the current buffer node
  2314. const bufferNode = getBufferNode(context);
  2315. if (
  2316. bufferNode &&
  2317. isSelectorListNode(bufferNode) &&
  2318. isRelativePseudoClassNode(getBufferNodeParent(context))
  2319. ) {
  2320. return bufferNode;
  2321. }
  2322. upToClosest(context, NODE.SELECTOR);
  2323. const selectorNode = getBufferNode(context);
  2324. if (!selectorNode) {
  2325. throw new Error(
  2326. "No SelectorNode, impossible to continue selector parsing by ExtendedCss",
  2327. );
  2328. }
  2329. const lastSelectorNodeChild = getLast(selectorNode.children);
  2330. const hasExtended =
  2331. lastSelectorNodeChild &&
  2332. isExtendedSelectorNode(lastSelectorNodeChild) && // parser position might be inside standard pseudo-class brackets which has space
  2333. // e.g. 'div:contains(/а/):nth-child(100n + 2)'
  2334. context.standardPseudoBracketsStack.length === 0;
  2335. const supposedPseudoClassNode =
  2336. hasExtended && getFirst(lastSelectorNodeChild.children);
  2337. let newNeededBufferNode = selectorNode;
  2338. if (supposedPseudoClassNode) {
  2339. // name of pseudo-class for last extended-node child for Selector node
  2340. const lastExtendedPseudoName =
  2341. hasExtended && supposedPseudoClassNode.name;
  2342. const isLastExtendedNameRelative =
  2343. lastExtendedPseudoName &&
  2344. isRelativePseudoClass(lastExtendedPseudoName);
  2345. const isLastExtendedNameAbsolute =
  2346. lastExtendedPseudoName &&
  2347. isAbsolutePseudoClass(lastExtendedPseudoName);
  2348. const hasRelativeExtended =
  2349. isLastExtendedNameRelative &&
  2350. context.extendedPseudoBracketsStack.length > 0 &&
  2351. context.extendedPseudoBracketsStack.length ===
  2352. context.extendedPseudoNamesStack.length;
  2353. const hasAbsoluteExtended =
  2354. isLastExtendedNameAbsolute &&
  2355. lastExtendedPseudoName === getLast(context.extendedPseudoNamesStack);
  2356. if (hasRelativeExtended) {
  2357. // return relative selector node to update later
  2358. context.pathToBufferNode.push(lastSelectorNodeChild);
  2359. newNeededBufferNode = supposedPseudoClassNode;
  2360. } else if (hasAbsoluteExtended) {
  2361. // return absolute selector node to update later
  2362. context.pathToBufferNode.push(lastSelectorNodeChild);
  2363. newNeededBufferNode = supposedPseudoClassNode;
  2364. }
  2365. } else if (hasExtended) {
  2366. // return selector node to add new regular selector node later
  2367. newNeededBufferNode = selectorNode;
  2368. } else {
  2369. // otherwise return last regular selector node to update later
  2370. newNeededBufferNode = getContextLastRegularSelectorNode(context);
  2371. } // update the path to buffer node properly
  2372. context.pathToBufferNode.push(newNeededBufferNode);
  2373. return newNeededBufferNode;
  2374. };
  2375. /**
  2376. * Checks values of few next tokens on colon token `:` and:
  2377. * - updates buffer node for following standard pseudo-class;
  2378. * - adds extended selector ast node for following extended pseudo-class;
  2379. * - validates some cases of `:remove()` and `:has()` usage.
  2380. *
  2381. * @param context Selector parser context.
  2382. * @param selector Selector.
  2383. * @param tokenValue Value of current token.
  2384. * @param nextTokenValue Value of token next to current one.
  2385. * @param nextToNextTokenValue Value of token next to next to current one.
  2386. *
  2387. * @throws An error on :remove() pseudo-class in selector
  2388. * or :has() inside regular pseudo limitation.
  2389. */
  2390. const handleNextTokenOnColon = (
  2391. context,
  2392. selector,
  2393. tokenValue,
  2394. nextTokenValue,
  2395. nextToNextTokenValue,
  2396. ) => {
  2397. if (!nextTokenValue) {
  2398. throw new Error(
  2399. `Invalid colon ':' at the end of selector: '${selector}'`,
  2400. );
  2401. }
  2402. if (!isSupportedPseudoClass(nextTokenValue.toLowerCase())) {
  2403. if (nextTokenValue.toLowerCase() === REMOVE_PSEUDO_MARKER) {
  2404. // :remove() pseudo-class should be handled before
  2405. // as it is not about element selecting but actions with elements
  2406. // e.g. 'body > div:empty:remove()'
  2407. throw new Error(
  2408. `${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${selector}'`,
  2409. );
  2410. } // if following token is not an extended pseudo
  2411. // the colon should be collected to value of RegularSelector
  2412. // e.g. '.entry_text:nth-child(2)'
  2413. updateBufferNode(context, tokenValue); // check the token after the pseudo and do balance parentheses later
  2414. // only if it is functional pseudo-class (standard with brackets, e.g. ':lang()').
  2415. // no brackets balance needed for such case,
  2416. // parser position is on first colon after the 'div':
  2417. // e.g. 'div:last-child:has(button.privacy-policy__btn)'
  2418. if (
  2419. nextToNextTokenValue &&
  2420. nextToNextTokenValue === BRACKET.PARENTHESES.LEFT && // no brackets balance needed for parentheses inside attribute value
  2421. // e.g. 'a[href="javascript:void(0)"]' <-- parser position is on colon `:`
  2422. // before `void` ↑
  2423. !context.isAttributeBracketsOpen
  2424. ) {
  2425. context.standardPseudoNamesStack.push(nextTokenValue);
  2426. }
  2427. } else {
  2428. // it is supported extended pseudo-class.
  2429. // Disallow :has() inside the pseudos accepting only compound selectors
  2430. // https://bugs.chromium.org/p/chromium/issues/detail?id=669058#c54 [2]
  2431. if (
  2432. HAS_PSEUDO_CLASS_MARKERS.includes(nextTokenValue) &&
  2433. context.standardPseudoNamesStack.length > 0
  2434. ) {
  2435. // eslint-disable-next-line max-len
  2436. throw new Error(
  2437. `Usage of :${nextTokenValue}() pseudo-class is not allowed inside regular pseudo: '${getLast(context.standardPseudoNamesStack)}'`,
  2438. );
  2439. } else {
  2440. // stop RegularSelector value collecting
  2441. upToClosest(context, NODE.SELECTOR); // add ExtendedSelector to Selector children
  2442. addAstNodeByType(context, NODE.EXTENDED_SELECTOR);
  2443. }
  2444. }
  2445. };
  2446. // e.g. ':is(.page, .main) > .banner' or '*:not(span):not(p)'
  2447. const IS_OR_NOT_PSEUDO_SELECTING_ROOT = `html ${ASTERISK}`;
  2448. /**
  2449. * Checks if there are any ExtendedSelector node in selector list.
  2450. *
  2451. * @param selectorList Ast SelectorList node.
  2452. *
  2453. * @returns True if `selectorList` has any inner ExtendedSelector node.
  2454. */
  2455. const hasExtendedSelector = (selectorList) => {
  2456. return selectorList.children.some((selectorNode) => {
  2457. return selectorNode.children.some((selectorNodeChild) => {
  2458. return isExtendedSelectorNode(selectorNodeChild);
  2459. });
  2460. });
  2461. };
  2462. /**
  2463. * Converts selector list of RegularSelector nodes to string.
  2464. *
  2465. * @param selectorList Ast SelectorList node.
  2466. *
  2467. * @returns String representation for selector list of regular selectors.
  2468. */
  2469. const selectorListOfRegularsToString = (selectorList) => {
  2470. // if there is no ExtendedSelector in relative SelectorList
  2471. // it means that each Selector node has single child — RegularSelector node
  2472. // and their values should be combined to string
  2473. const standardCssSelectors = selectorList.children.map((selectorNode) => {
  2474. const selectorOnlyChild = getNodeOnlyChild(
  2475. selectorNode,
  2476. "Ast Selector node should have RegularSelector node",
  2477. );
  2478. return getNodeValue(selectorOnlyChild);
  2479. });
  2480. return standardCssSelectors.join(`${COMMA}${SPACE}`);
  2481. };
  2482. /**
  2483. * Updates children of `node` replacing them with `newChildren`.
  2484. * Important: modifies input `node` which is passed by reference.
  2485. *
  2486. * @param node Ast node to update.
  2487. * @param newChildren Array of new children for ast node.
  2488. *
  2489. * @returns Updated ast node.
  2490. */
  2491. const updateNodeChildren = (node, newChildren) => {
  2492. node.children = newChildren;
  2493. return node;
  2494. };
  2495. /**
  2496. * Recursively checks whether the ExtendedSelector node should be optimized.
  2497. * It has to be recursive because RelativePseudoClass has inner SelectorList node.
  2498. *
  2499. * @param currExtendedSelectorNode Ast ExtendedSelector node.
  2500. *
  2501. * @returns True is ExtendedSelector should be optimized.
  2502. */
  2503. const shouldOptimizeExtendedSelector = (currExtendedSelectorNode) => {
  2504. if (currExtendedSelectorNode === null) {
  2505. return false;
  2506. }
  2507. const extendedPseudoClassNode = getPseudoClassNode(
  2508. currExtendedSelectorNode,
  2509. );
  2510. const pseudoName = getNodeName(extendedPseudoClassNode);
  2511. if (isAbsolutePseudoClass(pseudoName)) {
  2512. return false;
  2513. }
  2514. const relativeSelectorList = getRelativeSelectorListNode(
  2515. extendedPseudoClassNode,
  2516. );
  2517. const innerSelectorNodes = relativeSelectorList.children; // simple checking for standard selectors in arg of :not() or :is() pseudo-class
  2518. // e.g. 'div > *:is(div, a, span)'
  2519. if (isOptimizationPseudoClass(pseudoName)) {
  2520. const areAllSelectorNodeChildrenRegular = innerSelectorNodes.every(
  2521. (selectorNode) => {
  2522. try {
  2523. const selectorOnlyChild = getNodeOnlyChild(
  2524. selectorNode,
  2525. "Selector node should have RegularSelector",
  2526. ); // it means that the only child is RegularSelector and it can be optimized
  2527. return isRegularSelectorNode(selectorOnlyChild);
  2528. } catch (e) {
  2529. return false;
  2530. }
  2531. },
  2532. );
  2533. if (areAllSelectorNodeChildrenRegular) {
  2534. return true;
  2535. }
  2536. } // for other extended pseudo-classes than :not() and :is()
  2537. return innerSelectorNodes.some((selectorNode) => {
  2538. return selectorNode.children.some((selectorNodeChild) => {
  2539. if (!isExtendedSelectorNode(selectorNodeChild)) {
  2540. return false;
  2541. } // check inner ExtendedSelector recursively
  2542. // e.g. 'div:has(*:not(.header))'
  2543. return shouldOptimizeExtendedSelector(selectorNodeChild);
  2544. });
  2545. });
  2546. };
  2547. /**
  2548. * Returns optimized ExtendedSelector node if it can be optimized
  2549. * or null if ExtendedSelector is fully optimized while function execution
  2550. * which means that value of `prevRegularSelectorNode` is updated.
  2551. *
  2552. * @param currExtendedSelectorNode Current ExtendedSelector node to optimize.
  2553. * @param prevRegularSelectorNode Previous RegularSelector node.
  2554. *
  2555. * @returns Ast node or null.
  2556. */
  2557. const getOptimizedExtendedSelector = (
  2558. currExtendedSelectorNode,
  2559. prevRegularSelectorNode,
  2560. ) => {
  2561. if (!currExtendedSelectorNode) {
  2562. return null;
  2563. }
  2564. const extendedPseudoClassNode = getPseudoClassNode(
  2565. currExtendedSelectorNode,
  2566. );
  2567. const relativeSelectorList = getRelativeSelectorListNode(
  2568. extendedPseudoClassNode,
  2569. );
  2570. const hasInnerExtendedSelector =
  2571. hasExtendedSelector(relativeSelectorList);
  2572. if (!hasInnerExtendedSelector) {
  2573. // if there is no extended selectors for :not() or :is()
  2574. // e.g. 'div:not(.content, .main)'
  2575. const relativeSelectorListStr =
  2576. selectorListOfRegularsToString(relativeSelectorList);
  2577. const pseudoName = getNodeName(extendedPseudoClassNode); // eslint-disable-next-line max-len
  2578. const optimizedExtendedStr = `${COLON}${pseudoName}${BRACKET.PARENTHESES.LEFT}${relativeSelectorListStr}${BRACKET.PARENTHESES.RIGHT}`;
  2579. prevRegularSelectorNode.value = `${getNodeValue(prevRegularSelectorNode)}${optimizedExtendedStr}`;
  2580. return null;
  2581. } // eslint-disable-next-line @typescript-eslint/no-use-before-define
  2582. const optimizedRelativeSelectorList =
  2583. optimizeSelectorListNode(relativeSelectorList);
  2584. const optimizedExtendedPseudoClassNode = updateNodeChildren(
  2585. extendedPseudoClassNode,
  2586. [optimizedRelativeSelectorList],
  2587. );
  2588. return updateNodeChildren(currExtendedSelectorNode, [
  2589. optimizedExtendedPseudoClassNode,
  2590. ]);
  2591. };
  2592. /**
  2593. * Combines values of `previous` and `current` RegularSelector nodes.
  2594. * It may happen during the optimization when ExtendedSelector between RegularSelector node was optimized.
  2595. *
  2596. * @param current Current RegularSelector node.
  2597. * @param previous Previous RegularSelector node.
  2598. */
  2599. const optimizeCurrentRegularSelector = (current, previous) => {
  2600. previous.value = `${getNodeValue(previous)}${SPACE}${getNodeValue(current)}`;
  2601. };
  2602. /**
  2603. * Optimizes ast Selector node.
  2604. *
  2605. * @param selectorNode Ast Selector node.
  2606. *
  2607. * @returns Optimized ast node.
  2608. * @throws An error while collecting optimized nodes.
  2609. */
  2610. const optimizeSelectorNode = (selectorNode) => {
  2611. // non-optimized list of SelectorNode children
  2612. const rawSelectorNodeChildren = selectorNode.children; // for collecting optimized children list
  2613. const optimizedChildrenList = [];
  2614. let currentIndex = 0; // iterate through all children in non-optimized ast Selector node
  2615. while (currentIndex < rawSelectorNodeChildren.length) {
  2616. const currentChild = getItemByIndex(
  2617. rawSelectorNodeChildren,
  2618. currentIndex,
  2619. "currentChild should be specified",
  2620. ); // no need to optimize the very first child which is always RegularSelector node
  2621. if (currentIndex === 0) {
  2622. optimizedChildrenList.push(currentChild);
  2623. } else {
  2624. const prevRegularChild = getLastRegularChild(optimizedChildrenList);
  2625. if (isExtendedSelectorNode(currentChild)) {
  2626. // start checking with point is null
  2627. let optimizedExtendedSelector = null; // check whether the optimization is needed
  2628. let isOptimizationNeeded =
  2629. shouldOptimizeExtendedSelector(currentChild); // update optimizedExtendedSelector so it can be optimized recursively
  2630. // i.e. `getOptimizedExtendedSelector(optimizedExtendedSelector)` below
  2631. optimizedExtendedSelector = currentChild;
  2632. while (isOptimizationNeeded) {
  2633. // recursively optimize ExtendedSelector until no optimization needed
  2634. // e.g. div > *:is(.banner:not(.block))
  2635. optimizedExtendedSelector = getOptimizedExtendedSelector(
  2636. optimizedExtendedSelector,
  2637. prevRegularChild,
  2638. );
  2639. isOptimizationNeeded = shouldOptimizeExtendedSelector(
  2640. optimizedExtendedSelector,
  2641. );
  2642. } // if it was simple :not() of :is() with standard selector arg
  2643. // e.g. 'div:not([class][id])'
  2644. // or '.main > *:is([data-loaded], .banner)'
  2645. // after the optimization the ExtendedSelector node become part of RegularSelector
  2646. // so nothing to save eventually
  2647. // otherwise the optimized ExtendedSelector should be saved
  2648. // e.g. 'div:has(:not([class]))'
  2649. if (optimizedExtendedSelector !== null) {
  2650. optimizedChildrenList.push(optimizedExtendedSelector); // if optimization is not needed
  2651. const optimizedPseudoClass = getPseudoClassNode(
  2652. optimizedExtendedSelector,
  2653. );
  2654. const optimizedPseudoName = getNodeName(optimizedPseudoClass); // parent element checking is used to apply :is() and :not() pseudo-classes as extended.
  2655. // as there is no parentNode for root element (html)
  2656. // so element selection should be limited to it's children
  2657. // e.g. '*:is(:has(.page))' -> 'html *:is(has(.page))'
  2658. // or '*:not(:has(span))' -> 'html *:not(:has(span))'
  2659. if (
  2660. getNodeValue(prevRegularChild) === ASTERISK &&
  2661. isOptimizationPseudoClass(optimizedPseudoName)
  2662. ) {
  2663. prevRegularChild.value = IS_OR_NOT_PSEUDO_SELECTING_ROOT;
  2664. }
  2665. }
  2666. } else if (isRegularSelectorNode(currentChild)) {
  2667. // in non-optimized ast, RegularSelector node may follow ExtendedSelector which should be optimized
  2668. // for example, for 'div:not(.content) > .banner' schematically it looks like
  2669. // non-optimized ast: [
  2670. // 1. RegularSelector: 'div'
  2671. // 2. ExtendedSelector: 'not(.content)'
  2672. // 3. RegularSelector: '> .banner'
  2673. // ]
  2674. // which after the ExtendedSelector looks like
  2675. // partly optimized ast: [
  2676. // 1. RegularSelector: 'div:not(.content)'
  2677. // 2. RegularSelector: '> .banner'
  2678. // ]
  2679. // so second RegularSelector value should be combined with first one
  2680. // optimized ast: [
  2681. // 1. RegularSelector: 'div:not(.content) > .banner'
  2682. // ]
  2683. // here we check **children of selectorNode** after previous optimization if it was
  2684. const lastOptimizedChild = getLast(optimizedChildrenList) || null;
  2685. if (isRegularSelectorNode(lastOptimizedChild)) {
  2686. optimizeCurrentRegularSelector(currentChild, prevRegularChild);
  2687. }
  2688. }
  2689. }
  2690. currentIndex += 1;
  2691. }
  2692. return updateNodeChildren(selectorNode, optimizedChildrenList);
  2693. };
  2694. /**
  2695. * Optimizes ast SelectorList node.
  2696. *
  2697. * @param selectorListNode SelectorList node.
  2698. *
  2699. * @returns Optimized ast node.
  2700. */
  2701. const optimizeSelectorListNode = (selectorListNode) => {
  2702. return updateNodeChildren(
  2703. selectorListNode,
  2704. selectorListNode.children.map((s) => optimizeSelectorNode(s)),
  2705. );
  2706. };
  2707. /**
  2708. * Optimizes ast:
  2709. * If arg of :not() and :is() pseudo-classes does not contain extended selectors,
  2710. * native Document.querySelectorAll() can be used to query elements.
  2711. * It means that ExtendedSelector ast nodes can be removed
  2712. * and value of relevant RegularSelector node should be updated accordingly.
  2713. *
  2714. * @param ast Non-optimized ast.
  2715. *
  2716. * @returns Optimized ast.
  2717. */
  2718. const optimizeAst = (ast) => {
  2719. // ast is basically the selector list of selectors
  2720. return optimizeSelectorListNode(ast);
  2721. };
  2722. // https://github.com/AdguardTeam/ExtendedCss/issues/115
  2723. const XPATH_PSEUDO_SELECTING_ROOT = "body";
  2724. const NO_WHITESPACE_ERROR_PREFIX =
  2725. "No white space is allowed before or after extended pseudo-class name in selector";
  2726. /**
  2727. * Parses selector into ast for following element selection.
  2728. *
  2729. * @param selector Selector to parse.
  2730. *
  2731. * @returns Parsed ast.
  2732. * @throws An error on invalid selector.
  2733. */
  2734. const parse = (selector) => {
  2735. const tokens = tokenizeSelector(selector);
  2736. const context = {
  2737. ast: null,
  2738. pathToBufferNode: [],
  2739. extendedPseudoNamesStack: [],
  2740. extendedPseudoBracketsStack: [],
  2741. standardPseudoNamesStack: [],
  2742. standardPseudoBracketsStack: [],
  2743. isAttributeBracketsOpen: false,
  2744. attributeBuffer: "",
  2745. isRegexpOpen: false,
  2746. shouldOptimize: false,
  2747. };
  2748. let i = 0;
  2749. while (i < tokens.length) {
  2750. const token = tokens[i];
  2751. if (!token) {
  2752. break;
  2753. } // Token to process
  2754. const { type: tokenType, value: tokenValue } = token; // needed for SPACE and COLON tokens checking
  2755. const nextToken = tokens[i + 1];
  2756. const nextTokenType =
  2757. nextToken === null || nextToken === void 0 ? void 0 : nextToken.type;
  2758. const nextTokenValue =
  2759. nextToken === null || nextToken === void 0 ? void 0 : nextToken.value; // needed for limitations
  2760. // - :not() and :is() root element
  2761. // - :has() usage
  2762. // - white space before and after pseudo-class name
  2763. const nextToNextToken = tokens[i + 2];
  2764. const nextToNextTokenValue =
  2765. nextToNextToken === null || nextToNextToken === void 0
  2766. ? void 0
  2767. : nextToNextToken.value; // needed for COLON token checking for none-specified regular selector before extended one
  2768. // e.g. 'p, :hover'
  2769. // or '.banner, :contains(ads)'
  2770. const previousToken = tokens[i - 1];
  2771. const prevTokenType =
  2772. previousToken === null || previousToken === void 0
  2773. ? void 0
  2774. : previousToken.type;
  2775. const prevTokenValue =
  2776. previousToken === null || previousToken === void 0
  2777. ? void 0
  2778. : previousToken.value; // needed for proper parsing of regexp pattern arg
  2779. // e.g. ':matches-css(background-image: /^url\(https:\/\/example\.org\//)'
  2780. const previousToPreviousToken = tokens[i - 2];
  2781. const prevToPrevTokenValue =
  2782. previousToPreviousToken === null || previousToPreviousToken === void 0
  2783. ? void 0
  2784. : previousToPreviousToken.value;
  2785. let bufferNode = getBufferNode(context);
  2786. switch (tokenType) {
  2787. case TOKEN_TYPE.WORD:
  2788. if (bufferNode === null) {
  2789. // there is no buffer node only in one case — no ast collecting has been started
  2790. initAst(context, tokenValue);
  2791. } else if (isSelectorListNode(bufferNode)) {
  2792. // add new selector to selector list
  2793. addAstNodeByType(context, NODE.SELECTOR);
  2794. addAstNodeByType(context, NODE.REGULAR_SELECTOR, tokenValue);
  2795. } else if (isRegularSelectorNode(bufferNode)) {
  2796. updateBufferNode(context, tokenValue);
  2797. } else if (isExtendedSelectorNode(bufferNode)) {
  2798. // No white space is allowed between the name of extended pseudo-class
  2799. // and its opening parenthesis
  2800. // https://www.w3.org/TR/selectors-4/#pseudo-classes
  2801. // e.g. 'span:contains (text)'
  2802. if (
  2803. isWhiteSpaceChar(nextTokenValue) &&
  2804. nextToNextTokenValue === BRACKET.PARENTHESES.LEFT
  2805. ) {
  2806. throw new Error(`${NO_WHITESPACE_ERROR_PREFIX}: '${selector}'`);
  2807. }
  2808. const lowerCaseTokenValue = tokenValue.toLowerCase(); // save pseudo-class name for brackets balance checking
  2809. context.extendedPseudoNamesStack.push(lowerCaseTokenValue); // extended pseudo-class name are parsed in lower case
  2810. // as they should be case-insensitive
  2811. // https://www.w3.org/TR/selectors-4/#pseudo-classes
  2812. if (isAbsolutePseudoClass(lowerCaseTokenValue)) {
  2813. addAstNodeByType(
  2814. context,
  2815. NODE.ABSOLUTE_PSEUDO_CLASS,
  2816. lowerCaseTokenValue,
  2817. );
  2818. } else {
  2819. // if it is not absolute pseudo-class, it must be relative one
  2820. // add RelativePseudoClass with tokenValue as pseudo-class name to ExtendedSelector children
  2821. addAstNodeByType(
  2822. context,
  2823. NODE.RELATIVE_PSEUDO_CLASS,
  2824. lowerCaseTokenValue,
  2825. ); // for :not() and :is() pseudo-classes parsed ast should be optimized later
  2826. if (isOptimizationPseudoClass(lowerCaseTokenValue)) {
  2827. context.shouldOptimize = true;
  2828. }
  2829. }
  2830. } else if (isAbsolutePseudoClassNode(bufferNode)) {
  2831. // collect absolute pseudo-class arg
  2832. updateBufferNode(context, tokenValue);
  2833. } else if (isRelativePseudoClassNode(bufferNode)) {
  2834. initRelativeSubtree(context, tokenValue);
  2835. }
  2836. break;
  2837. case TOKEN_TYPE.MARK:
  2838. switch (tokenValue) {
  2839. case COMMA:
  2840. if (
  2841. !bufferNode ||
  2842. (typeof bufferNode !== "undefined" && !nextTokenValue)
  2843. ) {
  2844. // consider the selector is invalid if there is no bufferNode yet (e.g. ', a')
  2845. // or there is nothing after the comma while bufferNode is defined (e.g. 'div, ')
  2846. throw new Error(`'${selector}' is not a valid selector`);
  2847. } else if (isRegularSelectorNode(bufferNode)) {
  2848. if (context.isAttributeBracketsOpen) {
  2849. // the comma might be inside element attribute value
  2850. // e.g. 'div[data-comma="0,1"]'
  2851. updateBufferNode(context, tokenValue);
  2852. } else {
  2853. // new Selector should be collected to upper SelectorList
  2854. upToClosest(context, NODE.SELECTOR_LIST);
  2855. }
  2856. } else if (isAbsolutePseudoClassNode(bufferNode)) {
  2857. // the comma inside arg of absolute extended pseudo
  2858. // e.g. 'div:xpath(//h3[contains(text(),"Share it!")]/..)'
  2859. updateBufferNode(context, tokenValue);
  2860. } else if (isSelectorNode(bufferNode)) {
  2861. // new Selector should be collected to upper SelectorList
  2862. // if parser position is on Selector node
  2863. upToClosest(context, NODE.SELECTOR_LIST);
  2864. }
  2865. break;
  2866. case SPACE:
  2867. // it might be complex selector with extended pseudo-class inside it
  2868. // and the space is between that complex selector and following regular selector
  2869. // parser position is on ` ` before `span` now:
  2870. // e.g. 'div:has(img).banner span'
  2871. // so we need to check whether the new ast node should be added (example above)
  2872. // or previous regular selector node should be updated
  2873. if (
  2874. isRegularSelectorNode(bufferNode) && // no need to update the buffer node if attribute value is being parsed
  2875. // e.g. 'div:not([id])[style="position: absolute; z-index: 10000;"]'
  2876. // parser position inside attribute ↑
  2877. !context.isAttributeBracketsOpen
  2878. ) {
  2879. bufferNode = getUpdatedBufferNode(context);
  2880. }
  2881. if (isRegularSelectorNode(bufferNode)) {
  2882. // standard selectors with white space between colon and name of pseudo
  2883. // are invalid for native document.querySelectorAll() anyway,
  2884. // so throwing the error here is better
  2885. // than proper parsing of invalid selector and passing it further.
  2886. // first of all do not check attributes
  2887. // e.g. div[style="text-align: center"]
  2888. if (
  2889. !context.isAttributeBracketsOpen && // check the space after the colon and before the pseudo
  2890. // e.g. '.block: nth-child(2)
  2891. ((prevTokenValue === COLON &&
  2892. nextTokenType === TOKEN_TYPE.WORD) || // or after the pseudo and before the opening parenthesis
  2893. // e.g. '.block:nth-child (2)
  2894. (prevTokenType === TOKEN_TYPE.WORD &&
  2895. nextTokenValue === BRACKET.PARENTHESES.LEFT))
  2896. ) {
  2897. throw new Error(`'${selector}' is not a valid selector`);
  2898. } // collect current tokenValue to value of RegularSelector
  2899. // if it is the last token or standard selector continues after the space.
  2900. // otherwise it will be skipped
  2901. if (
  2902. !nextTokenValue ||
  2903. doesRegularContinueAfterSpace(
  2904. nextTokenType,
  2905. nextTokenValue,
  2906. ) || // we also should collect space inside attribute value
  2907. // e.g. `[onclick^="window.open ('https://example.com/share?url="]`
  2908. // parser position ↑
  2909. context.isAttributeBracketsOpen
  2910. ) {
  2911. updateBufferNode(context, tokenValue);
  2912. }
  2913. }
  2914. if (isAbsolutePseudoClassNode(bufferNode)) {
  2915. // space inside extended pseudo-class arg
  2916. // e.g. 'span:contains(some text)'
  2917. updateBufferNode(context, tokenValue);
  2918. }
  2919. if (isRelativePseudoClassNode(bufferNode)) {
  2920. // init with empty value RegularSelector
  2921. // as the space is not needed for selector value
  2922. // e.g. 'p:not( .content )'
  2923. initRelativeSubtree(context);
  2924. }
  2925. if (isSelectorNode(bufferNode)) {
  2926. // do NOT add RegularSelector if parser position on space BEFORE the comma in selector list
  2927. // e.g. '.block:has(> img) , .banner)'
  2928. if (
  2929. doesRegularContinueAfterSpace(nextTokenType, nextTokenValue)
  2930. ) {
  2931. // regular selector might be after the extended one.
  2932. // extra space before combinator or selector should not be collected
  2933. // e.g. '.banner:upward(2) .block'
  2934. // '.banner:upward(2) > .block'
  2935. // so no tokenValue passed to addAnySelectorNode()
  2936. addAstNodeByType(context, NODE.REGULAR_SELECTOR);
  2937. }
  2938. }
  2939. break;
  2940. case DESCENDANT_COMBINATOR:
  2941. case CHILD_COMBINATOR:
  2942. case NEXT_SIBLING_COMBINATOR:
  2943. case SUBSEQUENT_SIBLING_COMBINATOR:
  2944. case SEMICOLON:
  2945. case SLASH:
  2946. case BACKSLASH:
  2947. case SINGLE_QUOTE:
  2948. case DOUBLE_QUOTE:
  2949. case CARET:
  2950. case DOLLAR_SIGN:
  2951. case BRACKET.CURLY.LEFT:
  2952. case BRACKET.CURLY.RIGHT:
  2953. case ASTERISK:
  2954. case ID_MARKER:
  2955. case CLASS_MARKER:
  2956. case BRACKET.SQUARE.LEFT:
  2957. // it might be complex selector with extended pseudo-class inside it
  2958. // and the space is between that complex selector and following regular selector
  2959. // e.g. 'div:has(img).banner' // parser position is on `.` before `banner` now
  2960. // 'div:has(img)[attr]' // parser position is on `[` before `attr` now
  2961. // so we need to check whether the new ast node should be added (example above)
  2962. // or previous regular selector node should be updated
  2963. if (COMBINATORS.includes(tokenValue)) {
  2964. if (bufferNode === null) {
  2965. // cases where combinator at very beginning of a selector
  2966. // e.g. '> div'
  2967. // or '~ .banner'
  2968. // or even '+js(overlay-buster)' which not a selector at all
  2969. // but may be validated by FilterCompiler so error message should be appropriate
  2970. throw new Error(`'${selector}' is not a valid selector`);
  2971. }
  2972. bufferNode = getUpdatedBufferNode(context);
  2973. }
  2974. if (bufferNode === null) {
  2975. // no ast collecting has been started
  2976. // e.g. '.banner > p'
  2977. // or '#top > div.ad'
  2978. // or '[class][style][attr]'
  2979. // or '*:not(span)'
  2980. initAst(context, tokenValue);
  2981. if (isAttributeOpening(tokenValue, prevTokenValue)) {
  2982. // e.g. '[class^="banner-"]'
  2983. context.isAttributeBracketsOpen = true;
  2984. }
  2985. } else if (isRegularSelectorNode(bufferNode)) {
  2986. if (
  2987. tokenValue === BRACKET.CURLY.LEFT &&
  2988. !(context.isAttributeBracketsOpen || context.isRegexpOpen)
  2989. ) {
  2990. // e.g. 'div { content: "'
  2991. throw new Error(`'${selector}' is not a valid selector`);
  2992. } // collect the mark to the value of RegularSelector node
  2993. updateBufferNode(context, tokenValue);
  2994. if (isAttributeOpening(tokenValue, prevTokenValue)) {
  2995. // needed for proper handling element attribute value with comma
  2996. // e.g. 'div[data-comma="0,1"]'
  2997. context.isAttributeBracketsOpen = true;
  2998. }
  2999. } else if (isAbsolutePseudoClassNode(bufferNode)) {
  3000. // collect the mark to the arg of AbsolutePseudoClass node
  3001. updateBufferNode(context, tokenValue); // 'isRegexpOpen' flag is needed for brackets balancing inside extended pseudo-class arg
  3002. if (
  3003. tokenValue === SLASH &&
  3004. context.extendedPseudoNamesStack.length > 0
  3005. ) {
  3006. if (
  3007. prevTokenValue === SLASH &&
  3008. prevToPrevTokenValue === BACKSLASH
  3009. ) {
  3010. // it may be specific url regexp pattern in arg of pseudo-class
  3011. // e.g. ':matches-css(background-image: /^url\(https:\/\/example\.org\//)'
  3012. // parser position is on final slash before `)` ↑
  3013. context.isRegexpOpen = false;
  3014. } else if (prevTokenValue && prevTokenValue !== BACKSLASH) {
  3015. if (
  3016. isRegexpOpening(
  3017. context,
  3018. prevTokenValue,
  3019. getNodeValue(bufferNode),
  3020. )
  3021. ) {
  3022. context.isRegexpOpen = !context.isRegexpOpen;
  3023. } else {
  3024. // otherwise force `isRegexpOpen` flag to `false`
  3025. context.isRegexpOpen = false;
  3026. }
  3027. }
  3028. }
  3029. } else if (isRelativePseudoClassNode(bufferNode)) {
  3030. // add SelectorList to children of RelativePseudoClass node
  3031. initRelativeSubtree(context, tokenValue);
  3032. if (isAttributeOpening(tokenValue, prevTokenValue)) {
  3033. // besides of creating the relative subtree
  3034. // opening square bracket means start of attribute
  3035. // e.g. 'div:not([class="content"])'
  3036. // 'div:not([href*="window.print()"])'
  3037. context.isAttributeBracketsOpen = true;
  3038. }
  3039. } else if (isSelectorNode(bufferNode)) {
  3040. // after the extended pseudo closing parentheses
  3041. // parser position is on Selector node
  3042. // and regular selector can be after the extended one
  3043. // e.g. '.banner:upward(2)> .block'
  3044. // or '.inner:nth-ancestor(1)~ .banner'
  3045. if (COMBINATORS.includes(tokenValue)) {
  3046. addAstNodeByType(
  3047. context,
  3048. NODE.REGULAR_SELECTOR,
  3049. tokenValue,
  3050. );
  3051. } else if (!context.isRegexpOpen) {
  3052. // it might be complex selector with extended pseudo-class inside it.
  3053. // parser position is on `.` now:
  3054. // e.g. 'div:has(img).banner'
  3055. // so we need to get last regular selector node and update its value
  3056. bufferNode = getContextLastRegularSelectorNode(context);
  3057. updateBufferNode(context, tokenValue);
  3058. if (isAttributeOpening(tokenValue, prevTokenValue)) {
  3059. // handle attribute in compound selector after extended pseudo-class
  3060. // e.g. 'div:not(.top)[style="z-index: 10000;"]'
  3061. // parser position ↑
  3062. context.isAttributeBracketsOpen = true;
  3063. }
  3064. }
  3065. } else if (isSelectorListNode(bufferNode)) {
  3066. // add Selector to SelectorList
  3067. addAstNodeByType(context, NODE.SELECTOR); // and RegularSelector as it is always the first child of Selector
  3068. addAstNodeByType(context, NODE.REGULAR_SELECTOR, tokenValue);
  3069. if (isAttributeOpening(tokenValue, prevTokenValue)) {
  3070. // handle simple attribute selector in selector list
  3071. // e.g. '.banner, [class^="ad-"]'
  3072. context.isAttributeBracketsOpen = true;
  3073. }
  3074. }
  3075. break;
  3076. case BRACKET.SQUARE.RIGHT:
  3077. if (isRegularSelectorNode(bufferNode)) {
  3078. // unescaped `]` in regular selector allowed only inside attribute value
  3079. if (
  3080. !context.isAttributeBracketsOpen &&
  3081. prevTokenValue !== BACKSLASH
  3082. ) {
  3083. // e.g. 'div]'
  3084. // eslint-disable-next-line max-len
  3085. throw new Error(
  3086. `'${selector}' is not a valid selector due to '${tokenValue}' after '${getNodeValue(bufferNode)}'`,
  3087. );
  3088. } // needed for proper parsing regular selectors after the attributes with comma
  3089. // e.g. 'div[data-comma="0,1"] > img'
  3090. if (isAttributeClosing(context)) {
  3091. context.isAttributeBracketsOpen = false; // reset attribute buffer on closing `]`
  3092. context.attributeBuffer = "";
  3093. } // collect the bracket to the value of RegularSelector node
  3094. updateBufferNode(context, tokenValue);
  3095. }
  3096. if (isAbsolutePseudoClassNode(bufferNode)) {
  3097. // :xpath() expended pseudo-class arg might contain square bracket
  3098. // so it should be collected
  3099. // e.g. 'div:xpath(//h3[contains(text(),"Share it!")]/..)'
  3100. updateBufferNode(context, tokenValue);
  3101. }
  3102. break;
  3103. case COLON:
  3104. // No white space is allowed between the colon and the following name of the pseudo-class
  3105. // https://www.w3.org/TR/selectors-4/#pseudo-classes
  3106. // e.g. 'span: contains(text)'
  3107. if (
  3108. isWhiteSpaceChar(nextTokenValue) &&
  3109. nextToNextTokenValue &&
  3110. SUPPORTED_PSEUDO_CLASSES.includes(nextToNextTokenValue)
  3111. ) {
  3112. throw new Error(
  3113. `${NO_WHITESPACE_ERROR_PREFIX}: '${selector}'`,
  3114. );
  3115. }
  3116. if (bufferNode === null) {
  3117. // no ast collecting has been started
  3118. if (nextTokenValue === XPATH_PSEUDO_CLASS_MARKER) {
  3119. // limit applying of "naked" :xpath pseudo-class
  3120. // https://github.com/AdguardTeam/ExtendedCss/issues/115
  3121. initAst(context, XPATH_PSEUDO_SELECTING_ROOT);
  3122. } else if (
  3123. nextTokenValue === UPWARD_PSEUDO_CLASS_MARKER ||
  3124. nextTokenValue === NTH_ANCESTOR_PSEUDO_CLASS_MARKER
  3125. ) {
  3126. // selector should be specified before :nth-ancestor() or :upward()
  3127. // e.g. ':nth-ancestor(3)'
  3128. // or ':upward(span)'
  3129. throw new Error(
  3130. `${NO_SELECTOR_ERROR_PREFIX} before :${nextTokenValue}() pseudo-class`,
  3131. );
  3132. } else {
  3133. // make it more obvious if selector starts with pseudo with no tag specified
  3134. // e.g. ':has(a)' -> '*:has(a)'
  3135. // or ':empty' -> '*:empty'
  3136. initAst(context, ASTERISK);
  3137. } // bufferNode should be updated for following checking
  3138. bufferNode = getBufferNode(context);
  3139. }
  3140. if (isSelectorListNode(bufferNode)) {
  3141. // bufferNode is SelectorList after comma has been parsed.
  3142. // parser position is on colon now:
  3143. // e.g. 'img,:not(.content)'
  3144. addAstNodeByType(context, NODE.SELECTOR); // add empty value RegularSelector anyway as any selector should start with it
  3145. // and check previous token on the next step
  3146. addAstNodeByType(context, NODE.REGULAR_SELECTOR); // bufferNode should be updated for following checking
  3147. bufferNode = getBufferNode(context);
  3148. }
  3149. if (isRegularSelectorNode(bufferNode)) {
  3150. // it can be extended or standard pseudo
  3151. // e.g. '#share, :contains(share it)'
  3152. // or 'div,:hover'
  3153. // of 'div:has(+:contains(text))' // position is after '+'
  3154. if (
  3155. (prevTokenValue && COMBINATORS.includes(prevTokenValue)) ||
  3156. prevTokenValue === COMMA
  3157. ) {
  3158. // case with colon at the start of string - e.g. ':contains(text)'
  3159. // is covered by 'bufferNode === null' above at start of COLON checking
  3160. updateBufferNode(context, ASTERISK);
  3161. }
  3162. handleNextTokenOnColon(
  3163. context,
  3164. selector,
  3165. tokenValue,
  3166. nextTokenValue,
  3167. nextToNextTokenValue,
  3168. );
  3169. }
  3170. if (isSelectorNode(bufferNode)) {
  3171. // e.g. 'div:contains(text):'
  3172. if (!nextTokenValue) {
  3173. throw new Error(
  3174. `Invalid colon ':' at the end of selector: '${selector}'`,
  3175. );
  3176. } // after the extended pseudo closing parentheses
  3177. // parser position is on Selector node
  3178. // and there is might be another extended selector.
  3179. // parser position is on colon before 'upward':
  3180. // e.g. 'p:contains(PR):upward(2)'
  3181. if (isSupportedPseudoClass(nextTokenValue.toLowerCase())) {
  3182. // if supported extended pseudo-class is next to colon
  3183. // add ExtendedSelector to Selector children
  3184. addAstNodeByType(context, NODE.EXTENDED_SELECTOR);
  3185. } else if (
  3186. nextTokenValue.toLowerCase() === REMOVE_PSEUDO_MARKER
  3187. ) {
  3188. // :remove() pseudo-class should be handled before
  3189. // as it is not about element selecting but actions with elements
  3190. // e.g. '#banner:upward(2):remove()'
  3191. throw new Error(
  3192. `${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${selector}'`,
  3193. );
  3194. } else {
  3195. // otherwise it is standard pseudo after extended pseudo-class in complex selector
  3196. // and colon should be collected to value of previous RegularSelector
  3197. // e.g. 'body *:not(input)::selection'
  3198. // 'input:matches-css(padding: 10):checked'
  3199. bufferNode = getContextLastRegularSelectorNode(context);
  3200. handleNextTokenOnColon(
  3201. context,
  3202. selector,
  3203. tokenValue,
  3204. nextTokenType,
  3205. nextToNextTokenValue,
  3206. );
  3207. }
  3208. }
  3209. if (isAbsolutePseudoClassNode(bufferNode)) {
  3210. // :xpath() pseudo-class should be the last of extended pseudo-classes
  3211. if (
  3212. getNodeName(bufferNode) === XPATH_PSEUDO_CLASS_MARKER &&
  3213. nextTokenValue &&
  3214. SUPPORTED_PSEUDO_CLASSES.includes(nextTokenValue) &&
  3215. nextToNextTokenValue === BRACKET.PARENTHESES.LEFT
  3216. ) {
  3217. throw new Error(
  3218. `:xpath() pseudo-class should be the last in selector: '${selector}'`,
  3219. );
  3220. } // collecting arg for absolute pseudo-class
  3221. // e.g. 'div:matches-css(width:400px)'
  3222. updateBufferNode(context, tokenValue);
  3223. }
  3224. if (isRelativePseudoClassNode(bufferNode)) {
  3225. if (!nextTokenValue) {
  3226. // e.g. 'div:has(:'
  3227. throw new Error(
  3228. `Invalid pseudo-class arg at the end of selector: '${selector}'`,
  3229. );
  3230. } // make it more obvious if selector starts with pseudo with no tag specified
  3231. // parser position is on colon inside :has() arg
  3232. // e.g. 'div:has(:contains(text))'
  3233. // or 'div:not(:empty)'
  3234. initRelativeSubtree(context, ASTERISK);
  3235. if (!isSupportedPseudoClass(nextTokenValue.toLowerCase())) {
  3236. // collect the colon to value of RegularSelector
  3237. // e.g. 'div:not(:empty)'
  3238. updateBufferNode(context, tokenValue); // parentheses should be balanced only for functional pseudo-classes
  3239. // e.g. '.yellow:not(:nth-child(3))'
  3240. if (nextToNextTokenValue === BRACKET.PARENTHESES.LEFT) {
  3241. context.standardPseudoNamesStack.push(nextTokenValue);
  3242. }
  3243. } else {
  3244. // add ExtendedSelector to Selector children
  3245. // e.g. 'div:has(:contains(text))'
  3246. upToClosest(context, NODE.SELECTOR);
  3247. addAstNodeByType(context, NODE.EXTENDED_SELECTOR);
  3248. }
  3249. }
  3250. break;
  3251. case BRACKET.PARENTHESES.LEFT:
  3252. // start of pseudo-class arg
  3253. if (isAbsolutePseudoClassNode(bufferNode)) {
  3254. // no brackets balancing needed inside
  3255. // 1. :xpath() extended pseudo-class arg
  3256. // 2. regexp arg for other extended pseudo-classes
  3257. if (
  3258. getNodeName(bufferNode) !== XPATH_PSEUDO_CLASS_MARKER &&
  3259. context.isRegexpOpen
  3260. ) {
  3261. // if the parentheses is escaped it should be part of regexp
  3262. // collect it to arg of AbsolutePseudoClass
  3263. // e.g. 'div:matches-css(background-image: /^url\\("data:image\\/gif;base64.+/)'
  3264. updateBufferNode(context, tokenValue);
  3265. } else {
  3266. // otherwise brackets should be balanced
  3267. // e.g. 'div:xpath(//h3[contains(text(),"Share it!")]/..)'
  3268. context.extendedPseudoBracketsStack.push(tokenValue); // eslint-disable-next-line max-len
  3269. if (
  3270. context.extendedPseudoBracketsStack.length >
  3271. context.extendedPseudoNamesStack.length
  3272. ) {
  3273. updateBufferNode(context, tokenValue);
  3274. }
  3275. }
  3276. }
  3277. if (isRegularSelectorNode(bufferNode)) {
  3278. // continue RegularSelector value collecting for standard pseudo-classes
  3279. // e.g. '.banner:where(div)'
  3280. if (context.standardPseudoNamesStack.length > 0) {
  3281. updateBufferNode(context, tokenValue);
  3282. context.standardPseudoBracketsStack.push(tokenValue);
  3283. } // parentheses inside attribute value should be part of RegularSelector value
  3284. // e.g. 'div:not([href*="window.print()"])' <-- parser position
  3285. // is on the `(` after `print` ↑
  3286. if (context.isAttributeBracketsOpen) {
  3287. updateBufferNode(context, tokenValue);
  3288. }
  3289. }
  3290. if (isRelativePseudoClassNode(bufferNode)) {
  3291. // save opening bracket for balancing
  3292. // e.g. 'div:not()' // position is on `(`
  3293. context.extendedPseudoBracketsStack.push(tokenValue);
  3294. }
  3295. break;
  3296. case BRACKET.PARENTHESES.RIGHT:
  3297. if (isAbsolutePseudoClassNode(bufferNode)) {
  3298. // no brackets balancing needed inside
  3299. // 1. :xpath() extended pseudo-class arg
  3300. // 2. regexp arg for other extended pseudo-classes
  3301. if (
  3302. getNodeName(bufferNode) !== XPATH_PSEUDO_CLASS_MARKER &&
  3303. context.isRegexpOpen
  3304. ) {
  3305. // if closing bracket is part of regexp
  3306. // simply save it to pseudo-class arg
  3307. updateBufferNode(context, tokenValue);
  3308. } else {
  3309. // remove stacked open parentheses for brackets balance
  3310. // e.g. 'h3:contains((Ads))'
  3311. // or 'div:xpath(//h3[contains(text(),"Share it!")]/..)'
  3312. context.extendedPseudoBracketsStack.pop();
  3313. if (getNodeName(bufferNode) !== XPATH_PSEUDO_CLASS_MARKER) {
  3314. // for all other absolute pseudo-classes except :xpath()
  3315. // remove stacked name of extended pseudo-class
  3316. context.extendedPseudoNamesStack.pop(); // eslint-disable-next-line max-len
  3317. if (
  3318. context.extendedPseudoBracketsStack.length >
  3319. context.extendedPseudoNamesStack.length
  3320. ) {
  3321. // if brackets stack is not empty yet,
  3322. // save tokenValue to arg of AbsolutePseudoClass
  3323. // parser position on first closing bracket after 'Ads':
  3324. // e.g. 'h3:contains((Ads))'
  3325. updateBufferNode(context, tokenValue);
  3326. } else if (
  3327. context.extendedPseudoBracketsStack.length >= 0 &&
  3328. context.extendedPseudoNamesStack.length >= 0
  3329. ) {
  3330. // assume it is combined extended pseudo-classes
  3331. // parser position on first closing bracket after 'advert':
  3332. // e.g. 'div:has(.banner, :contains(advert))'
  3333. upToClosest(context, NODE.SELECTOR);
  3334. }
  3335. } else {
  3336. // for :xpath()
  3337. // eslint-disable-next-line max-len
  3338. if (
  3339. context.extendedPseudoBracketsStack.length <
  3340. context.extendedPseudoNamesStack.length
  3341. ) {
  3342. // remove stacked name of extended pseudo-class
  3343. // if there are less brackets than pseudo-class names
  3344. // with means last removes bracket was closing for pseudo-class
  3345. context.extendedPseudoNamesStack.pop();
  3346. } else {
  3347. // otherwise the bracket is part of arg
  3348. updateBufferNode(context, tokenValue);
  3349. }
  3350. }
  3351. }
  3352. }
  3353. if (isRegularSelectorNode(bufferNode)) {
  3354. if (context.isAttributeBracketsOpen) {
  3355. // parentheses inside attribute value should be part of RegularSelector value
  3356. // e.g. 'div:not([href*="window.print()"])' <-- parser position
  3357. // is on the `)` after `print(` ↑
  3358. updateBufferNode(context, tokenValue);
  3359. } else if (
  3360. context.standardPseudoNamesStack.length > 0 &&
  3361. context.standardPseudoBracketsStack.length > 0
  3362. ) {
  3363. // standard pseudo-class was processing.
  3364. // collect the closing bracket to value of RegularSelector
  3365. // parser position is on bracket after 'class' now:
  3366. // e.g. 'div:where(.class)'
  3367. updateBufferNode(context, tokenValue); // remove bracket and pseudo name from stacks
  3368. context.standardPseudoBracketsStack.pop();
  3369. const lastStandardPseudo =
  3370. context.standardPseudoNamesStack.pop();
  3371. if (!lastStandardPseudo) {
  3372. // standard pseudo should be in standardPseudoNamesStack
  3373. // as related to standardPseudoBracketsStack
  3374. throw new Error(
  3375. `Parsing error. Invalid selector: ${selector}`,
  3376. );
  3377. } // Disallow :has() after regular pseudo-elements
  3378. // https://bugs.chromium.org/p/chromium/issues/detail?id=669058#c54 [3]
  3379. if (
  3380. Object.values(REGULAR_PSEUDO_ELEMENTS).includes(
  3381. lastStandardPseudo,
  3382. ) && // check token which is next to closing parentheses and token after it
  3383. // parser position is on bracket after 'foo' now:
  3384. // e.g. '::part(foo):has(.a)'
  3385. nextTokenValue === COLON &&
  3386. nextToNextTokenValue &&
  3387. HAS_PSEUDO_CLASS_MARKERS.includes(nextToNextTokenValue)
  3388. ) {
  3389. // eslint-disable-next-line max-len
  3390. throw new Error(
  3391. `Usage of :${nextToNextTokenValue}() pseudo-class is not allowed after any regular pseudo-element: '${lastStandardPseudo}'`,
  3392. );
  3393. }
  3394. } else {
  3395. // extended pseudo-class was processing.
  3396. // e.g. 'div:has(h3)'
  3397. // remove bracket and pseudo name from stacks
  3398. context.extendedPseudoBracketsStack.pop();
  3399. context.extendedPseudoNamesStack.pop();
  3400. upToClosest(context, NODE.EXTENDED_SELECTOR); // go to upper selector for possible selector continuation after extended pseudo-class
  3401. // e.g. 'div:has(h3) > img'
  3402. upToClosest(context, NODE.SELECTOR);
  3403. }
  3404. }
  3405. if (isSelectorNode(bufferNode)) {
  3406. // after inner extended pseudo-class bufferNode is Selector.
  3407. // parser position is on last bracket now:
  3408. // e.g. 'div:has(.banner, :contains(ads))'
  3409. context.extendedPseudoBracketsStack.pop();
  3410. context.extendedPseudoNamesStack.pop();
  3411. upToClosest(context, NODE.EXTENDED_SELECTOR);
  3412. upToClosest(context, NODE.SELECTOR);
  3413. }
  3414. if (isRelativePseudoClassNode(bufferNode)) {
  3415. // save opening bracket for balancing
  3416. // e.g. 'div:not()' // position is on `)`
  3417. // context.extendedPseudoBracketsStack.push(tokenValue);
  3418. if (
  3419. context.extendedPseudoNamesStack.length > 0 &&
  3420. context.extendedPseudoBracketsStack.length > 0
  3421. ) {
  3422. context.extendedPseudoBracketsStack.pop();
  3423. context.extendedPseudoNamesStack.pop();
  3424. }
  3425. }
  3426. break;
  3427. case LINE_FEED:
  3428. case FORM_FEED:
  3429. case CARRIAGE_RETURN:
  3430. // such characters at start and end of selector should be trimmed
  3431. // so is there is one them among tokens, it is not valid selector
  3432. throw new Error(`'${selector}' is not a valid selector`);
  3433. case TAB:
  3434. // allow tab only inside attribute value
  3435. // as there are such valid rules in filter lists
  3436. // e.g. 'div[style^="margin-right: auto; text-align: left;',
  3437. // parser position ↑
  3438. if (
  3439. isRegularSelectorNode(bufferNode) &&
  3440. context.isAttributeBracketsOpen
  3441. ) {
  3442. updateBufferNode(context, tokenValue);
  3443. } else {
  3444. // otherwise not valid
  3445. throw new Error(`'${selector}' is not a valid selector`);
  3446. }
  3447. }
  3448. break;
  3449. // no default statement for Marks as they are limited to SUPPORTED_SELECTOR_MARKS
  3450. // and all other symbol combinations are tokenized as Word
  3451. // so error for invalid Word will be thrown later while element selecting by parsed ast
  3452. default:
  3453. throw new Error(`Unknown type of token: '${tokenValue}'`);
  3454. }
  3455. i += 1;
  3456. }
  3457. if (context.ast === null) {
  3458. throw new Error(`'${selector}' is not a valid selector`);
  3459. }
  3460. if (
  3461. context.extendedPseudoNamesStack.length > 0 ||
  3462. context.extendedPseudoBracketsStack.length > 0
  3463. ) {
  3464. // eslint-disable-next-line max-len
  3465. throw new Error(
  3466. `Unbalanced brackets for extended pseudo-class: '${getLast(context.extendedPseudoNamesStack)}'`,
  3467. );
  3468. }
  3469. if (context.isAttributeBracketsOpen) {
  3470. throw new Error(
  3471. `Unbalanced attribute brackets in selector: '${selector}'`,
  3472. );
  3473. }
  3474. return context.shouldOptimize ? optimizeAst(context.ast) : context.ast;
  3475. };
  3476. const natives = {
  3477. MutationObserver:
  3478. window.MutationObserver || window.WebKitMutationObserver,
  3479. };
  3480. /**
  3481. * Class NativeTextContent is needed to intercept and save the native Node textContent getter
  3482. * for proper work of :contains() pseudo-class as it may be mocked.
  3483. *
  3484. * @see {@link https://github.com/AdguardTeam/ExtendedCss/issues/127}
  3485. */
  3486. class NativeTextContent {
  3487. /**
  3488. * Native Node.
  3489. */
  3490. /**
  3491. * Native Node textContent getter.
  3492. */
  3493. /**
  3494. * Stores native node.
  3495. */
  3496. constructor() {
  3497. this.nativeNode = window.Node || Node;
  3498. }
  3499. /**
  3500. * Sets native Node textContext getter to `getter` class field.
  3501. */
  3502. setGetter() {
  3503. var _Object$getOwnPropert;
  3504. this.getter =
  3505. (_Object$getOwnPropert = Object.getOwnPropertyDescriptor(
  3506. this.nativeNode.prototype,
  3507. "textContent",
  3508. )) === null || _Object$getOwnPropert === void 0
  3509. ? void 0
  3510. : _Object$getOwnPropert.get;
  3511. }
  3512. }
  3513. const nativeTextContent = new NativeTextContent();
  3514. /**
  3515. * Returns textContent of passed domElement.
  3516. *
  3517. * @param domElement DOM element.
  3518. *
  3519. * @returns DOM element textContent.
  3520. */
  3521. const getNodeTextContent = (domElement) => {
  3522. if (nativeTextContent.getter) {
  3523. return nativeTextContent.getter.apply(domElement);
  3524. } // if ExtendedCss.init() has not been executed and there is no nodeTextContentGetter,
  3525. // use simple approach, especially when init() is not really needed, e.g. local tests
  3526. return domElement.textContent || "";
  3527. };
  3528. /**
  3529. * Returns element selector text based on it's tagName and attributes.
  3530. *
  3531. * @param element DOM element.
  3532. *
  3533. * @returns String representation of `element`.
  3534. */
  3535. const getElementSelectorDesc = (element) => {
  3536. let selectorText = element.tagName.toLowerCase();
  3537. selectorText += Array.from(element.attributes)
  3538. .map((attr) => {
  3539. return `[${attr.name}="${element.getAttribute(attr.name)}"]`;
  3540. })
  3541. .join("");
  3542. return selectorText;
  3543. };
  3544. /**
  3545. * Returns path to a DOM element as a selector string.
  3546. *
  3547. * @param inputEl Input element.
  3548. *
  3549. * @returns String path to a DOM element.
  3550. * @throws An error if `inputEl` in not instance of `Element`.
  3551. */
  3552. const getElementSelectorPath = (inputEl) => {
  3553. if (!(inputEl instanceof Element)) {
  3554. throw new Error("Function received argument with wrong type");
  3555. }
  3556. let el;
  3557. el = inputEl;
  3558. const path = []; // we need to check '!!el' first because it is possible
  3559. // that some ancestor of the inputEl was removed before it
  3560. while (!!el && el.nodeType === Node.ELEMENT_NODE) {
  3561. let selector = el.nodeName.toLowerCase();
  3562. if (el.id && typeof el.id === "string") {
  3563. selector += `#${el.id}`;
  3564. path.unshift(selector);
  3565. break;
  3566. }
  3567. let sibling = el;
  3568. let nth = 1;
  3569. while (sibling.previousElementSibling) {
  3570. sibling = sibling.previousElementSibling;
  3571. if (
  3572. sibling.nodeType === Node.ELEMENT_NODE &&
  3573. sibling.nodeName.toLowerCase() === selector
  3574. ) {
  3575. nth += 1;
  3576. }
  3577. }
  3578. if (nth !== 1) {
  3579. selector += `:nth-of-type(${nth})`;
  3580. }
  3581. path.unshift(selector);
  3582. el = el.parentElement;
  3583. }
  3584. return path.join(" > ");
  3585. };
  3586. /**
  3587. * Checks whether the element is instance of HTMLElement.
  3588. *
  3589. * @param element Element to check.
  3590. *
  3591. * @returns True if `element` is HTMLElement.
  3592. */
  3593. const isHtmlElement = (element) => {
  3594. return element instanceof HTMLElement;
  3595. };
  3596. /**
  3597. * Takes `element` and returns its parent element.
  3598. *
  3599. * @param element Element.
  3600. * @param errorMessage Optional error message to throw.
  3601. *
  3602. * @returns Parent of `element`.
  3603. * @throws An error if element has no parent element.
  3604. */
  3605. const getParent = (element, errorMessage) => {
  3606. const { parentElement } = element;
  3607. if (!parentElement) {
  3608. throw new Error(errorMessage || "Element does no have parent element");
  3609. }
  3610. return parentElement;
  3611. };
  3612. /**
  3613. * Checks whether the `error` has `message` property which type is string.
  3614. *
  3615. * @param error Error object.
  3616. *
  3617. * @returns True if `error` has message.
  3618. */
  3619. const isErrorWithMessage = (error) => {
  3620. return (
  3621. typeof error === "object" &&
  3622. error !== null &&
  3623. "message" in error &&
  3624. typeof error.message === "string"
  3625. );
  3626. };
  3627. /**
  3628. * Converts `maybeError` to error object with message.
  3629. *
  3630. * @param maybeError Possible error.
  3631. *
  3632. * @returns Error object with defined `message` property.
  3633. */
  3634. const toErrorWithMessage = (maybeError) => {
  3635. if (isErrorWithMessage(maybeError)) {
  3636. return maybeError;
  3637. }
  3638. try {
  3639. return new Error(JSON.stringify(maybeError));
  3640. } catch {
  3641. // fallback in case if there is an error happened during the maybeError stringifying
  3642. // like with circular references for example
  3643. return new Error(String(maybeError));
  3644. }
  3645. };
  3646. /**
  3647. * Returns error message from `error`.
  3648. * May be helpful to handle caught errors.
  3649. *
  3650. * @param error Error object.
  3651. *
  3652. * @returns Message of `error`.
  3653. */
  3654. const getErrorMessage = (error) => {
  3655. return toErrorWithMessage(error).message;
  3656. };
  3657. const logger = {
  3658. /**
  3659. * Safe console.error version.
  3660. */
  3661. error:
  3662. typeof console !== "undefined" && console.error && console.error.bind
  3663. ? console.error.bind(window.console)
  3664. : console.error,
  3665. /**
  3666. * Safe console.info version.
  3667. */
  3668. info:
  3669. typeof console !== "undefined" && console.info && console.info.bind
  3670. ? console.info.bind(window.console)
  3671. : console.info,
  3672. };
  3673. /**
  3674. * Returns string without suffix.
  3675. *
  3676. * @param str Input string.
  3677. * @param suffix Needed to remove.
  3678. *
  3679. * @returns String without suffix.
  3680. */
  3681. const removeSuffix = (str, suffix) => {
  3682. const index = str.indexOf(suffix, str.length - suffix.length);
  3683. if (index >= 0) {
  3684. return str.substring(0, index);
  3685. }
  3686. return str;
  3687. };
  3688. /**
  3689. * Replaces all `pattern`s with `replacement` in `input` string.
  3690. * String.replaceAll() polyfill because it is not supported by old browsers, e.g. Chrome 55.
  3691. *
  3692. * @see {@link https://caniuse.com/?search=String.replaceAll}
  3693. *
  3694. * @param input Input string to process.
  3695. * @param pattern Find in the input string.
  3696. * @param replacement Replace the pattern with.
  3697. *
  3698. * @returns Modified string.
  3699. */
  3700. const replaceAll = (input, pattern, replacement) => {
  3701. if (!input) {
  3702. return input;
  3703. }
  3704. return input.split(pattern).join(replacement);
  3705. };
  3706. /**
  3707. * Converts string pattern to regular expression.
  3708. *
  3709. * @param str String to convert.
  3710. *
  3711. * @returns Regular expression converted from pattern `str`.
  3712. */
  3713. const toRegExp = (str) => {
  3714. if (str.startsWith(SLASH) && str.endsWith(SLASH)) {
  3715. return new RegExp(str.slice(1, -1));
  3716. }
  3717. const escaped = str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  3718. return new RegExp(escaped);
  3719. };
  3720. /**
  3721. * Converts any simple type value to string type,
  3722. * e.g. `undefined` -> `'undefined'`.
  3723. *
  3724. * @param value Any type value.
  3725. *
  3726. * @returns String representation of `value`.
  3727. */
  3728. const convertTypeIntoString = (value) => {
  3729. let output;
  3730. switch (value) {
  3731. case undefined:
  3732. output = "undefined";
  3733. break;
  3734. case null:
  3735. output = "null";
  3736. break;
  3737. default:
  3738. output = value.toString();
  3739. }
  3740. return output;
  3741. };
  3742. /**
  3743. * Converts instance of string value into other simple types,
  3744. * e.g. `'null'` -> `null`, `'true'` -> `true`.
  3745. *
  3746. * @param value String-type value.
  3747. *
  3748. * @returns Its own type representation of string-type `value`.
  3749. */
  3750. const convertTypeFromString = (value) => {
  3751. const numValue = Number(value);
  3752. let output;
  3753. if (!Number.isNaN(numValue)) {
  3754. output = numValue;
  3755. } else {
  3756. switch (value) {
  3757. case "undefined":
  3758. output = undefined;
  3759. break;
  3760. case "null":
  3761. output = null;
  3762. break;
  3763. case "true":
  3764. output = true;
  3765. break;
  3766. case "false":
  3767. output = false;
  3768. break;
  3769. default:
  3770. output = value;
  3771. }
  3772. }
  3773. return output;
  3774. };
  3775. const SAFARI_USER_AGENT_REGEXP =
  3776. /\sVersion\/(\d{2}\.\d)(.+\s|\s)(Safari)\//;
  3777. const isSafariBrowser = SAFARI_USER_AGENT_REGEXP.test(navigator.userAgent);
  3778. /**
  3779. * Checks whether the browser userAgent is supported.
  3780. *
  3781. * @param userAgent User agent of browser.
  3782. *
  3783. * @returns False only for Internet Explorer.
  3784. */
  3785. const isUserAgentSupported = (userAgent) => {
  3786. // do not support Internet Explorer
  3787. if (userAgent.includes("MSIE") || userAgent.includes("Trident/")) {
  3788. return false;
  3789. }
  3790. return true;
  3791. };
  3792. /**
  3793. * Checks whether the current browser is supported.
  3794. *
  3795. * @returns False for Internet Explorer, otherwise true.
  3796. */
  3797. const isBrowserSupported = () => {
  3798. return isUserAgentSupported(navigator.userAgent);
  3799. };
  3800. /**
  3801. * CSS_PROPERTY is needed for style values normalization.
  3802. *
  3803. * IMPORTANT: it is used as 'const' instead of 'enum' to avoid side effects
  3804. * during ExtendedCss import into other libraries.
  3805. */
  3806. const CSS_PROPERTY = {
  3807. BACKGROUND: "background",
  3808. BACKGROUND_IMAGE: "background-image",
  3809. CONTENT: "content",
  3810. OPACITY: "opacity",
  3811. };
  3812. const REGEXP_ANY_SYMBOL = ".*";
  3813. const REGEXP_WITH_FLAGS_REGEXP = /^\s*\/.*\/[gmisuy]*\s*$/;
  3814. /**
  3815. * Removes quotes for specified content value.
  3816. *
  3817. * For example, content style declaration with `::before` can be set as '-' (e.g. unordered list)
  3818. * which displayed as simple dash `-` with no quotes.
  3819. * But CSSStyleDeclaration.getPropertyValue('content') will return value
  3820. * wrapped into quotes, e.g. '"-"', which should be removed
  3821. * because filters maintainers does not use any quotes in real rules.
  3822. *
  3823. * @param str Input string.
  3824. *
  3825. * @returns String with no quotes for content value.
  3826. */
  3827. const removeContentQuotes = (str) => {
  3828. return str.replace(/^(["'])([\s\S]*)\1$/, "$2");
  3829. };
  3830. /**
  3831. * Adds quotes for specified background url value.
  3832. *
  3833. * If background-image is specified **without** quotes:
  3834. * e.g. 'background: url(data:image/gif;base64,R0lGODlhAQA7)'.
  3835. *
  3836. * CSSStyleDeclaration.getPropertyValue('background-image') may return value **with** quotes:
  3837. * e.g. 'background: url("data:image/gif;base64,R0lGODlhAQA7")'.
  3838. *
  3839. * So we add quotes for compatibility since filters maintainers might use quotes in real rules.
  3840. *
  3841. * @param str Input string.
  3842. *
  3843. * @returns String with unified quotes for background url value.
  3844. */
  3845. const addUrlPropertyQuotes = (str) => {
  3846. if (!str.includes('url("')) {
  3847. const re = /url\((.*?)\)/g;
  3848. return str.replace(re, 'url("$1")');
  3849. }
  3850. return str;
  3851. };
  3852. /**
  3853. * Adds quotes to url arg for consistent property value matching.
  3854. */
  3855. const addUrlQuotesTo = {
  3856. regexpArg: (str) => {
  3857. // e.g. /^url\\([a-z]{4}:[a-z]{5}/
  3858. // or /^url\\(data\\:\\image\\/gif;base64.+/
  3859. const re = /(\^)?url(\\)?\\\((\w|\[\w)/g;
  3860. return str.replace(re, '$1url$2\\(\\"?$3');
  3861. },
  3862. noneRegexpArg: addUrlPropertyQuotes,
  3863. };
  3864. /**
  3865. * Escapes regular expression string.
  3866. *
  3867. * @see {@link https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/regexp}
  3868. *
  3869. * @param str Input string.
  3870. *
  3871. * @returns Escaped regular expression string.
  3872. */
  3873. const escapeRegExp = (str) => {
  3874. // should be escaped . * + ? ^ $ { } ( ) | [ ] / \
  3875. // except of * | ^
  3876. const specials = [
  3877. ".",
  3878. "+",
  3879. "?",
  3880. "$",
  3881. "{",
  3882. "}",
  3883. "(",
  3884. ")",
  3885. "[",
  3886. "]",
  3887. "\\",
  3888. "/",
  3889. ];
  3890. const specialsRegex = new RegExp(`[${specials.join("\\")}]`, "g");
  3891. return str.replace(specialsRegex, "\\$&");
  3892. };
  3893. /**
  3894. * Converts :matches-css() arg property value match to regexp.
  3895. *
  3896. * @param rawValue Style match value pattern.
  3897. *
  3898. * @returns Arg of :matches-css() converted to regular expression.
  3899. */
  3900. const convertStyleMatchValueToRegexp = (rawValue) => {
  3901. let value;
  3902. if (rawValue.startsWith(SLASH) && rawValue.endsWith(SLASH)) {
  3903. // For regex patterns double quotes `"` and backslashes `\` should be escaped
  3904. value = addUrlQuotesTo.regexpArg(rawValue);
  3905. value = value.slice(1, -1);
  3906. } else {
  3907. // For non-regex patterns parentheses `(` `)` and square brackets `[` `]`
  3908. // should be unescaped, because their escaping in filter rules is required
  3909. value = addUrlQuotesTo.noneRegexpArg(rawValue);
  3910. value = value.replace(/\\([\\()[\]"])/g, "$1");
  3911. value = escapeRegExp(value); // e.g. div:matches-css(background-image: url(data:*))
  3912. value = replaceAll(value, ASTERISK, REGEXP_ANY_SYMBOL);
  3913. }
  3914. return new RegExp(value, "i");
  3915. };
  3916. /**
  3917. * Makes some properties values compatible.
  3918. *
  3919. * @param propertyName Name of style property.
  3920. * @param propertyValue Value of style property.
  3921. *
  3922. * @returns Normalized values for some CSS properties.
  3923. */
  3924. const normalizePropertyValue = (propertyName, propertyValue) => {
  3925. let normalized = "";
  3926. switch (propertyName) {
  3927. case CSS_PROPERTY.BACKGROUND:
  3928. case CSS_PROPERTY.BACKGROUND_IMAGE:
  3929. // sometimes url property does not have quotes
  3930. // so we add them for consistent matching
  3931. normalized = addUrlPropertyQuotes(propertyValue);
  3932. break;
  3933. case CSS_PROPERTY.CONTENT:
  3934. normalized = removeContentQuotes(propertyValue);
  3935. break;
  3936. case CSS_PROPERTY.OPACITY:
  3937. // https://bugs.webkit.org/show_bug.cgi?id=93445
  3938. normalized = isSafariBrowser
  3939. ? (Math.round(parseFloat(propertyValue) * 100) / 100).toString()
  3940. : propertyValue;
  3941. break;
  3942. default:
  3943. normalized = propertyValue;
  3944. }
  3945. return normalized;
  3946. };
  3947. /**
  3948. * Returns domElement style property value
  3949. * by css property name and standard pseudo-element.
  3950. *
  3951. * @param domElement DOM element.
  3952. * @param propertyName CSS property name.
  3953. * @param regularPseudoElement Standard pseudo-element — '::before', '::after' etc.
  3954. *
  3955. * @returns String containing the value of a specified CSS property.
  3956. */
  3957. const getComputedStylePropertyValue = (
  3958. domElement,
  3959. propertyName,
  3960. regularPseudoElement,
  3961. ) => {
  3962. const style = window.getComputedStyle(domElement, regularPseudoElement);
  3963. const propertyValue = style.getPropertyValue(propertyName);
  3964. return normalizePropertyValue(propertyName, propertyValue);
  3965. };
  3966. /**
  3967. * Parses arg of absolute pseudo-class into 'name' and 'value' if set.
  3968. *
  3969. * Used for :matches-css() - with COLON as separator,
  3970. * for :matches-attr() and :matches-property() - with EQUAL_SIGN as separator.
  3971. *
  3972. * @param pseudoArg Arg of pseudo-class.
  3973. * @param separator Divider symbol.
  3974. *
  3975. * @returns Parsed 'matches' pseudo-class arg data.
  3976. */
  3977. const getPseudoArgData = (pseudoArg, separator) => {
  3978. const index = pseudoArg.indexOf(separator);
  3979. let name;
  3980. let value;
  3981. if (index > -1) {
  3982. name = pseudoArg.substring(0, index).trim();
  3983. value = pseudoArg.substring(index + 1).trim();
  3984. } else {
  3985. name = pseudoArg;
  3986. }
  3987. return {
  3988. name,
  3989. value,
  3990. };
  3991. };
  3992. /**
  3993. * Parses :matches-css() pseudo-class arg
  3994. * where regular pseudo-element can be a part of arg
  3995. * e.g. 'div:matches-css(before, color: rgb(255, 255, 255))' <-- obsolete `:matches-css-before()`.
  3996. *
  3997. * @param pseudoName Pseudo-class name.
  3998. * @param rawArg Pseudo-class arg.
  3999. *
  4000. * @returns Parsed :matches-css() pseudo-class arg data.
  4001. * @throws An error on invalid `rawArg`.
  4002. */
  4003. const parseStyleMatchArg = (pseudoName, rawArg) => {
  4004. const { name, value } = getPseudoArgData(rawArg, COMMA);
  4005. let regularPseudoElement = name;
  4006. let styleMatchArg = value; // check whether the string part before the separator is valid regular pseudo-element,
  4007. // otherwise `regularPseudoElement` is null, and `styleMatchArg` is rawArg
  4008. if (!Object.values(REGULAR_PSEUDO_ELEMENTS).includes(name)) {
  4009. regularPseudoElement = null;
  4010. styleMatchArg = rawArg;
  4011. }
  4012. if (!styleMatchArg) {
  4013. throw new Error(
  4014. `Required style property argument part is missing in :${pseudoName}() arg: '${rawArg}'`,
  4015. );
  4016. } // if regularPseudoElement is not `null`
  4017. if (regularPseudoElement) {
  4018. // pseudo-element should have two colon marks for Window.getComputedStyle() due to the syntax:
  4019. // https://www.w3.org/TR/selectors-4/#pseudo-element-syntax
  4020. // ':matches-css(before, content: ads)' ->> '::before'
  4021. regularPseudoElement = `${COLON}${COLON}${regularPseudoElement}`;
  4022. }
  4023. return {
  4024. regularPseudoElement,
  4025. styleMatchArg,
  4026. };
  4027. };
  4028. /**
  4029. * Checks whether the domElement is matched by :matches-css() arg.
  4030. *
  4031. * @param argsData Pseudo-class name, arg, and dom element to check.
  4032. *
  4033. @returns True if DOM element is matched.
  4034. * @throws An error on invalid pseudo-class arg.
  4035. */
  4036. const isStyleMatched = (argsData) => {
  4037. const { pseudoName, pseudoArg, domElement } = argsData;
  4038. const { regularPseudoElement, styleMatchArg } = parseStyleMatchArg(
  4039. pseudoName,
  4040. pseudoArg,
  4041. );
  4042. const { name: matchName, value: matchValue } = getPseudoArgData(
  4043. styleMatchArg,
  4044. COLON,
  4045. );
  4046. if (!matchName || !matchValue) {
  4047. throw new Error(
  4048. `Required property name or value is missing in :${pseudoName}() arg: '${styleMatchArg}'`,
  4049. );
  4050. }
  4051. let valueRegexp;
  4052. try {
  4053. valueRegexp = convertStyleMatchValueToRegexp(matchValue);
  4054. } catch (e) {
  4055. logger.error(getErrorMessage(e));
  4056. throw new Error(
  4057. `Invalid argument of :${pseudoName}() pseudo-class: '${styleMatchArg}'`,
  4058. );
  4059. }
  4060. const value = getComputedStylePropertyValue(
  4061. domElement,
  4062. matchName,
  4063. regularPseudoElement,
  4064. );
  4065. return valueRegexp && valueRegexp.test(value);
  4066. };
  4067. /**
  4068. * Validates string arg for :matches-attr() and :matches-property().
  4069. *
  4070. * @param arg Pseudo-class arg.
  4071. *
  4072. * @returns True if 'matches' pseudo-class string arg is valid.
  4073. */
  4074. const validateStrMatcherArg = (arg) => {
  4075. if (arg.includes(SLASH)) {
  4076. return false;
  4077. }
  4078. if (!/^[\w-]+$/.test(arg)) {
  4079. return false;
  4080. }
  4081. return true;
  4082. };
  4083. /**
  4084. * Returns valid arg for :matches-attr() and :matcher-property().
  4085. *
  4086. * @param rawArg Arg pattern.
  4087. * @param [isWildcardAllowed=false] Flag for wildcard (`*`) using as pseudo-class arg.
  4088. *
  4089. * @returns Valid arg for :matches-attr() and :matcher-property().
  4090. * @throws An error on invalid `rawArg`.
  4091. */
  4092. const getValidMatcherArg = function (rawArg) {
  4093. let isWildcardAllowed =
  4094. arguments.length > 1 && arguments[1] !== undefined
  4095. ? arguments[1]
  4096. : false;
  4097. // if rawArg is missing for pseudo-class
  4098. // e.g. :matches-attr()
  4099. // error will be thrown before getValidMatcherArg() is called:
  4100. // name or arg is missing in AbsolutePseudoClass
  4101. let arg;
  4102. if (
  4103. rawArg.length > 1 &&
  4104. rawArg.startsWith(DOUBLE_QUOTE) &&
  4105. rawArg.endsWith(DOUBLE_QUOTE)
  4106. ) {
  4107. rawArg = rawArg.slice(1, -1);
  4108. }
  4109. if (rawArg === "") {
  4110. // e.g. :matches-property("")
  4111. throw new Error("Argument should be specified. Empty arg is invalid.");
  4112. }
  4113. if (rawArg.startsWith(SLASH) && rawArg.endsWith(SLASH)) {
  4114. // e.g. :matches-property("//")
  4115. if (rawArg.length > 2) {
  4116. arg = toRegExp(rawArg);
  4117. } else {
  4118. throw new Error(`Invalid regexp: '${rawArg}'`);
  4119. }
  4120. } else if (rawArg.includes(ASTERISK)) {
  4121. if (rawArg === ASTERISK && !isWildcardAllowed) {
  4122. // e.g. :matches-attr(*)
  4123. throw new Error(`Argument should be more specific than ${rawArg}`);
  4124. }
  4125. arg = replaceAll(rawArg, ASTERISK, REGEXP_ANY_SYMBOL);
  4126. arg = new RegExp(arg);
  4127. } else {
  4128. if (!validateStrMatcherArg(rawArg)) {
  4129. throw new Error(`Invalid argument: '${rawArg}'`);
  4130. }
  4131. arg = rawArg;
  4132. }
  4133. return arg;
  4134. };
  4135. /**
  4136. * Parses pseudo-class argument and returns parsed data.
  4137. *
  4138. * @param pseudoName Extended pseudo-class name.
  4139. * @param pseudoArg Extended pseudo-class argument.
  4140. *
  4141. * @returns Parsed pseudo-class argument data.
  4142. * @throws An error if attribute name is missing in pseudo-class arg.
  4143. */
  4144. const getRawMatchingData = (pseudoName, pseudoArg) => {
  4145. const { name: rawName, value: rawValue } = getPseudoArgData(
  4146. pseudoArg,
  4147. EQUAL_SIGN,
  4148. );
  4149. if (!rawName) {
  4150. throw new Error(
  4151. `Required attribute name is missing in :${pseudoName} arg: ${pseudoArg}`,
  4152. );
  4153. }
  4154. return {
  4155. rawName,
  4156. rawValue,
  4157. };
  4158. };
  4159. /**
  4160. * Checks whether the domElement is matched by :matches-attr() arg.
  4161. *
  4162. * @param argsData Pseudo-class name, arg, and dom element to check.
  4163. *
  4164. @returns True if DOM element is matched.
  4165. * @throws An error on invalid arg of pseudo-class.
  4166. */
  4167. const isAttributeMatched = (argsData) => {
  4168. const { pseudoName, pseudoArg, domElement } = argsData;
  4169. const elementAttributes = domElement.attributes; // no match if dom element has no attributes
  4170. if (elementAttributes.length === 0) {
  4171. return false;
  4172. }
  4173. const { rawName: rawAttrName, rawValue: rawAttrValue } =
  4174. getRawMatchingData(pseudoName, pseudoArg);
  4175. let attrNameMatch;
  4176. try {
  4177. attrNameMatch = getValidMatcherArg(rawAttrName);
  4178. } catch (e) {
  4179. const errorMessage = getErrorMessage(e);
  4180. logger.error(errorMessage);
  4181. throw new SyntaxError(errorMessage);
  4182. }
  4183. let isMatched = false;
  4184. let i = 0;
  4185. while (i < elementAttributes.length && !isMatched) {
  4186. const attr = elementAttributes[i];
  4187. if (!attr) {
  4188. break;
  4189. }
  4190. const isNameMatched =
  4191. attrNameMatch instanceof RegExp
  4192. ? attrNameMatch.test(attr.name)
  4193. : attrNameMatch === attr.name;
  4194. if (!rawAttrValue) {
  4195. // for rules with no attribute value specified
  4196. // e.g. :matches-attr("/regex/") or :matches-attr("attr-name")
  4197. isMatched = isNameMatched;
  4198. } else {
  4199. let attrValueMatch;
  4200. try {
  4201. attrValueMatch = getValidMatcherArg(rawAttrValue);
  4202. } catch (e) {
  4203. const errorMessage = getErrorMessage(e);
  4204. logger.error(errorMessage);
  4205. throw new SyntaxError(errorMessage);
  4206. }
  4207. const isValueMatched =
  4208. attrValueMatch instanceof RegExp
  4209. ? attrValueMatch.test(attr.value)
  4210. : attrValueMatch === attr.value;
  4211. isMatched = isNameMatched && isValueMatched;
  4212. }
  4213. i += 1;
  4214. }
  4215. return isMatched;
  4216. };
  4217. /**
  4218. * Parses raw :matches-property() arg which may be chain of properties.
  4219. *
  4220. * @param input Argument of :matches-property().
  4221. *
  4222. * @returns Arg of :matches-property() as array of strings or regular expressions.
  4223. * @throws An error on invalid chain.
  4224. */
  4225. const parseRawPropChain = (input) => {
  4226. if (
  4227. input.length > 1 &&
  4228. input.startsWith(DOUBLE_QUOTE) &&
  4229. input.endsWith(DOUBLE_QUOTE)
  4230. ) {
  4231. input = input.slice(1, -1);
  4232. }
  4233. const chainChunks = input.split(DOT);
  4234. const chainPatterns = [];
  4235. let patternBuffer = "";
  4236. let isRegexpPattern = false;
  4237. let i = 0;
  4238. while (i < chainChunks.length) {
  4239. const chunk = getItemByIndex(
  4240. chainChunks,
  4241. i,
  4242. `Invalid pseudo-class arg: '${input}'`,
  4243. );
  4244. if (
  4245. chunk.startsWith(SLASH) &&
  4246. chunk.endsWith(SLASH) &&
  4247. chunk.length > 2
  4248. ) {
  4249. // regexp pattern with no dot in it, e.g. /propName/
  4250. chainPatterns.push(chunk);
  4251. } else if (chunk.startsWith(SLASH)) {
  4252. // if chunk is a start of regexp pattern
  4253. isRegexpPattern = true;
  4254. patternBuffer += chunk;
  4255. } else if (chunk.endsWith(SLASH)) {
  4256. isRegexpPattern = false; // restore dot removed while splitting
  4257. // e.g. testProp./.{1,5}/
  4258. patternBuffer += `.${chunk}`;
  4259. chainPatterns.push(patternBuffer);
  4260. patternBuffer = "";
  4261. } else {
  4262. // if there are few dots in regexp pattern
  4263. // so chunk might be in the middle of it
  4264. if (isRegexpPattern) {
  4265. patternBuffer += chunk;
  4266. } else {
  4267. // otherwise it is string pattern
  4268. chainPatterns.push(chunk);
  4269. }
  4270. }
  4271. i += 1;
  4272. }
  4273. if (patternBuffer.length > 0) {
  4274. throw new Error(`Invalid regexp property pattern '${input}'`);
  4275. }
  4276. const chainMatchPatterns = chainPatterns.map((pattern) => {
  4277. if (pattern.length === 0) {
  4278. // e.g. '.prop.id' or 'nested..test'
  4279. throw new Error(
  4280. `Empty pattern '${pattern}' is invalid in chain '${input}'`,
  4281. );
  4282. }
  4283. let validPattern;
  4284. try {
  4285. validPattern = getValidMatcherArg(pattern, true);
  4286. } catch (e) {
  4287. logger.error(getErrorMessage(e));
  4288. throw new Error(
  4289. `Invalid property pattern '${pattern}' in property chain '${input}'`,
  4290. );
  4291. }
  4292. return validPattern;
  4293. });
  4294. return chainMatchPatterns;
  4295. };
  4296. /**
  4297. * Checks if the property exists in the base object (recursively).
  4298. *
  4299. * @param base Element to check.
  4300. * @param chain Array of objects - parsed string property chain.
  4301. * @param [output=[]] Result acc.
  4302. *
  4303. * @returns Array of parsed data — representation of `base`-related `chain`.
  4304. */
  4305. const filterRootsByRegexpChain = function (base, chain) {
  4306. let output =
  4307. arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
  4308. const tempProp = getFirst(chain);
  4309. if (chain.length === 1) {
  4310. let key;
  4311. for (key in base) {
  4312. if (tempProp instanceof RegExp) {
  4313. if (tempProp.test(key)) {
  4314. output.push({
  4315. base,
  4316. prop: key,
  4317. value: base[key],
  4318. });
  4319. }
  4320. } else if (tempProp === key) {
  4321. output.push({
  4322. base,
  4323. prop: tempProp,
  4324. value: base[key],
  4325. });
  4326. }
  4327. }
  4328. return output;
  4329. } // if there is a regexp prop in input chain
  4330. // e.g. 'unit./^ad.+/.src' for 'unit.ad-1gf2.src unit.ad-fgd34.src'),
  4331. // every base keys should be tested by regexp and it can be more that one results
  4332. if (tempProp instanceof RegExp) {
  4333. const nextProp = chain.slice(1);
  4334. const baseKeys = [];
  4335. for (const key in base) {
  4336. if (tempProp.test(key)) {
  4337. baseKeys.push(key);
  4338. }
  4339. }
  4340. baseKeys.forEach((key) => {
  4341. var _Object$getOwnPropert;
  4342. const item =
  4343. (_Object$getOwnPropert = Object.getOwnPropertyDescriptor(
  4344. base,
  4345. key,
  4346. )) === null || _Object$getOwnPropert === void 0
  4347. ? void 0
  4348. : _Object$getOwnPropert.value;
  4349. filterRootsByRegexpChain(item, nextProp, output);
  4350. });
  4351. }
  4352. if (base && typeof tempProp === "string") {
  4353. var _Object$getOwnPropert2;
  4354. const nextBase =
  4355. (_Object$getOwnPropert2 = Object.getOwnPropertyDescriptor(
  4356. base,
  4357. tempProp,
  4358. )) === null || _Object$getOwnPropert2 === void 0
  4359. ? void 0
  4360. : _Object$getOwnPropert2.value;
  4361. chain = chain.slice(1);
  4362. if (nextBase !== undefined) {
  4363. filterRootsByRegexpChain(nextBase, chain, output);
  4364. }
  4365. }
  4366. return output;
  4367. };
  4368. /**
  4369. * Checks whether the domElement is matched by :matches-property() arg.
  4370. *
  4371. * @param argsData Pseudo-class name, arg, and dom element to check.
  4372. *
  4373. @returns True if DOM element is matched.
  4374. * @throws An error on invalid prop in chain.
  4375. */
  4376. const isPropertyMatched = (argsData) => {
  4377. const { pseudoName, pseudoArg, domElement } = argsData;
  4378. const { rawName: rawPropertyName, rawValue: rawPropertyValue } =
  4379. getRawMatchingData(pseudoName, pseudoArg); // chained property name cannot include '/' or '.'
  4380. // so regex prop names with such escaped characters are invalid
  4381. if (rawPropertyName.includes("\\/") || rawPropertyName.includes("\\.")) {
  4382. throw new Error(
  4383. `Invalid :${pseudoName} name pattern: ${rawPropertyName}`,
  4384. );
  4385. }
  4386. let propChainMatches;
  4387. try {
  4388. propChainMatches = parseRawPropChain(rawPropertyName);
  4389. } catch (e) {
  4390. const errorMessage = getErrorMessage(e);
  4391. logger.error(errorMessage);
  4392. throw new SyntaxError(errorMessage);
  4393. }
  4394. const ownerObjArr = filterRootsByRegexpChain(
  4395. domElement,
  4396. propChainMatches,
  4397. );
  4398. if (ownerObjArr.length === 0) {
  4399. return false;
  4400. }
  4401. let isMatched = true;
  4402. if (rawPropertyValue) {
  4403. let propValueMatch;
  4404. try {
  4405. propValueMatch = getValidMatcherArg(rawPropertyValue);
  4406. } catch (e) {
  4407. const errorMessage = getErrorMessage(e);
  4408. logger.error(errorMessage);
  4409. throw new SyntaxError(errorMessage);
  4410. }
  4411. if (propValueMatch) {
  4412. for (let i = 0; i < ownerObjArr.length; i += 1) {
  4413. var _ownerObjArr$i;
  4414. const realValue =
  4415. (_ownerObjArr$i = ownerObjArr[i]) === null ||
  4416. _ownerObjArr$i === void 0
  4417. ? void 0
  4418. : _ownerObjArr$i.value;
  4419. if (propValueMatch instanceof RegExp) {
  4420. isMatched = propValueMatch.test(convertTypeIntoString(realValue));
  4421. } else {
  4422. // handle 'null' and 'undefined' property values set as string
  4423. if (realValue === "null" || realValue === "undefined") {
  4424. isMatched = propValueMatch === realValue;
  4425. break;
  4426. }
  4427. isMatched = convertTypeFromString(propValueMatch) === realValue;
  4428. }
  4429. if (isMatched) {
  4430. break;
  4431. }
  4432. }
  4433. }
  4434. }
  4435. return isMatched;
  4436. };
  4437. /**
  4438. * Checks whether the textContent is matched by :contains arg.
  4439. *
  4440. * @param argsData Pseudo-class name, arg, and dom element to check.
  4441. *
  4442. @returns True if DOM element is matched.
  4443. * @throws An error on invalid arg of pseudo-class.
  4444. */
  4445. const isTextMatched = (argsData) => {
  4446. const { pseudoName, pseudoArg, domElement } = argsData;
  4447. const textContent = getNodeTextContent(domElement);
  4448. let isTextContentMatched;
  4449. let pseudoArgToMatch = pseudoArg;
  4450. if (
  4451. pseudoArgToMatch.startsWith(SLASH) &&
  4452. REGEXP_WITH_FLAGS_REGEXP.test(pseudoArgToMatch)
  4453. ) {
  4454. // regexp arg
  4455. const flagsIndex = pseudoArgToMatch.lastIndexOf("/");
  4456. const flagsStr = pseudoArgToMatch.substring(flagsIndex + 1);
  4457. pseudoArgToMatch = pseudoArgToMatch
  4458. .substring(0, flagsIndex + 1)
  4459. .slice(1, -1)
  4460. .replace(/\\([\\"])/g, "$1");
  4461. let regex;
  4462. try {
  4463. regex = new RegExp(pseudoArgToMatch, flagsStr);
  4464. } catch (e) {
  4465. throw new Error(
  4466. `Invalid argument of :${pseudoName}() pseudo-class: ${pseudoArg}`,
  4467. );
  4468. }
  4469. isTextContentMatched = regex.test(textContent);
  4470. } else {
  4471. // none-regexp arg
  4472. pseudoArgToMatch = pseudoArgToMatch.replace(/\\([\\()[\]"])/g, "$1");
  4473. isTextContentMatched = textContent.includes(pseudoArgToMatch);
  4474. }
  4475. return isTextContentMatched;
  4476. };
  4477. /**
  4478. * Validates number arg for :nth-ancestor() and :upward() pseudo-classes.
  4479. *
  4480. * @param rawArg Raw arg of pseudo-class.
  4481. * @param pseudoName Pseudo-class name.
  4482. *
  4483. * @returns Valid number arg for :nth-ancestor() and :upward().
  4484. * @throws An error on invalid `rawArg`.
  4485. */
  4486. const getValidNumberAncestorArg = (rawArg, pseudoName) => {
  4487. const deep = Number(rawArg);
  4488. if (Number.isNaN(deep) || deep < 1 || deep >= 256) {
  4489. throw new Error(
  4490. `Invalid argument of :${pseudoName} pseudo-class: '${rawArg}'`,
  4491. );
  4492. }
  4493. return deep;
  4494. };
  4495. /**
  4496. * Returns nth ancestor by 'deep' number arg OR undefined if ancestor range limit exceeded.
  4497. *
  4498. * @param domElement DOM element to find ancestor for.
  4499. * @param nth Depth up to needed ancestor.
  4500. * @param pseudoName Pseudo-class name.
  4501. *
  4502. * @returns Ancestor element found in DOM, or null if not found.
  4503. * @throws An error on invalid `nth` arg.
  4504. */
  4505. const getNthAncestor = (domElement, nth, pseudoName) => {
  4506. let ancestor = null;
  4507. let i = 0;
  4508. while (i < nth) {
  4509. ancestor = domElement.parentElement;
  4510. if (!ancestor) {
  4511. throw new Error(
  4512. `Out of DOM: Argument of :${pseudoName}() pseudo-class is too big '${nth}'.`,
  4513. );
  4514. }
  4515. domElement = ancestor;
  4516. i += 1;
  4517. }
  4518. return ancestor;
  4519. };
  4520. /**
  4521. * Validates standard CSS selector.
  4522. *
  4523. * @param selector Standard selector.
  4524. *
  4525. * @returns True if standard CSS selector is valid.
  4526. */
  4527. const validateStandardSelector = (selector) => {
  4528. let isValid;
  4529. try {
  4530. document.querySelectorAll(selector);
  4531. isValid = true;
  4532. } catch (e) {
  4533. isValid = false;
  4534. }
  4535. return isValid;
  4536. };
  4537. /**
  4538. * Wrapper to run matcher `callback` with `args`
  4539. * and throw error with `errorMessage` if `callback` run fails.
  4540. *
  4541. * @param callback Matcher callback.
  4542. * @param argsData Args needed for matcher callback.
  4543. * @param errorMessage Error message.
  4544. *
  4545. * @returns True if `callback` returns true.
  4546. * @throws An error if `callback` fails.
  4547. */
  4548. const matcherWrapper = (callback, argsData, errorMessage) => {
  4549. let isMatched;
  4550. try {
  4551. isMatched = callback(argsData);
  4552. } catch (e) {
  4553. logger.error(getErrorMessage(e));
  4554. throw new Error(errorMessage);
  4555. }
  4556. return isMatched;
  4557. };
  4558. /**
  4559. * Generates common error message to throw while matching element `propDesc`.
  4560. *
  4561. * @param propDesc Text to describe what element 'prop' pseudo-class is trying to match.
  4562. * @param pseudoName Pseudo-class name.
  4563. * @param pseudoArg Pseudo-class arg.
  4564. *
  4565. * @returns Generated error message string.
  4566. */
  4567. const getAbsolutePseudoError = (propDesc, pseudoName, pseudoArg) => {
  4568. // eslint-disable-next-line max-len
  4569. return `${MATCHING_ELEMENT_ERROR_PREFIX} ${propDesc}, may be invalid :${pseudoName}() pseudo-class arg: '${pseudoArg}'`;
  4570. };
  4571. /**
  4572. * Checks whether the domElement is matched by absolute extended pseudo-class argument.
  4573. *
  4574. * @param domElement Page element.
  4575. * @param pseudoName Pseudo-class name.
  4576. * @param pseudoArg Pseudo-class arg.
  4577. *
  4578. * @returns True if `domElement` is matched by absolute pseudo-class.
  4579. * @throws An error on unknown absolute pseudo-class.
  4580. */
  4581. const isMatchedByAbsolutePseudo = (domElement, pseudoName, pseudoArg) => {
  4582. let argsData;
  4583. let errorMessage;
  4584. let callback;
  4585. switch (pseudoName) {
  4586. case CONTAINS_PSEUDO:
  4587. case HAS_TEXT_PSEUDO:
  4588. case ABP_CONTAINS_PSEUDO:
  4589. callback = isTextMatched;
  4590. argsData = {
  4591. pseudoName,
  4592. pseudoArg,
  4593. domElement,
  4594. };
  4595. errorMessage = getAbsolutePseudoError(
  4596. "text content",
  4597. pseudoName,
  4598. pseudoArg,
  4599. );
  4600. break;
  4601. case MATCHES_CSS_PSEUDO:
  4602. case MATCHES_CSS_AFTER_PSEUDO:
  4603. case MATCHES_CSS_BEFORE_PSEUDO:
  4604. callback = isStyleMatched;
  4605. argsData = {
  4606. pseudoName,
  4607. pseudoArg,
  4608. domElement,
  4609. };
  4610. errorMessage = getAbsolutePseudoError("style", pseudoName, pseudoArg);
  4611. break;
  4612. case MATCHES_ATTR_PSEUDO_CLASS_MARKER:
  4613. callback = isAttributeMatched;
  4614. argsData = {
  4615. domElement,
  4616. pseudoName,
  4617. pseudoArg,
  4618. };
  4619. errorMessage = getAbsolutePseudoError(
  4620. "attributes",
  4621. pseudoName,
  4622. pseudoArg,
  4623. );
  4624. break;
  4625. case MATCHES_PROPERTY_PSEUDO_CLASS_MARKER:
  4626. callback = isPropertyMatched;
  4627. argsData = {
  4628. domElement,
  4629. pseudoName,
  4630. pseudoArg,
  4631. };
  4632. errorMessage = getAbsolutePseudoError(
  4633. "properties",
  4634. pseudoName,
  4635. pseudoArg,
  4636. );
  4637. break;
  4638. default:
  4639. throw new Error(`Unknown absolute pseudo-class :${pseudoName}()`);
  4640. }
  4641. return matcherWrapper(callback, argsData, errorMessage);
  4642. };
  4643. const findByAbsolutePseudoPseudo = {
  4644. /**
  4645. * Returns list of nth ancestors relative to every dom node from domElements list.
  4646. *
  4647. * @param domElements DOM elements.
  4648. * @param rawPseudoArg Number arg of :nth-ancestor() or :upward() pseudo-class.
  4649. * @param pseudoName Pseudo-class name.
  4650. *
  4651. * @returns Array of ancestor DOM elements.
  4652. */
  4653. nthAncestor: (domElements, rawPseudoArg, pseudoName) => {
  4654. const deep = getValidNumberAncestorArg(rawPseudoArg, pseudoName);
  4655. const ancestors = domElements
  4656. .map((domElement) => {
  4657. let ancestor = null;
  4658. try {
  4659. ancestor = getNthAncestor(domElement, deep, pseudoName);
  4660. } catch (e) {
  4661. logger.error(getErrorMessage(e));
  4662. }
  4663. return ancestor;
  4664. })
  4665. .filter(isHtmlElement);
  4666. return ancestors;
  4667. },
  4668. /**
  4669. * Returns list of elements by xpath expression, evaluated on every dom node from domElements list.
  4670. *
  4671. * @param domElements DOM elements.
  4672. * @param rawPseudoArg Arg of :xpath() pseudo-class.
  4673. *
  4674. * @returns Array of DOM elements matched by xpath expression.
  4675. */
  4676. xpath: (domElements, rawPseudoArg) => {
  4677. const foundElements = domElements.map((domElement) => {
  4678. const result = [];
  4679. let xpathResult;
  4680. try {
  4681. xpathResult = document.evaluate(
  4682. rawPseudoArg,
  4683. domElement,
  4684. null,
  4685. window.XPathResult.UNORDERED_NODE_ITERATOR_TYPE,
  4686. null,
  4687. );
  4688. } catch (e) {
  4689. logger.error(getErrorMessage(e));
  4690. throw new Error(
  4691. `Invalid argument of :xpath() pseudo-class: '${rawPseudoArg}'`,
  4692. );
  4693. }
  4694. let node = xpathResult.iterateNext();
  4695. while (node) {
  4696. if (isHtmlElement(node)) {
  4697. result.push(node);
  4698. }
  4699. node = xpathResult.iterateNext();
  4700. }
  4701. return result;
  4702. });
  4703. return flatten(foundElements);
  4704. },
  4705. /**
  4706. * Returns list of closest ancestors relative to every dom node from domElements list.
  4707. *
  4708. * @param domElements DOM elements.
  4709. * @param rawPseudoArg Standard selector arg of :upward() pseudo-class.
  4710. *
  4711. * @returns Array of closest ancestor DOM elements.
  4712. * @throws An error if `rawPseudoArg` is not a valid standard selector.
  4713. */
  4714. upward: (domElements, rawPseudoArg) => {
  4715. if (!validateStandardSelector(rawPseudoArg)) {
  4716. throw new Error(
  4717. `Invalid argument of :upward pseudo-class: '${rawPseudoArg}'`,
  4718. );
  4719. }
  4720. const closestAncestors = domElements
  4721. .map((domElement) => {
  4722. // closest to parent element should be found
  4723. // otherwise `.base:upward(.base)` will return itself too, not only ancestor
  4724. const parent = domElement.parentElement;
  4725. if (!parent) {
  4726. return null;
  4727. }
  4728. return parent.closest(rawPseudoArg);
  4729. })
  4730. .filter(isHtmlElement);
  4731. return closestAncestors;
  4732. },
  4733. };
  4734. /**
  4735. * Calculated selector text which is needed to :has(), :is() and :not() pseudo-classes.
  4736. * Contains calculated part (depends on the processed element)
  4737. * and value of RegularSelector which is next to selector by.
  4738. *
  4739. * Native Document.querySelectorAll() does not select exact descendant elements
  4740. * but match all page elements satisfying the selector,
  4741. * so extra specification is needed for proper descendants selection
  4742. * e.g. 'div:has(> img)'.
  4743. *
  4744. * Its calculation depends on extended selector.
  4745. */
  4746. /**
  4747. * Combined `:scope` pseudo-class and **child** combinator — `:scope>`.
  4748. */
  4749. const scopeDirectChildren = `${SCOPE_CSS_PSEUDO_CLASS}${CHILD_COMBINATOR}`;
  4750. /**
  4751. * Combined `:scope` pseudo-class and **descendant** combinator — `:scope `.
  4752. */
  4753. const scopeAnyChildren = `${SCOPE_CSS_PSEUDO_CLASS}${DESCENDANT_COMBINATOR}`;
  4754. /**
  4755. * Type for relative pseudo-class helpers args.
  4756. */
  4757. /**
  4758. * Returns the first of RegularSelector child node for `selectorNode`.
  4759. *
  4760. * @param selectorNode Ast Selector node.
  4761. * @param pseudoName Name of relative pseudo-class.
  4762. *
  4763. * @returns Ast RegularSelector node.
  4764. */
  4765. const getFirstInnerRegularChild = (selectorNode, pseudoName) => {
  4766. return getFirstRegularChild(
  4767. selectorNode.children,
  4768. `RegularSelector is missing for :${pseudoName}() pseudo-class`,
  4769. );
  4770. }; // TODO: fix for <forgiving-relative-selector-list>
  4771. // https://github.com/AdguardTeam/ExtendedCss/issues/154
  4772. /**
  4773. * Checks whether the element has all relative elements specified by pseudo-class arg.
  4774. * Used for :has() pseudo-class.
  4775. *
  4776. * @param argsData Relative pseudo-class helpers args data.
  4777. *
  4778. * @returns True if **all selectors** from argsData.relativeSelectorList is **matched** for argsData.element.
  4779. */
  4780. const hasRelativesBySelectorList = (argsData) => {
  4781. const { element, relativeSelectorList, pseudoName } = argsData;
  4782. return relativeSelectorList.children // Array.every() is used here as each Selector node from SelectorList should exist on page
  4783. .every((selectorNode) => {
  4784. // selectorList.children always starts with regular selector as any selector generally
  4785. const relativeRegularSelector = getFirstInnerRegularChild(
  4786. selectorNode,
  4787. pseudoName,
  4788. );
  4789. let specifiedSelector = "";
  4790. let rootElement = null;
  4791. const regularSelector = getNodeValue(relativeRegularSelector);
  4792. if (
  4793. regularSelector.startsWith(NEXT_SIBLING_COMBINATOR) ||
  4794. regularSelector.startsWith(SUBSEQUENT_SIBLING_COMBINATOR)
  4795. ) {
  4796. /**
  4797. * For matching the element by "element:has(+ next-sibling)" and "element:has(~ sibling)"
  4798. * we check whether the element's parentElement has specific direct child combination,
  4799. * e.g. 'h1:has(+ .share)' -> `h1Node.parentElement.querySelectorAll(':scope > h1 + .share')`.
  4800. *
  4801. * @see {@link https://www.w3.org/TR/selectors-4/#relational}
  4802. */
  4803. rootElement = element.parentElement;
  4804. const elementSelectorText = getElementSelectorDesc(element);
  4805. specifiedSelector = `${scopeDirectChildren}${elementSelectorText}${regularSelector}`;
  4806. } else if (regularSelector === ASTERISK) {
  4807. /**
  4808. * :scope specification is needed for proper descendants selection
  4809. * as native element.querySelectorAll() does not select exact element descendants
  4810. * e.g. 'a:has(> img)' -> `aNode.querySelectorAll(':scope > img')`.
  4811. *
  4812. * For 'any selector' as arg of relative simplicity should be set for all inner elements
  4813. * e.g. 'div:has(*)' -> `divNode.querySelectorAll(':scope *')`
  4814. * which means empty div with no child element.
  4815. */
  4816. rootElement = element;
  4817. specifiedSelector = `${scopeAnyChildren}${ASTERISK}`;
  4818. } else {
  4819. /**
  4820. * As it described above, inner elements should be found using `:scope` pseudo-class
  4821. * e.g. 'a:has(> img)' -> `aNode.querySelectorAll(':scope > img')`
  4822. * OR '.block(div > span)' -> `blockClassNode.querySelectorAll(':scope div > span')`.
  4823. */
  4824. specifiedSelector = `${scopeAnyChildren}${regularSelector}`;
  4825. rootElement = element;
  4826. }
  4827. if (!rootElement) {
  4828. throw new Error(
  4829. `Selection by :${pseudoName}() pseudo-class is not possible`,
  4830. );
  4831. }
  4832. let relativeElements;
  4833. try {
  4834. // eslint-disable-next-line @typescript-eslint/no-use-before-define
  4835. relativeElements = getElementsForSelectorNode(
  4836. selectorNode,
  4837. rootElement,
  4838. specifiedSelector,
  4839. );
  4840. } catch (e) {
  4841. logger.error(getErrorMessage(e)); // fail for invalid selector
  4842. throw new Error(
  4843. `Invalid selector for :${pseudoName}() pseudo-class: '${regularSelector}'`,
  4844. );
  4845. }
  4846. return relativeElements.length > 0;
  4847. });
  4848. };
  4849. /**
  4850. * Checks whether the element is an any element specified by pseudo-class arg.
  4851. * Used for :is() pseudo-class.
  4852. *
  4853. * @param argsData Relative pseudo-class helpers args data.
  4854. *
  4855. * @returns True if **any selector** from argsData.relativeSelectorList is **matched** for argsData.element.
  4856. */
  4857. const isAnyElementBySelectorList = (argsData) => {
  4858. const { element, relativeSelectorList, pseudoName } = argsData;
  4859. return relativeSelectorList.children // Array.some() is used here as any selector from selector list should exist on page
  4860. .some((selectorNode) => {
  4861. // selectorList.children always starts with regular selector
  4862. const relativeRegularSelector = getFirstInnerRegularChild(
  4863. selectorNode,
  4864. pseudoName,
  4865. );
  4866. /**
  4867. * For checking the element by 'div:is(.banner)'
  4868. * we check whether the element's parentElement has any specific direct child.
  4869. */
  4870. const rootElement = getParent(
  4871. element,
  4872. `Selection by :${pseudoName}() pseudo-class is not possible`,
  4873. );
  4874. /**
  4875. * So we calculate the element "description" by it's tagname and attributes for targeting
  4876. * and use it to specify the selection
  4877. * e.g. `div:is(.banner)` --> `divNode.parentElement.querySelectorAll(':scope > .banner')`.
  4878. */
  4879. const specifiedSelector = `${scopeDirectChildren}${getNodeValue(relativeRegularSelector)}`;
  4880. let anyElements;
  4881. try {
  4882. // eslint-disable-next-line @typescript-eslint/no-use-before-define
  4883. anyElements = getElementsForSelectorNode(
  4884. selectorNode,
  4885. rootElement,
  4886. specifiedSelector,
  4887. );
  4888. } catch (e) {
  4889. // do not fail on invalid selectors for :is()
  4890. return false;
  4891. } // TODO: figure out how to handle complex selectors with extended pseudo-classes
  4892. // (check readme - extended-css-is-limitations)
  4893. // because `element` and `anyElements` may be from different DOM levels
  4894. return anyElements.includes(element);
  4895. });
  4896. };
  4897. /**
  4898. * Checks whether the element is not an element specified by pseudo-class arg.
  4899. * Used for :not() pseudo-class.
  4900. *
  4901. * @param argsData Relative pseudo-class helpers args data.
  4902. *
  4903. * @returns True if **any selector** from argsData.relativeSelectorList is **not matched** for argsData.element.
  4904. */
  4905. const notElementBySelectorList = (argsData) => {
  4906. const { element, relativeSelectorList, pseudoName } = argsData;
  4907. return relativeSelectorList.children // Array.every() is used here as element should not be selected by any selector from selector list
  4908. .every((selectorNode) => {
  4909. // selectorList.children always starts with regular selector
  4910. const relativeRegularSelector = getFirstInnerRegularChild(
  4911. selectorNode,
  4912. pseudoName,
  4913. );
  4914. /**
  4915. * For checking the element by 'div:not([data="content"])
  4916. * we check whether the element's parentElement has any specific direct child.
  4917. */
  4918. const rootElement = getParent(
  4919. element,
  4920. `Selection by :${pseudoName}() pseudo-class is not possible`,
  4921. );
  4922. /**
  4923. * So we calculate the element "description" by it's tagname and attributes for targeting
  4924. * and use it to specify the selection
  4925. * e.g. `div:not(.banner)` --> `divNode.parentElement.querySelectorAll(':scope > .banner')`.
  4926. */
  4927. const specifiedSelector = `${scopeDirectChildren}${getNodeValue(relativeRegularSelector)}`;
  4928. let anyElements;
  4929. try {
  4930. // eslint-disable-next-line @typescript-eslint/no-use-before-define
  4931. anyElements = getElementsForSelectorNode(
  4932. selectorNode,
  4933. rootElement,
  4934. specifiedSelector,
  4935. );
  4936. } catch (e) {
  4937. // fail on invalid selectors for :not()
  4938. logger.error(getErrorMessage(e)); // eslint-disable-next-line max-len
  4939. throw new Error(
  4940. `Invalid selector for :${pseudoName}() pseudo-class: '${getNodeValue(relativeRegularSelector)}'`,
  4941. );
  4942. } // TODO: figure out how to handle up-looking pseudo-classes inside :not()
  4943. // (check readme - extended-css-not-limitations)
  4944. // because `element` and `anyElements` may be from different DOM levels
  4945. return !anyElements.includes(element);
  4946. });
  4947. };
  4948. /**
  4949. * Selects dom elements by value of RegularSelector.
  4950. *
  4951. * @param regularSelectorNode RegularSelector node.
  4952. * @param root Root DOM element.
  4953. * @param specifiedSelector @see {@link SpecifiedSelector}.
  4954. *
  4955. * @returns Array of DOM elements.
  4956. * @throws An error if RegularSelector node value is an invalid selector.
  4957. */
  4958. const getByRegularSelector = (
  4959. regularSelectorNode,
  4960. root,
  4961. specifiedSelector,
  4962. ) => {
  4963. const selectorText = specifiedSelector
  4964. ? specifiedSelector
  4965. : getNodeValue(regularSelectorNode);
  4966. let selectedElements = [];
  4967. try {
  4968. selectedElements = Array.from(root.querySelectorAll(selectorText));
  4969. } catch (e) {
  4970. throw new Error(
  4971. `Error: unable to select by '${selectorText}' ${getErrorMessage(e)}`,
  4972. );
  4973. }
  4974. return selectedElements;
  4975. };
  4976. /**
  4977. * Returns list of dom elements filtered or selected by ExtendedSelector node.
  4978. *
  4979. * @param domElements Array of DOM elements.
  4980. * @param extendedSelectorNode ExtendedSelector node.
  4981. *
  4982. * @returns Array of DOM elements.
  4983. * @throws An error on unknown pseudo-class,
  4984. * absent or invalid arg of extended pseudo-class, etc.
  4985. */
  4986. const getByExtendedSelector = (domElements, extendedSelectorNode) => {
  4987. let foundElements = [];
  4988. const extendedPseudoClassNode = getPseudoClassNode(extendedSelectorNode);
  4989. const pseudoName = getNodeName(extendedPseudoClassNode);
  4990. if (isAbsolutePseudoClass(pseudoName)) {
  4991. // absolute extended pseudo-classes should have an argument
  4992. const absolutePseudoArg = getNodeValue(
  4993. extendedPseudoClassNode,
  4994. `Missing arg for :${pseudoName}() pseudo-class`,
  4995. );
  4996. if (pseudoName === NTH_ANCESTOR_PSEUDO_CLASS_MARKER) {
  4997. // :nth-ancestor()
  4998. foundElements = findByAbsolutePseudoPseudo.nthAncestor(
  4999. domElements,
  5000. absolutePseudoArg,
  5001. pseudoName,
  5002. );
  5003. } else if (pseudoName === XPATH_PSEUDO_CLASS_MARKER) {
  5004. // :xpath()
  5005. try {
  5006. document.createExpression(absolutePseudoArg, null);
  5007. } catch (e) {
  5008. throw new Error(
  5009. `Invalid argument of :${pseudoName}() pseudo-class: '${absolutePseudoArg}'`,
  5010. );
  5011. }
  5012. foundElements = findByAbsolutePseudoPseudo.xpath(
  5013. domElements,
  5014. absolutePseudoArg,
  5015. );
  5016. } else if (pseudoName === UPWARD_PSEUDO_CLASS_MARKER) {
  5017. // :upward()
  5018. if (Number.isNaN(Number(absolutePseudoArg))) {
  5019. // so arg is selector, not a number
  5020. foundElements = findByAbsolutePseudoPseudo.upward(
  5021. domElements,
  5022. absolutePseudoArg,
  5023. );
  5024. } else {
  5025. foundElements = findByAbsolutePseudoPseudo.nthAncestor(
  5026. domElements,
  5027. absolutePseudoArg,
  5028. pseudoName,
  5029. );
  5030. }
  5031. } else {
  5032. // all other absolute extended pseudo-classes
  5033. // e.g. contains, matches-attr, etc.
  5034. foundElements = domElements.filter((element) => {
  5035. return isMatchedByAbsolutePseudo(
  5036. element,
  5037. pseudoName,
  5038. absolutePseudoArg,
  5039. );
  5040. });
  5041. }
  5042. } else if (isRelativePseudoClass(pseudoName)) {
  5043. const relativeSelectorList = getRelativeSelectorListNode(
  5044. extendedPseudoClassNode,
  5045. );
  5046. let relativePredicate;
  5047. switch (pseudoName) {
  5048. case HAS_PSEUDO_CLASS_MARKER:
  5049. case ABP_HAS_PSEUDO_CLASS_MARKER:
  5050. relativePredicate = (element) =>
  5051. hasRelativesBySelectorList({
  5052. element,
  5053. relativeSelectorList,
  5054. pseudoName,
  5055. });
  5056. break;
  5057. case IS_PSEUDO_CLASS_MARKER:
  5058. relativePredicate = (element) =>
  5059. isAnyElementBySelectorList({
  5060. element,
  5061. relativeSelectorList,
  5062. pseudoName,
  5063. });
  5064. break;
  5065. case NOT_PSEUDO_CLASS_MARKER:
  5066. relativePredicate = (element) =>
  5067. notElementBySelectorList({
  5068. element,
  5069. relativeSelectorList,
  5070. pseudoName,
  5071. });
  5072. break;
  5073. default:
  5074. throw new Error(`Unknown relative pseudo-class: '${pseudoName}'`);
  5075. }
  5076. foundElements = domElements.filter(relativePredicate);
  5077. } else {
  5078. // extra check is parser missed something
  5079. throw new Error(`Unknown extended pseudo-class: '${pseudoName}'`);
  5080. }
  5081. return foundElements;
  5082. };
  5083. /**
  5084. * Returns list of dom elements which is selected by RegularSelector value.
  5085. *
  5086. * @param domElements Array of DOM elements.
  5087. * @param regularSelectorNode RegularSelector node.
  5088. *
  5089. * @returns Array of DOM elements.
  5090. * @throws An error if RegularSelector has not value.
  5091. */
  5092. const getByFollowingRegularSelector = (
  5093. domElements,
  5094. regularSelectorNode,
  5095. ) => {
  5096. // array of arrays because of Array.map() later
  5097. let foundElements = [];
  5098. const value = getNodeValue(regularSelectorNode);
  5099. if (value.startsWith(CHILD_COMBINATOR)) {
  5100. // e.g. div:has(> img) > .banner
  5101. foundElements = domElements.map((root) => {
  5102. const specifiedSelector = `${SCOPE_CSS_PSEUDO_CLASS}${value}`;
  5103. return getByRegularSelector(
  5104. regularSelectorNode,
  5105. root,
  5106. specifiedSelector,
  5107. );
  5108. });
  5109. } else if (
  5110. value.startsWith(NEXT_SIBLING_COMBINATOR) ||
  5111. value.startsWith(SUBSEQUENT_SIBLING_COMBINATOR)
  5112. ) {
  5113. // e.g. div:has(> img) + .banner
  5114. // or div:has(> img) ~ .banner
  5115. foundElements = domElements.map((element) => {
  5116. const rootElement = element.parentElement;
  5117. if (!rootElement) {
  5118. // do not throw error if there in no parent for element
  5119. // e.g. '*:contains(text)' selects `html` which has no parentElement
  5120. return [];
  5121. }
  5122. const elementSelectorText = getElementSelectorDesc(element);
  5123. const specifiedSelector = `${scopeDirectChildren}${elementSelectorText}${value}`;
  5124. const selected = getByRegularSelector(
  5125. regularSelectorNode,
  5126. rootElement,
  5127. specifiedSelector,
  5128. );
  5129. return selected;
  5130. });
  5131. } else {
  5132. // space-separated regular selector after extended one
  5133. // e.g. div:has(> img) .banner
  5134. foundElements = domElements.map((root) => {
  5135. const specifiedSelector = `${scopeAnyChildren}${getNodeValue(regularSelectorNode)}`;
  5136. return getByRegularSelector(
  5137. regularSelectorNode,
  5138. root,
  5139. specifiedSelector,
  5140. );
  5141. });
  5142. } // foundElements should be flattened
  5143. // as getByRegularSelector() returns elements array, and Array.map() collects them to array
  5144. return flatten(foundElements);
  5145. };
  5146. /**
  5147. * Returns elements nodes for Selector node.
  5148. * As far as any selector always starts with regular part,
  5149. * it selects by RegularSelector first and checks found elements later.
  5150. *
  5151. * Relative pseudo-classes has it's own subtree so getElementsForSelectorNode is called recursively.
  5152. *
  5153. * 'specifiedSelector' is needed for :has(), :is(), and :not() pseudo-classes
  5154. * as native querySelectorAll() does not select exact element descendants even if it is called on 'div'
  5155. * e.g. ':scope' specification is needed for proper descendants selection for 'div:has(> img)'.
  5156. * So we check `divNode.querySelectorAll(':scope > img').length > 0`.
  5157. *
  5158. * @param selectorNode Selector node.
  5159. * @param root Root DOM element.
  5160. * @param specifiedSelector Needed element specification.
  5161. *
  5162. * @returns Array of DOM elements.
  5163. * @throws An error if there is no selectorNodeChild.
  5164. */
  5165. const getElementsForSelectorNode = (
  5166. selectorNode,
  5167. root,
  5168. specifiedSelector,
  5169. ) => {
  5170. let selectedElements = [];
  5171. let i = 0;
  5172. while (i < selectorNode.children.length) {
  5173. const selectorNodeChild = getItemByIndex(
  5174. selectorNode.children,
  5175. i,
  5176. "selectorNodeChild should be specified",
  5177. );
  5178. if (i === 0) {
  5179. // any selector always starts with regular selector
  5180. selectedElements = getByRegularSelector(
  5181. selectorNodeChild,
  5182. root,
  5183. specifiedSelector,
  5184. );
  5185. } else if (isExtendedSelectorNode(selectorNodeChild)) {
  5186. // filter previously selected elements by next selector nodes
  5187. selectedElements = getByExtendedSelector(
  5188. selectedElements,
  5189. selectorNodeChild,
  5190. );
  5191. } else if (isRegularSelectorNode(selectorNodeChild)) {
  5192. selectedElements = getByFollowingRegularSelector(
  5193. selectedElements,
  5194. selectorNodeChild,
  5195. );
  5196. }
  5197. i += 1;
  5198. }
  5199. return selectedElements;
  5200. };
  5201. /**
  5202. * Selects elements by ast.
  5203. *
  5204. * @param ast Ast of parsed selector.
  5205. * @param doc Document.
  5206. *
  5207. * @returns Array of DOM elements.
  5208. */
  5209. const selectElementsByAst = function (ast) {
  5210. let doc =
  5211. arguments.length > 1 && arguments[1] !== undefined
  5212. ? arguments[1]
  5213. : document;
  5214. const selectedElements = []; // ast root is SelectorList node;
  5215. // it has Selector nodes as children which should be processed separately
  5216. ast.children.forEach((selectorNode) => {
  5217. selectedElements.push(...getElementsForSelectorNode(selectorNode, doc));
  5218. }); // selectedElements should be flattened as it is array of arrays with elements
  5219. const uniqueElements = [...new Set(flatten(selectedElements))];
  5220. return uniqueElements;
  5221. };
  5222. /**
  5223. * Class of ExtCssDocument is needed for caching.
  5224. * For making cache related to each new instance of class, not global.
  5225. */
  5226. class ExtCssDocument {
  5227. /**
  5228. * Cache with selectors and their AST parsing results.
  5229. */
  5230. /**
  5231. * Creates new ExtCssDocument and inits new `astCache`.
  5232. */
  5233. constructor() {
  5234. this.astCache = new Map();
  5235. }
  5236. /**
  5237. * Saves selector and it's ast to cache.
  5238. *
  5239. * @param selector Standard or extended selector.
  5240. * @param ast Selector ast.
  5241. */
  5242. saveAstToCache(selector, ast) {
  5243. this.astCache.set(selector, ast);
  5244. }
  5245. /**
  5246. * Returns ast from cache for given selector.
  5247. *
  5248. * @param selector Standard or extended selector.
  5249. *
  5250. * @returns Previously parsed ast found in cache, or null if not found.
  5251. */
  5252. getAstFromCache(selector) {
  5253. const cachedAst = this.astCache.get(selector) || null;
  5254. return cachedAst;
  5255. }
  5256. /**
  5257. * Returns selector ast:
  5258. * - if cached ast exists — returns it;
  5259. * - if no cached ast — saves newly parsed ast to cache and returns it.
  5260. *
  5261. * @param selector Standard or extended selector.
  5262. *
  5263. * @returns Ast for `selector`.
  5264. */
  5265. getSelectorAst(selector) {
  5266. let ast = this.getAstFromCache(selector);
  5267. if (!ast) {
  5268. ast = parse(selector);
  5269. }
  5270. this.saveAstToCache(selector, ast);
  5271. return ast;
  5272. }
  5273. /**
  5274. * Selects elements by selector.
  5275. *
  5276. * @param selector Standard or extended selector.
  5277. *
  5278. * @returns Array of DOM elements.
  5279. */
  5280. querySelectorAll(selector) {
  5281. const ast = this.getSelectorAst(selector);
  5282. return selectElementsByAst(ast);
  5283. }
  5284. }
  5285. const extCssDocument = new ExtCssDocument();
  5286. /**
  5287. * Converts array of `entries` to object.
  5288. * Object.fromEntries() polyfill because it is not supported by old browsers, e.g. Chrome 55.
  5289. * Only first two elements of `entries` array matter, other will be skipped silently.
  5290. *
  5291. * @see {@link https://caniuse.com/?search=Object.fromEntries}
  5292. *
  5293. * @param entries Array of pairs.
  5294. *
  5295. * @returns Object converted from `entries`.
  5296. */
  5297. const getObjectFromEntries = (entries) => {
  5298. const object = {};
  5299. entries.forEach((el) => {
  5300. const [key, value] = el;
  5301. object[key] = value;
  5302. });
  5303. return object;
  5304. };
  5305. const DEBUG_PSEUDO_PROPERTY_KEY = "debug";
  5306. /**
  5307. * Checks the presence of :remove() pseudo-class and validates it while parsing the selector part of css rule.
  5308. *
  5309. * @param rawSelector Selector which may contain :remove() pseudo-class.
  5310. *
  5311. * @returns Parsed selector data with selector and styles.
  5312. * @throws An error on invalid :remove() position.
  5313. */
  5314. const parseRemoveSelector = (rawSelector) => {
  5315. /**
  5316. * No error will be thrown on invalid selector as it will be validated later
  5317. * so it's better to explicitly specify 'any' selector for :remove() pseudo-class by '*',
  5318. * e.g. '.banner > *:remove()' instead of '.banner > :remove()'.
  5319. */
  5320. // ':remove()'
  5321. // eslint-disable-next-line max-len
  5322. const VALID_REMOVE_MARKER = `${COLON}${REMOVE_PSEUDO_MARKER}${BRACKET.PARENTHESES.LEFT}${BRACKET.PARENTHESES.RIGHT}`; // ':remove(' - needed for validation rules like 'div:remove(2)'
  5323. const INVALID_REMOVE_MARKER = `${COLON}${REMOVE_PSEUDO_MARKER}${BRACKET.PARENTHESES.LEFT}`;
  5324. let selector;
  5325. let shouldRemove = false;
  5326. const firstIndex = rawSelector.indexOf(VALID_REMOVE_MARKER);
  5327. if (firstIndex === 0) {
  5328. // e.g. ':remove()'
  5329. throw new Error(
  5330. `${REMOVE_ERROR_PREFIX.NO_TARGET_SELECTOR}: '${rawSelector}'`,
  5331. );
  5332. } else if (firstIndex > 0) {
  5333. if (firstIndex !== rawSelector.lastIndexOf(VALID_REMOVE_MARKER)) {
  5334. // rule with more than one :remove() pseudo-class is invalid
  5335. // e.g. '.block:remove() > .banner:remove()'
  5336. throw new Error(
  5337. `${REMOVE_ERROR_PREFIX.MULTIPLE_USAGE}: '${rawSelector}'`,
  5338. );
  5339. } else if (
  5340. firstIndex + VALID_REMOVE_MARKER.length <
  5341. rawSelector.length
  5342. ) {
  5343. // remove pseudo-class should be last in the rule
  5344. // e.g. '.block:remove():upward(2)'
  5345. throw new Error(
  5346. `${REMOVE_ERROR_PREFIX.INVALID_POSITION}: '${rawSelector}'`,
  5347. );
  5348. } else {
  5349. // valid :remove() pseudo-class position
  5350. selector = rawSelector.substring(0, firstIndex);
  5351. shouldRemove = true;
  5352. }
  5353. } else if (rawSelector.includes(INVALID_REMOVE_MARKER)) {
  5354. // it is not valid if ':remove()' is absent in rule but just ':remove(' is present
  5355. // e.g. 'div:remove(0)'
  5356. throw new Error(
  5357. `${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${rawSelector}'`,
  5358. );
  5359. } else {
  5360. // there is no :remove() pseudo-class in rule
  5361. selector = rawSelector;
  5362. }
  5363. const stylesOfSelector = shouldRemove
  5364. ? [
  5365. {
  5366. property: REMOVE_PSEUDO_MARKER,
  5367. value: PSEUDO_PROPERTY_POSITIVE_VALUE,
  5368. },
  5369. ]
  5370. : [];
  5371. return {
  5372. selector,
  5373. stylesOfSelector,
  5374. };
  5375. };
  5376. /**
  5377. * Parses cropped selector part found before `{`.
  5378. *
  5379. * @param selectorBuffer Buffered selector to parse.
  5380. * @param extCssDoc Needed for caching of selector ast.
  5381. *
  5382. * @returns Parsed validation data for cropped part of stylesheet which may be a selector.
  5383. * @throws An error on unsupported CSS features, e.g. at-rules.
  5384. */
  5385. const parseSelectorRulePart = (selectorBuffer, extCssDoc) => {
  5386. let selector = selectorBuffer.trim();
  5387. if (selector.startsWith(AT_RULE_MARKER)) {
  5388. throw new Error(`${NO_AT_RULE_ERROR_PREFIX}: '${selector}'.`);
  5389. }
  5390. let removeSelectorData;
  5391. try {
  5392. removeSelectorData = parseRemoveSelector(selector);
  5393. } catch (e) {
  5394. logger.error(getErrorMessage(e));
  5395. throw new Error(`${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${selector}'`);
  5396. }
  5397. let stylesOfSelector = [];
  5398. let success = false;
  5399. let ast;
  5400. try {
  5401. selector = removeSelectorData.selector;
  5402. stylesOfSelector = removeSelectorData.stylesOfSelector; // validate found selector by parsing it to ast
  5403. // so if it is invalid error will be thrown
  5404. ast = extCssDoc.getSelectorAst(selector);
  5405. success = true;
  5406. } catch (e) {
  5407. success = false;
  5408. }
  5409. return {
  5410. success,
  5411. selector,
  5412. ast,
  5413. stylesOfSelector,
  5414. };
  5415. };
  5416. /**
  5417. * Creates a map for storing raw results of css rules parsing.
  5418. * Used for merging styles for same selector.
  5419. *
  5420. * @returns Map where **key** is `selector`
  5421. * and **value** is object with `ast` and `styles`.
  5422. */
  5423. const createRawResultsMap = () => {
  5424. return new Map();
  5425. };
  5426. /**
  5427. * Saves rules data for unique selectors.
  5428. *
  5429. * @param rawResults Previously collected results of parsing.
  5430. * @param rawRuleData Parsed rule data.
  5431. *
  5432. * @throws An error if there is no rawRuleData.styles or rawRuleData.ast.
  5433. */
  5434. const saveToRawResults = (rawResults, rawRuleData) => {
  5435. const { selector, ast, rawStyles } = rawRuleData;
  5436. if (!rawStyles) {
  5437. throw new Error(`No style declaration for selector: '${selector}'`);
  5438. }
  5439. if (!ast) {
  5440. throw new Error(`No ast parsed for selector: '${selector}'`);
  5441. }
  5442. const storedRuleData = rawResults.get(selector);
  5443. if (!storedRuleData) {
  5444. rawResults.set(selector, {
  5445. ast,
  5446. styles: rawStyles,
  5447. });
  5448. } else {
  5449. storedRuleData.styles.push(...rawStyles);
  5450. }
  5451. };
  5452. /**
  5453. * Checks whether the 'remove' property positively set in styles
  5454. * with only one positive value - 'true'.
  5455. *
  5456. * @param styles Array of styles.
  5457. *
  5458. * @returns True if there is 'remove' property with 'true' value in `styles`.
  5459. */
  5460. const isRemoveSetInStyles = (styles) => {
  5461. return styles.some((s) => {
  5462. return (
  5463. s.property === REMOVE_PSEUDO_MARKER &&
  5464. s.value === PSEUDO_PROPERTY_POSITIVE_VALUE
  5465. );
  5466. });
  5467. };
  5468. /**
  5469. * Returns 'debug' property value which is set in styles.
  5470. *
  5471. * @param styles Array of styles.
  5472. *
  5473. * @returns Value of 'debug' property if it is set in `styles`,
  5474. * or `undefined` if the property is not found.
  5475. */
  5476. const getDebugStyleValue = (styles) => {
  5477. const debugStyle = styles.find((s) => {
  5478. return s.property === DEBUG_PSEUDO_PROPERTY_KEY;
  5479. });
  5480. return debugStyle === null || debugStyle === void 0
  5481. ? void 0
  5482. : debugStyle.value;
  5483. };
  5484. /**
  5485. * Prepares final RuleData.
  5486. * Handles `debug` and `remove` in raw rule data styles.
  5487. *
  5488. * @param rawRuleData Raw data of selector css rule parsing.
  5489. *
  5490. * @returns Parsed ExtendedCss rule data.
  5491. * @throws An error if rawRuleData.ast or rawRuleData.rawStyles not defined.
  5492. */
  5493. const prepareRuleData = (rawRuleData) => {
  5494. const { selector, ast, rawStyles } = rawRuleData;
  5495. if (!ast) {
  5496. throw new Error(`AST should be parsed for selector: '${selector}'`);
  5497. }
  5498. if (!rawStyles) {
  5499. throw new Error(`Styles should be parsed for selector: '${selector}'`);
  5500. }
  5501. const ruleData = {
  5502. selector,
  5503. ast,
  5504. };
  5505. const debugValue = getDebugStyleValue(rawStyles);
  5506. const shouldRemove = isRemoveSetInStyles(rawStyles);
  5507. let styles = rawStyles;
  5508. if (debugValue) {
  5509. // get rid of 'debug' from styles
  5510. styles = rawStyles.filter(
  5511. (s) => s.property !== DEBUG_PSEUDO_PROPERTY_KEY,
  5512. ); // and set it as separate property only if its value is valid
  5513. // which is 'true' or 'global'
  5514. if (
  5515. debugValue === PSEUDO_PROPERTY_POSITIVE_VALUE ||
  5516. debugValue === DEBUG_PSEUDO_PROPERTY_GLOBAL_VALUE
  5517. ) {
  5518. ruleData.debug = debugValue;
  5519. }
  5520. }
  5521. if (shouldRemove) {
  5522. // no other styles are needed to apply if 'remove' is set
  5523. ruleData.style = {
  5524. [REMOVE_PSEUDO_MARKER]: PSEUDO_PROPERTY_POSITIVE_VALUE,
  5525. };
  5526. /**
  5527. * 'content' property is needed for ExtCssConfiguration.beforeStyleApplied().
  5528. *
  5529. * @see {@link BeforeStyleAppliedCallback}
  5530. */
  5531. const contentStyle = styles.find(
  5532. (s) => s.property === CONTENT_CSS_PROPERTY,
  5533. );
  5534. if (contentStyle) {
  5535. ruleData.style[CONTENT_CSS_PROPERTY] = contentStyle.value;
  5536. }
  5537. } else {
  5538. // otherwise all styles should be applied.
  5539. // every style property will be unique because of their converting into object
  5540. if (styles.length > 0) {
  5541. const stylesAsEntries = styles.map((style) => {
  5542. const { property, value } = style;
  5543. return [property, value];
  5544. });
  5545. const preparedStyleData = getObjectFromEntries(stylesAsEntries);
  5546. ruleData.style = preparedStyleData;
  5547. }
  5548. }
  5549. return ruleData;
  5550. };
  5551. /**
  5552. * Combines previously parsed css rules data objects
  5553. * into rules which are ready to apply.
  5554. *
  5555. * @param rawResults Previously parsed css rules data objects.
  5556. *
  5557. * @returns Parsed ExtendedCss rule data.
  5558. */
  5559. const combineRulesData = (rawResults) => {
  5560. const results = [];
  5561. rawResults.forEach((value, key) => {
  5562. const selector = key;
  5563. const { ast, styles: rawStyles } = value;
  5564. results.push(
  5565. prepareRuleData({
  5566. selector,
  5567. ast,
  5568. rawStyles,
  5569. }),
  5570. );
  5571. });
  5572. return results;
  5573. };
  5574. /**
  5575. * Trims `rawStyle` and splits it into tokens.
  5576. *
  5577. * @param rawStyle Style declaration block content inside curly bracket — `{` and `}` —
  5578. * can be a single style declaration or a list of declarations.
  5579. *
  5580. * @returns Array of tokens supported for style declaration block.
  5581. */
  5582. const tokenizeStyleBlock = (rawStyle) => {
  5583. const styleDeclaration = rawStyle.trim();
  5584. return tokenize(styleDeclaration, SUPPORTED_STYLE_DECLARATION_MARKS);
  5585. };
  5586. /**
  5587. * Describes possible style declaration parts.
  5588. *
  5589. * IMPORTANT: it is used as 'const' instead of 'enum' to avoid side effects
  5590. * during ExtendedCss import into other libraries.
  5591. */
  5592. const DECLARATION_PART = {
  5593. PROPERTY: "property",
  5594. VALUE: "value",
  5595. };
  5596. /**
  5597. * Checks whether the quotes has been opened for style value.
  5598. *
  5599. * @param context Style block parser context.
  5600. *
  5601. * @returns True if style value has already opened quotes.
  5602. */
  5603. const isValueQuotesOpen = (context) => {
  5604. return context.bufferValue !== "" && context.valueQuoteMark !== null;
  5605. };
  5606. /**
  5607. * Saves parsed property and value to collection of parsed styles.
  5608. * Prunes context buffers for property and value.
  5609. *
  5610. * @param context Style block parser context.
  5611. */
  5612. const collectStyle = (context) => {
  5613. context.styles.push({
  5614. property: context.bufferProperty.trim(),
  5615. value: context.bufferValue.trim(),
  5616. }); // reset buffers
  5617. context.bufferProperty = "";
  5618. context.bufferValue = "";
  5619. };
  5620. /**
  5621. * Handles token which is supposed to be a part of style **property**.
  5622. *
  5623. * @param context Style block parser context.
  5624. * @param styleBlock Whole style block which is being parsed.
  5625. * @param token Current token.
  5626. *
  5627. * @throws An error on invalid token.
  5628. */
  5629. const processPropertyToken = (context, styleBlock, token) => {
  5630. const { value: tokenValue } = token;
  5631. switch (token.type) {
  5632. case TOKEN_TYPE.WORD:
  5633. if (context.bufferProperty.length > 0) {
  5634. // e.g. 'padding top: 0;' - current tokenValue is 'top' which is not valid
  5635. throw new Error(
  5636. `Invalid style property in style block: '${styleBlock}'`,
  5637. );
  5638. }
  5639. context.bufferProperty += tokenValue;
  5640. break;
  5641. case TOKEN_TYPE.MARK:
  5642. // only colon and whitespaces are allowed while style property parsing
  5643. if (tokenValue === COLON) {
  5644. if (context.bufferProperty.trim().length === 0) {
  5645. // e.g. such style block: '{ : none; }'
  5646. throw new Error(
  5647. `Missing style property before ':' in style block: '${styleBlock}'`,
  5648. );
  5649. } // the property successfully collected
  5650. context.bufferProperty = context.bufferProperty.trim(); // prepare for value collecting
  5651. context.processing = DECLARATION_PART.VALUE; // the property buffer shall be reset after the value is successfully collected
  5652. } else if (WHITE_SPACE_CHARACTERS.includes(tokenValue));
  5653. else {
  5654. // if after the property there is anything other than ':' except whitespace, this is a parse error
  5655. // https://www.w3.org/TR/css-syntax-3/#consume-declaration
  5656. throw new Error(
  5657. `Invalid style declaration in style block: '${styleBlock}'`,
  5658. );
  5659. }
  5660. break;
  5661. default:
  5662. throw new Error(
  5663. `Unsupported style property character: '${tokenValue}' in style block: '${styleBlock}'`,
  5664. );
  5665. }
  5666. };
  5667. /**
  5668. * Handles token which is supposed to be a part of style **value**.
  5669. *
  5670. * @param context Style block parser context.
  5671. * @param styleBlock Whole style block which is being parsed.
  5672. * @param token Current token.
  5673. *
  5674. * @throws An error on invalid token.
  5675. */
  5676. const processValueToken = (context, styleBlock, token) => {
  5677. const { value: tokenValue } = token;
  5678. if (token.type === TOKEN_TYPE.WORD) {
  5679. // simply collect to buffer
  5680. context.bufferValue += tokenValue;
  5681. } else {
  5682. // otherwise check the mark
  5683. switch (tokenValue) {
  5684. case COLON:
  5685. // the ':' character inside of the value should be inside of quotes
  5686. // otherwise the value is not valid
  5687. // e.g. 'content: display: none'
  5688. // parser is here ↑
  5689. if (!isValueQuotesOpen(context)) {
  5690. // eslint-disable-next-line max-len
  5691. throw new Error(
  5692. `Invalid style value for property '${context.bufferProperty}' in style block: '${styleBlock}'`,
  5693. );
  5694. } // collect the colon inside quotes
  5695. // e.g. 'content: "test:123"'
  5696. // parser is here ↑
  5697. context.bufferValue += tokenValue;
  5698. break;
  5699. case SEMICOLON:
  5700. if (isValueQuotesOpen(context)) {
  5701. // ';' inside quotes is part of style value
  5702. // e.g. 'content: "test;"'
  5703. context.bufferValue += tokenValue;
  5704. } else {
  5705. // otherwise the value is successfully collected
  5706. // save parsed style
  5707. collectStyle(context); // prepare for value collecting
  5708. context.processing = DECLARATION_PART.PROPERTY;
  5709. }
  5710. break;
  5711. case SINGLE_QUOTE:
  5712. case DOUBLE_QUOTE:
  5713. // if quotes are not open
  5714. if (context.valueQuoteMark === null) {
  5715. // save the opening quote mark for later comparison
  5716. context.valueQuoteMark = tokenValue;
  5717. } else if (
  5718. !context.bufferValue.endsWith(BACKSLASH) && // otherwise a quote appeared in the value earlier,
  5719. // and non-escaped quote should be checked whether it is a closing quote
  5720. context.valueQuoteMark === tokenValue
  5721. ) {
  5722. context.valueQuoteMark = null;
  5723. } // always save the quote to the buffer
  5724. // but after the context.bufferValue is checked for BACKSLASH above
  5725. // e.g. 'content: "test:123"'
  5726. // 'content: "\""'
  5727. context.bufferValue += tokenValue;
  5728. break;
  5729. case BACKSLASH:
  5730. if (!isValueQuotesOpen(context)) {
  5731. // eslint-disable-next-line max-len
  5732. throw new Error(
  5733. `Invalid style value for property '${context.bufferProperty}' in style block: '${styleBlock}'`,
  5734. );
  5735. } // collect the backslash inside quotes
  5736. // e.g. ' content: "\"" '
  5737. // parser is here ↑
  5738. context.bufferValue += tokenValue;
  5739. break;
  5740. case SPACE:
  5741. case TAB:
  5742. case CARRIAGE_RETURN:
  5743. case LINE_FEED:
  5744. case FORM_FEED:
  5745. // whitespace should be collected only if the value collecting started
  5746. // which means inside of the value
  5747. // e.g. 'width: 100% !important'
  5748. // parser is here ↑
  5749. if (context.bufferValue.length > 0) {
  5750. context.bufferValue += tokenValue;
  5751. } // otherwise it can be omitted
  5752. // e.g. 'width: 100% !important'
  5753. // here ↑
  5754. break;
  5755. default:
  5756. throw new Error(`Unknown style declaration token: '${tokenValue}'`);
  5757. }
  5758. }
  5759. };
  5760. /**
  5761. * Parses css rule style block.
  5762. *
  5763. * @param rawStyleBlock Style block to parse.
  5764. *
  5765. * @returns Array of style declarations.
  5766. * @throws An error on invalid style block.
  5767. */
  5768. const parseStyleBlock = (rawStyleBlock) => {
  5769. const styleBlock = rawStyleBlock.trim();
  5770. const tokens = tokenizeStyleBlock(styleBlock);
  5771. const context = {
  5772. // style declaration parsing always starts with 'property'
  5773. processing: DECLARATION_PART.PROPERTY,
  5774. styles: [],
  5775. bufferProperty: "",
  5776. bufferValue: "",
  5777. valueQuoteMark: null,
  5778. };
  5779. let i = 0;
  5780. while (i < tokens.length) {
  5781. const token = tokens[i];
  5782. if (!token) {
  5783. break;
  5784. }
  5785. if (context.processing === DECLARATION_PART.PROPERTY) {
  5786. processPropertyToken(context, styleBlock, token);
  5787. } else if (context.processing === DECLARATION_PART.VALUE) {
  5788. processValueToken(context, styleBlock, token);
  5789. } else {
  5790. throw new Error("Style declaration parsing failed");
  5791. }
  5792. i += 1;
  5793. } // unbalanced value quotes
  5794. // e.g. 'content: "test} '
  5795. if (isValueQuotesOpen(context)) {
  5796. throw new Error(
  5797. `Unbalanced style declaration quotes in style block: '${styleBlock}'`,
  5798. );
  5799. } // collected property and value have not been saved to styles;
  5800. // it is possible for style block with no semicolon at the end
  5801. // e.g. such style block: '{ display: none }'
  5802. if (context.bufferProperty.length > 0) {
  5803. if (context.bufferValue.length === 0) {
  5804. // e.g. such style blocks:
  5805. // '{ display: }'
  5806. // '{ remove }'
  5807. // eslint-disable-next-line max-len
  5808. throw new Error(
  5809. `Missing style value for property '${context.bufferProperty}' in style block '${styleBlock}'`,
  5810. );
  5811. }
  5812. collectStyle(context);
  5813. } // rule with empty style block
  5814. // e.g. 'div { }'
  5815. if (context.styles.length === 0) {
  5816. throw new Error(STYLE_ERROR_PREFIX.NO_STYLE);
  5817. }
  5818. return context.styles;
  5819. };
  5820. /**
  5821. * Returns array of positions of `{` in `cssRule`.
  5822. *
  5823. * @param cssRule CSS rule.
  5824. *
  5825. * @returns Array of left curly bracket indexes.
  5826. */
  5827. const getLeftCurlyBracketIndexes = (cssRule) => {
  5828. const indexes = [];
  5829. for (let i = 0; i < cssRule.length; i += 1) {
  5830. if (cssRule[i] === BRACKET.CURLY.LEFT) {
  5831. indexes.push(i);
  5832. }
  5833. }
  5834. return indexes;
  5835. }; // TODO: use `extCssDoc` for caching of style block parser results
  5836. /**
  5837. * Parses CSS rule into rules data object:
  5838. * 1. Find the last `{` mark in the rule
  5839. * which supposed to be a divider between selector and style block.
  5840. * 2. Validates found string part before the `{` via selector parser; and if:
  5841. * - parsing failed – get the previous `{` in the rule,
  5842. * and validates a new rule part again [2];
  5843. * - parsing successful — saves a found rule part as selector and parses the style block.
  5844. *
  5845. * @param rawCssRule Single CSS rule to parse.
  5846. * @param extCssDoc ExtCssDocument which is used for selector ast caching.
  5847. *
  5848. * @returns Array of rules data which contains:
  5849. * - selector as string;
  5850. * - ast to query elements by;
  5851. * - map of styles to apply.
  5852. * @throws An error on invalid css rule syntax:
  5853. * - unsupported CSS features – comments and at-rules
  5854. * - invalid selector or style block.
  5855. */
  5856. const parseRule = (rawCssRule, extCssDoc) => {
  5857. var _rawRuleData$selector;
  5858. const cssRule = rawCssRule.trim();
  5859. if (
  5860. cssRule.includes(`${SLASH}${ASTERISK}`) &&
  5861. cssRule.includes(`${ASTERISK}${SLASH}`)
  5862. ) {
  5863. throw new Error(STYLE_ERROR_PREFIX.NO_COMMENT);
  5864. }
  5865. const leftCurlyBracketIndexes = getLeftCurlyBracketIndexes(cssRule); // rule with style block but no selector
  5866. // e.g. '{ display: none; }'
  5867. if (getFirst(leftCurlyBracketIndexes) === 0) {
  5868. throw new Error(NO_SELECTOR_ERROR_PREFIX);
  5869. }
  5870. let selectorData; // if rule has `{` but there is no `}`
  5871. if (
  5872. leftCurlyBracketIndexes.length > 0 &&
  5873. !cssRule.includes(BRACKET.CURLY.RIGHT)
  5874. ) {
  5875. throw new Error(
  5876. `${STYLE_ERROR_PREFIX.NO_STYLE} OR ${STYLE_ERROR_PREFIX.UNCLOSED_STYLE}`,
  5877. );
  5878. }
  5879. if (
  5880. // if rule has no `{`
  5881. leftCurlyBracketIndexes.length === 0 || // or `}`
  5882. !cssRule.includes(BRACKET.CURLY.RIGHT)
  5883. ) {
  5884. try {
  5885. // the whole css rule considered as "selector part"
  5886. // which may contain :remove() pseudo-class
  5887. selectorData = parseSelectorRulePart(cssRule, extCssDoc);
  5888. if (selectorData.success) {
  5889. var _selectorData$stylesO;
  5890. // rule with no style block has valid :remove() pseudo-class
  5891. // which is parsed into "styles"
  5892. // e.g. 'div:remove()'
  5893. // but also it can be just selector with no styles
  5894. // e.g. 'div'
  5895. // which should not be considered as valid css rule
  5896. if (
  5897. ((_selectorData$stylesO = selectorData.stylesOfSelector) ===
  5898. null || _selectorData$stylesO === void 0
  5899. ? void 0
  5900. : _selectorData$stylesO.length) === 0
  5901. ) {
  5902. throw new Error(STYLE_ERROR_PREFIX.NO_STYLE_OR_REMOVE);
  5903. }
  5904. return {
  5905. selector: selectorData.selector.trim(),
  5906. ast: selectorData.ast,
  5907. rawStyles: selectorData.stylesOfSelector,
  5908. };
  5909. } else {
  5910. // not valid selector
  5911. throw new Error("Invalid selector");
  5912. }
  5913. } catch (e) {
  5914. throw new Error(getErrorMessage(e));
  5915. }
  5916. }
  5917. let selectorBuffer;
  5918. let styleBlockBuffer;
  5919. const rawRuleData = {
  5920. selector: "",
  5921. }; // css rule should be parsed from its end
  5922. for (let i = leftCurlyBracketIndexes.length - 1; i > -1; i -= 1) {
  5923. const index = leftCurlyBracketIndexes[i];
  5924. if (!index) {
  5925. throw new Error(
  5926. `Impossible to continue, no '{' to process for rule: '${cssRule}'`,
  5927. );
  5928. } // selector is before `{`, style block is after it
  5929. selectorBuffer = cssRule.slice(0, index); // skip curly brackets
  5930. styleBlockBuffer = cssRule.slice(index + 1, cssRule.length - 1);
  5931. selectorData = parseSelectorRulePart(selectorBuffer, extCssDoc);
  5932. if (selectorData.success) {
  5933. var _rawRuleData$rawStyle;
  5934. // selector successfully parsed
  5935. rawRuleData.selector = selectorData.selector.trim();
  5936. rawRuleData.ast = selectorData.ast;
  5937. rawRuleData.rawStyles = selectorData.stylesOfSelector; // style block should be parsed
  5938. // TODO: add cache for style block parsing
  5939. const parsedStyles = parseStyleBlock(styleBlockBuffer);
  5940. (_rawRuleData$rawStyle = rawRuleData.rawStyles) === null ||
  5941. _rawRuleData$rawStyle === void 0
  5942. ? void 0
  5943. : _rawRuleData$rawStyle.push(...parsedStyles); // stop rule parsing
  5944. break;
  5945. } else {
  5946. // if selector was not parsed successfully
  5947. // continue with next index of `{`
  5948. continue;
  5949. }
  5950. }
  5951. if (
  5952. ((_rawRuleData$selector = rawRuleData.selector) === null ||
  5953. _rawRuleData$selector === void 0
  5954. ? void 0
  5955. : _rawRuleData$selector.length) === 0
  5956. ) {
  5957. // skip the rule as selector
  5958. throw new Error("Selector in not valid");
  5959. }
  5960. return rawRuleData;
  5961. };
  5962. /**
  5963. * Parses array of CSS rules into array of rules data objects.
  5964. * Invalid rules are skipped and not applied,
  5965. * and the errors are logged.
  5966. *
  5967. * @param rawCssRules Array of rules to parse.
  5968. * @param extCssDoc Needed for selector ast caching.
  5969. *
  5970. * @returns Array of parsed valid rules data.
  5971. */
  5972. const parseRules$1 = (rawCssRules, extCssDoc) => {
  5973. const rawResults = createRawResultsMap();
  5974. const warnings = []; // trim all rules and find unique ones
  5975. const uniqueRules = [...new Set(rawCssRules.map((r) => r.trim()))];
  5976. uniqueRules.forEach((rule) => {
  5977. try {
  5978. saveToRawResults(rawResults, parseRule(rule, extCssDoc));
  5979. } catch (e) {
  5980. // skip the invalid rule
  5981. const errorMessage = getErrorMessage(e);
  5982. warnings.push(`'${rule}' - error: '${errorMessage}'`);
  5983. }
  5984. }); // log info about skipped invalid rules
  5985. if (warnings.length > 0) {
  5986. logger.info(`Invalid rules:\n ${warnings.join("\n ")}`);
  5987. }
  5988. return combineRulesData(rawResults);
  5989. };
  5990. const REGEXP_DECLARATION_END = /[;}]/g;
  5991. const REGEXP_DECLARATION_DIVIDER = /[;:}]/g;
  5992. const REGEXP_NON_WHITESPACE = /\S/g;
  5993. /**
  5994. * Interface for stylesheet parser context.
  5995. */
  5996. /**
  5997. * Resets rule data buffer to init value after rule successfully collected.
  5998. *
  5999. * @param context Stylesheet parser context.
  6000. */
  6001. const restoreRuleAcc = (context) => {
  6002. context.rawRuleData = {
  6003. selector: "",
  6004. };
  6005. };
  6006. /**
  6007. * Parses cropped selector part found before `{` previously.
  6008. *
  6009. * @param context Stylesheet parser context.
  6010. * @param extCssDoc Needed for caching of selector ast.
  6011. *
  6012. * @returns Parsed validation data for cropped part of stylesheet which may be a selector.
  6013. * @throws An error on unsupported CSS features, e.g. at-rules.
  6014. */
  6015. const parseSelectorPart = (context, extCssDoc) => {
  6016. let selector = context.selectorBuffer.trim();
  6017. if (selector.startsWith(AT_RULE_MARKER)) {
  6018. throw new Error(`${NO_AT_RULE_ERROR_PREFIX}: '${selector}'.`);
  6019. }
  6020. let removeSelectorData;
  6021. try {
  6022. removeSelectorData = parseRemoveSelector(selector);
  6023. } catch (e) {
  6024. logger.error(getErrorMessage(e));
  6025. throw new Error(`${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${selector}'`);
  6026. }
  6027. if (context.nextIndex === -1) {
  6028. if (selector === removeSelectorData.selector) {
  6029. // rule should have style or pseudo-class :remove()
  6030. throw new Error(
  6031. `${STYLE_ERROR_PREFIX.NO_STYLE_OR_REMOVE}: '${context.cssToParse}'`,
  6032. );
  6033. } // stop parsing as there is no style declaration and selector parsed fine
  6034. context.cssToParse = "";
  6035. }
  6036. let stylesOfSelector = [];
  6037. let success = false;
  6038. let ast;
  6039. try {
  6040. selector = removeSelectorData.selector;
  6041. stylesOfSelector = removeSelectorData.stylesOfSelector; // validate found selector by parsing it to ast
  6042. // so if it is invalid error will be thrown
  6043. ast = extCssDoc.getSelectorAst(selector);
  6044. success = true;
  6045. } catch (e) {
  6046. success = false;
  6047. }
  6048. if (context.nextIndex > 0) {
  6049. // slice found valid selector part off
  6050. // and parse rest of stylesheet later
  6051. context.cssToParse = context.cssToParse.slice(context.nextIndex);
  6052. }
  6053. return {
  6054. success,
  6055. selector,
  6056. ast,
  6057. stylesOfSelector,
  6058. };
  6059. };
  6060. /**
  6061. * Recursively parses style declaration string into `Style`s.
  6062. *
  6063. * @param context Stylesheet parser context.
  6064. * @param styles Array of styles.
  6065. *
  6066. * @throws An error on invalid style declaration.
  6067. * @returns A number index of the next `}` in `this.cssToParse`.
  6068. */
  6069. const parseUntilClosingBracket = (context, styles) => {
  6070. // Expects ":", ";", and "}".
  6071. REGEXP_DECLARATION_DIVIDER.lastIndex = context.nextIndex;
  6072. let match = REGEXP_DECLARATION_DIVIDER.exec(context.cssToParse);
  6073. if (match === null) {
  6074. throw new Error(
  6075. `${STYLE_ERROR_PREFIX.INVALID_STYLE}: '${context.cssToParse}'`,
  6076. );
  6077. }
  6078. let matchPos = match.index;
  6079. let matched = match[0];
  6080. if (matched === BRACKET.CURLY.RIGHT) {
  6081. const declarationChunk = context.cssToParse.slice(
  6082. context.nextIndex,
  6083. matchPos,
  6084. );
  6085. if (declarationChunk.trim().length === 0) {
  6086. // empty style declaration
  6087. // e.g. 'div { }'
  6088. if (styles.length === 0) {
  6089. throw new Error(
  6090. `${STYLE_ERROR_PREFIX.NO_STYLE}: '${context.cssToParse}'`,
  6091. );
  6092. } // else valid style parsed before it
  6093. // e.g. '{ display: none; }' -- position is after ';'
  6094. } else {
  6095. // closing curly bracket '}' is matched before colon ':'
  6096. // trimmed declarationChunk is not a space, between ';' and '}',
  6097. // e.g. 'visible }' in style '{ display: none; visible }' after part before ';' is parsed
  6098. throw new Error(
  6099. `${STYLE_ERROR_PREFIX.INVALID_STYLE}: '${context.cssToParse}'`,
  6100. );
  6101. }
  6102. return matchPos;
  6103. }
  6104. if (matched === COLON) {
  6105. const colonIndex = matchPos; // Expects ";" and "}".
  6106. REGEXP_DECLARATION_END.lastIndex = colonIndex;
  6107. match = REGEXP_DECLARATION_END.exec(context.cssToParse);
  6108. if (match === null) {
  6109. throw new Error(
  6110. `${STYLE_ERROR_PREFIX.UNCLOSED_STYLE}: '${context.cssToParse}'`,
  6111. );
  6112. }
  6113. matchPos = match.index;
  6114. matched = match[0]; // Populates the `styleMap` key-value map.
  6115. const property = context.cssToParse
  6116. .slice(context.nextIndex, colonIndex)
  6117. .trim();
  6118. if (property.length === 0) {
  6119. throw new Error(
  6120. `${STYLE_ERROR_PREFIX.NO_PROPERTY}: '${context.cssToParse}'`,
  6121. );
  6122. }
  6123. const value = context.cssToParse.slice(colonIndex + 1, matchPos).trim();
  6124. if (value.length === 0) {
  6125. throw new Error(
  6126. `${STYLE_ERROR_PREFIX.NO_VALUE}: '${context.cssToParse}'`,
  6127. );
  6128. }
  6129. styles.push({
  6130. property,
  6131. value,
  6132. }); // finish style parsing if '}' is found
  6133. // e.g. '{ display: none }' -- no ';' at the end of declaration
  6134. if (matched === BRACKET.CURLY.RIGHT) {
  6135. return matchPos;
  6136. }
  6137. } // matchPos is the position of the next ';'
  6138. // crop 'cssToParse' and re-run the loop
  6139. context.cssToParse = context.cssToParse.slice(matchPos + 1);
  6140. context.nextIndex = 0;
  6141. return parseUntilClosingBracket(context, styles); // Should be a subject of tail-call optimization.
  6142. };
  6143. /**
  6144. * Parses next style declaration part in stylesheet.
  6145. *
  6146. * @param context Stylesheet parser context.
  6147. *
  6148. * @returns Array of style data objects.
  6149. */
  6150. const parseNextStyle = (context) => {
  6151. const styles = [];
  6152. const styleEndPos = parseUntilClosingBracket(context, styles); // find next rule after the style declaration
  6153. REGEXP_NON_WHITESPACE.lastIndex = styleEndPos + 1;
  6154. const match = REGEXP_NON_WHITESPACE.exec(context.cssToParse);
  6155. if (match === null) {
  6156. context.cssToParse = "";
  6157. return styles;
  6158. }
  6159. const matchPos = match.index; // cut out matched style declaration for previous selector
  6160. context.cssToParse = context.cssToParse.slice(matchPos);
  6161. return styles;
  6162. };
  6163. /**
  6164. * Parses stylesheet of rules into rules data objects (non-recursively):
  6165. * 1. Iterates through stylesheet string.
  6166. * 2. Finds first `{` which can be style declaration start or part of selector.
  6167. * 3. Validates found string part via selector parser; and if:
  6168. * - it throws error — saves string part to buffer as part of selector,
  6169. * slice next stylesheet part to `{` [2] and validates again [3];
  6170. * - no error — saves found string part as selector and starts to parse styles (recursively).
  6171. *
  6172. * @param rawStylesheet Raw stylesheet as string.
  6173. * @param extCssDoc ExtCssDocument which uses cache while selectors parsing.
  6174. * @throws An error on unsupported CSS features, e.g. comments or invalid stylesheet syntax.
  6175. * @returns Array of rules data which contains:
  6176. * - selector as string;
  6177. * - ast to query elements by;
  6178. * - map of styles to apply.
  6179. */
  6180. const parseStylesheet = (rawStylesheet, extCssDoc) => {
  6181. const stylesheet = rawStylesheet.trim();
  6182. if (
  6183. stylesheet.includes(`${SLASH}${ASTERISK}`) &&
  6184. stylesheet.includes(`${ASTERISK}${SLASH}`)
  6185. ) {
  6186. throw new Error(
  6187. `${STYLE_ERROR_PREFIX.NO_COMMENT} in stylesheet: '${stylesheet}'`,
  6188. );
  6189. }
  6190. const context = {
  6191. // any stylesheet should start with selector
  6192. isSelector: true,
  6193. // init value of parser position
  6194. nextIndex: 0,
  6195. // init value of cssToParse
  6196. cssToParse: stylesheet,
  6197. // buffer for collecting selector part
  6198. selectorBuffer: "",
  6199. // accumulator for rules
  6200. rawRuleData: {
  6201. selector: "",
  6202. },
  6203. };
  6204. const rawResults = createRawResultsMap();
  6205. let selectorData; // context.cssToParse is going to be cropped while its parsing
  6206. while (context.cssToParse) {
  6207. if (context.isSelector) {
  6208. // find index of first opening curly bracket
  6209. // which may mean start of style part and end of selector one
  6210. context.nextIndex = context.cssToParse.indexOf(BRACKET.CURLY.LEFT); // rule should not start with style, selector is required
  6211. // e.g. '{ display: none; }'
  6212. if (context.selectorBuffer.length === 0 && context.nextIndex === 0) {
  6213. throw new Error(
  6214. `${STYLE_ERROR_PREFIX.NO_SELECTOR}: '${context.cssToParse}'`,
  6215. );
  6216. }
  6217. if (context.nextIndex === -1) {
  6218. // no style declaration in rule
  6219. // but rule still may contain :remove() pseudo-class
  6220. context.selectorBuffer = context.cssToParse;
  6221. } else {
  6222. // collect string parts before opening curly bracket
  6223. // until valid selector collected
  6224. context.selectorBuffer += context.cssToParse.slice(
  6225. 0,
  6226. context.nextIndex,
  6227. );
  6228. }
  6229. selectorData = parseSelectorPart(context, extCssDoc);
  6230. if (selectorData.success) {
  6231. // selector successfully parsed
  6232. context.rawRuleData.selector = selectorData.selector.trim();
  6233. context.rawRuleData.ast = selectorData.ast;
  6234. context.rawRuleData.rawStyles = selectorData.stylesOfSelector;
  6235. context.isSelector = false; // save rule data if there is no style declaration
  6236. if (context.nextIndex === -1) {
  6237. saveToRawResults(rawResults, context.rawRuleData); // clean up ruleContext
  6238. restoreRuleAcc(context);
  6239. } else {
  6240. // skip the opening curly bracket at the start of style declaration part
  6241. context.nextIndex = 1;
  6242. context.selectorBuffer = "";
  6243. }
  6244. } else {
  6245. // if selector was not successfully parsed parseSelectorPart(), continue stylesheet parsing:
  6246. // save the found bracket to buffer and proceed to next loop iteration
  6247. context.selectorBuffer += BRACKET.CURLY.LEFT; // delete `{` from cssToParse
  6248. context.cssToParse = context.cssToParse.slice(1);
  6249. }
  6250. } else {
  6251. var _context$rawRuleData$;
  6252. // style declaration should be parsed
  6253. const parsedStyles = parseNextStyle(context); // styles can be parsed from selector part if it has :remove() pseudo-class
  6254. // e.g. '.banner:remove() { debug: true; }'
  6255. (_context$rawRuleData$ = context.rawRuleData.rawStyles) === null ||
  6256. _context$rawRuleData$ === void 0
  6257. ? void 0
  6258. : _context$rawRuleData$.push(...parsedStyles); // save rule data to results
  6259. saveToRawResults(rawResults, context.rawRuleData);
  6260. context.nextIndex = 0; // clean up ruleContext
  6261. restoreRuleAcc(context); // parse next rule selector after style successfully parsed
  6262. context.isSelector = true;
  6263. }
  6264. }
  6265. return combineRulesData(rawResults);
  6266. };
  6267. /**
  6268. * Checks whether passed `arg` is number type.
  6269. *
  6270. * @param arg Value to check.
  6271. *
  6272. * @returns True if `arg` is number and not NaN.
  6273. */
  6274. const isNumber = (arg) => {
  6275. return typeof arg === "number" && !Number.isNaN(arg);
  6276. };
  6277. /**
  6278. * The purpose of ThrottleWrapper is to throttle calls of the function
  6279. * that applies ExtendedCss rules. The reasoning here is that the function calls
  6280. * are triggered by MutationObserver and there may be many mutations in a short period of time.
  6281. * We do not want to apply rules on every mutation so we use this helper to make sure
  6282. * that there is only one call in the given amount of time.
  6283. */
  6284. class ThrottleWrapper {
  6285. /**
  6286. * Creates new ThrottleWrapper.
  6287. * The {@link callback} should be executed not more often than {@link ThrottleWrapper.THROTTLE_DELAY_MS}.
  6288. *
  6289. * @param callback The callback.
  6290. */
  6291. constructor(callback) {
  6292. this.callback = callback;
  6293. this.executeCallback = this.executeCallback.bind(this);
  6294. }
  6295. /**
  6296. * Calls the {@link callback} function and update bounded throttle wrapper properties.
  6297. */
  6298. executeCallback() {
  6299. this.lastRunTime = performance.now();
  6300. if (isNumber(this.timerId)) {
  6301. clearTimeout(this.timerId);
  6302. delete this.timerId;
  6303. }
  6304. this.callback();
  6305. }
  6306. /**
  6307. * Schedules the {@link executeCallback} function execution via setTimeout.
  6308. * It may triggered by MutationObserver job which may occur too ofter, so we limit the function execution:
  6309. *
  6310. * 1. If {@link timerId} is set, ignore the call, because the function is already scheduled to be executed;
  6311. *
  6312. * 2. If {@link lastRunTime} is set, we need to check the time elapsed time since the last call. If it is
  6313. * less than {@link ThrottleWrapper.THROTTLE_DELAY_MS}, we schedule the function execution after the remaining time.
  6314. *
  6315. * Otherwise, we execute the function asynchronously to ensure that it is executed
  6316. * in the correct order with respect to DOM events, by deferring its execution until after
  6317. * those tasks have completed.
  6318. */
  6319. run() {
  6320. if (isNumber(this.timerId)) {
  6321. // there is a pending execution scheduled
  6322. return;
  6323. }
  6324. if (isNumber(this.lastRunTime)) {
  6325. const elapsedTime = performance.now() - this.lastRunTime;
  6326. if (elapsedTime < ThrottleWrapper.THROTTLE_DELAY_MS) {
  6327. this.timerId = window.setTimeout(
  6328. this.executeCallback,
  6329. ThrottleWrapper.THROTTLE_DELAY_MS - elapsedTime,
  6330. );
  6331. return;
  6332. }
  6333. }
  6334. /**
  6335. * We use `setTimeout` instead `requestAnimationFrame`
  6336. * here because requestAnimationFrame can be delayed for a long time
  6337. * when the browser saves battery or the engine is heavily loaded.
  6338. */
  6339. this.timerId = window.setTimeout(this.executeCallback);
  6340. }
  6341. }
  6342. _defineProperty(ThrottleWrapper, "THROTTLE_DELAY_MS", 150);
  6343. const LAST_EVENT_TIMEOUT_MS = 10;
  6344. const IGNORED_EVENTS = [
  6345. "mouseover",
  6346. "mouseleave",
  6347. "mouseenter",
  6348. "mouseout",
  6349. ];
  6350. const SUPPORTED_EVENTS = [
  6351. // keyboard events
  6352. "keydown",
  6353. "keypress",
  6354. "keyup",
  6355. // mouse events
  6356. "auxclick",
  6357. "click",
  6358. "contextmenu",
  6359. "dblclick",
  6360. "mousedown",
  6361. "mouseenter",
  6362. "mouseleave",
  6363. "mousemove",
  6364. "mouseover",
  6365. "mouseout",
  6366. "mouseup",
  6367. "pointerlockchange",
  6368. "pointerlockerror",
  6369. "select",
  6370. "wheel",
  6371. ]; // 'wheel' event makes scrolling in Safari twitchy
  6372. // https://github.com/AdguardTeam/ExtendedCss/issues/120
  6373. const SAFARI_PROBLEMATIC_EVENTS = ["wheel"];
  6374. /**
  6375. * We use EventTracker to track the event that is likely to cause the mutation.
  6376. * The problem is that we cannot use `window.event` directly from the mutation observer call
  6377. * as we're not in the event handler context anymore.
  6378. */
  6379. class EventTracker {
  6380. /**
  6381. * Creates new EventTracker.
  6382. */
  6383. constructor() {
  6384. _defineProperty(this, "getLastEventType", () => this.lastEventType);
  6385. _defineProperty(this, "getTimeSinceLastEvent", () => {
  6386. if (!this.lastEventTime) {
  6387. return null;
  6388. }
  6389. return Date.now() - this.lastEventTime;
  6390. });
  6391. this.trackedEvents = isSafariBrowser
  6392. ? SUPPORTED_EVENTS.filter(
  6393. (event) => !SAFARI_PROBLEMATIC_EVENTS.includes(event),
  6394. )
  6395. : SUPPORTED_EVENTS;
  6396. this.trackedEvents.forEach((eventName) => {
  6397. document.documentElement.addEventListener(
  6398. eventName,
  6399. this.trackEvent,
  6400. true,
  6401. );
  6402. });
  6403. }
  6404. /**
  6405. * Callback for event listener for events tracking.
  6406. *
  6407. * @param event Any event.
  6408. */
  6409. trackEvent(event) {
  6410. this.lastEventType = event.type;
  6411. this.lastEventTime = Date.now();
  6412. }
  6413. /**
  6414. * Checks whether the last caught event should be ignored.
  6415. *
  6416. * @returns True if event should be ignored.
  6417. */
  6418. isIgnoredEventType() {
  6419. const lastEventType = this.getLastEventType();
  6420. const sinceLastEventTime = this.getTimeSinceLastEvent();
  6421. return (
  6422. !!lastEventType &&
  6423. IGNORED_EVENTS.includes(lastEventType) &&
  6424. !!sinceLastEventTime &&
  6425. sinceLastEventTime < LAST_EVENT_TIMEOUT_MS
  6426. );
  6427. }
  6428. /**
  6429. * Stops event tracking by removing event listener.
  6430. */
  6431. stopTracking() {
  6432. this.trackedEvents.forEach((eventName) => {
  6433. document.documentElement.removeEventListener(
  6434. eventName,
  6435. this.trackEvent,
  6436. true,
  6437. );
  6438. });
  6439. }
  6440. }
  6441. /**
  6442. * We are trying to limit the number of callback calls by not calling it on all kind of "hover" events.
  6443. * The rationale behind this is that "hover" events often cause attributes modification,
  6444. * but re-applying extCSS rules will be useless as these attribute changes are usually transient.
  6445. *
  6446. * @param mutations DOM elements mutation records.
  6447. * @returns True if all mutations are about attributes changes, otherwise false.
  6448. */
  6449. function shouldIgnoreMutations(mutations) {
  6450. // ignore if all mutations are about attributes changes
  6451. return !mutations.some((m) => m.type !== "attributes");
  6452. }
  6453. /**
  6454. * Adds new {@link context.domMutationObserver} instance and connect it to document.
  6455. *
  6456. * @param context ExtendedCss context.
  6457. */
  6458. function observeDocument(context) {
  6459. if (context.isDomObserved) {
  6460. return;
  6461. } // enable dynamically added elements handling
  6462. context.isDomObserved = true;
  6463. context.domMutationObserver = new natives.MutationObserver(
  6464. (mutations) => {
  6465. if (!mutations || mutations.length === 0) {
  6466. return;
  6467. }
  6468. const eventTracker = new EventTracker();
  6469. if (
  6470. eventTracker.isIgnoredEventType() &&
  6471. shouldIgnoreMutations(mutations)
  6472. ) {
  6473. return;
  6474. } // save instance of EventTracker to context
  6475. // for removing its event listeners on disconnectDocument() while mainDisconnect()
  6476. context.eventTracker = eventTracker;
  6477. context.scheduler.run();
  6478. },
  6479. );
  6480. context.domMutationObserver.observe(document, {
  6481. childList: true,
  6482. subtree: true,
  6483. attributes: true,
  6484. attributeFilter: ["id", "class"],
  6485. });
  6486. }
  6487. /**
  6488. * Disconnect from {@link context.domMutationObserver}.
  6489. *
  6490. * @param context ExtendedCss context.
  6491. */
  6492. function disconnectDocument(context) {
  6493. if (!context.isDomObserved) {
  6494. return;
  6495. } // disable dynamically added elements handling
  6496. context.isDomObserved = false;
  6497. if (context.domMutationObserver) {
  6498. context.domMutationObserver.disconnect();
  6499. } // clean up event listeners
  6500. if (context.eventTracker) {
  6501. context.eventTracker.stopTracking();
  6502. }
  6503. }
  6504. const CONTENT_ATTR_PREFIX_REGEXP = /^("|')adguard.+?/;
  6505. /**
  6506. * Removes affectedElement.node from DOM.
  6507. *
  6508. * @param context ExtendedCss context.
  6509. * @param affectedElement Affected element.
  6510. */
  6511. const removeElement = (context, affectedElement) => {
  6512. const { node } = affectedElement;
  6513. affectedElement.removed = true;
  6514. const elementSelector = getElementSelectorPath(node); // check if the element has been already removed earlier
  6515. const elementRemovalsCounter =
  6516. context.removalsStatistic[elementSelector] || 0; // if removals attempts happened more than specified we do not try to remove node again
  6517. if (elementRemovalsCounter > MAX_STYLE_PROTECTION_COUNT) {
  6518. logger.error(
  6519. `ExtendedCss: infinite loop protection for selector: '${elementSelector}'`,
  6520. );
  6521. return;
  6522. }
  6523. if (node.parentElement) {
  6524. node.parentElement.removeChild(node);
  6525. context.removalsStatistic[elementSelector] = elementRemovalsCounter + 1;
  6526. }
  6527. };
  6528. /**
  6529. * Sets style to the specified DOM node.
  6530. *
  6531. * @param node DOM element.
  6532. * @param style Style to set.
  6533. */
  6534. const setStyleToElement = (node, style) => {
  6535. if (!(node instanceof HTMLElement)) {
  6536. return;
  6537. }
  6538. Object.keys(style).forEach((prop) => {
  6539. // Apply this style only to existing properties
  6540. // We cannot use hasOwnProperty here (does not work in FF)
  6541. if (
  6542. typeof node.style.getPropertyValue(prop.toString()) !== "undefined"
  6543. ) {
  6544. let value = style[prop];
  6545. if (!value) {
  6546. return;
  6547. } // do not apply 'content' style given by tsurlfilter
  6548. // which is needed only for BeforeStyleAppliedCallback
  6549. if (
  6550. prop === CONTENT_CSS_PROPERTY &&
  6551. value.match(CONTENT_ATTR_PREFIX_REGEXP)
  6552. ) {
  6553. return;
  6554. } // First we should remove !important attribute (or it won't be applied')
  6555. value = removeSuffix(value.trim(), "!important").trim();
  6556. node.style.setProperty(prop, value, "important");
  6557. }
  6558. });
  6559. };
  6560. /**
  6561. * Checks the required properties of `affectedElement`
  6562. * **before** `beforeStyleApplied()` execution.
  6563. *
  6564. * @param affectedElement Affected element.
  6565. *
  6566. * @returns False if there is no `node` or `rules`
  6567. * or `rules` is not an array.
  6568. */
  6569. const isIAffectedElement = (affectedElement) => {
  6570. return (
  6571. "node" in affectedElement &&
  6572. "rules" in affectedElement &&
  6573. affectedElement.rules instanceof Array
  6574. );
  6575. };
  6576. /**
  6577. * Checks the required properties of `affectedElement`
  6578. * **after** `beforeStyleApplied()` execution.
  6579. * These properties are needed for proper internal usage.
  6580. *
  6581. * @param affectedElement Affected element.
  6582. *
  6583. * @returns False if there is no `node` or `rules`
  6584. * or `rules` is not an array.
  6585. */
  6586. const isAffectedElement = (affectedElement) => {
  6587. return (
  6588. "node" in affectedElement &&
  6589. "originalStyle" in affectedElement &&
  6590. "rules" in affectedElement &&
  6591. affectedElement.rules instanceof Array
  6592. );
  6593. };
  6594. /**
  6595. * Applies style to the specified DOM node.
  6596. *
  6597. * @param context ExtendedCss context.
  6598. * @param rawAffectedElement Object containing DOM node and rule to be applied.
  6599. *
  6600. * @throws An error if affectedElement has no style to apply.
  6601. */
  6602. const applyStyle = (context, rawAffectedElement) => {
  6603. if (rawAffectedElement.protectionObserver) {
  6604. // style is already applied and protected by the observer
  6605. return;
  6606. }
  6607. let affectedElement;
  6608. if (context.beforeStyleApplied) {
  6609. if (!isIAffectedElement(rawAffectedElement)) {
  6610. throw new Error(
  6611. "Returned IAffectedElement should have 'node' and 'rules' properties",
  6612. );
  6613. }
  6614. affectedElement = context.beforeStyleApplied(rawAffectedElement);
  6615. if (!affectedElement) {
  6616. throw new Error(
  6617. "Callback 'beforeStyleApplied' should return IAffectedElement",
  6618. );
  6619. }
  6620. } else {
  6621. affectedElement = rawAffectedElement;
  6622. }
  6623. if (!isAffectedElement(affectedElement)) {
  6624. throw new Error(
  6625. "Returned IAffectedElement should have 'node' and 'rules' properties",
  6626. );
  6627. }
  6628. const { node, rules } = affectedElement;
  6629. for (let i = 0; i < rules.length; i += 1) {
  6630. const rule = rules[i];
  6631. const selector =
  6632. rule === null || rule === void 0 ? void 0 : rule.selector;
  6633. const style = rule === null || rule === void 0 ? void 0 : rule.style;
  6634. const debug = rule === null || rule === void 0 ? void 0 : rule.debug; // rule may not have style to apply
  6635. // e.g. 'div:has(> a) { debug: true }' -> means no style to apply, and enable debug mode
  6636. if (style) {
  6637. if (style[REMOVE_PSEUDO_MARKER] === PSEUDO_PROPERTY_POSITIVE_VALUE) {
  6638. removeElement(context, affectedElement);
  6639. return;
  6640. }
  6641. setStyleToElement(node, style);
  6642. } else if (!debug) {
  6643. // but rule should not have both style and debug properties
  6644. throw new Error(
  6645. `No style declaration in rule for selector: '${selector}'`,
  6646. );
  6647. }
  6648. }
  6649. };
  6650. /**
  6651. * Reverts style for the affected object.
  6652. *
  6653. * @param affectedElement Affected element.
  6654. */
  6655. const revertStyle = (affectedElement) => {
  6656. if (affectedElement.protectionObserver) {
  6657. affectedElement.protectionObserver.disconnect();
  6658. }
  6659. affectedElement.node.style.cssText = affectedElement.originalStyle;
  6660. };
  6661. /**
  6662. * ExtMutationObserver is a wrapper over regular MutationObserver with one additional function:
  6663. * it keeps track of the number of times we called the "ProtectionCallback".
  6664. *
  6665. * We use an instance of this to monitor styles added by ExtendedCss
  6666. * and to make sure these styles are recovered if the page script attempts to modify them.
  6667. *
  6668. * However, we want to avoid endless loops of modification if the page script repeatedly modifies the styles.
  6669. * So we keep track of the number of calls and observe() makes a decision
  6670. * whether to continue recovering the styles or not.
  6671. */
  6672. class ExtMutationObserver {
  6673. /**
  6674. * Extra property for keeping 'style fix counts'.
  6675. */
  6676. /**
  6677. * Creates new ExtMutationObserver.
  6678. *
  6679. * @param protectionCallback Callback which execution should be counted.
  6680. */
  6681. constructor(protectionCallback) {
  6682. this.styleProtectionCount = 0;
  6683. this.observer = new natives.MutationObserver((mutations) => {
  6684. if (!mutations.length) {
  6685. return;
  6686. }
  6687. this.styleProtectionCount += 1;
  6688. protectionCallback(mutations, this);
  6689. });
  6690. }
  6691. /**
  6692. * Starts to observe target element,
  6693. * prevents infinite loop of observing due to the limited number of times of callback runs.
  6694. *
  6695. * @param target Target to observe.
  6696. * @param options Mutation observer options.
  6697. */
  6698. observe(target, options) {
  6699. if (this.styleProtectionCount < MAX_STYLE_PROTECTION_COUNT) {
  6700. this.observer.observe(target, options);
  6701. } else {
  6702. logger.error("ExtendedCss: infinite loop protection for style");
  6703. }
  6704. }
  6705. /**
  6706. * Stops ExtMutationObserver from observing any mutations.
  6707. * Until the `observe()` is used again, `protectionCallback` will not be invoked.
  6708. */
  6709. disconnect() {
  6710. this.observer.disconnect();
  6711. }
  6712. }
  6713. const PROTECTION_OBSERVER_OPTIONS = {
  6714. attributes: true,
  6715. attributeOldValue: true,
  6716. attributeFilter: ["style"],
  6717. };
  6718. /**
  6719. * Creates MutationObserver protection callback.
  6720. *
  6721. * @param styles Styles data object.
  6722. *
  6723. * @returns Callback for styles protection.
  6724. */
  6725. const createProtectionCallback = (styles) => {
  6726. const protectionCallback = (mutations, extObserver) => {
  6727. if (!mutations[0]) {
  6728. return;
  6729. }
  6730. const { target } = mutations[0];
  6731. extObserver.disconnect();
  6732. styles.forEach((style) => {
  6733. setStyleToElement(target, style);
  6734. });
  6735. extObserver.observe(target, PROTECTION_OBSERVER_OPTIONS);
  6736. };
  6737. return protectionCallback;
  6738. };
  6739. /**
  6740. * Sets up a MutationObserver which protects style attributes from changes.
  6741. *
  6742. * @param node DOM node.
  6743. * @param rules Rule data objects.
  6744. * @returns Mutation observer used to protect attribute or null if there's nothing to protect.
  6745. */
  6746. const protectStyleAttribute = (node, rules) => {
  6747. if (!natives.MutationObserver) {
  6748. return null;
  6749. }
  6750. const styles = [];
  6751. rules.forEach((ruleData) => {
  6752. const { style } = ruleData; // some rules might have only debug property in style declaration
  6753. // e.g. 'div:has(> a) { debug: true }' -> parsed to boolean `ruleData.debug`
  6754. // so no style is fine, and here we should collect only valid styles to protect
  6755. if (style) {
  6756. styles.push(style);
  6757. }
  6758. });
  6759. const protectionObserver = new ExtMutationObserver(
  6760. createProtectionCallback(styles),
  6761. );
  6762. protectionObserver.observe(node, PROTECTION_OBSERVER_OPTIONS);
  6763. return protectionObserver;
  6764. };
  6765. const STATS_DECIMAL_DIGITS_COUNT = 4;
  6766. /**
  6767. * A helper class for applied rule stats.
  6768. */
  6769. class TimingStats {
  6770. /**
  6771. * Creates new TimingStats.
  6772. */
  6773. constructor() {
  6774. this.appliesTimings = [];
  6775. this.appliesCount = 0;
  6776. this.timingsSum = 0;
  6777. this.meanTiming = 0;
  6778. this.squaredSum = 0;
  6779. this.standardDeviation = 0;
  6780. }
  6781. /**
  6782. * Observe target element and mark observer as active.
  6783. *
  6784. * @param elapsedTimeMs Time in ms.
  6785. */
  6786. push(elapsedTimeMs) {
  6787. this.appliesTimings.push(elapsedTimeMs);
  6788. this.appliesCount += 1;
  6789. this.timingsSum += elapsedTimeMs;
  6790. this.meanTiming = this.timingsSum / this.appliesCount;
  6791. this.squaredSum += elapsedTimeMs * elapsedTimeMs;
  6792. this.standardDeviation = Math.sqrt(
  6793. this.squaredSum / this.appliesCount - Math.pow(this.meanTiming, 2),
  6794. );
  6795. }
  6796. }
  6797. /**
  6798. * Makes the timestamps more readable.
  6799. *
  6800. * @param timestamp Raw timestamp.
  6801. *
  6802. * @returns Fine-looking timestamps.
  6803. */
  6804. const beautifyTimingNumber = (timestamp) => {
  6805. return Number(timestamp.toFixed(STATS_DECIMAL_DIGITS_COUNT));
  6806. };
  6807. /**
  6808. * Improves timing stats readability.
  6809. *
  6810. * @param rawTimings Collected timings with raw timestamp.
  6811. *
  6812. * @returns Fine-looking timing stats.
  6813. */
  6814. const beautifyTimings = (rawTimings) => {
  6815. return {
  6816. appliesTimings: rawTimings.appliesTimings.map((t) =>
  6817. beautifyTimingNumber(t),
  6818. ),
  6819. appliesCount: beautifyTimingNumber(rawTimings.appliesCount),
  6820. timingsSum: beautifyTimingNumber(rawTimings.timingsSum),
  6821. meanTiming: beautifyTimingNumber(rawTimings.meanTiming),
  6822. standardDeviation: beautifyTimingNumber(rawTimings.standardDeviation),
  6823. };
  6824. };
  6825. /**
  6826. * Prints timing information if debugging mode is enabled.
  6827. *
  6828. * @param context ExtendedCss context.
  6829. */
  6830. const printTimingInfo = (context) => {
  6831. if (context.areTimingsPrinted) {
  6832. return;
  6833. }
  6834. context.areTimingsPrinted = true;
  6835. const timingsLogData = {};
  6836. context.parsedRules.forEach((ruleData) => {
  6837. if (ruleData.timingStats) {
  6838. const { selector, style, debug, matchedElements } = ruleData; // style declaration for some rules is parsed to debug property and no style to apply
  6839. // e.g. 'div:has(> a) { debug: true }'
  6840. if (!style && !debug) {
  6841. throw new Error(
  6842. `Rule should have style declaration for selector: '${selector}'`,
  6843. );
  6844. }
  6845. const selectorData = {
  6846. selectorParsed: selector,
  6847. timings: beautifyTimings(ruleData.timingStats),
  6848. }; // `ruleData.style` may contain `remove` pseudo-property
  6849. // and make logs look better
  6850. if (
  6851. style &&
  6852. style[REMOVE_PSEUDO_MARKER] === PSEUDO_PROPERTY_POSITIVE_VALUE
  6853. ) {
  6854. selectorData.removed = true; // no matchedElements for such case as they are removed after ExtendedCss applied
  6855. } else {
  6856. selectorData.styleApplied = style || null;
  6857. selectorData.matchedElements = matchedElements;
  6858. }
  6859. timingsLogData[selector] = selectorData;
  6860. }
  6861. });
  6862. if (Object.keys(timingsLogData).length === 0) {
  6863. return;
  6864. } // add location.href to the message to distinguish frames
  6865. logger.info(
  6866. "[ExtendedCss] Timings in milliseconds for %o:\n%o",
  6867. window.location.href,
  6868. timingsLogData,
  6869. );
  6870. };
  6871. /**
  6872. * Finds affectedElement object for the specified DOM node.
  6873. *
  6874. * @param affElements Array of affected elements — context.affectedElements.
  6875. * @param domNode DOM node.
  6876. * @returns Found affectedElement or undefined.
  6877. */
  6878. const findAffectedElement = (affElements, domNode) => {
  6879. return affElements.find((affEl) => affEl.node === domNode);
  6880. };
  6881. /**
  6882. * Applies specified rule and returns list of elements affected.
  6883. *
  6884. * @param context ExtendedCss context.
  6885. * @param ruleData Rule to apply.
  6886. * @returns List of elements affected by the rule.
  6887. */
  6888. const applyRule = (context, ruleData) => {
  6889. // debugging mode can be enabled in two ways:
  6890. // 1. for separate rules - by `{ debug: true; }`
  6891. // 2. for all rules simultaneously by:
  6892. // - `{ debug: global; }` in any rule
  6893. // - positive `debug` property in ExtCssConfiguration
  6894. const isDebuggingMode = !!ruleData.debug || context.debug;
  6895. let startTime;
  6896. if (isDebuggingMode) {
  6897. startTime = performance.now();
  6898. }
  6899. const { ast } = ruleData;
  6900. const nodes = []; // selector can be successfully parser into ast with no error
  6901. // but its applying by native Document.querySelectorAll() still may throw an error
  6902. // e.g. 'div[..banner]'
  6903. try {
  6904. nodes.push(...selectElementsByAst(ast));
  6905. } catch (e) {
  6906. // log the error only in debug mode
  6907. if (context.debug) {
  6908. logger.error(getErrorMessage(e));
  6909. }
  6910. }
  6911. nodes.forEach((node) => {
  6912. let affectedElement = findAffectedElement(
  6913. context.affectedElements,
  6914. node,
  6915. );
  6916. if (affectedElement) {
  6917. affectedElement.rules.push(ruleData);
  6918. applyStyle(context, affectedElement);
  6919. } else {
  6920. // Applying style first time
  6921. const originalStyle = node.style.cssText;
  6922. affectedElement = {
  6923. node,
  6924. // affected DOM node
  6925. rules: [ruleData],
  6926. // rule to be applied
  6927. originalStyle,
  6928. // original node style
  6929. protectionObserver: null, // style attribute observer
  6930. };
  6931. applyStyle(context, affectedElement);
  6932. context.affectedElements.push(affectedElement);
  6933. }
  6934. });
  6935. if (isDebuggingMode && startTime) {
  6936. const elapsedTimeMs = performance.now() - startTime;
  6937. if (!ruleData.timingStats) {
  6938. ruleData.timingStats = new TimingStats();
  6939. }
  6940. ruleData.timingStats.push(elapsedTimeMs);
  6941. }
  6942. return nodes;
  6943. };
  6944. /**
  6945. * Applies filtering rules.
  6946. *
  6947. * @param context ExtendedCss context.
  6948. */
  6949. const applyRules = (context) => {
  6950. const newSelectedElements = []; // some rules could make call - selector.querySelectorAll() temporarily to change node id attribute
  6951. // this caused MutationObserver to call recursively
  6952. // https://github.com/AdguardTeam/ExtendedCss/issues/81
  6953. disconnectDocument(context);
  6954. context.parsedRules.forEach((ruleData) => {
  6955. const nodes = applyRule(context, ruleData);
  6956. Array.prototype.push.apply(newSelectedElements, nodes); // save matched elements to ruleData as linked to applied rule
  6957. // only for debugging purposes
  6958. if (ruleData.debug) {
  6959. ruleData.matchedElements = nodes;
  6960. }
  6961. }); // Now revert styles for elements which are no more affected
  6962. let affLength = context.affectedElements.length; // do nothing if there is no elements to process
  6963. while (affLength) {
  6964. const affectedElement = context.affectedElements[affLength - 1];
  6965. if (!affectedElement) {
  6966. break;
  6967. }
  6968. if (!newSelectedElements.includes(affectedElement.node)) {
  6969. // Time to revert style
  6970. revertStyle(affectedElement);
  6971. context.affectedElements.splice(affLength - 1, 1);
  6972. } else if (!affectedElement.removed) {
  6973. // Add style protection observer
  6974. // Protect "style" attribute from changes
  6975. if (!affectedElement.protectionObserver) {
  6976. affectedElement.protectionObserver = protectStyleAttribute(
  6977. affectedElement.node,
  6978. affectedElement.rules,
  6979. );
  6980. }
  6981. }
  6982. affLength -= 1;
  6983. } // After styles are applied we can start observe again
  6984. observeDocument(context);
  6985. printTimingInfo(context);
  6986. };
  6987. /**
  6988. * Result of selector validation.
  6989. */
  6990. /**
  6991. * Main class of ExtendedCss lib.
  6992. *
  6993. * Parses css stylesheet with any selectors (passed to its argument as styleSheet),
  6994. * and guarantee its applying as mutation observer is used to prevent the restyling of needed elements by other scripts.
  6995. * This style protection is limited to 50 times to avoid infinite loop (MAX_STYLE_PROTECTION_COUNT).
  6996. * Our own ThrottleWrapper is used for styles applying to avoid too often lib reactions on page mutations.
  6997. *
  6998. * Constructor creates the instance of class which should be run be `apply()` method to apply the rules,
  6999. * and the applying can be stopped by `dispose()`.
  7000. *
  7001. * Can be used to select page elements by selector with `query()` method (similar to `Document.querySelectorAll()`),
  7002. * which does not require instance creating.
  7003. */
  7004. class ExtendedCss {
  7005. /**
  7006. * Creates new ExtendedCss.
  7007. *
  7008. * @param configuration ExtendedCss configuration.
  7009. */
  7010. constructor(configuration) {
  7011. if (!configuration) {
  7012. throw new Error("ExtendedCss configuration should be provided.");
  7013. }
  7014. this.applyRulesCallbackListener =
  7015. this.applyRulesCallbackListener.bind(this);
  7016. this.context = {
  7017. beforeStyleApplied: configuration.beforeStyleApplied,
  7018. debug: false,
  7019. affectedElements: [],
  7020. isDomObserved: false,
  7021. removalsStatistic: {},
  7022. parsedRules: [],
  7023. scheduler: new ThrottleWrapper(this.applyRulesCallbackListener),
  7024. }; // TODO: throw an error instead of logging and handle it in related products.
  7025. if (!isBrowserSupported()) {
  7026. logger.error("Browser is not supported by ExtendedCss");
  7027. return;
  7028. } // at least 'styleSheet' or 'cssRules' should be provided
  7029. if (!configuration.styleSheet && !configuration.cssRules) {
  7030. throw new Error(
  7031. "ExtendedCss configuration should have 'styleSheet' or 'cssRules' defined.",
  7032. );
  7033. } // 'styleSheet' and 'cssRules' are optional
  7034. // and both can be provided at the same time
  7035. // so both should be parsed and applied in such case
  7036. if (configuration.styleSheet) {
  7037. // stylesheet parsing can fail on some invalid selectors
  7038. try {
  7039. this.context.parsedRules.push(
  7040. ...parseStylesheet(configuration.styleSheet, extCssDocument),
  7041. );
  7042. } catch (e) {
  7043. // eslint-disable-next-line max-len
  7044. throw new Error(
  7045. `Pass the rules as configuration.cssRules since configuration.styleSheet cannot be parsed because of: '${getErrorMessage(e)}'`,
  7046. );
  7047. }
  7048. }
  7049. if (configuration.cssRules) {
  7050. this.context.parsedRules.push(
  7051. ...parseRules$1(configuration.cssRules, extCssDocument),
  7052. );
  7053. } // true if set in configuration
  7054. // or any rule in styleSheet has `debug: global`
  7055. this.context.debug =
  7056. configuration.debug ||
  7057. this.context.parsedRules.some((ruleData) => {
  7058. return ruleData.debug === DEBUG_PSEUDO_PROPERTY_GLOBAL_VALUE;
  7059. });
  7060. if (
  7061. this.context.beforeStyleApplied &&
  7062. typeof this.context.beforeStyleApplied !== "function"
  7063. ) {
  7064. // eslint-disable-next-line max-len
  7065. throw new Error(
  7066. `Invalid configuration. Type of 'beforeStyleApplied' should be a function, received: '${typeof this.context.beforeStyleApplied}'`,
  7067. );
  7068. }
  7069. }
  7070. /**
  7071. * Invokes {@link applyRules} function with current app context.
  7072. *
  7073. * This method is bound to the class instance in the constructor because it is called
  7074. * in {@link ThrottleWrapper} and on the DOMContentLoaded event.
  7075. */
  7076. applyRulesCallbackListener() {
  7077. applyRules(this.context);
  7078. }
  7079. /**
  7080. * Initializes ExtendedCss.
  7081. *
  7082. * Should be executed on page ASAP,
  7083. * otherwise the :contains() pseudo-class may work incorrectly.
  7084. */
  7085. init() {
  7086. /**
  7087. * Native Node textContent getter must be intercepted as soon as possible,
  7088. * and stored as it is needed for proper work of :contains() pseudo-class
  7089. * because DOM Node prototype 'textContent' property may be mocked.
  7090. *
  7091. * @see {@link https://github.com/AdguardTeam/ExtendedCss/issues/127}
  7092. */
  7093. nativeTextContent.setGetter();
  7094. }
  7095. /**
  7096. * Applies stylesheet rules on page.
  7097. */
  7098. apply() {
  7099. applyRules(this.context);
  7100. if (document.readyState !== "complete") {
  7101. document.addEventListener(
  7102. "DOMContentLoaded",
  7103. this.applyRulesCallbackListener,
  7104. false,
  7105. );
  7106. }
  7107. }
  7108. /**
  7109. * Disposes ExtendedCss and removes our styles from matched elements.
  7110. */
  7111. dispose() {
  7112. disconnectDocument(this.context);
  7113. this.context.affectedElements.forEach((el) => {
  7114. revertStyle(el);
  7115. });
  7116. document.removeEventListener(
  7117. "DOMContentLoaded",
  7118. this.applyRulesCallbackListener,
  7119. false,
  7120. );
  7121. }
  7122. /**
  7123. * Exposed for testing purposes only.
  7124. *
  7125. * @returns Array of AffectedElement data objects.
  7126. */
  7127. getAffectedElements() {
  7128. return this.context.affectedElements;
  7129. }
  7130. /**
  7131. * Returns a list of the document's elements that match the specified selector.
  7132. * Uses ExtCssDocument.querySelectorAll().
  7133. *
  7134. * @param selector Selector text.
  7135. * @param [noTiming=true] If true — do not print the timings to the console.
  7136. *
  7137. * @throws An error if selector is not valid.
  7138. * @returns A list of elements that match the selector.
  7139. */
  7140. static query(selector) {
  7141. let noTiming =
  7142. arguments.length > 1 && arguments[1] !== undefined
  7143. ? arguments[1]
  7144. : true;
  7145. if (typeof selector !== "string") {
  7146. throw new Error("Selector should be defined as a string.");
  7147. }
  7148. const start = performance.now();
  7149. try {
  7150. return extCssDocument.querySelectorAll(selector);
  7151. } finally {
  7152. const end = performance.now();
  7153. if (!noTiming) {
  7154. logger.info(
  7155. `[ExtendedCss] Elapsed: ${Math.round((end - start) * 1000)} μs.`,
  7156. );
  7157. }
  7158. }
  7159. }
  7160. /**
  7161. * Validates selector.
  7162. *
  7163. * @param inputSelector Selector text to validate.
  7164. *
  7165. * @returns Result of selector validation.
  7166. */
  7167. static validate(inputSelector) {
  7168. try {
  7169. // ExtendedCss in general supports :remove() in selector
  7170. // but ExtendedCss.query() does not support it as it should be parsed by stylesheet parser.
  7171. // so for validation we have to handle selectors with `:remove()` in it
  7172. const { selector } = parseRemoveSelector(inputSelector);
  7173. ExtendedCss.query(selector);
  7174. return {
  7175. ok: true,
  7176. error: null,
  7177. };
  7178. } catch (e) {
  7179. // not valid input `selector` should be logged eventually
  7180. const error = `Error: Invalid selector: '${inputSelector}' -- ${getErrorMessage(e)}`;
  7181. return {
  7182. ok: false,
  7183. error,
  7184. };
  7185. }
  7186. }
  7187. }
  7188. const /** 元素规则 */ CRRE =
  7189. /^(\[\$domain=)?(~?[\w-]+(?:(?:\.[\w-]+)*\.(?:[\w-]+|\*))?(?:(?:,|\|)~?[\w-]+(?:(?:\.[\w-]+)*\.(?:[\w-]+|\*))?)*)?(?:])?(#@?\$?\??#)([^\s^+@][^@]*(?:['"[(]+.*['"\])]+)*[^@]*)\s*$/,
  7190. /** 基本规则 */
  7191. BRRE =
  7192. /^(?:@@?)(?:\/(.*[^\\])\/|(\|\|?)?(https?:\/\/)?([^\s"<>`]+?[|^]?))?\$((?:(?:~?[\w-]+(?:=[^$]+)?|_+)(?:[^\\],|$))+)/,
  7193. /** 预存 CSS */
  7194. CCRE =
  7195. /^\/\*\s(\d)(\|)?(.+?)\s\*\/\s((.+?)\s*(?:\{\s*[a-zA-Z-]+\s*:\s*.+\}|,))\s*$/,
  7196. /** 预存注释 */
  7197. CMRE = /\/\*\s*\d.+?\s*\*\//g,
  7198. /** CSS 选择器 */
  7199. CSRE = /^(.+?)\s*{\s*[a-zA-Z-]+\s*:\s*.+}\s*$/,
  7200. BROpts = [
  7201. "elemhide",
  7202. "ehide",
  7203. "specifichide",
  7204. "shide",
  7205. "generichide",
  7206. "ghide",
  7207. ],
  7208. CRFlags = ["##", "#@#", "#?#", "#@?#", "#$#", "#@$#", "#$?#", "#@$?#"],
  7209. styleBoxes = ["genHideCss", "genExtraCss", "spcHideCss", "spcExtraCss"];
  7210. /**
  7211. * 处理 禁用元素隐藏规则
  7212. * @param rule 禁用元素隐藏规则
  7213. * @returns 失败返回 null,成功返回 { rule: BRule; bad: boolean }
  7214. */
  7215. function bRuleSpliter(rule) {
  7216. const group = rule.match(BRRE);
  7217. if (!group) {
  7218. return null;
  7219. }
  7220. const [, regex, pipe, proto, body, option] = group,
  7221. options = option.split(","),
  7222. sepChar = "[^\\w\\.%-]",
  7223. anyChar = '(?:[^\\s"<>`]*)',
  7224. eh = hasSome(options, ["elemhide", "ehide"]),
  7225. sh = hasSome(options, ["specifichide", "shide"]),
  7226. gh = hasSome(options, ["generichide", "ghide"]);
  7227. let domains = [];
  7228. options.forEach((opt) => {
  7229. if (opt.startsWith("domain=")) {
  7230. domains = opt.slice(7).split("|");
  7231. }
  7232. });
  7233. let match = "";
  7234. if (regex) {
  7235. match = regex;
  7236. } else if (body) {
  7237. match += pipe
  7238. ? proto
  7239. ? `^${proto}`
  7240. : `^https?://(?:[\\w-]+\\.)*?`
  7241. : `^${anyChar}`;
  7242. match += body
  7243. .replace(/[-\\$+.()[\]{}]/g, "\\$&")
  7244. .replaceAll("^", "$^")
  7245. .replace(/\|$/, "$")
  7246. .replaceAll("|", "\\|")
  7247. .replace(/\*$/, "")
  7248. .replaceAll("*", anyChar)
  7249. .replace(/\$\^$/, `(?:${sepChar}.*|$)`)
  7250. .replaceAll("$^", sepChar);
  7251. } else if (domains.length > 0) {
  7252. match = domains;
  7253. }
  7254. return {
  7255. rule: {
  7256. rule,
  7257. match,
  7258. level: eh || (gh && sh) ? 3 : sh ? 2 : gh ? 1 : 0,
  7259. },
  7260. bad: options.includes("badfilter"),
  7261. };
  7262. }
  7263. /**
  7264. * 判断是否为禁用元素隐藏规则
  7265. * @param {string} rule ABP 规则
  7266. * @returns {boolean} 判断结果
  7267. */
  7268. function isBasicRule(rule) {
  7269. return BRRE.test(rule) && hasSome(rule, BROpts);
  7270. }
  7271. /**
  7272. * 检查 BRule 对象是否匹配应用地址
  7273. * @param {?BRule} rule BRule 对象
  7274. * @param {string=} url 应用地址
  7275. * @returns {number} 应用级别,不匹配返回 0
  7276. */
  7277. function bRuleParser(rule, url = location.href) {
  7278. return rule
  7279. ? (Array.isArray(rule.match) && domainChecker(rule.match)[0]) ||
  7280. (!Array.isArray(rule.match) && new RegExp(rule.match).test(url))
  7281. ? rule.level
  7282. : 0
  7283. : 0;
  7284. }
  7285. /**
  7286. * 裁剪提取 ETag
  7287. * @param {?string} header 请求头中的 ETag 属性字符串
  7288. * @returns {?string} ETag 属性字符串,未找到返回 null
  7289. */
  7290. function getEtag(header) {
  7291. var _a;
  7292. let result = null;
  7293. if (!header) {
  7294. return null;
  7295. }
  7296. [
  7297. /[e|E][t|T]ag(?:=|:)\s?\[?(?:W\/)?"(?:gz\[)?(\w+)\]?"\]?/,
  7298. // 海阔世界
  7299. /^(?:W\/)?"(?:gz\[)?(\w+)\]?"/,
  7300. ].forEach((re) => {
  7301. result !== null && result !== void 0
  7302. ? result
  7303. : (result = header.match(re));
  7304. });
  7305. return (_a =
  7306. result === null || result === void 0 ? void 0 : result[1]) !== null &&
  7307. _a !== void 0
  7308. ? _a
  7309. : null;
  7310. }
  7311. /**
  7312. * 检查 ABP 域名范围是否匹配当前域名
  7313. * @param {string[]} domains 一组 ABP 域名
  7314. * @param {string=} currDomain 当前域名
  7315. * @returns {boolean[]} [ 是否匹配, 是否是通用规则 ]
  7316. */
  7317. function domainChecker(domains, currDomain = location.hostname) {
  7318. var _a;
  7319. const results = [],
  7320. invResults = [],
  7321. urlSuffix =
  7322. (_a = /\.+?[\w\d-]+$/.exec(currDomain)) === null || _a === void 0
  7323. ? void 0
  7324. : _a[0];
  7325. let totalResult = [0, false],
  7326. black = false,
  7327. white = false,
  7328. match = false;
  7329. domains.forEach((domain) => {
  7330. const invert = domain[0] === "~";
  7331. if (invert) {
  7332. domain = domain.slice(1);
  7333. }
  7334. if (domain.endsWith(".*") && urlSuffix) {
  7335. domain = domain.replace(".*", urlSuffix);
  7336. }
  7337. const result = currDomain.endsWith(domain);
  7338. if (invert) {
  7339. if (result) {
  7340. white = true;
  7341. }
  7342. invResults.push([domain.length, !result]);
  7343. } else {
  7344. if (result) {
  7345. black = true;
  7346. }
  7347. results.push([domain.length, result]);
  7348. }
  7349. });
  7350. if (results.length > 0 && !black) {
  7351. match = false;
  7352. } else if (invResults.length > 0 && !white) {
  7353. match = true;
  7354. } else {
  7355. results.forEach((r) => {
  7356. if (r[0] >= totalResult[0] && r[1]) {
  7357. totalResult = r;
  7358. }
  7359. });
  7360. invResults.forEach((r) => {
  7361. if (r[0] >= totalResult[0] && !r[1]) {
  7362. totalResult = r;
  7363. }
  7364. });
  7365. match = totalResult[1];
  7366. }
  7367. return [match, results.length === 0];
  7368. }
  7369. /**
  7370. * 检查“句子”内容或“给定单词”中是否存在任一“匹配单词”
  7371. * @param {(string | string[])} str 一个“句子”或一组“给定单词”
  7372. * @param {string[]} arr 一组“匹配单词”
  7373. * @returns {boolean} 结果
  7374. */
  7375. function hasSome(str, arr) {
  7376. return arr.some((word) => str.includes(word));
  7377. }
  7378. /**
  7379. * 处理 ABP 元素隐藏规则
  7380. * @param {string} rule ABP 元素隐藏规则
  7381. * @returns {(Rule | undefined)} Rule 对象,失败返回 undefined
  7382. */
  7383. function ruleLoader(rule) {
  7384. if (
  7385. hasSome(rule, [
  7386. ":matches-path(",
  7387. ":min-text-length(",
  7388. ":watch-attr(",
  7389. ":-abp-properties(",
  7390. ":matches-property(",
  7391. ])
  7392. ) {
  7393. return;
  7394. }
  7395. rule = rule.trim();
  7396. // 如果 #$# 不包含 {} 就排除
  7397. // 可以尽量排除 Snippet Filters
  7398. if (
  7399. /(?:\w|\*|]|^)#\$#/.test(rule) &&
  7400. !/{\s*[a-zA-Z-]+\s*:\s*.+}\s*$/.test(rule)
  7401. ) {
  7402. return;
  7403. }
  7404. // ## -> #?#
  7405. if (
  7406. /(?:\w|\*|]|^)#@?\$?#/.test(rule) &&
  7407. hasSome(rule, [
  7408. ":has(",
  7409. ":-abp-has(",
  7410. "[-ext-has=",
  7411. ":has-text(",
  7412. ":contains(",
  7413. ":-abp-contains(",
  7414. "[-ext-contains=",
  7415. ":matches-css(",
  7416. "[-ext-matches-css=",
  7417. ":matches-css-before(",
  7418. "[-ext-matches-css-before=",
  7419. ":matches-css-after(",
  7420. "[-ext-matches-css-after=",
  7421. ":matches-attr(",
  7422. ":nth-ancestor(",
  7423. ":upward(",
  7424. ":xpath(",
  7425. ":remove()",
  7426. ":not(",
  7427. ])
  7428. ) {
  7429. rule = rule.replace(/(\w|\*|]|^)#(@?\$?)#/, "$1#$2?#");
  7430. }
  7431. // :style(...) 转换
  7432. // example.com#?##id:style(color: red)
  7433. // example.com#$?##id { color: red }
  7434. if (rule.includes(":style(")) {
  7435. rule = rule
  7436. .replace(/(\w|\*|]|^)#(@?)(\??)#/, "$1#$2$$$3#")
  7437. .replace(/:style\(\s*/, " { ")
  7438. .replace(/\s*\)$/, " }");
  7439. }
  7440. // 解构
  7441. const group = rule.match(CRRE);
  7442. if (group) {
  7443. const [, isDomain, place = "*", flag, sel] = group,
  7444. type = CRFlags.indexOf(flag),
  7445. [match, generic] =
  7446. place === "*"
  7447. ? [true, true]
  7448. : domainChecker(place.split(isDomain ? "|" : ","));
  7449. if (sel && match) {
  7450. return {
  7451. black: type % 2 ? "white" : "black",
  7452. type: Math.floor(type / 2),
  7453. place: (isDomain ? "|" : "") + place,
  7454. generic,
  7455. sel,
  7456. };
  7457. }
  7458. }
  7459. }
  7460. /**
  7461. * 转换 Rule 对象为 CSS 规则或选择器
  7462. * @param {Rule} rule Rule 对象
  7463. * @param {string} preset 默认 CSS 声明,需要带 {}
  7464. * @param {boolean} full css 值,`true` CSS 规则,`false` 选择器带逗号
  7465. * @returns 返回如下对象
  7466. * ```ts
  7467. * type cssO = {
  7468. * // CSS 规则或选择器
  7469. * css: string;
  7470. * // 选择器
  7471. * sel: string;
  7472. * // Rule 对象中是否包含 CSS 声明
  7473. * isStyle: boolean;
  7474. * }
  7475. * ```
  7476. */
  7477. function ruleToCss(rule, preset, full) {
  7478. var _a, _b;
  7479. const isStyle = /}\s*$/.test(rule.sel);
  7480. return {
  7481. css: `/* ${rule.type}${rule.place} */ ${rule.sel + (!isStyle ? (full ? " " + preset.replace(/^\s{2,}/g, " ").replaceAll("\n", "") : ",") : "")} \n`,
  7482. sel: isStyle
  7483. ? (_b =
  7484. (_a = rule.sel.match(CSRE)) === null || _a === void 0
  7485. ? void 0
  7486. : _a[1]) !== null && _b !== void 0
  7487. ? _b
  7488. : rule.sel
  7489. : rule.sel,
  7490. isStyle,
  7491. };
  7492. }
  7493. /**
  7494. * 转换 CSS 规则为 ABP 规则 AdGuard 格式
  7495. * @param {string} css CSS 规则
  7496. * @returns 返回如下对象,失败返回 null
  7497. * ```ts
  7498. * type abpO = {
  7499. * // ABP 规则
  7500. * abp: string;
  7501. * // 选择器
  7502. * sel: string;
  7503. * // 规则类型
  7504. * type: 0 | 1 | 2 | 3;
  7505. * }
  7506. * ```
  7507. */
  7508. function cssToAbp(css) {
  7509. var _a;
  7510. const flags = ["##", "#?#", "#$#", "#$?#"];
  7511. const [, typeStr, isDomain, place, style, sel] =
  7512. (_a = css.match(CCRE)) !== null && _a !== void 0 ? _a : [];
  7513. if (typeStr === void 0) {
  7514. return null;
  7515. }
  7516. const type = parseInt(typeStr);
  7517. return {
  7518. abp: `${place === "*" ? "" : isDomain ? `[$domain=${place}]` : place}${flags[type]}${type >= 2 ? style : sel}`,
  7519. type,
  7520. sel,
  7521. };
  7522. }
  7523. /**
  7524. * 给 URL 添加时间戳,防止缓存
  7525. * @see https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest_API/Using_XMLHttpRequest#%E7%BB%95%E8%BF%87%E7%BC%93%E5%AD%98
  7526. * @param {string} url 原始 URL
  7527. * @returns {string} 处理后的 URL
  7528. */
  7529. function addTimeParam(url) {
  7530. return url + (/\?/.test(url) ? "&" : "?") + new Date().getTime();
  7531. }
  7532.  
  7533. /**
  7534. * 处理所有 BRule 对象,统计得出应用级别
  7535. *
  7536. * 应用级别:
  7537. *
  7538. * 0 应用所有规则
  7539. *
  7540. * 1 只应用特定规则
  7541. *
  7542. * 2 只应用通用规则
  7543. *
  7544. * 3 禁用所有规则
  7545. * @async
  7546. * @returns {Promise.<number>} 应用级别
  7547. */
  7548. function parseBRules() {
  7549. return __awaiter(this, void 0, void 0, function* () {
  7550. var _a;
  7551. data.appliedLevel = 0;
  7552. const brules =
  7553. (_a = yield values.brules()) !== null && _a !== void 0
  7554. ? _a
  7555. : defaultValues.brules;
  7556. brules.forEach((br) => {
  7557. const level = bRuleParser(br);
  7558. if (level > 0) {
  7559. data.bRules.push(br);
  7560. if (level !== data.appliedLevel) {
  7561. data.appliedLevel = data.appliedLevel === 0 ? level : 3;
  7562. }
  7563. }
  7564. });
  7565. return data.appliedLevel;
  7566. });
  7567. }
  7568. /**
  7569. * 根据 CSS 容器编号和应用级别,计算是否应用 CSS
  7570. *
  7571. * CSS 容器编号:
  7572. *
  7573. * 0 通用标准 CSS 规则
  7574. *
  7575. * 1 通用扩充 CSS 规则
  7576. *
  7577. * 2 特定标准 CSS 规则
  7578. *
  7579. * 3 特定扩充 CSS 规则
  7580. * @param {number} type CSS 容器编号
  7581. * @returns {boolean} 是否应用 CSS
  7582. */
  7583. function canApplyCss(type) {
  7584. return (data.appliedLevel & (type >= 2 ? 2 : 1)) == 0;
  7585. }
  7586.  
  7587. /**
  7588. * 清空存储规则并更新脚本菜单
  7589. */
  7590. function cleanRules() {
  7591. if (
  7592. confirm(`是否清空存储规则 ?
  7593.  
  7594. 如果要卸载脚本,点击 确定 以后不要刷新,也不要打开任何新页面,
  7595. (如果可以)立即清空脚本存储(全选,删除,填 {},保存),然后删除脚本`)
  7596. ) {
  7597. values.rules(null);
  7598. values.time(null);
  7599. values.etags(null);
  7600. values.brules(null);
  7601. getSavedHosts().then((saves) =>
  7602. saves.forEach((host) => values.css(null, host)),
  7603. );
  7604. data.isClean = true;
  7605. gmMenu("update");
  7606. gmMenu("export");
  7607. gmMenu("count", () => location.reload());
  7608. }
  7609. }
  7610. /**
  7611. * 生成并自动下载广告拦截报告
  7612. */
  7613. function reportRecord() {
  7614. let text = "";
  7615. function pushRecord(css) {
  7616. const match = cssToAbp(css);
  7617. if (match === null) return;
  7618. const { abp: item, type, sel } = match,
  7619. count =
  7620. type % 2 === 1
  7621. ? ExtendedCss.query(sel).length
  7622. : document.querySelectorAll(sel).length;
  7623. if (count > 0) {
  7624. text += `
  7625. ! 匹配元素数量: ${count}
  7626. ${item}
  7627. `;
  7628. }
  7629. }
  7630. data.bRules.forEach((br) => {
  7631. if (br.level > 0) {
  7632. text += `
  7633. ! 禁用${["", "通用", "特定", "所有"][br.level]}元素隐藏
  7634. ${br.rule}
  7635. `;
  7636. }
  7637. });
  7638. styleBoxes.forEach((box, i) => {
  7639. if (canApplyCss(i)) {
  7640. data[box]
  7641. .split("\n")
  7642. .filter((css, i, csss) => csss.indexOf(css) === i)
  7643. .forEach((css) => pushRecord(css));
  7644. }
  7645. });
  7646. if (text.length > 0) {
  7647. const blobUrl = URL.createObjectURL(
  7648. new Blob([
  7649. `[Adblock Plus 2.0]
  7650. ! 应用地址:
  7651. ! ${location.href}
  7652. ${text}`,
  7653. ]),
  7654. );
  7655. downUrl(blobUrl, `拦截报告_${location.hostname}.txt`);
  7656. } else {
  7657. alert("这个页面没有任何规则生效");
  7658. }
  7659. }
  7660. /**
  7661. * 切换网站禁用状态并自动刷新
  7662. */
  7663. function switchDisabledStat() {
  7664. values.black().then((disaList) => {
  7665. const disas = disaList !== null && disaList !== void 0 ? disaList : [];
  7666. data.disabled = !disas.includes(location.hostname);
  7667. if (data.disabled) {
  7668. disas.push(location.hostname);
  7669. } else {
  7670. disas.splice(disas.indexOf(location.hostname), 1);
  7671. }
  7672. values.black(disas).finally(() => location.reload());
  7673. });
  7674. }
  7675.  
  7676. /**
  7677. * 保存预存的 CSS
  7678. * @async
  7679. * @returns {Promise.<void>}
  7680. */
  7681. function saveCss() {
  7682. return __awaiter(this, void 0, void 0, function* () {
  7683. if (data.autoCleanSize > 0) {
  7684. const cssLength = yield getCssLength();
  7685. if (cssLength[1] >= data.autoCleanSize) {
  7686. getSavedHosts().then((saves) =>
  7687. __awaiter(this, void 0, void 0, function* () {
  7688. for (let i = 0; i < saves.length; i++) {
  7689. const host = saves[i];
  7690. yield values.css(null, host);
  7691. }
  7692. gmMenu("count", cleanRules);
  7693. }),
  7694. );
  7695. }
  7696. }
  7697. const styles = {
  7698. needUpdate: false,
  7699. genHideCss: data.genHideCss,
  7700. genExtraCss: data.genExtraCss,
  7701. spcHideCss: data.spcHideCss,
  7702. spcExtraCss: data.spcExtraCss,
  7703. };
  7704. yield values.css(styles);
  7705. return;
  7706. });
  7707. }
  7708. /**
  7709. * 读取预存的 CSS
  7710. * @async
  7711. * @returns {Promise.<void>}
  7712. */
  7713. function readCss() {
  7714. return __awaiter(this, void 0, void 0, function* () {
  7715. var _a;
  7716. const styles =
  7717. (_a = yield values.css()) !== null && _a !== void 0
  7718. ? _a
  7719. : defaultValues.css;
  7720. if (!hasSome(Object.keys(styles), styleBoxes)) {
  7721. yield values.css(defaultValues.css);
  7722. return;
  7723. }
  7724. styleBoxes.forEach((sname) => {
  7725. var _a;
  7726. if (styles[sname].length > 0) {
  7727. data.saved = true;
  7728. data.update =
  7729. (_a = styles.needUpdate) !== null && _a !== void 0 ? _a : true;
  7730. data[sname] = styles[sname];
  7731. }
  7732. });
  7733. return;
  7734. });
  7735. }
  7736.  
  7737. /**
  7738. * 计算自定义规则的 Hash
  7739. * @async
  7740. * @param {boolean} saveHash 是否存储 Hash
  7741. * @returns {Promise.<string>} Hash
  7742. */
  7743. function getCustomHash(saveHash) {
  7744. return __awaiter(this, void 0, void 0, function* () {
  7745. if (location.protocol === "https:") {
  7746. const hash = new Uint32Array(
  7747. yield window.crypto.subtle.digest(
  7748. "SHA-1",
  7749. yield new Blob([data.customRules]).arrayBuffer(),
  7750. ),
  7751. ).toString();
  7752. if (saveHash) {
  7753. yield values.hash(hash);
  7754. }
  7755. return hash;
  7756. } else {
  7757. return defaultValues.hash;
  7758. }
  7759. });
  7760. }
  7761. /**
  7762. * 从各个来源收集规则
  7763. * @async
  7764. * @param {boolean} apply 是否立即应用规则
  7765. * @returns {Promise.<void>} 返回空的 Promise
  7766. */
  7767. function initRules(apply) {
  7768. return __awaiter(this, void 0, void 0, function* () {
  7769. var _a;
  7770. let abpRules = defaultValues.rules;
  7771. data.receivedRules = "";
  7772. abpRules =
  7773. (_a = yield values.rules()) !== null && _a !== void 0
  7774. ? _a
  7775. : defaultValues.rules;
  7776. for (const rule of presets.onlineRules) {
  7777. const resRule = yield getRuleFromResource(rule.标识);
  7778. if (resRule && !abpRules[rule.标识]) {
  7779. abpRules[rule.标识] = resRule;
  7780. }
  7781. }
  7782. Object.keys(abpRules).forEach((name) => {
  7783. data.receivedRules += "\n" + abpRules[name];
  7784. });
  7785. data.allRules = data.customRules + data.receivedRules;
  7786. if (apply) {
  7787. yield parseRules();
  7788. }
  7789. return;
  7790. });
  7791. }
  7792. /**
  7793. * 将所有支持的 ABP 规则筛选后转换为 CSS 和 BRule 对象
  7794. * @async
  7795. * @returns {Promise.<void>} 返回空的 Promise
  7796. */
  7797. function parseRules() {
  7798. return __awaiter(this, void 0, void 0, function* () {
  7799. const bRuleSet = new Set(),
  7800. bRuleBad = [],
  7801. hRules = [
  7802. {
  7803. rules: new Map(),
  7804. whites: new Set(),
  7805. },
  7806. {
  7807. rules: new Map(),
  7808. whites: new Set(),
  7809. },
  7810. {
  7811. rules: new Map(),
  7812. whites: new Set(),
  7813. },
  7814. {
  7815. rules: new Map(),
  7816. whites: new Set(),
  7817. },
  7818. ];
  7819. function addRule(rule, box) {
  7820. const { css, sel, isStyle } = ruleToCss(rule, data.preset, false);
  7821. const index = (box % 2) + (rule.generic ? 0 : 2);
  7822. const checkResult = ExtendedCss.validate(sel);
  7823. const typeError = isStyle && rule.type < 2;
  7824. if (checkResult.ok && !typeError) {
  7825. data[styleBoxes[index]] += css;
  7826. data.appliedCount++;
  7827. } else {
  7828. console.error(
  7829. "选择器检查错误:",
  7830. rule,
  7831. typeError || checkResult.error,
  7832. );
  7833. }
  7834. }
  7835. data.allRules.split("\n").forEach((rule) => {
  7836. if (isBasicRule(rule)) {
  7837. const brule = bRuleSpliter(rule);
  7838. if (brule) {
  7839. if (brule.bad) {
  7840. bRuleBad.push(brule.rule);
  7841. } else {
  7842. bRuleSet.add(brule.rule);
  7843. }
  7844. }
  7845. } else {
  7846. const ruleObj = ruleLoader(rule);
  7847. if (typeof ruleObj != "undefined") {
  7848. if (ruleObj.black === "black") {
  7849. if (!hRules[ruleObj.type].whites.has(ruleObj.sel)) {
  7850. hRules[ruleObj.type].rules.set(ruleObj.sel, ruleObj);
  7851. }
  7852. } else {
  7853. if (hRules[ruleObj.type].rules.has(ruleObj.sel)) {
  7854. hRules[ruleObj.type].rules.delete(ruleObj.sel);
  7855. }
  7856. hRules[ruleObj.type].whites.add(ruleObj.sel);
  7857. }
  7858. }
  7859. }
  7860. });
  7861. bRuleBad.forEach((brule) => bRuleSet.delete(brule));
  7862. values.brules(Array.from(bRuleSet));
  7863. hRules.forEach((rules, type) => {
  7864. rules.rules.forEach((rule) => addRule(rule, type));
  7865. });
  7866. styleBoxes.forEach((box) => {
  7867. if (data[box] !== "") {
  7868. data[box] +=
  7869. "html > head:valid " +
  7870. data.preset.replace(/^\s{2,}/g, " ").replaceAll("\n", "");
  7871. }
  7872. });
  7873. yield gmMenu("count", cleanRules);
  7874. yield saveCss();
  7875. if (!data.saved) {
  7876. yield styleApply();
  7877. }
  7878. return;
  7879. });
  7880. }
  7881. /**
  7882. * 应用规则
  7883. * @async
  7884. * @returns {Promise.<void>} 返回空的 Promise
  7885. */
  7886. function styleApply() {
  7887. return __awaiter(this, void 0, void 0, function* () {
  7888. if ((yield parseBRules()) === 3) return;
  7889. for (const type of [0, 1, 2, 3]) {
  7890. if (canApplyCss(type)) {
  7891. styleApplyExec(type);
  7892. }
  7893. }
  7894. gmMenu("export", reportRecord);
  7895. return;
  7896. });
  7897. }
  7898. /**
  7899. * 应用 CSS
  7900. * @param {number} type CSS 容器编号
  7901. */
  7902. function styleApplyExec(type) {
  7903. const csss = data[styleBoxes[type]]
  7904. .replace(CMRE, "")
  7905. .replaceAll("\n", "");
  7906. if (csss !== "") {
  7907. new ExtendedCss({
  7908. styleSheet: csss,
  7909. }).apply();
  7910. if (!(type % 2 == 1)) {
  7911. addStyle(csss);
  7912. }
  7913. }
  7914. }
  7915.  
  7916. /**
  7917. * 注册/刷新 更新脚本菜单
  7918. * @async
  7919. */
  7920. function makeInitMenu() {
  7921. return __awaiter(this, void 0, void 0, function* () {
  7922. yield gmMenu("count", cleanRules);
  7923. yield gmMenu("update", () => {
  7924. performUpdate(true).then(() => {
  7925. location.reload();
  7926. });
  7927. });
  7928. return;
  7929. });
  7930. }
  7931. /**
  7932. * 从 Response 的请求头部提取 ETag
  7933. * @param resp Response
  7934. * @returns {?string} ETag,未找到返回 null
  7935. */
  7936. function extrEtag(resp) {
  7937. var _a, _b, _c;
  7938. const etag = getEtag(
  7939. typeof (resp === null || resp === void 0 ? void 0 : resp.headers) ==
  7940. "object"
  7941. ? // 海阔世界
  7942. (_b =
  7943. (_a = resp.headers) === null || _a === void 0
  7944. ? void 0
  7945. : _a.etag) === null || _b === void 0
  7946. ? void 0
  7947. : _b[0]
  7948. : typeof (resp === null || resp === void 0
  7949. ? void 0
  7950. : resp.responseHeaders) == "string"
  7951. ? // Tampermonkey
  7952. resp.responseHeaders
  7953. : // Appara
  7954. (_c =
  7955. resp === null || resp === void 0
  7956. ? void 0
  7957. : resp.getAllResponseHeaders) === null || _c === void 0
  7958. ? void 0
  7959. : _c.call(resp),
  7960. );
  7961. return etag;
  7962. }
  7963. /**
  7964. * 获取订阅规则:存储规则
  7965. * @async
  7966. * @param rule 订阅规则对象
  7967. * @param resp Response
  7968. * @returns {Promise.<void>}
  7969. */
  7970. function storeRule(rule, resp) {
  7971. return __awaiter(this, void 0, void 0, function* () {
  7972. var _a, _c;
  7973. let savedRules = defaultValues.rules;
  7974. savedRules =
  7975. (_a = yield values.rules()) !== null && _a !== void 0
  7976. ? _a
  7977. : defaultValues.rules;
  7978. if (resp.responseText) {
  7979. savedRules[rule.标识] = rule.筛选后存储
  7980. ? resp.responseText
  7981. .split("\n")
  7982. .filter((rule) => CRRE.test(rule) || isBasicRule(rule))
  7983. .join("\n")
  7984. : resp.responseText;
  7985. yield values.rules(savedRules);
  7986. if (savedRules[rule.标识].length !== 0) {
  7987. const etag = extrEtag(resp),
  7988. savedEtags =
  7989. (_c = yield values.etags()) !== null && _c !== void 0
  7990. ? _c
  7991. : defaultValues.etags;
  7992. if (etag) {
  7993. savedEtags[rule.标识] = etag;
  7994. yield values.etags(savedEtags);
  7995. }
  7996. }
  7997. data.receivedRules += "\n" + savedRules[rule.标识];
  7998. }
  7999. return;
  8000. });
  8001. }
  8002. /**
  8003. * 获取订阅规则:下载内容
  8004. * @async
  8005. * @param rule 订阅规则对象
  8006. * @returns {Promise.<boolean>} 下载是否成功 (内容不为空)
  8007. */
  8008. function fetchRuleBody(rule) {
  8009. return __awaiter(this, void 0, void 0, function* () {
  8010. var _a;
  8011. const url = addTimeParam(rule.地址);
  8012. const getResp = yield promiseXhr({
  8013. method: "GET",
  8014. responseType: "text",
  8015. url,
  8016. }).catch((error) => {
  8017. console.error("规则: ", url, " 下载错误: ", error);
  8018. });
  8019. if (
  8020. (_a =
  8021. getResp === null || getResp === void 0
  8022. ? void 0
  8023. : getResp.responseText) === null || _a === void 0
  8024. ? void 0
  8025. : _a.length
  8026. ) {
  8027. yield storeRule(rule, getResp);
  8028. return true;
  8029. } else {
  8030. return false;
  8031. }
  8032. });
  8033. }
  8034. /**
  8035. * 获取订阅规则:判断 ETag 并更新
  8036. * @async
  8037. * @param rule 订阅规则对象
  8038. * @param resp Response
  8039. * @returns {Promise.<void>}
  8040. */
  8041. function fetchRuleGet(rule, resp) {
  8042. return __awaiter(this, void 0, void 0, function* () {
  8043. var _a;
  8044. const etag = extrEtag(resp),
  8045. savedEtags = yield values.etags();
  8046. if (
  8047. (_a =
  8048. resp === null || resp === void 0 ? void 0 : resp.responseText) ===
  8049. null || _a === void 0
  8050. ? void 0
  8051. : _a.length
  8052. ) {
  8053. yield storeRule(rule, resp);
  8054. if (
  8055. etag !==
  8056. (savedEtags === null || savedEtags === void 0
  8057. ? void 0
  8058. : savedEtags[rule.标识])
  8059. ) {
  8060. return;
  8061. } else {
  8062. return Promise.reject("ETag 一致");
  8063. }
  8064. } else {
  8065. if (
  8066. etag !==
  8067. (savedEtags === null || savedEtags === void 0
  8068. ? void 0
  8069. : savedEtags[rule.标识])
  8070. ) {
  8071. if (yield fetchRuleBody(rule)) {
  8072. return;
  8073. } else {
  8074. return Promise.reject("GET 失败");
  8075. }
  8076. } else return Promise.reject("ETag 一致");
  8077. }
  8078. });
  8079. }
  8080. /**
  8081. * 获取订阅规则:获取 ETag
  8082. * @async
  8083. * @param rule 订阅规则对象
  8084. * @returns {Promise.<void>}
  8085. */
  8086. function fetchRule(rule) {
  8087. return __awaiter(this, void 0, void 0, function* () {
  8088. var _a;
  8089. let headRespError = {
  8090. error: "noxhr",
  8091. };
  8092. const url = addTimeParam(rule.地址);
  8093. const headResp = yield promiseXhr({
  8094. method: "HEAD",
  8095. responseType: "text",
  8096. url,
  8097. }).catch((error) => {
  8098. headRespError = error;
  8099. console.error("规则: ", url, " HEAD 错误: ", error);
  8100. });
  8101. if (!headResp) {
  8102. // Via HEAD 会超时,但可以得到 ETag
  8103. if (
  8104. (_a =
  8105. headRespError === null || headRespError === void 0
  8106. ? void 0
  8107. : headRespError.resp) === null || _a === void 0
  8108. ? void 0
  8109. : _a.responseHeaders
  8110. ) {
  8111. return yield fetchRuleGet(rule, headRespError.resp);
  8112. } else {
  8113. return Promise.reject("HEAD 失败");
  8114. }
  8115. } else {
  8116. return yield fetchRuleGet(rule, headResp);
  8117. }
  8118. });
  8119. }
  8120. /**
  8121. * 获取订阅规则:主函数
  8122. * @async
  8123. * @returns {Promise.<void>}
  8124. */
  8125. function fetchRules() {
  8126. return __awaiter(this, void 0, void 0, function* () {
  8127. let hasUpdate = presets.onlineRules.length;
  8128. data.updating = true;
  8129. yield gmMenu("update", () => void 0);
  8130. for (const rule of presets.onlineRules) {
  8131. if (rule.在线更新) {
  8132. yield fetchRule(rule).catch((error) => {
  8133. console.error("获取规则 ", rule, " 发生错误: ", error);
  8134. hasUpdate--;
  8135. });
  8136. } else {
  8137. hasUpdate--;
  8138. }
  8139. }
  8140. values.time(new Date().toLocaleString("zh-CN"));
  8141. data.updating = false;
  8142. yield makeInitMenu();
  8143. if (hasUpdate > 0) {
  8144. for (const host of yield getSavedHosts()) {
  8145. if (host === location.hostname) {
  8146. yield initRules(true);
  8147. } else {
  8148. const save = yield values.css(void 0, host);
  8149. if (save) {
  8150. save.needUpdate = true;
  8151. yield values.css(save, host);
  8152. }
  8153. }
  8154. }
  8155. }
  8156. return;
  8157. });
  8158. }
  8159. /**
  8160. * 更新订阅规则
  8161. * @async
  8162. * @param {boolean} force 无条件更新
  8163. * @returns {Promise.<void>}
  8164. */
  8165. function performUpdate(force) {
  8166. return __awaiter(this, void 0, void 0, function* () {
  8167. var _a;
  8168. const oldTime = new Date(
  8169. (_a = yield values.time()) !== null && _a !== void 0
  8170. ? _a
  8171. : defaultValues.time,
  8172. ).getDate();
  8173. const newTime = new Date().getDate();
  8174. return data.isFrame
  8175. ? Promise.reject()
  8176. : force || oldTime !== newTime
  8177. ? fetchRules()
  8178. : Promise.resolve();
  8179. });
  8180. }
  8181. function main() {
  8182. return __awaiter(this, void 0, void 0, function* () {
  8183. var _a, _b;
  8184. if (!location.protocol.startsWith("http")) return;
  8185. // 初始化 data
  8186. data.disabled =
  8187. (_b =
  8188. (_a = yield values.black()) === null || _a === void 0
  8189. ? void 0
  8190. : _a.includes(location.hostname)) !== null && _b !== void 0
  8191. ? _b
  8192. : false;
  8193. data.preset = yield getUserConfig("css");
  8194. data.timeout = yield getUserConfig("timeout");
  8195. data.tryCount = yield getUserConfig("tryCount");
  8196. data.tryTimeout = yield getUserConfig("tryTimeout");
  8197. data.headTimeout = yield getUserConfig("headTimeout");
  8198. data.customRules = yield getUserConfig("rules");
  8199. data.autoCleanSize = yield getUserConfig("autoCleanSize");
  8200. data.customRules += "\n" + getComment();
  8201. let finish = false;
  8202. gmMenu("disable", switchDisabledStat);
  8203. if (data.disabled) {
  8204. gmMenu("count", cleanRules);
  8205. return;
  8206. }
  8207. if (yield getSavedHosts(location.hostname)) {
  8208. yield readCss();
  8209. }
  8210. const hash = yield getCustomHash(false);
  8211. saved: {
  8212. yield makeInitMenu();
  8213. if ((yield values.hash()) !== hash) {
  8214. yield getCustomHash(true);
  8215. yield initRules(true);
  8216. break saved;
  8217. }
  8218. if (data.saved) {
  8219. styleApply();
  8220. if (!data.update) break saved;
  8221. }
  8222. yield initRules(false);
  8223. if (data.receivedRules.length === 0) {
  8224. yield performUpdate(true);
  8225. yield gmMenu("count");
  8226. yield initRules(true);
  8227. finish = true;
  8228. } else yield parseRules();
  8229. }
  8230. if (!finish) {
  8231. try {
  8232. yield performUpdate(false);
  8233. } catch (_error) {
  8234. console.warn("iframe: ", location.href, " 取消更新");
  8235. }
  8236. }
  8237. });
  8238. }
  8239. runOnce(data.mutex, main);
  8240. })($polyfills);
  8241. })();