@adguard/agtree 2.0.0-alpha.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /*
2
- * AGTree v2.0.0-alpha.0 (build date: Mon, 29 Jul 2024 13:43:09 GMT)
2
+ * AGTree v2.0.0 (build date: Thu, 15 Aug 2024 15:06:58 GMT)
3
3
  * (c) 2024 Adguard Software Ltd.
4
4
  * Released under the MIT license
5
5
  * https://github.com/AdguardTeam/tsurlfilter/tree/master/packages/agtree#readme
@@ -743,6 +743,15 @@ class StringUtils {
743
743
  }
744
744
  }
745
745
 
746
+ /**
747
+ * Possible operators in the logical expression.
748
+ */
749
+ var OperatorValue;
750
+ (function (OperatorValue) {
751
+ OperatorValue["Not"] = "!";
752
+ OperatorValue["And"] = "&&";
753
+ OperatorValue["Or"] = "||";
754
+ })(OperatorValue || (OperatorValue = {}));
746
755
  /**
747
756
  * Represents the different comment markers that can be used in an adblock rule.
748
757
  *
@@ -2938,15 +2947,6 @@ var ParenthesisNodeBinaryPropMap;
2938
2947
  ParenthesisNodeBinaryPropMap[ParenthesisNodeBinaryPropMap["Start"] = 2] = "Start";
2939
2948
  ParenthesisNodeBinaryPropMap[ParenthesisNodeBinaryPropMap["End"] = 3] = "End";
2940
2949
  })(ParenthesisNodeBinaryPropMap || (ParenthesisNodeBinaryPropMap = {}));
2941
- /**
2942
- * Possible operators in the logical expression.
2943
- */
2944
- var OperatorValue;
2945
- (function (OperatorValue) {
2946
- OperatorValue["Not"] = "!";
2947
- OperatorValue["And"] = "&&";
2948
- OperatorValue["Or"] = "||";
2949
- })(OperatorValue || (OperatorValue = {}));
2950
2950
  /**
2951
2951
  * Possible token types in the logical expression.
2952
2952
  */
@@ -2992,7 +2992,9 @@ const getOperatorOrFail = binary => {
2992
2992
  /**
2993
2993
  * Serialization map for known variables.
2994
2994
  */
2995
- const KNOWN_VARIABLES_MAP = new Map([['ext_abp', 0], ['ext_ublock', 1], ['ext_ubol', 2], ['ext_devbuild', 3], ['env_chromium', 4], ['env_edge', 5], ['env_firefox', 6], ['env_mobile', 7], ['env_safari', 8], ['env_mv3', 9], ['false', 10], ['cap_html_filtering', 11], ['cap_user_stylesheet', 12], ['adguard', 13], ['adguard_app_windows', 14], ['adguard_app_mac', 15], ['adguard_app_android', 16], ['adguard_app_ios', 17], ['adguard_ext_safari', 18], ['adguard_ext_chromium', 19], ['adguard_ext_firefox', 20], ['adguard_ext_edge', 21], ['adguard_ext_opera', 22], ['adguard_ext_android_cb', 23]]);
2995
+ const KNOWN_VARIABLES_MAP = new Map([['ext_abp', 0], ['ext_ublock', 1], ['ext_ubol', 2], ['ext_devbuild', 3], ['env_chromium', 4], ['env_edge', 5], ['env_firefox', 6], ['env_mobile', 7], ['env_safari', 8], ['env_mv3', 9], ['false', 10], ['cap_html_filtering', 11], ['cap_user_stylesheet', 12], ['adguard', 13], ['adguard_app_windows', 14], ['adguard_app_mac', 15], ['adguard_app_android', 16], ['adguard_app_ios', 17], ['adguard_ext_safari', 18], ['adguard_ext_chromium', 19], ['adguard_ext_firefox', 20], ['adguard_ext_edge', 21], ['adguard_ext_opera', 22], ['adguard_ext_android_cb', 23]
2996
+ // TODO: Add 'adguard_ext_chromium_mv3' to the list
2997
+ ]);
2996
2998
  /**
2997
2999
  * Deserialization map for known variables.
2998
3000
  */
@@ -5696,7 +5698,7 @@ class CssTokenStream {
5696
5698
  * @returns An array containing the number of tokens skipped and the number of tokens skipped without leading and
5697
5699
  * trailing whitespace tokens.
5698
5700
  */
5699
- skipUntilEx(type, balance) {
5701
+ skipUntilExt(type, balance) {
5700
5702
  let i = this.index;
5701
5703
  let firstNonWsToken = -1; // -1 means no non-whitespace token found yet
5702
5704
  let lastNonWsToken = -1; // -1 means no non-whitespace token found yet
@@ -5924,7 +5926,7 @@ class AdgCssInjectionParser extends ParserBase {
5924
5926
  // └ this one
5925
5927
  const {
5926
5928
  skippedTrimmed: selectorTokensLength
5927
- } = stream.skipUntilEx(TokenType$1.OpenCurlyBracket, balanceShift + 1);
5929
+ } = stream.skipUntilExt(TokenType$1.OpenCurlyBracket, balanceShift + 1);
5928
5930
  stream.expect(TokenType$1.OpenCurlyBracket);
5929
5931
  // If the skipped tokens count is 0 without leading and trailing whitespace characters, then the selector list
5930
5932
  // is empty
@@ -6164,6 +6166,7 @@ class AbpSnippetInjectionBodyParser extends ParserBase {
6164
6166
  *
6165
6167
  * @note Only 256 values can be represented this way.
6166
6168
  */
6169
+ // TODO: Update this map with the actual values
6167
6170
  static FREQUENT_ARGS_SERIALIZATION_MAP = new Map([['abort-current-inline-script', 0], ['abort-on-property-read', 1], ['abort-on-property-write', 2], ['json-prune', 3], ['log', 4], ['prevent-listener', 5], ['cookie-remover', 6], ['override-property-read', 7], ['abort-on-iframe-property-read', 8], ['abort-on-iframe-property-write', 9], ['freeze-element', 10], ['json-override', 11], ['simulate-mouse-event', 12], ['strip-fetch-query-parameter', 13], ['hide-if-contains', 14], ['hide-if-contains-image', 15], ['hide-if-contains-image-hash', 16], ['hide-if-contains-similar-text', 17], ['hide-if-contains-visible-text', 18], ['hide-if-contains-and-matches-style', 19], ['hide-if-graph-matches', 20], ['hide-if-has-and-matches-style', 21], ['hide-if-labelled-by', 22], ['hide-if-matches-xpath', 23], ['hide-if-matches-computed-xpath', 24], ['hide-if-shadow-contains', 25], ['debug', 26], ['trace', 27], ['race', 28]]);
6168
6171
  /**
6169
6172
  * Value map for binary deserialization. This helps to reduce the size of the serialized data,
@@ -6291,6 +6294,7 @@ class UboScriptletInjectionBodyParser extends ParserBase {
6291
6294
  *
6292
6295
  * @note Only 256 values can be represented this way.
6293
6296
  */
6297
+ // TODO: Update this map with the actual values
6294
6298
  static FREQUENT_ARGS_SERIALIZATION_MAP = new Map([['abort-current-script.js', 0], ['acs.js', 1], ['abort-current-inline-script.js', 2], ['acis.js', 3], ['abort-on-property-read.js', 4], ['aopr.js', 5], ['abort-on-property-write.js', 6], ['aopw.js', 7], ['abort-on-stack-trace.js', 8], ['aost.js', 9], ['adjust-setInterval.js', 10], ['nano-setInterval-booster.js', 11], ['nano-sib.js', 12], ['adjust-setTimeout.js', 13], ['nano-setTimeout-booster.js', 14], ['nano-stb.js', 15], ['close-window.js', 16], ['window-close-if.js', 17], ['disable-newtab-links.js', 18], ['evaldata-prune.js', 19], ['json-prune.js', 20], ['addEventListener-logger.js', 21], ['aell.js', 22], ['m3u-prune.js', 23], ['nowebrtc.js', 24], ['addEventListener-defuser.js', 25], ['aeld.js', 26], ['prevent-addEventListener.js', 27], ['adfly-defuser.js', 28], ['noeval-if.js', 29], ['prevent-eval-if.js', 30], ['no-fetch-if.js', 31], ['prevent-fetch.js', 32], ['no-xhr-if.js', 33], ['prevent-xhr.js', 34], ['prevent-refresh.js', 35], ['refresh-defuser.js', 36], ['no-requestAnimationFrame-if.js', 37], ['norafif.js', 38], ['prevent-requestAnimationFrame.js', 39], ['no-setInterval-if.js', 40], ['nosiif.js', 41], ['prevent-setInterval.js', 42], ['setInterval-defuser.js', 43], ['no-setTimeout-if.js', 44], ['nostif.js', 45], ['prevent-setTimeout.js', 46], ['setTimeout-defuser.js', 47], ['no-window-open-if.js', 48], ['nowoif.js', 49], ['prevent-window-open.js', 50], ['window.open-defuser.js', 51], ['remove-attr.js', 52], ['ra.js', 53], ['remove-class.js', 54], ['rc.js', 55], ['remove-cookie.js', 56], ['cookie-remover.js', 57], ['remove-node-text.js', 58], ['rmnt.js', 59], ['set-attr.js', 60], ['set-constant.js', 61], ['set.js', 62], ['set-cookie.js', 63], ['set-local-storage-item.js', 64], ['set-session-storage-item.js', 65], ['xml-prune.js', 66], ['webrtc-if.js', 67], ['overlay-buster.js', 68], ['alert-buster.js', 69], ['golem.de.js', 70], ['href-sanitizer.js', 71], ['call-nothrow.js', 72], ['window.name-defuser.js', 73], ['spoof-css.js', 74], ['trusted-set-constant.js', 75], ['trusted-set.js', 76], ['trusted-set-cookie.js', 77], ['trusted-set-local-storage-item.js', 78], ['trusted-replace-fetch-response.js', 79], ['json-prune-fetch-response.js', 80], ['json-prune-xhr-response.js', 81], ['trusted-replace-xhr-response.js', 82], ['multiup.js', 83], ['prevent-canvas.js', 84], ['set-cookie-reload.js', 85], ['trusted-set-cookie-reload.js', 86], ['trusted-click-element.js', 87], ['trusted-prune-inbound-object.js', 88], ['trusted-prune-outbound-object.js', 89], ['trusted-set-session-storage-item.js', 90], ['trusted-replace-node-text.js', 91], ['trusted-rpnt.js', 92], ['replace-node-text.js', 93], ['rpnt.js', 94]]);
6295
6299
  /**
6296
6300
  * Value map for binary deserialization. This helps to reduce the size of the serialized data,
@@ -6428,6 +6432,7 @@ class AdgScriptletInjectionBodyParser extends ParserBase {
6428
6432
  *
6429
6433
  * @note Only 256 values can be represented this way.
6430
6434
  */
6435
+ // TODO: Update this map with the actual values
6431
6436
  static FREQUENT_ARGS_SERIALIZATION_MAP = new Map([['abort-current-inline-script', 0], ['abort-on-property-read', 1], ['abort-on-property-write', 2], ['abort-on-stack-trace', 3], ['adjust-setInterval', 4], ['adjust-setTimeout', 5], ['close-window', 6], ['debug-current-inline-script', 7], ['debug-on-property-read', 8], ['debug-on-property-write', 9], ['dir-string', 10], ['disable-newtab-links', 11], ['evaldata-prune', 12], ['json-prune', 13], ['log', 14], ['log-addEventListener', 15], ['log-eval', 16], ['log-on-stack-trace', 17], ['m3u-prune', 18], ['noeval', 19], ['nowebrtc', 20], ['no-topics', 21], ['prevent-addEventListener', 22], ['prevent-adfly', 23], ['prevent-bab', 24], ['prevent-eval-if', 25], ['prevent-fab-3.2.0', 26], ['prevent-fetch', 27], ['prevent-xhr', 28], ['prevent-popads-net', 29], ['prevent-refresh', 30], ['prevent-requestAnimationFrame', 31], ['prevent-setInterval', 32], ['prevent-setTimeout', 33], ['prevent-window-open', 34], ['remove-attr', 35], ['remove-class', 36], ['remove-cookie', 37], ['remove-node-text', 38], ['set-attr', 39], ['set-constant', 40], ['set-cookie', 41], ['set-cookie-reload', 42], ['set-local-storage-item', 43], ['set-popads-dummy', 44], ['set-session-storage-item', 45], ['xml-prune', 46]]);
6432
6437
  /**
6433
6438
  * Value map for binary deserialization. This helps to reduce the size of the serialized data,
@@ -21507,6 +21512,30 @@ function getScriptletName(scriptletNode) {
21507
21512
  }
21508
21513
  return scriptletNode.children[0]?.value ?? EMPTY;
21509
21514
  }
21515
+ /**
21516
+ * Transform the nth argument of the scriptlet node
21517
+ *
21518
+ * @param scriptletNode Scriptlet node to transform argument of
21519
+ * @param index Index of the argument to transform (index 0 is the scriptlet name)
21520
+ * @param transform Function to transform the argument
21521
+ */
21522
+ function transformNthScriptletArgument(scriptletNode, index, transform) {
21523
+ const child = scriptletNode.children[index];
21524
+ if (!isUndefined(child) && !isNull(child)) {
21525
+ child.value = transform(child.value);
21526
+ }
21527
+ }
21528
+ /**
21529
+ * Transform all arguments of the scriptlet node
21530
+ *
21531
+ * @param scriptletNode Scriptlet node to transform arguments of
21532
+ * @param transform Function to transform the arguments
21533
+ */
21534
+ function transformAllScriptletArguments(scriptletNode, transform) {
21535
+ for (let i = 0; i < scriptletNode.children.length; i += 1) {
21536
+ transformNthScriptletArgument(scriptletNode, i, transform);
21537
+ }
21538
+ }
21510
21539
  /**
21511
21540
  * Set name of the scriptlet.
21512
21541
  * Modifies input `scriptletNode` if needed.
@@ -21515,10 +21544,7 @@ function getScriptletName(scriptletNode) {
21515
21544
  * @param name Name to set
21516
21545
  */
21517
21546
  function setScriptletName(scriptletNode, name) {
21518
- if (scriptletNode.children.length > 0 && !isNull(scriptletNode.children[0])) {
21519
- // eslint-disable-next-line no-param-reassign
21520
- scriptletNode.children[0].value = name;
21521
- }
21547
+ transformNthScriptletArgument(scriptletNode, 0, () => name);
21522
21548
  }
21523
21549
  /**
21524
21550
  * Set quote type of the scriptlet parameters
@@ -21527,1422 +21553,1559 @@ function setScriptletName(scriptletNode, name) {
21527
21553
  * @param quoteType Preferred quote type
21528
21554
  */
21529
21555
  function setScriptletQuoteType(scriptletNode, quoteType) {
21530
- if (scriptletNode.children.length > 0) {
21531
- for (let i = 0; i < scriptletNode.children.length; i += 1) {
21532
- const child = scriptletNode.children[i];
21533
- if (isNull(child)) {
21534
- continue;
21535
- }
21536
- // eslint-disable-next-line no-param-reassign
21537
- child.value = QuoteUtils.setStringQuoteType(child.value, quoteType);
21538
- }
21539
- }
21556
+ transformAllScriptletArguments(scriptletNode, value => QuoteUtils.setStringQuoteType(value, quoteType));
21540
21557
  }
21541
21558
 
21542
21559
  /**
21543
- * @file Scriptlet injection rule converter
21560
+ * @file Compatibility tables for redirects.
21544
21561
  */
21545
- const ABP_SCRIPTLET_PREFIX = 'abp-';
21546
- const UBO_SCRIPTLET_PREFIX = 'ubo-';
21547
21562
  /**
21548
- * Scriptlet injection rule converter class
21563
+ * Prefix for resource redirection names.
21564
+ */
21565
+ const ABP_RESOURCE_PREFIX = 'abp-resource:';
21566
+ const ABP_RESOURCE_PREFIX_LENGTH = ABP_RESOURCE_PREFIX.length;
21567
+ /**
21568
+ * Transforms the name of an ABP redirect to a normalized form.
21549
21569
  *
21550
- * @todo Implement `convertToUbo` and `convertToAbp`
21570
+ * @param name Redirect name to normalize.
21571
+ *
21572
+ * @returns Normalized redirect name.
21573
+ *
21574
+ * @example
21575
+ * abpRedirectNameNormalizer('abp-resource:my-resource') // => 'my-resource'
21551
21576
  */
21552
- class ScriptletRuleConverter extends RuleConverterBase {
21577
+ const abpRedirectNameNormalizer = name => {
21578
+ if (name.startsWith(ABP_RESOURCE_PREFIX)) {
21579
+ return name.slice(ABP_RESOURCE_PREFIX_LENGTH);
21580
+ }
21581
+ return name;
21582
+ };
21583
+ /**
21584
+ * Compatibility table for redirects.
21585
+ */
21586
+ class RedirectsCompatibilityTable extends CompatibilityTableBase {
21553
21587
  /**
21554
- * Converts a scriptlet injection rule to AdGuard format, if possible.
21588
+ * Creates a new instance of the compatibility table for redirects.
21555
21589
  *
21556
- * @param rule Rule node to convert
21557
- * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
21558
- * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
21559
- * If the rule was not converted, the result array will contain the original node with the same object reference
21560
- * @throws If the rule is invalid or cannot be converted
21590
+ * @param data Compatibility table data.
21561
21591
  */
21562
- static convertToAdg(rule) {
21563
- // Ignore AdGuard rules
21564
- if (rule.syntax === AdblockSyntax.Adg) {
21565
- return createNodeConversionResult([rule], false);
21566
- }
21567
- const separator = rule.separator.value;
21568
- let convertedSeparator = separator;
21569
- convertedSeparator = rule.exception ? CosmeticRuleSeparator.AdgJsInjectionException : CosmeticRuleSeparator.AdgJsInjection;
21570
- const convertedScriptlets = [];
21571
- for (const scriptlet of rule.body.children) {
21572
- // Clone the node to avoid any side effects
21573
- const scriptletClone = cloneScriptletRuleNode(scriptlet);
21574
- // Remove possible quotes just to make it easier to work with the scriptlet name
21575
- const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), QuoteType.None);
21576
- // Add prefix if it's not already there
21577
- let prefix;
21578
- switch (rule.syntax) {
21579
- case AdblockSyntax.Abp:
21580
- prefix = ABP_SCRIPTLET_PREFIX;
21581
- break;
21582
- case AdblockSyntax.Ubo:
21583
- prefix = UBO_SCRIPTLET_PREFIX;
21584
- break;
21585
- default:
21586
- prefix = EMPTY;
21587
- }
21588
- if (!scriptletName.startsWith(prefix)) {
21589
- setScriptletName(scriptletClone, `${prefix}${scriptletName}`);
21590
- }
21591
- // ADG scriptlet parameters should be quoted, and single quoted are preferred
21592
- setScriptletQuoteType(scriptletClone, QuoteType.Single);
21593
- convertedScriptlets.push(scriptletClone);
21594
- }
21595
- return createNodeConversionResult(convertedScriptlets.map(scriptlet => {
21596
- const res = {
21597
- category: rule.category,
21598
- type: rule.type,
21599
- syntax: AdblockSyntax.Adg,
21600
- exception: rule.exception,
21601
- domains: cloneDomainListNode(rule.domains),
21602
- separator: {
21603
- type: 'Value',
21604
- value: convertedSeparator
21605
- },
21606
- body: {
21607
- type: rule.body.type,
21608
- children: [scriptlet]
21609
- }
21610
- };
21611
- if (rule.modifiers) {
21612
- res.modifiers = cloneModifierListNode(rule.modifiers);
21613
- }
21614
- return res;
21615
- }), true);
21592
+ constructor(data) {
21593
+ super(data, abpRedirectNameNormalizer);
21616
21594
  }
21617
21595
  }
21596
+ /**
21597
+ * Deep freeze the compatibility table data to avoid accidental modifications.
21598
+ */
21599
+ deepFreeze(redirectsCompatibilityTableData);
21600
+ /**
21601
+ * Compatibility table instance for redirects.
21602
+ */
21603
+ const redirectsCompatibilityTable = new RedirectsCompatibilityTable(redirectsCompatibilityTableData);
21618
21604
 
21619
21605
  /**
21620
- * @file Utility functions for working with modifier nodes
21606
+ * @file Compatibility tables for scriptlets.
21621
21607
  */
21622
21608
  /**
21623
- * Creates a modifier node
21624
- *
21625
- * @param name Name of the modifier
21626
- * @param value Value of the modifier
21627
- * @param exception Whether the modifier is an exception
21628
- * @returns Modifier node
21609
+ * Compatibility table for scriptlets.
21629
21610
  */
21630
- function createModifierNode(name, value = undefined, exception = false) {
21631
- const result = {
21632
- type: 'Modifier',
21633
- exception,
21634
- name: {
21635
- type: 'Value',
21636
- value: name
21637
- }
21638
- };
21639
- if (!isUndefined(value)) {
21640
- result.value = {
21641
- type: 'Value',
21642
- value
21643
- };
21644
- }
21645
- return result;
21646
- }
21611
+ class ScriptletsCompatibilityTable extends CompatibilityTableBase {}
21647
21612
  /**
21648
- * Creates a modifier list node
21649
- *
21650
- * @param modifiers Modifiers to put in the list (optional, defaults to an empty list)
21651
- * @returns Modifier list node
21613
+ * Deep freeze the compatibility table data to avoid accidental modifications.
21652
21614
  */
21653
- function createModifierListNode(modifiers = []) {
21654
- const result = {
21655
- type: 'ModifierList',
21656
- // We need to clone the modifiers to avoid side effects
21657
- children: modifiers.length ? clone(modifiers) : []
21658
- };
21659
- return result;
21660
- }
21661
-
21615
+ deepFreeze(scriptletsCompatibilityTableData);
21662
21616
  /**
21663
- * A very simple map extension that allows to store multiple values for the same key
21664
- * by storing them in an array.
21665
- *
21666
- * @todo Add more methods if needed
21617
+ * Compatibility table instance for scriptlets.
21667
21618
  */
21668
- class MultiValueMap extends Map {
21669
- /**
21670
- * Adds a value to the map. If the key already exists, the value will be appended to the existing array,
21671
- * otherwise a new array will be created for the key.
21672
- *
21673
- * @param key Key to add
21674
- * @param values Value(s) to add
21675
- */
21676
- add(key, ...values) {
21677
- let currentValues = super.get(key);
21678
- if (isUndefined(currentValues)) {
21679
- currentValues = [];
21680
- super.set(key, values);
21681
- }
21682
- currentValues.push(...values);
21683
- }
21684
- }
21619
+ const scriptletsCompatibilityTable = new ScriptletsCompatibilityTable(scriptletsCompatibilityTableData);
21685
21620
 
21621
+ /* eslint-disable no-bitwise */
21686
21622
  /**
21687
- * @file Cosmetic rule modifier converter from uBO to ADG
21623
+ * @file Platform schema.
21688
21624
  */
21689
- const UBO_MATCHES_PATH_OPERATOR = 'matches-path';
21690
- const ADG_PATH_MODIFIER = 'path';
21691
21625
  /**
21692
- * Special characters in modifier regexps that should be escaped
21626
+ * Platform separator, e.g. 'adg_os_any|adg_safari_any' means any AdGuard OS platform and
21627
+ * any AdGuard Safari content blocker platform.
21693
21628
  */
21694
- const SPECIAL_MODIFIER_REGEX_CHARS = new Set([OPEN_SQUARE_BRACKET, CLOSE_SQUARE_BRACKET, COMMA, ESCAPE_CHARACTER]);
21629
+ const PLATFORM_SEPARATOR = '|';
21695
21630
  /**
21696
- * Helper class for converting cosmetic rule modifiers from uBO to ADG
21631
+ * Platform negation character, e.g. 'adg_any|~adg_safari_any' means any AdGuard product except
21632
+ * Safari content blockers.
21697
21633
  */
21698
- class AdgCosmeticRuleModifierConverter {
21699
- /**
21700
- * Converts a uBO cosmetic rule modifier list to ADG, if possible.
21701
- *
21702
- * @param modifierList Cosmetic rule modifier list node to convert
21703
- * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
21704
- * the converted node, and its `isConverted` flag indicates whether the original node was converted.
21705
- * If the node was not converted, the result will contain the original node with the same object reference
21706
- * @throws If the modifier list cannot be converted
21707
- * @see {@link https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#cosmetic-filter-operators}
21708
- */
21709
- static convertFromUbo(modifierList) {
21710
- const conversionMap = new MultiValueMap();
21711
- modifierList.children.forEach((modifier, index) => {
21712
- // :matches-path
21713
- if (modifier.name.value === UBO_MATCHES_PATH_OPERATOR) {
21714
- if (!modifier.value) {
21715
- throw new RuleConversionError(`'${UBO_MATCHES_PATH_OPERATOR}' operator requires a value`);
21716
- }
21717
- const value = RegExpUtils.isRegexPattern(modifier.value.value) ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS) : modifier.value.value;
21718
- // Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
21719
- conversionMap.add(index, createModifierNode(ADG_PATH_MODIFIER,
21720
- // We should negate the regexp if the modifier is an exception
21721
- modifier.exception
21722
- // eslint-disable-next-line max-len
21723
- ? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(value))}${REGEX_MARKER}` : value));
21724
- }
21725
- });
21726
- // Check if we have any converted modifiers
21727
- if (conversionMap.size) {
21728
- const modifierListClone = clone(modifierList);
21729
- // Replace the original modifiers with the converted ones
21730
- modifierListClone.children = modifierListClone.children.map((modifier, index) => {
21731
- const convertedModifier = conversionMap.get(index);
21732
- return convertedModifier ?? modifier;
21733
- }).flat();
21734
- return createConversionResult(modifierListClone, true);
21634
+ const PLATFORM_NEGATION = '~';
21635
+ /**
21636
+ * Parses a raw platform string into a platform bitmask.
21637
+ *
21638
+ * @param rawPlatforms Raw platform string, e.g. 'adg_safari_any|adg_os_any'.
21639
+ *
21640
+ * @returns Platform bitmask.
21641
+ */
21642
+ const parseRawPlatforms = rawPlatforms => {
21643
+ // e.g. 'adg_safari_any|adg_os_any'
21644
+ const rawPlatformList = rawPlatforms.split(PLATFORM_SEPARATOR).map(rawPlatform => rawPlatform.trim());
21645
+ let result = 0;
21646
+ for (let rawPlatform of rawPlatformList) {
21647
+ // negation, e.g. 'adg_any|~adg_safari_any' means any AdGuard product except Safari content blockers
21648
+ let negated = false;
21649
+ if (rawPlatform.startsWith(PLATFORM_NEGATION)) {
21650
+ negated = true;
21651
+ rawPlatform = rawPlatform.slice(1).trim();
21735
21652
  }
21736
- // Otherwise, just return the original modifier list
21737
- return createConversionResult(modifierList, false);
21738
- }
21739
- }
21740
- const ERROR_MESSAGES$1 = {
21741
- // eslint-disable-next-line max-len
21742
- INVALID_ATTRIBUTE_VALUE: `Expected '${getFormattedTokenName(TokenType$1.Ident)}' or '${getFormattedTokenName(TokenType$1.String)}' as attribute value, but got '%s' with value '%s`
21653
+ const platform = SPECIFIC_PLATFORM_MAP.get(rawPlatform) ?? GENERIC_PLATFORM_MAP.get(rawPlatform);
21654
+ if (isUndefined(platform)) {
21655
+ throw new Error(`Unknown platform: ${rawPlatform}`);
21656
+ }
21657
+ if (negated) {
21658
+ result &= ~platform;
21659
+ } else {
21660
+ result |= platform;
21661
+ }
21662
+ }
21663
+ if (result === 0) {
21664
+ throw new Error('No platforms specified');
21665
+ }
21666
+ return result;
21743
21667
  };
21744
- var PseudoClasses;
21745
- (function (PseudoClasses) {
21746
- PseudoClasses["AbpContains"] = "-abp-contains";
21747
- PseudoClasses["AbpHas"] = "-abp-has";
21748
- PseudoClasses["Contains"] = "contains";
21749
- PseudoClasses["Has"] = "has";
21750
- PseudoClasses["HasText"] = "has-text";
21751
- PseudoClasses["MatchesCss"] = "matches-css";
21752
- PseudoClasses["MatchesCssAfter"] = "matches-css-after";
21753
- PseudoClasses["MatchesCssBefore"] = "matches-css-before";
21754
- PseudoClasses["Not"] = "not";
21755
- })(PseudoClasses || (PseudoClasses = {}));
21756
- var PseudoElements;
21757
- (function (PseudoElements) {
21758
- PseudoElements["After"] = "after";
21759
- PseudoElements["Before"] = "before";
21760
- })(PseudoElements || (PseudoElements = {}));
21761
- const PSEUDO_ELEMENT_NAMES = new Set([PseudoElements.After, PseudoElements.Before]);
21762
21668
  /**
21763
- * CSS selector converter
21764
- *
21765
- * @todo Implement `convertToUbo` and `convertToAbp`
21669
+ * Platform schema.
21766
21670
  */
21767
- class CssSelectorConverter extends ConverterBase {
21768
- /**
21769
- * Converts Extended CSS elements to AdGuard-compatible ones
21770
- *
21771
- * @param selectorList Selector list to convert
21772
- * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
21773
- * the converted node, and its `isConverted` flag indicates whether the original node was converted.
21774
- * If the node was not converted, the result will contain the original node with the same object reference
21775
- * @throws If the rule is invalid or incompatible
21776
- */
21777
- static convertToAdg(selectorList) {
21778
- const stream = selectorList instanceof CssTokenStream ? selectorList : new CssTokenStream(selectorList);
21779
- const converted = [];
21780
- const convertAndPushPseudo = pseudo => {
21781
- switch (pseudo) {
21782
- case PseudoClasses.AbpContains:
21783
- case PseudoClasses.HasText:
21784
- converted.push(PseudoClasses.Contains);
21785
- converted.push(OPEN_PARENTHESIS);
21786
- break;
21787
- case PseudoClasses.AbpHas:
21788
- converted.push(PseudoClasses.Has);
21789
- converted.push(OPEN_PARENTHESIS);
21790
- break;
21791
- // a bit special case:
21792
- // - `:matches-css-before(...)` → `:matches-css(before, ...)`
21793
- // - `:matches-css-after(...)` → `:matches-css(after, ...)`
21794
- case PseudoClasses.MatchesCssBefore:
21795
- case PseudoClasses.MatchesCssAfter:
21796
- converted.push(PseudoClasses.MatchesCss);
21797
- converted.push(OPEN_PARENTHESIS);
21798
- converted.push(pseudo.substring(PseudoClasses.MatchesCss.length + 1));
21799
- converted.push(COMMA);
21800
- break;
21801
- default:
21802
- converted.push(pseudo);
21803
- converted.push(OPEN_PARENTHESIS);
21804
- break;
21805
- }
21806
- };
21807
- while (!stream.isEof()) {
21808
- const token = stream.getOrFail();
21809
- if (token.type === TokenType$1.Colon) {
21810
- // Advance colon
21811
- stream.advance();
21812
- converted.push(COLON);
21813
- const tempToken = stream.getOrFail();
21814
- // Double colon is a pseudo-element
21815
- if (tempToken.type === TokenType$1.Colon) {
21816
- stream.advance();
21817
- converted.push(COLON);
21818
- continue;
21819
- }
21820
- if (tempToken.type === TokenType$1.Ident) {
21821
- const name = stream.source.slice(tempToken.start, tempToken.end);
21822
- if (PSEUDO_ELEMENT_NAMES.has(name)) {
21823
- // Add an extra colon to the name
21824
- converted.push(COLON);
21825
- converted.push(name);
21826
- } else {
21827
- // Add the name as is
21828
- converted.push(name);
21829
- }
21830
- // Advance the names
21831
- stream.advance();
21832
- } else if (tempToken.type === TokenType$1.Function) {
21833
- const name = stream.source.slice(tempToken.start, tempToken.end - 1); // omit the last parenthesis
21834
- // :-abp-contains(...) → :contains(...)
21835
- // :has-text(...) → :contains(...)
21836
- // :-abp-has(...) → :has(...)
21837
- convertAndPushPseudo(name);
21838
- // Advance the function name
21839
- stream.advance();
21840
- }
21841
- } else if (token.type === TokenType$1.OpenSquareBracket) {
21842
- let tempToken;
21843
- const {
21844
- start
21845
- } = token;
21846
- stream.advance();
21847
- // Converts legacy Extended CSS selectors to the modern Extended CSS syntax.
21848
- // For example:
21849
- // - `[-ext-has=...]` → `:has(...)`
21850
- // - `[-ext-contains=...]` → `:contains(...)`
21851
- // - `[-ext-matches-css-before=...]` → `:matches-css(before, ...)`
21852
- stream.skipWhitespace();
21853
- stream.expect(TokenType$1.Ident);
21854
- tempToken = stream.getOrFail();
21855
- let attr = stream.source.slice(tempToken.start, tempToken.end);
21856
- // Skip if the attribute name is not a legacy Extended CSS one
21857
- if (!(attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX) || attr.startsWith(ABP_EXT_CSS_PREFIX))) {
21858
- converted.push(stream.source.slice(start, tempToken.end));
21859
- stream.advance();
21860
- continue;
21861
- }
21862
- if (attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX)) {
21863
- attr = attr.slice(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX.length);
21864
- }
21865
- stream.advance();
21866
- stream.skipWhitespace();
21867
- // Next token should be an equality operator (=), because Extended CSS attribute selectors
21868
- // do not support other operators
21869
- stream.expect(TokenType$1.Delim, {
21870
- value: EQUALS
21871
- });
21872
- stream.advance();
21873
- // Skip optional whitespace after the operator
21874
- stream.skipWhitespace();
21875
- // Parse attribute value
21876
- tempToken = stream.getOrFail();
21877
- // According to the spec, attribute value should be an identifier or a string
21878
- if (tempToken.type !== TokenType$1.Ident && tempToken.type !== TokenType$1.String) {
21879
- throw new Error(sprintf(ERROR_MESSAGES$1.INVALID_ATTRIBUTE_VALUE, getFormattedTokenName(tempToken.type), stream.source.slice(tempToken.start, tempToken.end)));
21880
- }
21881
- const value = stream.source.slice(tempToken.start, tempToken.end);
21882
- // Advance the attribute value
21883
- stream.advance();
21884
- // Skip optional whitespace after the attribute value
21885
- stream.skipWhitespace();
21886
- // Next character should be a closing square bracket
21887
- // We don't allow flags for Extended CSS attribute selectors
21888
- stream.expect(TokenType$1.CloseSquareBracket);
21889
- stream.advance();
21890
- converted.push(COLON);
21891
- convertAndPushPseudo(attr);
21892
- let processedValue = value.slice(1, -1); // omit the quotes
21893
- if (attr === PseudoClasses.Has) {
21894
- // TODO: Optimize this to avoid double tokenization
21895
- processedValue = CssSelectorConverter.convertToAdg(processedValue).result;
21896
- }
21897
- converted.push(processedValue);
21898
- converted.push(CLOSE_PARENTHESIS);
21899
- } else {
21900
- converted.push(stream.source.slice(token.start, token.end));
21901
- // Advance the token
21902
- stream.advance();
21903
- }
21904
- }
21905
- const convertedSelectorList = converted.join(EMPTY);
21906
- return createConversionResult(convertedSelectorList, stream.source !== convertedSelectorList);
21907
- }
21671
+ zod.string().min(1).transform(value => parseRawPlatforms(value));
21672
+ function getDefaultExportFromCjs(x) {
21673
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
21908
21674
  }
21675
+ var mapObj$1 = {
21676
+ exports: {}
21677
+ };
21678
+ const isObject$1 = value => typeof value === 'object' && value !== null;
21679
+ const mapObjectSkip = Symbol('skip');
21909
21680
 
21910
- /**
21911
- * @file CSS injection rule converter
21912
- */
21913
- /**
21914
- * CSS injection rule converter class
21915
- *
21916
- * @todo Implement `convertToUbo` and `convertToAbp`
21917
- */
21918
- class CssInjectionRuleConverter extends RuleConverterBase {
21919
- /**
21920
- * Converts a CSS injection rule to AdGuard format, if possible.
21921
- *
21922
- * @param rule Rule node to convert
21923
- * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
21924
- * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
21925
- * If the rule was not converted, the result array will contain the original node with the same object reference
21926
- * @throws If the rule is invalid or cannot be converted
21927
- */
21928
- static convertToAdg(rule) {
21929
- const separator = rule.separator.value;
21930
- let convertedSeparator = separator;
21931
- const stream = new CssTokenStream(rule.body.selectorList.value);
21932
- const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
21933
- // Change the separator if the rule contains ExtendedCSS elements,
21934
- // but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
21935
- // because sometimes we use it to force executing ExtendedCSS library.
21936
- if (stream.hasAnySelectorExtendedCssNodeStrict() || rule.body.remove) {
21937
- convertedSeparator = rule.exception ? CosmeticRuleSeparator.AdgExtendedCssInjectionException : CosmeticRuleSeparator.AdgExtendedCssInjection;
21938
- } else if (rule.syntax !== AdblockSyntax.Adg) {
21939
- // If the original rule syntax is not AdGuard, use the default separator
21940
- // e.g. if the input rule is from uBO, we need to convert ## to #$#.
21941
- convertedSeparator = rule.exception ? CosmeticRuleSeparator.AdgCssInjectionException : CosmeticRuleSeparator.AdgCssInjection;
21942
- }
21943
- // Check if the rule needs to be converted
21944
- if (!(rule.syntax === AdblockSyntax.Common || rule.syntax === AdblockSyntax.Adg) || separator !== convertedSeparator || convertedSelectorList.isConverted) {
21945
- // TODO: Replace with custom clone method
21946
- const ruleClone = clone(rule);
21947
- ruleClone.syntax = AdblockSyntax.Adg;
21948
- ruleClone.separator.value = convertedSeparator;
21949
- ruleClone.body.selectorList.value = convertedSelectorList.result;
21950
- return createNodeConversionResult([ruleClone], true);
21951
- }
21952
- // Otherwise, return the original rule
21953
- return createNodeConversionResult([rule], false);
21681
+ // Customized for this use-case
21682
+ const isObjectCustom = value => isObject$1(value) && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
21683
+ const mapObject = (object, mapper, options, isSeen = new WeakMap()) => {
21684
+ options = {
21685
+ deep: false,
21686
+ target: {},
21687
+ ...options
21688
+ };
21689
+ if (isSeen.has(object)) {
21690
+ return isSeen.get(object);
21954
21691
  }
21955
- }
21692
+ isSeen.set(object, options.target);
21693
+ const {
21694
+ target
21695
+ } = options;
21696
+ delete options.target;
21697
+ const mapArray = array => array.map(element => isObjectCustom(element) ? mapObject(element, mapper, options, isSeen) : element);
21698
+ if (Array.isArray(object)) {
21699
+ return mapArray(object);
21700
+ }
21701
+ for (const [key, value] of Object.entries(object)) {
21702
+ const mapResult = mapper(key, value, object);
21703
+ if (mapResult === mapObjectSkip) {
21704
+ continue;
21705
+ }
21706
+ let [newKey, newValue, {
21707
+ shouldRecurse = true
21708
+ } = {}] = mapResult;
21956
21709
 
21957
- /**
21958
- * @file Element hiding rule converter
21959
- */
21960
- /**
21961
- * Element hiding rule converter class
21962
- *
21963
- * @todo Implement `convertToUbo` and `convertToAbp`
21964
- */
21965
- class ElementHidingRuleConverter extends RuleConverterBase {
21966
- /**
21967
- * Converts an element hiding rule to AdGuard format, if possible.
21968
- *
21969
- * @param rule Rule node to convert
21970
- * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
21971
- * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
21972
- * If the rule was not converted, the result array will contain the original node with the same object reference
21973
- * @throws If the rule is invalid or cannot be converted
21974
- */
21975
- static convertToAdg(rule) {
21976
- const separator = rule.separator.value;
21977
- let convertedSeparator = separator;
21978
- const stream = new CssTokenStream(rule.body.selectorList.value);
21979
- const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
21980
- // Change the separator if the rule contains ExtendedCSS elements,
21981
- // but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
21982
- // because sometimes we use it to force executing ExtendedCSS library.
21983
- if (stream.hasAnySelectorExtendedCssNodeStrict()) {
21984
- convertedSeparator = rule.exception ? CosmeticRuleSeparator.ExtendedElementHidingException : CosmeticRuleSeparator.ExtendedElementHiding;
21710
+ // Drop `__proto__` keys.
21711
+ if (newKey === '__proto__') {
21712
+ continue;
21985
21713
  }
21986
- // Check if the rule needs to be converted
21987
- if (!(rule.syntax === AdblockSyntax.Common || rule.syntax === AdblockSyntax.Adg) || separator !== convertedSeparator || convertedSelectorList.isConverted) {
21988
- // TODO: Replace with custom clone method
21989
- const ruleClone = clone(rule);
21990
- ruleClone.syntax = AdblockSyntax.Adg;
21991
- ruleClone.separator.value = convertedSeparator;
21992
- ruleClone.body.selectorList.value = convertedSelectorList.result;
21993
- return createNodeConversionResult([ruleClone], true);
21714
+ if (options.deep && shouldRecurse && isObjectCustom(newValue)) {
21715
+ newValue = Array.isArray(newValue) ? mapArray(newValue) : mapObject(newValue, mapper, options, isSeen);
21994
21716
  }
21995
- // Otherwise, return the original rule
21996
- return createNodeConversionResult([rule], false);
21717
+ target[newKey] = newValue;
21997
21718
  }
21998
- }
21999
-
22000
- /**
22001
- * @file Utility functions for working with network rule nodes
22002
- */
22003
- /**
22004
- * Creates a network rule node
22005
- *
22006
- * @param pattern Rule pattern
22007
- * @param modifiers Rule modifiers (optional, default: undefined)
22008
- * @param exception Exception rule flag (optional, default: false)
22009
- * @param syntax Adblock syntax (optional, default: Common)
22010
- * @returns Network rule node
22011
- */
22012
- function createNetworkRuleNode(pattern, modifiers = undefined, exception = false, syntax = AdblockSyntax.Common) {
22013
- const result = {
22014
- category: RuleCategory.Network,
22015
- type: NetworkRuleType.NetworkRule,
22016
- syntax,
22017
- exception,
22018
- pattern: {
22019
- type: 'Value',
22020
- value: pattern
21719
+ return target;
21720
+ };
21721
+ mapObj$1.exports = (object, mapper, options) => {
21722
+ if (!isObject$1(object)) {
21723
+ throw new TypeError(`Expected an object, got \`${object}\` (${typeof object})`);
21724
+ }
21725
+ return mapObject(object, mapper, options);
21726
+ };
21727
+ mapObj$1.exports.mapObjectSkip = mapObjectSkip;
21728
+ var mapObjExports = mapObj$1.exports;
21729
+ var camelcase = {
21730
+ exports: {}
21731
+ };
21732
+ const UPPERCASE = /[\p{Lu}]/u;
21733
+ const LOWERCASE = /[\p{Ll}]/u;
21734
+ const LEADING_CAPITAL = /^[\p{Lu}](?![\p{Lu}])/gu;
21735
+ const IDENTIFIER = /([\p{Alpha}\p{N}_]|$)/u;
21736
+ const SEPARATORS = /[_.\- ]+/;
21737
+ const LEADING_SEPARATORS = new RegExp('^' + SEPARATORS.source);
21738
+ const SEPARATORS_AND_IDENTIFIER = new RegExp(SEPARATORS.source + IDENTIFIER.source, 'gu');
21739
+ const NUMBERS_AND_IDENTIFIER = new RegExp('\\d+' + IDENTIFIER.source, 'gu');
21740
+ const preserveCamelCase = (string, toLowerCase, toUpperCase) => {
21741
+ let isLastCharLower = false;
21742
+ let isLastCharUpper = false;
21743
+ let isLastLastCharUpper = false;
21744
+ for (let i = 0; i < string.length; i++) {
21745
+ const character = string[i];
21746
+ if (isLastCharLower && UPPERCASE.test(character)) {
21747
+ string = string.slice(0, i) + '-' + string.slice(i);
21748
+ isLastCharLower = false;
21749
+ isLastLastCharUpper = isLastCharUpper;
21750
+ isLastCharUpper = true;
21751
+ i++;
21752
+ } else if (isLastCharUpper && isLastLastCharUpper && LOWERCASE.test(character)) {
21753
+ string = string.slice(0, i - 1) + '-' + string.slice(i - 1);
21754
+ isLastLastCharUpper = isLastCharUpper;
21755
+ isLastCharUpper = false;
21756
+ isLastCharLower = true;
21757
+ } else {
21758
+ isLastCharLower = toLowerCase(character) === character && toUpperCase(character) !== character;
21759
+ isLastLastCharUpper = isLastCharUpper;
21760
+ isLastCharUpper = toUpperCase(character) === character && toLowerCase(character) !== character;
22021
21761
  }
21762
+ }
21763
+ return string;
21764
+ };
21765
+ const preserveConsecutiveUppercase = (input, toLowerCase) => {
21766
+ LEADING_CAPITAL.lastIndex = 0;
21767
+ return input.replace(LEADING_CAPITAL, m1 => toLowerCase(m1));
21768
+ };
21769
+ const postProcess = (input, toUpperCase) => {
21770
+ SEPARATORS_AND_IDENTIFIER.lastIndex = 0;
21771
+ NUMBERS_AND_IDENTIFIER.lastIndex = 0;
21772
+ return input.replace(SEPARATORS_AND_IDENTIFIER, (_, identifier) => toUpperCase(identifier)).replace(NUMBERS_AND_IDENTIFIER, m => toUpperCase(m));
21773
+ };
21774
+ const camelCase$1 = (input, options) => {
21775
+ if (!(typeof input === 'string' || Array.isArray(input))) {
21776
+ throw new TypeError('Expected the input to be `string | string[]`');
21777
+ }
21778
+ options = {
21779
+ pascalCase: false,
21780
+ preserveConsecutiveUppercase: false,
21781
+ ...options
22022
21782
  };
22023
- if (!isUndefined(modifiers)) {
22024
- result.modifiers = clone(modifiers);
21783
+ if (Array.isArray(input)) {
21784
+ input = input.map(x => x.trim()).filter(x => x.length).join('-');
21785
+ } else {
21786
+ input = input.trim();
22025
21787
  }
22026
- return result;
22027
- }
22028
-
22029
- /**
22030
- * @file Converter for request header removal rules
22031
- */
22032
- const UBO_RESPONSEHEADER_FN = 'responseheader';
22033
- const ADG_REMOVEHEADER_MODIFIER = 'removeheader';
22034
- const ERROR_MESSAGES = {
22035
- EMPTY_PARAMETER: `Empty parameter for '${UBO_RESPONSEHEADER_FN}' function`,
22036
- EXPECTED_END_OF_RULE: "Expected end of rule, but got '%s'",
22037
- MULTIPLE_DOMAINS_NOT_SUPPORTED: 'Multiple domains are not supported yet'
21788
+ if (input.length === 0) {
21789
+ return '';
21790
+ }
21791
+ const toLowerCase = options.locale === false ? string => string.toLowerCase() : string => string.toLocaleLowerCase(options.locale);
21792
+ const toUpperCase = options.locale === false ? string => string.toUpperCase() : string => string.toLocaleUpperCase(options.locale);
21793
+ if (input.length === 1) {
21794
+ return options.pascalCase ? toUpperCase(input) : toLowerCase(input);
21795
+ }
21796
+ const hasUpperCase = input !== toLowerCase(input);
21797
+ if (hasUpperCase) {
21798
+ input = preserveCamelCase(input, toLowerCase, toUpperCase);
21799
+ }
21800
+ input = input.replace(LEADING_SEPARATORS, '');
21801
+ if (options.preserveConsecutiveUppercase) {
21802
+ input = preserveConsecutiveUppercase(input, toLowerCase);
21803
+ } else {
21804
+ input = toLowerCase(input);
21805
+ }
21806
+ if (options.pascalCase) {
21807
+ input = toUpperCase(input.charAt(0)) + input.slice(1);
21808
+ }
21809
+ return postProcess(input, toUpperCase);
22038
21810
  };
22039
- /**
22040
- * Converter for request header removal rules
22041
- *
22042
- * @todo Implement `convertToUbo` (ABP currently doesn't support header removal rules)
22043
- */
22044
- class HeaderRemovalRuleConverter extends RuleConverterBase {
22045
- /**
22046
- * Converts a header removal rule to AdGuard syntax, if possible.
22047
- *
22048
- * @param rule Rule node to convert
22049
- * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22050
- * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22051
- * If the rule was not converted, the result array will contain the original node with the same object reference
22052
- * @throws If the rule is invalid or cannot be converted
22053
- * @example
22054
- * If the input rule is:
22055
- * ```adblock
22056
- * example.com##^responseheader(header-name)
22057
- * ```
22058
- * The output will be:
22059
- * ```adblock
22060
- * ||example.com^$removeheader=header-name
22061
- * ```
22062
- */
22063
- static convertToAdg(rule) {
22064
- // TODO: Add support for ABP syntax once it starts supporting header removal rules
22065
- // Leave the rule as is if it's not a header removal rule
22066
- if (rule.category !== RuleCategory.Cosmetic || rule.type !== CosmeticRuleType.HtmlFilteringRule) {
22067
- return createNodeConversionResult([rule], false);
21811
+ camelcase.exports = camelCase$1;
21812
+ // TODO: Remove this for the next major release
21813
+ camelcase.exports.default = camelCase$1;
21814
+ var camelcaseExports = camelcase.exports;
21815
+ class QuickLRU {
21816
+ constructor(options = {}) {
21817
+ if (!(options.maxSize && options.maxSize > 0)) {
21818
+ throw new TypeError('`maxSize` must be a number greater than 0');
22068
21819
  }
22069
- const stream = new CssTokenStream(rule.body.value);
22070
- let token;
22071
- // Skip leading whitespace
22072
- stream.skipWhitespace();
22073
- // Next token should be the `^` followed by a `responseheader` function
22074
- token = stream.get();
22075
- if (!token || token.type !== TokenType$1.Delim || rule.body.value[token.start] !== UBO_HTML_MASK) {
22076
- return createNodeConversionResult([rule], false);
21820
+ this.maxSize = options.maxSize;
21821
+ this.onEviction = options.onEviction;
21822
+ this.cache = new Map();
21823
+ this.oldCache = new Map();
21824
+ this._size = 0;
21825
+ }
21826
+ _set(key, value) {
21827
+ this.cache.set(key, value);
21828
+ this._size++;
21829
+ if (this._size >= this.maxSize) {
21830
+ this._size = 0;
21831
+ if (typeof this.onEviction === 'function') {
21832
+ for (const [key, value] of this.oldCache.entries()) {
21833
+ this.onEviction(key, value);
21834
+ }
21835
+ }
21836
+ this.oldCache = this.cache;
21837
+ this.cache = new Map();
22077
21838
  }
22078
- stream.advance();
22079
- token = stream.get();
22080
- if (!token) {
22081
- return createNodeConversionResult([rule], false);
21839
+ }
21840
+ get(key) {
21841
+ if (this.cache.has(key)) {
21842
+ return this.cache.get(key);
21843
+ }
21844
+ if (this.oldCache.has(key)) {
21845
+ const value = this.oldCache.get(key);
21846
+ this.oldCache.delete(key);
21847
+ this._set(key, value);
21848
+ return value;
21849
+ }
21850
+ }
21851
+ set(key, value) {
21852
+ if (this.cache.has(key)) {
21853
+ this.cache.set(key, value);
21854
+ } else {
21855
+ this._set(key, value);
21856
+ }
21857
+ return this;
21858
+ }
21859
+ has(key) {
21860
+ return this.cache.has(key) || this.oldCache.has(key);
21861
+ }
21862
+ peek(key) {
21863
+ if (this.cache.has(key)) {
21864
+ return this.cache.get(key);
22082
21865
  }
22083
- const functionName = rule.body.value.slice(token.start, token.end - 1);
22084
- if (functionName !== UBO_RESPONSEHEADER_FN) {
22085
- return createNodeConversionResult([rule], false);
21866
+ if (this.oldCache.has(key)) {
21867
+ return this.oldCache.get(key);
22086
21868
  }
22087
- // Parse the parameter
22088
- const paramStart = token.end;
22089
- stream.skipUntilBalanced();
22090
- const paramEnd = stream.getOrFail().end;
22091
- const param = rule.body.value.slice(paramStart, paramEnd - 1).trim();
22092
- // Do not allow empty parameter
22093
- if (param.length === 0) {
22094
- throw new RuleConversionError(ERROR_MESSAGES.EMPTY_PARAMETER);
21869
+ }
21870
+ delete(key) {
21871
+ const deleted = this.cache.delete(key);
21872
+ if (deleted) {
21873
+ this._size--;
22095
21874
  }
22096
- stream.expect(TokenType$1.CloseParenthesis);
22097
- stream.advance();
22098
- // Skip trailing whitespace after the function call
22099
- stream.skipWhitespace();
22100
- // Expect the end of the rule - so nothing should be left in the stream
22101
- if (!stream.isEof()) {
22102
- token = stream.getOrFail();
22103
- throw new RuleConversionError(sprintf(ERROR_MESSAGES.EXPECTED_END_OF_RULE, getFormattedTokenName(token.type)));
21875
+ return this.oldCache.delete(key) || deleted;
21876
+ }
21877
+ clear() {
21878
+ this.cache.clear();
21879
+ this.oldCache.clear();
21880
+ this._size = 0;
21881
+ }
21882
+ *keys() {
21883
+ for (const [key] of this) {
21884
+ yield key;
22104
21885
  }
22105
- // Prepare network rule pattern
22106
- const pattern = [];
22107
- if (rule.domains.children.length === 1) {
22108
- // If the rule has only one domain, we can use a simple network rule pattern:
22109
- // ||single-domain-from-the-rule^
22110
- pattern.push(ADBLOCK_URL_START, rule.domains.children[0].value, ADBLOCK_URL_SEPARATOR);
22111
- } else if (rule.domains.children.length > 1) {
22112
- // TODO: Add support for multiple domains, for example:
22113
- // example.com,example.org,example.net##^responseheader(header-name)
22114
- // We should consider allowing $domain with $removeheader modifier,
22115
- // for example:
22116
- // $removeheader=header-name,domain=example.com|example.org|example.net
22117
- throw new RuleConversionError(ERROR_MESSAGES.MULTIPLE_DOMAINS_NOT_SUPPORTED);
21886
+ }
21887
+ *values() {
21888
+ for (const [, value] of this) {
21889
+ yield value;
22118
21890
  }
22119
- // Prepare network rule modifiers
22120
- const modifiers = createModifierListNode();
22121
- modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, param));
22122
- // Construct the network rule
22123
- return createNodeConversionResult([createNetworkRuleNode(pattern.join(EMPTY), modifiers,
22124
- // Copy the exception flag
22125
- rule.exception, AdblockSyntax.Adg)], true);
22126
21891
  }
22127
- }
22128
-
22129
- /**
22130
- * @file Cosmetic rule converter
22131
- */
22132
- /**
22133
- * Cosmetic rule converter class (also known as "non-basic rule converter")
22134
- *
22135
- * @todo Implement `convertToUbo` and `convertToAbp`
22136
- */
22137
- class CosmeticRuleConverter extends RuleConverterBase {
22138
- /**
22139
- * Converts a cosmetic rule to AdGuard syntax, if possible.
22140
- *
22141
- * @param rule Rule node to convert
22142
- * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22143
- * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22144
- * If the rule was not converted, the result array will contain the original node with the same object reference
22145
- * @throws If the rule is invalid or cannot be converted
22146
- */
22147
- static convertToAdg(rule) {
22148
- let subconverterResult;
22149
- // Convert cosmetic rule based on its type
22150
- switch (rule.type) {
22151
- case CosmeticRuleType.ElementHidingRule:
22152
- subconverterResult = ElementHidingRuleConverter.convertToAdg(rule);
22153
- break;
22154
- case CosmeticRuleType.ScriptletInjectionRule:
22155
- subconverterResult = ScriptletRuleConverter.convertToAdg(rule);
22156
- break;
22157
- case CosmeticRuleType.CssInjectionRule:
22158
- subconverterResult = CssInjectionRuleConverter.convertToAdg(rule);
22159
- break;
22160
- case CosmeticRuleType.HtmlFilteringRule:
22161
- // Handle special case: uBO response header filtering rule
22162
- // TODO: Optimize double CSS tokenization here
22163
- subconverterResult = HeaderRemovalRuleConverter.convertToAdg(rule);
22164
- if (subconverterResult.isConverted) {
22165
- break;
22166
- }
22167
- subconverterResult = HtmlRuleConverter.convertToAdg(rule);
22168
- break;
22169
- // Note: Currently, only ADG supports JS injection rules, so we don't need to convert them
22170
- case CosmeticRuleType.JsInjectionRule:
22171
- subconverterResult = createNodeConversionResult([rule], false);
22172
- break;
22173
- default:
22174
- throw new RuleConversionError('Unsupported cosmetic rule type');
21892
+ *[Symbol.iterator]() {
21893
+ for (const item of this.cache) {
21894
+ yield item;
22175
21895
  }
22176
- let convertedModifiers;
22177
- // Convert cosmetic rule modifiers, if any
22178
- if (rule.modifiers) {
22179
- if (rule.syntax === AdblockSyntax.Ubo) {
22180
- // uBO doesn't support this rule:
22181
- // example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
22182
- if (rule.type === CosmeticRuleType.ScriptletInjectionRule) {
22183
- throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
22184
- }
22185
- convertedModifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(rule.modifiers);
22186
- } else if (rule.syntax === AdblockSyntax.Abp) {
22187
- // TODO: Implement once ABP starts supporting cosmetic rule modifiers
22188
- throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
21896
+ for (const item of this.oldCache) {
21897
+ const [key] = item;
21898
+ if (!this.cache.has(key)) {
21899
+ yield item;
22189
21900
  }
22190
21901
  }
22191
- if (subconverterResult.result.length > 1 || subconverterResult.isConverted || convertedModifiers && convertedModifiers.isConverted) {
22192
- // Add modifier list to the subconverter result rules
22193
- subconverterResult.result.forEach(subconverterRule => {
22194
- if (convertedModifiers && subconverterRule.category === RuleCategory.Cosmetic) {
22195
- // eslint-disable-next-line no-param-reassign
22196
- subconverterRule.modifiers = convertedModifiers.result;
22197
- }
22198
- });
22199
- return subconverterResult;
21902
+ }
21903
+ get size() {
21904
+ let oldCacheSize = 0;
21905
+ for (const key of this.oldCache.keys()) {
21906
+ if (!this.cache.has(key)) {
21907
+ oldCacheSize++;
21908
+ }
22200
21909
  }
22201
- return createNodeConversionResult([rule], false);
21910
+ return Math.min(this._size + oldCacheSize, this.maxSize);
22202
21911
  }
22203
21912
  }
21913
+ var quickLru = QuickLRU;
21914
+ const mapObj = mapObjExports;
21915
+ const camelCase = camelcaseExports;
21916
+ const QuickLru = quickLru;
21917
+ const has = (array, key) => array.some(x => {
21918
+ if (typeof x === 'string') {
21919
+ return x === key;
21920
+ }
21921
+ x.lastIndex = 0;
21922
+ return x.test(key);
21923
+ });
21924
+ const cache = new QuickLru({
21925
+ maxSize: 100000
21926
+ });
22204
21927
 
22205
- /**
22206
- * @file Compatibility tables for redirects.
22207
- */
22208
- /**
22209
- * Prefix for resource redirection names.
22210
- */
22211
- const ABP_RESOURCE_PREFIX = 'abp-resource:';
22212
- const ABP_RESOURCE_PREFIX_LENGTH = ABP_RESOURCE_PREFIX.length;
22213
- /**
22214
- * Transforms the name of an ABP redirect to a normalized form.
22215
- *
22216
- * @param name Redirect name to normalize.
22217
- *
22218
- * @returns Normalized redirect name.
22219
- *
22220
- * @example
22221
- * abpRedirectNameNormalizer('abp-resource:my-resource') // => 'my-resource'
22222
- */
22223
- const abpRedirectNameNormalizer = name => {
22224
- if (name.startsWith(ABP_RESOURCE_PREFIX)) {
22225
- return name.slice(ABP_RESOURCE_PREFIX_LENGTH);
21928
+ // Reproduces behavior from `map-obj`
21929
+ const isObject = value => typeof value === 'object' && value !== null && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
21930
+ const camelCaseConvert = (input, options) => {
21931
+ if (!isObject(input)) {
21932
+ return input;
22226
21933
  }
22227
- return name;
21934
+ options = {
21935
+ deep: false,
21936
+ pascalCase: false,
21937
+ ...options
21938
+ };
21939
+ const {
21940
+ exclude,
21941
+ pascalCase,
21942
+ stopPaths,
21943
+ deep
21944
+ } = options;
21945
+ const stopPathsSet = new Set(stopPaths);
21946
+ const makeMapper = parentPath => (key, value) => {
21947
+ if (deep && isObject(value)) {
21948
+ const path = parentPath === undefined ? key : `${parentPath}.${key}`;
21949
+ if (!stopPathsSet.has(path)) {
21950
+ value = mapObj(value, makeMapper(path));
21951
+ }
21952
+ }
21953
+ if (!(exclude && has(exclude, key))) {
21954
+ const cacheKey = pascalCase ? `${key}_` : key;
21955
+ if (cache.has(cacheKey)) {
21956
+ key = cache.get(cacheKey);
21957
+ } else {
21958
+ const returnValue = camelCase(key, {
21959
+ pascalCase,
21960
+ locale: false
21961
+ });
21962
+ if (key.length < 100) {
21963
+ // Prevent abuse
21964
+ cache.set(cacheKey, returnValue);
21965
+ }
21966
+ key = returnValue;
21967
+ }
21968
+ }
21969
+ return [key, value];
21970
+ };
21971
+ return mapObj(input, makeMapper(undefined));
22228
21972
  };
22229
- /**
22230
- * Compatibility table for redirects.
22231
- */
22232
- class RedirectsCompatibilityTable extends CompatibilityTableBase {
22233
- /**
22234
- * Creates a new instance of the compatibility table for redirects.
22235
- *
22236
- * @param data Compatibility table data.
22237
- */
22238
- constructor(data) {
22239
- super(data, abpRedirectNameNormalizer);
21973
+ var camelcaseKeys = (input, options) => {
21974
+ if (Array.isArray(input)) {
21975
+ return Object.keys(input).map(key => camelCaseConvert(input[key], options));
22240
21976
  }
22241
- }
22242
- /**
22243
- * Deep freeze the compatibility table data to avoid accidental modifications.
22244
- */
22245
- deepFreeze(redirectsCompatibilityTableData);
22246
- /**
22247
- * Compatibility table instance for redirects.
22248
- */
22249
- const redirectsCompatibilityTable = new RedirectsCompatibilityTable(redirectsCompatibilityTableData);
21977
+ return camelCaseConvert(input, options);
21978
+ };
21979
+ var camelCaseKeys = /*@__PURE__*/getDefaultExportFromCjs(camelcaseKeys);
22250
21980
 
22251
21981
  /**
22252
- * @file Compatibility tables for scriptlets.
21982
+ * @file Zod camelCase utility.
22253
21983
  */
21984
+ // eslint-disable-next-line import/no-extraneous-dependencies
22254
21985
  /**
22255
- * Compatibility table for scriptlets.
21986
+ * Transforms Zod schema to camelCase.
21987
+ *
21988
+ * @param zod Zod schema.
21989
+ *
21990
+ * @returns Zod schema with camelCase properties.
21991
+ *
21992
+ * @see {@link https://github.com/colinhacks/zod/issues/486#issuecomment-1501097361}
22256
21993
  */
22257
- class ScriptletsCompatibilityTable extends CompatibilityTableBase {}
21994
+ const zodToCamelCase = zod => {
21995
+ return zod.transform(val => camelCaseKeys(val));
21996
+ };
21997
+
22258
21998
  /**
22259
- * Deep freeze the compatibility table data to avoid accidental modifications.
21999
+ * @file Base compatibility data schema, which is commonly used in compatibility tables.
22260
22000
  */
22261
- deepFreeze(scriptletsCompatibilityTableData);
22262
22001
  /**
22263
- * Compatibility table instance for scriptlets.
22002
+ * Zod schema for boolean values. Accepts both boolean and string values.
22264
22003
  */
22265
- const scriptletsCompatibilityTable = new ScriptletsCompatibilityTable(scriptletsCompatibilityTableData);
22266
-
22267
- /* eslint-disable no-bitwise */
22004
+ const booleanSchema = zod.union([zod.string().transform(val => val.trim().toLowerCase() === 'true'), zod.boolean()]);
22268
22005
  /**
22269
- * @file Platform schema.
22006
+ * Zod schema for non-empty string values.
22270
22007
  */
22008
+ const nonEmptyStringSchema = zod.string().transform(val => val.trim()).pipe(zod.string().min(1));
22271
22009
  /**
22272
- * Platform separator, e.g. 'adg_os_any|adg_safari_any' means any AdGuard OS platform and
22273
- * any AdGuard Safari content blocker platform.
22010
+ * Zod schema for base compatibility data.
22011
+ * Here we use snake_case properties because the compatibility data is stored in YAML files.
22274
22012
  */
22275
- const PLATFORM_SEPARATOR = '|';
22013
+ const baseCompatibilityDataSchema = zod.object({
22014
+ /**
22015
+ * Name of the actual entity.
22016
+ */
22017
+ name: nonEmptyStringSchema,
22018
+ /**
22019
+ * List of aliases for the entity (if any).
22020
+ */
22021
+ aliases: zod.array(nonEmptyStringSchema).nullable().default(null),
22022
+ /**
22023
+ * Short description of the actual entity.
22024
+ * If not specified or it's value is `null`, then the description is not available.
22025
+ */
22026
+ description: nonEmptyStringSchema.nullable().default(null),
22027
+ /**
22028
+ * Link to the documentation. If not specified or it's value is `null`, then the documentation is not available.
22029
+ */
22030
+ docs: nonEmptyStringSchema.nullable().default(null),
22031
+ /**
22032
+ * The version of the adblocker in which the entity was added.
22033
+ * For AdGuard resources, the version of the library is specified.
22034
+ */
22035
+ version_added: nonEmptyStringSchema.nullable().default(null),
22036
+ /**
22037
+ * The version of the adblocker when the entity was removed.
22038
+ */
22039
+ version_removed: nonEmptyStringSchema.nullable().default(null),
22040
+ /**
22041
+ * Describes whether the entity is deprecated.
22042
+ */
22043
+ deprecated: booleanSchema.default(false),
22044
+ /**
22045
+ * Message that describes why the entity is deprecated.
22046
+ * If not specified or it's value is `null`, then the message is not available.
22047
+ * It's value is omitted if the entity is not marked as deprecated.
22048
+ */
22049
+ deprecation_message: nonEmptyStringSchema.nullable().default(null),
22050
+ /**
22051
+ * Describes whether the entity is removed; for *already removed* features.
22052
+ */
22053
+ removed: booleanSchema.default(false),
22054
+ /**
22055
+ * Message that describes why the entity is removed.
22056
+ * If not specified or it's value is `null`, then the message is not available.
22057
+ * It's value is omitted if the entity is not marked as deprecated.
22058
+ */
22059
+ removal_message: nonEmptyStringSchema.nullable().default(null)
22060
+ });
22276
22061
  /**
22277
- * Platform negation character, e.g. 'adg_any|~adg_safari_any' means any AdGuard product except
22278
- * Safari content blockers.
22062
+ * Zod schema for base compatibility data with camelCase properties.
22279
22063
  */
22280
- const PLATFORM_NEGATION = '~';
22064
+ zodToCamelCase(baseCompatibilityDataSchema);
22281
22065
  /**
22282
- * Parses a raw platform string into a platform bitmask.
22283
- *
22284
- * @param rawPlatforms Raw platform string, e.g. 'adg_safari_any|adg_os_any'.
22066
+ * Refinement logic for base compatibility data.
22285
22067
  *
22286
- * @returns Platform bitmask.
22287
- */
22288
- const parseRawPlatforms = rawPlatforms => {
22289
- // e.g. 'adg_safari_any|adg_os_any'
22290
- const rawPlatformList = rawPlatforms.split(PLATFORM_SEPARATOR).map(rawPlatform => rawPlatform.trim());
22291
- let result = 0;
22292
- for (let rawPlatform of rawPlatformList) {
22293
- // negation, e.g. 'adg_any|~adg_safari_any' means any AdGuard product except Safari content blockers
22294
- let negated = false;
22295
- if (rawPlatform.startsWith(PLATFORM_NEGATION)) {
22296
- negated = true;
22297
- rawPlatform = rawPlatform.slice(1).trim();
22298
- }
22299
- const platform = SPECIFIC_PLATFORM_MAP.get(rawPlatform) ?? GENERIC_PLATFORM_MAP.get(rawPlatform);
22300
- if (isUndefined(platform)) {
22301
- throw new Error(`Unknown platform: ${rawPlatform}`);
22302
- }
22303
- if (negated) {
22304
- result &= ~platform;
22305
- } else {
22306
- result |= platform;
22307
- }
22308
- }
22309
- if (result === 0) {
22310
- throw new Error('No platforms specified');
22311
- }
22312
- return result;
22313
- };
22314
- /**
22315
- * Platform schema.
22068
+ * @param data Base compatibility data.
22069
+ * @param ctx Refinement context.
22316
22070
  */
22317
- zod.string().min(1).transform(value => parseRawPlatforms(value));
22318
- function getDefaultExportFromCjs(x) {
22319
- return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
22320
- }
22321
- var mapObj$1 = {
22322
- exports: {}
22323
- };
22324
- const isObject$1 = value => typeof value === 'object' && value !== null;
22325
- const mapObjectSkip = Symbol('skip');
22326
-
22327
- // Customized for this use-case
22328
- const isObjectCustom = value => isObject$1(value) && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
22329
- const mapObject = (object, mapper, options, isSeen = new WeakMap()) => {
22330
- options = {
22331
- deep: false,
22332
- target: {},
22333
- ...options
22334
- };
22335
- if (isSeen.has(object)) {
22336
- return isSeen.get(object);
22337
- }
22338
- isSeen.set(object, options.target);
22339
- const {
22340
- target
22341
- } = options;
22342
- delete options.target;
22343
- const mapArray = array => array.map(element => isObjectCustom(element) ? mapObject(element, mapper, options, isSeen) : element);
22344
- if (Array.isArray(object)) {
22345
- return mapArray(object);
22346
- }
22347
- for (const [key, value] of Object.entries(object)) {
22348
- const mapResult = mapper(key, value, object);
22349
- if (mapResult === mapObjectSkip) {
22350
- continue;
22351
- }
22352
- let [newKey, newValue, {
22353
- shouldRecurse = true
22354
- } = {}] = mapResult;
22355
-
22356
- // Drop `__proto__` keys.
22357
- if (newKey === '__proto__') {
22358
- continue;
22359
- }
22360
- if (options.deep && shouldRecurse && isObjectCustom(newValue)) {
22361
- newValue = Array.isArray(newValue) ? mapArray(newValue) : mapObject(newValue, mapper, options, isSeen);
22362
- }
22363
- target[newKey] = newValue;
22364
- }
22365
- return target;
22366
- };
22367
- mapObj$1.exports = (object, mapper, options) => {
22368
- if (!isObject$1(object)) {
22369
- throw new TypeError(`Expected an object, got \`${object}\` (${typeof object})`);
22370
- }
22371
- return mapObject(object, mapper, options);
22372
- };
22373
- mapObj$1.exports.mapObjectSkip = mapObjectSkip;
22374
- var mapObjExports = mapObj$1.exports;
22375
- var camelcase = {
22376
- exports: {}
22377
- };
22378
- const UPPERCASE = /[\p{Lu}]/u;
22379
- const LOWERCASE = /[\p{Ll}]/u;
22380
- const LEADING_CAPITAL = /^[\p{Lu}](?![\p{Lu}])/gu;
22381
- const IDENTIFIER = /([\p{Alpha}\p{N}_]|$)/u;
22382
- const SEPARATORS = /[_.\- ]+/;
22383
- const LEADING_SEPARATORS = new RegExp('^' + SEPARATORS.source);
22384
- const SEPARATORS_AND_IDENTIFIER = new RegExp(SEPARATORS.source + IDENTIFIER.source, 'gu');
22385
- const NUMBERS_AND_IDENTIFIER = new RegExp('\\d+' + IDENTIFIER.source, 'gu');
22386
- const preserveCamelCase = (string, toLowerCase, toUpperCase) => {
22387
- let isLastCharLower = false;
22388
- let isLastCharUpper = false;
22389
- let isLastLastCharUpper = false;
22390
- for (let i = 0; i < string.length; i++) {
22391
- const character = string[i];
22392
- if (isLastCharLower && UPPERCASE.test(character)) {
22393
- string = string.slice(0, i) + '-' + string.slice(i);
22394
- isLastCharLower = false;
22395
- isLastLastCharUpper = isLastCharUpper;
22396
- isLastCharUpper = true;
22397
- i++;
22398
- } else if (isLastCharUpper && isLastLastCharUpper && LOWERCASE.test(character)) {
22399
- string = string.slice(0, i - 1) + '-' + string.slice(i - 1);
22400
- isLastLastCharUpper = isLastCharUpper;
22401
- isLastCharUpper = false;
22402
- isLastCharLower = true;
22403
- } else {
22404
- isLastCharLower = toLowerCase(character) === character && toUpperCase(character) !== character;
22405
- isLastLastCharUpper = isLastCharUpper;
22406
- isLastCharUpper = toUpperCase(character) === character && toLowerCase(character) !== character;
22407
- }
22408
- }
22409
- return string;
22410
- };
22411
- const preserveConsecutiveUppercase = (input, toLowerCase) => {
22412
- LEADING_CAPITAL.lastIndex = 0;
22413
- return input.replace(LEADING_CAPITAL, m1 => toLowerCase(m1));
22414
- };
22415
- const postProcess = (input, toUpperCase) => {
22416
- SEPARATORS_AND_IDENTIFIER.lastIndex = 0;
22417
- NUMBERS_AND_IDENTIFIER.lastIndex = 0;
22418
- return input.replace(SEPARATORS_AND_IDENTIFIER, (_, identifier) => toUpperCase(identifier)).replace(NUMBERS_AND_IDENTIFIER, m => toUpperCase(m));
22419
- };
22420
- const camelCase$1 = (input, options) => {
22421
- if (!(typeof input === 'string' || Array.isArray(input))) {
22422
- throw new TypeError('Expected the input to be `string | string[]`');
22423
- }
22424
- options = {
22425
- pascalCase: false,
22426
- preserveConsecutiveUppercase: false,
22427
- ...options
22428
- };
22429
- if (Array.isArray(input)) {
22430
- input = input.map(x => x.trim()).filter(x => x.length).join('-');
22431
- } else {
22432
- input = input.trim();
22433
- }
22434
- if (input.length === 0) {
22435
- return '';
22436
- }
22437
- const toLowerCase = options.locale === false ? string => string.toLowerCase() : string => string.toLocaleLowerCase(options.locale);
22438
- const toUpperCase = options.locale === false ? string => string.toUpperCase() : string => string.toLocaleUpperCase(options.locale);
22439
- if (input.length === 1) {
22440
- return options.pascalCase ? toUpperCase(input) : toLowerCase(input);
22441
- }
22442
- const hasUpperCase = input !== toLowerCase(input);
22443
- if (hasUpperCase) {
22444
- input = preserveCamelCase(input, toLowerCase, toUpperCase);
22071
+ const baseRefineLogic = (data, ctx) => {
22072
+ if (data.deprecated && !data.deprecation_message) {
22073
+ ctx.addIssue({
22074
+ code: zod.ZodIssueCode.custom,
22075
+ message: 'deprecation_message is required for deprecated modifiers'
22076
+ });
22445
22077
  }
22446
- input = input.replace(LEADING_SEPARATORS, '');
22447
- if (options.preserveConsecutiveUppercase) {
22448
- input = preserveConsecutiveUppercase(input, toLowerCase);
22449
- } else {
22450
- input = toLowerCase(input);
22078
+ if (!data.deprecated && data.deprecation_message) {
22079
+ ctx.addIssue({
22080
+ code: zod.ZodIssueCode.custom,
22081
+ message: 'deprecation_message is only allowed for deprecated modifiers'
22082
+ });
22451
22083
  }
22452
- if (options.pascalCase) {
22453
- input = toUpperCase(input.charAt(0)) + input.slice(1);
22084
+ if (data.aliases && data.aliases.length !== new Set(data.aliases).size) {
22085
+ ctx.addIssue({
22086
+ code: zod.ZodIssueCode.custom,
22087
+ message: 'Aliases must be unique'
22088
+ });
22454
22089
  }
22455
- return postProcess(input, toUpperCase);
22456
22090
  };
22457
- camelcase.exports = camelCase$1;
22458
- // TODO: Remove this for the next major release
22459
- camelcase.exports.default = camelCase$1;
22460
- var camelcaseExports = camelcase.exports;
22461
- class QuickLRU {
22462
- constructor(options = {}) {
22463
- if (!(options.maxSize && options.maxSize > 0)) {
22464
- throw new TypeError('`maxSize` must be a number greater than 0');
22465
- }
22466
- this.maxSize = options.maxSize;
22467
- this.onEviction = options.onEviction;
22468
- this.cache = new Map();
22469
- this.oldCache = new Map();
22470
- this._size = 0;
22471
- }
22472
- _set(key, value) {
22473
- this.cache.set(key, value);
22474
- this._size++;
22475
- if (this._size >= this.maxSize) {
22476
- this._size = 0;
22477
- if (typeof this.onEviction === 'function') {
22478
- for (const [key, value] of this.oldCache.entries()) {
22479
- this.onEviction(key, value);
22480
- }
22481
- }
22482
- this.oldCache = this.cache;
22483
- this.cache = new Map();
22484
- }
22091
+
22092
+ /**
22093
+ * Checks if error has message.
22094
+ *
22095
+ * @param error Error object.
22096
+ * @returns If param is error.
22097
+ */
22098
+ function isErrorWithMessage(error) {
22099
+ return typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string';
22100
+ }
22101
+ /**
22102
+ * Converts error to the error with message.
22103
+ *
22104
+ * @param maybeError Possible error.
22105
+ * @returns Error with message.
22106
+ */
22107
+ function toErrorWithMessage(maybeError) {
22108
+ if (isErrorWithMessage(maybeError)) {
22109
+ return maybeError;
22485
22110
  }
22486
- get(key) {
22487
- if (this.cache.has(key)) {
22488
- return this.cache.get(key);
22489
- }
22490
- if (this.oldCache.has(key)) {
22491
- const value = this.oldCache.get(key);
22492
- this.oldCache.delete(key);
22493
- this._set(key, value);
22494
- return value;
22495
- }
22111
+ try {
22112
+ return new Error(JSON.stringify(maybeError));
22113
+ } catch {
22114
+ // fallback in case there's an error stringifying the maybeError
22115
+ // like with circular references for example.
22116
+ return new Error(String(maybeError));
22496
22117
  }
22497
- set(key, value) {
22498
- if (this.cache.has(key)) {
22499
- this.cache.set(key, value);
22500
- } else {
22501
- this._set(key, value);
22502
- }
22503
- return this;
22118
+ }
22119
+ /**
22120
+ * Converts error object to error with message. This method might be helpful to handle thrown errors.
22121
+ *
22122
+ * @param error Error object.
22123
+ *
22124
+ * @returns Message of the error.
22125
+ */
22126
+ function getErrorMessage(error) {
22127
+ return toErrorWithMessage(error).message;
22128
+ }
22129
+
22130
+ /**
22131
+ * @file Schema for modifier data.
22132
+ */
22133
+ /**
22134
+ * Known validators that don't need to be validated as regex.
22135
+ */
22136
+ const KNOWN_VALIDATORS = new Set(['domain', 'pipe_separated_domains', 'regexp', 'url']);
22137
+ /**
22138
+ * Zod schema for modifier data.
22139
+ */
22140
+ zodToCamelCase(baseCompatibilityDataSchema.extend({
22141
+ /**
22142
+ * List of modifiers that are incompatible with the actual one.
22143
+ */
22144
+ conflicts: zod.array(nonEmptyStringSchema).nullable().default(null),
22145
+ /**
22146
+ * The actual modifier is incompatible with all other modifiers, except the ones listed in `conflicts`.
22147
+ */
22148
+ inverse_conflicts: booleanSchema.default(false),
22149
+ /**
22150
+ * Describes whether the actual modifier supports value assignment. For example, `$domain` is assignable,
22151
+ * so it can be used like this: `$domain=domain.com\|~subdomain.domain.com`, where `=` is the assignment operator
22152
+ * and `domain.com\|~subdomain.domain.com` is the value.
22153
+ */
22154
+ assignable: booleanSchema.default(false),
22155
+ /**
22156
+ * Describes whether the actual modifier can be negated. For example, `$third-party` is negatable,
22157
+ * so it can be used like this: `$~third-party`.
22158
+ */
22159
+ negatable: booleanSchema.default(true),
22160
+ /**
22161
+ * The actual modifier can only be used in blocking rules, it cannot be used in exceptions.
22162
+ * If it's value is `true`, then the modifier can be used only in blocking rules.
22163
+ * `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
22164
+ */
22165
+ block_only: booleanSchema.default(false),
22166
+ /**
22167
+ * The actual modifier can only be used in exceptions, it cannot be used in blocking rules.
22168
+ * If it's value is `true`, then the modifier can be used only in exceptions.
22169
+ * `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
22170
+ */
22171
+ exception_only: booleanSchema.default(false),
22172
+ /**
22173
+ * Describes whether the *assignable* modifier value is required.
22174
+ * For example, `$cookie` is assignable but it can be used without a value in exception rules:
22175
+ * `@@\|\|example.com^$cookie`.
22176
+ * If `false`, the `value_format` is required, e.g. the value of `$app` should always be specified
22177
+ */
22178
+ value_optional: booleanSchema.default(false),
22179
+ /**
22180
+ * Describes the format of the value for the *assignable* modifier.
22181
+ * Its value can be a regex pattern or a known validator name (e.g. `domain`, `pipe_separated_domains`, etc.).
22182
+ */
22183
+ value_format: nonEmptyStringSchema.nullable().default(null)
22184
+ }).superRefine((data, ctx) => {
22185
+ // TODO: find something better, for now we can't add refine logic to the base schema:
22186
+ // https://github.com/colinhacks/zod/issues/454#issuecomment-848370721
22187
+ baseRefineLogic(data, ctx);
22188
+ if (data.block_only && data.exception_only) {
22189
+ ctx.addIssue({
22190
+ code: zod.ZodIssueCode.custom,
22191
+ message: 'block_only and exception_only are mutually exclusive'
22192
+ });
22504
22193
  }
22505
- has(key) {
22506
- return this.cache.has(key) || this.oldCache.has(key);
22194
+ if (data.assignable && !data.value_format) {
22195
+ ctx.addIssue({
22196
+ code: zod.ZodIssueCode.custom,
22197
+ message: 'value_format is required for assignable modifiers'
22198
+ });
22507
22199
  }
22508
- peek(key) {
22509
- if (this.cache.has(key)) {
22510
- return this.cache.get(key);
22511
- }
22512
- if (this.oldCache.has(key)) {
22513
- return this.oldCache.get(key);
22200
+ if (data.value_format) {
22201
+ const valueFormat = data.value_format.trim();
22202
+ // if it is a known validator, we don't need to validate it further
22203
+ if (KNOWN_VALIDATORS.has(valueFormat)) {
22204
+ return;
22514
22205
  }
22515
- }
22516
- delete(key) {
22517
- const deleted = this.cache.delete(key);
22518
- if (deleted) {
22519
- this._size--;
22206
+ // otherwise, we need to validate it as a regex
22207
+ try {
22208
+ XRegExp(valueFormat);
22209
+ } catch (error) {
22210
+ ctx.addIssue({
22211
+ code: zod.ZodIssueCode.custom,
22212
+ message: getErrorMessage(error)
22213
+ });
22520
22214
  }
22521
- return this.oldCache.delete(key) || deleted;
22522
22215
  }
22523
- clear() {
22524
- this.cache.clear();
22525
- this.oldCache.clear();
22526
- this._size = 0;
22216
+ }));
22217
+
22218
+ /**
22219
+ * @file Schema for redirect data.
22220
+ */
22221
+ /**
22222
+ * Zod schema for redirect data.
22223
+ */
22224
+ zodToCamelCase(baseCompatibilityDataSchema.extend({
22225
+ /**
22226
+ * Whether the redirect is blocking.
22227
+ */
22228
+ is_blocking: booleanSchema.default(false)
22229
+ }).superRefine(baseRefineLogic));
22230
+
22231
+ /**
22232
+ * @file Schema for scriptlet data.
22233
+ */
22234
+ /**
22235
+ * Zod schema for scriptlet parameter data.
22236
+ */
22237
+ const scriptletParameterSchema = zod.object({
22238
+ /**
22239
+ * Name of the actual parameter.
22240
+ */
22241
+ name: nonEmptyStringSchema,
22242
+ /**
22243
+ * Describes whether the parameter is required. Empty parameters are not allowed.
22244
+ */
22245
+ required: booleanSchema,
22246
+ /**
22247
+ * Short description of the parameter.
22248
+ * If not specified or it's value is `null`,then the description is not available.
22249
+ */
22250
+ description: nonEmptyStringSchema.nullable().default(null),
22251
+ /**
22252
+ * Regular expression that matches the value of the parameter.
22253
+ * If it's value is `null`, then the parameter value is not checked.
22254
+ */
22255
+ pattern: nonEmptyStringSchema.nullable().default(null),
22256
+ /**
22257
+ * Default value of the parameter (if any).
22258
+ */
22259
+ default: nonEmptyStringSchema.nullable().default(null),
22260
+ /**
22261
+ * Describes whether the parameter is used only for debugging purposes.
22262
+ */
22263
+ debug: booleanSchema.default(false)
22264
+ });
22265
+ /**
22266
+ * Zod schema for scriptlet parameters.
22267
+ */
22268
+ const scriptletParametersSchema = zod.array(scriptletParameterSchema);
22269
+ /**
22270
+ * Zod schema for scriptlet data.
22271
+ */
22272
+ zodToCamelCase(baseCompatibilityDataSchema.extend({
22273
+ /**
22274
+ * List of parameters that the scriptlet accepts.
22275
+ * **Every** parameter should be listed here, because we check that the scriptlet is used correctly
22276
+ * (e.g. that the number of parameters is correct).
22277
+ */
22278
+ parameters: scriptletParametersSchema.optional()
22279
+ }).superRefine((data, ctx) => {
22280
+ // TODO: find something better, for now we can't add refine logic to the base schema:
22281
+ // https://github.com/colinhacks/zod/issues/454#issuecomment-848370721
22282
+ baseRefineLogic(data, ctx);
22283
+ // we don't allow required parameters after optional ones
22284
+ if (!data.parameters) {
22285
+ return;
22527
22286
  }
22528
- *keys() {
22529
- for (const [key] of this) {
22530
- yield key;
22287
+ let optionalFound = false;
22288
+ for (const parameter of data.parameters) {
22289
+ if (optionalFound && parameter.required) {
22290
+ ctx.addIssue({
22291
+ code: zod.ZodIssueCode.custom,
22292
+ message: 'Required parameters must be before optional ones'
22293
+ });
22531
22294
  }
22532
- }
22533
- *values() {
22534
- for (const [, value] of this) {
22535
- yield value;
22295
+ if (!parameter.required) {
22296
+ optionalFound = true;
22536
22297
  }
22537
22298
  }
22538
- *[Symbol.iterator]() {
22539
- for (const item of this.cache) {
22540
- yield item;
22299
+ }));
22300
+
22301
+ /**
22302
+ * @file Scriptlet injection rule converter
22303
+ */
22304
+ const ABP_SCRIPTLET_PREFIX = 'abp-';
22305
+ const UBO_SCRIPTLET_PREFIX = 'ubo-';
22306
+ const UBO_SCRIPTLET_PREFIX_LENGTH = UBO_SCRIPTLET_PREFIX.length;
22307
+ const UBO_SCRIPTLET_JS_SUFFIX = '.js';
22308
+ const UBO_SCRIPTLET_JS_SUFFIX_LENGTH = UBO_SCRIPTLET_JS_SUFFIX.length;
22309
+ const COMMA_SEPARATOR = ',';
22310
+ const ADG_SET_CONSTANT_NAME = 'set-constant';
22311
+ const ADG_SET_CONSTANT_EMPTY_STRING = '';
22312
+ const ADG_SET_CONSTANT_EMPTY_ARRAY = 'emptyArr';
22313
+ const ADG_SET_CONSTANT_EMPTY_OBJECT = 'emptyObj';
22314
+ const UBO_SET_CONSTANT_EMPTY_STRING = '\'\'';
22315
+ const UBO_SET_CONSTANT_EMPTY_ARRAY = '[]';
22316
+ const UBO_SET_CONSTANT_EMPTY_OBJECT = '{}';
22317
+ const ADG_PREVENT_FETCH_NAME = 'prevent-fetch';
22318
+ const ADG_PREVENT_FETCH_EMPTY_STRING = '';
22319
+ const ADG_PREVENT_FETCH_WILDCARD = '*';
22320
+ const UBO_NO_FETCH_IF_WILDCARD = '/^/';
22321
+ const setConstantAdgToUboMap = {
22322
+ [ADG_SET_CONSTANT_EMPTY_STRING]: UBO_SET_CONSTANT_EMPTY_STRING,
22323
+ [ADG_SET_CONSTANT_EMPTY_ARRAY]: UBO_SET_CONSTANT_EMPTY_ARRAY,
22324
+ [ADG_SET_CONSTANT_EMPTY_OBJECT]: UBO_SET_CONSTANT_EMPTY_OBJECT
22325
+ };
22326
+ /**
22327
+ * Scriptlet injection rule converter class
22328
+ *
22329
+ * @todo Implement `convertToUbo` and `convertToAbp`
22330
+ */
22331
+ class ScriptletRuleConverter extends RuleConverterBase {
22332
+ /**
22333
+ * Converts a scriptlet injection rule to AdGuard format, if possible.
22334
+ *
22335
+ * @param rule Rule node to convert
22336
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22337
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22338
+ * If the rule was not converted, the result array will contain the original node with the same object reference
22339
+ * @throws If the rule is invalid or cannot be converted
22340
+ */
22341
+ static convertToAdg(rule) {
22342
+ // Ignore AdGuard rules
22343
+ if (rule.syntax === AdblockSyntax.Adg) {
22344
+ return createNodeConversionResult([rule], false);
22541
22345
  }
22542
- for (const item of this.oldCache) {
22543
- const [key] = item;
22544
- if (!this.cache.has(key)) {
22545
- yield item;
22346
+ const separator = rule.separator.value;
22347
+ let convertedSeparator = separator;
22348
+ convertedSeparator = rule.exception ? CosmeticRuleSeparator.AdgJsInjectionException : CosmeticRuleSeparator.AdgJsInjection;
22349
+ const convertedScriptlets = [];
22350
+ // Special case: empty uBO exception scriptlet, e.g. `example.com#@#+js()`
22351
+ if (rule.syntax === AdblockSyntax.Ubo && rule.body.children.length === 1 && rule.body.children[0].children.length === 0) {
22352
+ convertedScriptlets.push(rule.body.children[0]);
22353
+ } else {
22354
+ for (const scriptlet of rule.body.children) {
22355
+ // Clone the node to avoid any side effects
22356
+ const scriptletClone = cloneScriptletRuleNode(scriptlet);
22357
+ // Remove possible quotes just to make it easier to work with the scriptlet name
22358
+ const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), QuoteType.None);
22359
+ // Add prefix if it's not already there
22360
+ let prefix;
22361
+ switch (rule.syntax) {
22362
+ case AdblockSyntax.Abp:
22363
+ prefix = ABP_SCRIPTLET_PREFIX;
22364
+ break;
22365
+ case AdblockSyntax.Ubo:
22366
+ prefix = UBO_SCRIPTLET_PREFIX;
22367
+ break;
22368
+ default:
22369
+ prefix = EMPTY;
22370
+ }
22371
+ if (!scriptletName.startsWith(prefix)) {
22372
+ setScriptletName(scriptletClone, `${prefix}${scriptletName}`);
22373
+ }
22374
+ // ADG scriptlet parameters should be quoted, and single quoted are preferred
22375
+ setScriptletQuoteType(scriptletClone, QuoteType.Single);
22376
+ convertedScriptlets.push(scriptletClone);
22546
22377
  }
22547
22378
  }
22548
- }
22549
- get size() {
22550
- let oldCacheSize = 0;
22551
- for (const key of this.oldCache.keys()) {
22552
- if (!this.cache.has(key)) {
22553
- oldCacheSize++;
22379
+ return createNodeConversionResult(convertedScriptlets.map(scriptlet => {
22380
+ const res = {
22381
+ category: rule.category,
22382
+ type: rule.type,
22383
+ syntax: AdblockSyntax.Adg,
22384
+ exception: rule.exception,
22385
+ domains: cloneDomainListNode(rule.domains),
22386
+ separator: {
22387
+ type: 'Value',
22388
+ value: convertedSeparator
22389
+ },
22390
+ body: {
22391
+ type: rule.body.type,
22392
+ children: [scriptlet]
22393
+ }
22394
+ };
22395
+ if (rule.modifiers) {
22396
+ res.modifiers = cloneModifierListNode(rule.modifiers);
22554
22397
  }
22555
- }
22556
- return Math.min(this._size + oldCacheSize, this.maxSize);
22557
- }
22558
- }
22559
- var quickLru = QuickLRU;
22560
- const mapObj = mapObjExports;
22561
- const camelCase = camelcaseExports;
22562
- const QuickLru = quickLru;
22563
- const has = (array, key) => array.some(x => {
22564
- if (typeof x === 'string') {
22565
- return x === key;
22566
- }
22567
- x.lastIndex = 0;
22568
- return x.test(key);
22569
- });
22570
- const cache = new QuickLru({
22571
- maxSize: 100000
22572
- });
22573
-
22574
- // Reproduces behavior from `map-obj`
22575
- const isObject = value => typeof value === 'object' && value !== null && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
22576
- const camelCaseConvert = (input, options) => {
22577
- if (!isObject(input)) {
22578
- return input;
22398
+ return res;
22399
+ }), true);
22579
22400
  }
22580
- options = {
22581
- deep: false,
22582
- pascalCase: false,
22583
- ...options
22584
- };
22585
- const {
22586
- exclude,
22587
- pascalCase,
22588
- stopPaths,
22589
- deep
22590
- } = options;
22591
- const stopPathsSet = new Set(stopPaths);
22592
- const makeMapper = parentPath => (key, value) => {
22593
- if (deep && isObject(value)) {
22594
- const path = parentPath === undefined ? key : `${parentPath}.${key}`;
22595
- if (!stopPathsSet.has(path)) {
22596
- value = mapObj(value, makeMapper(path));
22597
- }
22401
+ /**
22402
+ * Converts a scriptlet injection rule to uBlock format, if possible.
22403
+ *
22404
+ * @param rule Rule node to convert
22405
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22406
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22407
+ * If the rule was not converted, the result array will contain the original node with the same object reference
22408
+ * @throws If the rule is invalid or cannot be converted
22409
+ */
22410
+ static convertToUbo(rule) {
22411
+ // Ignore uBlock rules
22412
+ if (rule.syntax === AdblockSyntax.Ubo) {
22413
+ return createNodeConversionResult([rule], false);
22598
22414
  }
22599
- if (!(exclude && has(exclude, key))) {
22600
- const cacheKey = pascalCase ? `${key}_` : key;
22601
- if (cache.has(cacheKey)) {
22602
- key = cache.get(cacheKey);
22603
- } else {
22604
- const returnValue = camelCase(key, {
22605
- pascalCase,
22606
- locale: false
22415
+ const separator = rule.separator.value;
22416
+ let convertedSeparator = separator;
22417
+ convertedSeparator = rule.exception ? CosmeticRuleSeparator.ElementHidingException : CosmeticRuleSeparator.ElementHiding;
22418
+ const convertedScriptlets = [];
22419
+ // Special case: empty AdGuard exception scriptlet, e.g. `example.com#%#//scriptlet()`
22420
+ if (rule.syntax === AdblockSyntax.Adg && rule.body.children.length === 1 && rule.body.children[0].children.length === 0) {
22421
+ convertedScriptlets.push(rule.body.children[0]);
22422
+ } else {
22423
+ for (const scriptlet of rule.body.children) {
22424
+ // Clone the node to avoid any side effects
22425
+ const scriptletClone = cloneScriptletRuleNode(scriptlet);
22426
+ // Remove possible quotes just to make it easier to work with the scriptlet name
22427
+ const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), QuoteType.None);
22428
+ let uboScriptletName;
22429
+ if (rule.syntax === AdblockSyntax.Adg && scriptletName.startsWith(UBO_SCRIPTLET_PREFIX)) {
22430
+ // Special case: AdGuard syntax 'preserves' the original scriptlet name,
22431
+ // so we need to convert it back by removing the uBO prefix
22432
+ uboScriptletName = scriptletName.slice(UBO_SCRIPTLET_PREFIX_LENGTH);
22433
+ } else {
22434
+ // Otherwise, try to find the corresponding uBO scriptlet name, or use the original one if not found
22435
+ const uboScriptlet = scriptletsCompatibilityTable.getFirst(scriptletName, GenericPlatform.UboAny);
22436
+ uboScriptletName = uboScriptlet?.name ?? scriptletName;
22437
+ }
22438
+ // Remove the '.js' suffix if it's there - its presence is not mandatory
22439
+ if (uboScriptletName.endsWith(UBO_SCRIPTLET_JS_SUFFIX)) {
22440
+ uboScriptletName = uboScriptletName.slice(0, -UBO_SCRIPTLET_JS_SUFFIX_LENGTH);
22441
+ }
22442
+ setScriptletName(scriptletClone, uboScriptletName);
22443
+ setScriptletQuoteType(scriptletClone, QuoteType.None);
22444
+ // Escape unescaped commas in parameters, because uBlock Origin uses them as separators.
22445
+ // For example, the following AdGuard rule:
22446
+ //
22447
+ // example.com#%#//scriptlet('spoof-css', '.adsbygoogle, #ads', 'visibility', 'visible')
22448
+ //
22449
+ // ↓↓ should be converted to ↓↓
22450
+ //
22451
+ // example.com##+js(spoof-css.js, .adsbygoogle\, #ads, visibility, visible)
22452
+ // ------------ ------------------- ---------- -------
22453
+ // arg 0 arg 1 arg 2 arg 3
22454
+ //
22455
+ // and we need to escape the comma in the second argument to prevent it from being treated
22456
+ // as two separate arguments.
22457
+ transformAllScriptletArguments(scriptletClone, value => {
22458
+ return QuoteUtils.escapeUnescapedOccurrences(value, COMMA_SEPARATOR);
22607
22459
  });
22608
- if (key.length < 100) {
22609
- // Prevent abuse
22610
- cache.set(cacheKey, returnValue);
22460
+ // Some scriptlets have special values that need to be converted
22461
+ switch (scriptletName) {
22462
+ case ADG_SET_CONSTANT_NAME:
22463
+ transformNthScriptletArgument(scriptletClone, 2, value => {
22464
+ return setConstantAdgToUboMap[value] ?? value;
22465
+ });
22466
+ break;
22467
+ case ADG_PREVENT_FETCH_NAME:
22468
+ transformNthScriptletArgument(scriptletClone, 1, value => {
22469
+ if (value === ADG_PREVENT_FETCH_EMPTY_STRING || value === ADG_PREVENT_FETCH_WILDCARD) {
22470
+ return UBO_NO_FETCH_IF_WILDCARD;
22471
+ }
22472
+ return value;
22473
+ });
22474
+ break;
22611
22475
  }
22612
- key = returnValue;
22476
+ convertedScriptlets.push(scriptletClone);
22613
22477
  }
22614
22478
  }
22615
- return [key, value];
22616
- };
22617
- return mapObj(input, makeMapper(undefined));
22618
- };
22619
- var camelcaseKeys = (input, options) => {
22620
- if (Array.isArray(input)) {
22621
- return Object.keys(input).map(key => camelCaseConvert(input[key], options));
22479
+ return createNodeConversionResult(convertedScriptlets.map(scriptlet => {
22480
+ const res = {
22481
+ category: rule.category,
22482
+ type: rule.type,
22483
+ syntax: AdblockSyntax.Ubo,
22484
+ exception: rule.exception,
22485
+ domains: cloneDomainListNode(rule.domains),
22486
+ separator: {
22487
+ type: 'Value',
22488
+ value: convertedSeparator
22489
+ },
22490
+ body: {
22491
+ type: rule.body.type,
22492
+ children: [scriptlet]
22493
+ }
22494
+ };
22495
+ if (rule.modifiers) {
22496
+ res.modifiers = cloneModifierListNode(rule.modifiers);
22497
+ }
22498
+ return res;
22499
+ }), true);
22622
22500
  }
22623
- return camelCaseConvert(input, options);
22624
- };
22625
- var camelCaseKeys = /*@__PURE__*/getDefaultExportFromCjs(camelcaseKeys);
22501
+ }
22626
22502
 
22627
22503
  /**
22628
- * @file Zod camelCase utility.
22504
+ * @file Utility functions for working with modifier nodes
22629
22505
  */
22630
- // eslint-disable-next-line import/no-extraneous-dependencies
22631
22506
  /**
22632
- * Transforms Zod schema to camelCase.
22633
- *
22634
- * @param zod Zod schema.
22507
+ * Creates a modifier node
22635
22508
  *
22636
- * @returns Zod schema with camelCase properties.
22509
+ * @param name Name of the modifier
22510
+ * @param value Value of the modifier
22511
+ * @param exception Whether the modifier is an exception
22512
+ * @returns Modifier node
22513
+ */
22514
+ function createModifierNode(name, value = undefined, exception = false) {
22515
+ const result = {
22516
+ type: 'Modifier',
22517
+ exception,
22518
+ name: {
22519
+ type: 'Value',
22520
+ value: name
22521
+ }
22522
+ };
22523
+ if (!isUndefined(value)) {
22524
+ result.value = {
22525
+ type: 'Value',
22526
+ value
22527
+ };
22528
+ }
22529
+ return result;
22530
+ }
22531
+ /**
22532
+ * Creates a modifier list node
22637
22533
  *
22638
- * @see {@link https://github.com/colinhacks/zod/issues/486#issuecomment-1501097361}
22534
+ * @param modifiers Modifiers to put in the list (optional, defaults to an empty list)
22535
+ * @returns Modifier list node
22639
22536
  */
22640
- const zodToCamelCase = zod => {
22641
- return zod.transform(val => camelCaseKeys(val));
22642
- };
22537
+ function createModifierListNode(modifiers = []) {
22538
+ const result = {
22539
+ type: 'ModifierList',
22540
+ // We need to clone the modifiers to avoid side effects
22541
+ children: modifiers.length ? clone(modifiers) : []
22542
+ };
22543
+ return result;
22544
+ }
22643
22545
 
22644
22546
  /**
22645
- * @file Base compatibility data schema, which is commonly used in compatibility tables.
22547
+ * A very simple map extension that allows to store multiple values for the same key
22548
+ * by storing them in an array.
22549
+ *
22550
+ * @todo Add more methods if needed
22646
22551
  */
22552
+ class MultiValueMap extends Map {
22553
+ /**
22554
+ * Adds a value to the map. If the key already exists, the value will be appended to the existing array,
22555
+ * otherwise a new array will be created for the key.
22556
+ *
22557
+ * @param key Key to add
22558
+ * @param values Value(s) to add
22559
+ */
22560
+ add(key, ...values) {
22561
+ let currentValues = super.get(key);
22562
+ if (isUndefined(currentValues)) {
22563
+ currentValues = [];
22564
+ super.set(key, values);
22565
+ }
22566
+ currentValues.push(...values);
22567
+ }
22568
+ }
22569
+
22647
22570
  /**
22648
- * Zod schema for boolean values. Accepts both boolean and string values.
22571
+ * @file Cosmetic rule modifier converter from uBO to ADG
22649
22572
  */
22650
- const booleanSchema = zod.union([zod.string().transform(val => val.trim().toLowerCase() === 'true'), zod.boolean()]);
22573
+ const UBO_MATCHES_PATH_OPERATOR = 'matches-path';
22574
+ const ADG_PATH_MODIFIER = 'path';
22651
22575
  /**
22652
- * Zod schema for non-empty string values.
22576
+ * Special characters in modifier regexps that should be escaped
22653
22577
  */
22654
- const nonEmptyStringSchema = zod.string().transform(val => val.trim()).pipe(zod.string().min(1));
22578
+ const SPECIAL_MODIFIER_REGEX_CHARS = new Set([OPEN_SQUARE_BRACKET, CLOSE_SQUARE_BRACKET, COMMA, ESCAPE_CHARACTER]);
22655
22579
  /**
22656
- * Zod schema for base compatibility data.
22657
- * Here we use snake_case properties because the compatibility data is stored in YAML files.
22580
+ * Helper class for converting cosmetic rule modifiers from uBO to ADG
22658
22581
  */
22659
- const baseCompatibilityDataSchema = zod.object({
22660
- /**
22661
- * Name of the actual entity.
22662
- */
22663
- name: nonEmptyStringSchema,
22664
- /**
22665
- * List of aliases for the entity (if any).
22666
- */
22667
- aliases: zod.array(nonEmptyStringSchema).nullable().default(null),
22668
- /**
22669
- * Short description of the actual entity.
22670
- * If not specified or it's value is `null`, then the description is not available.
22671
- */
22672
- description: nonEmptyStringSchema.nullable().default(null),
22673
- /**
22674
- * Link to the documentation. If not specified or it's value is `null`, then the documentation is not available.
22675
- */
22676
- docs: nonEmptyStringSchema.nullable().default(null),
22677
- /**
22678
- * The version of the adblocker in which the entity was added.
22679
- * For AdGuard resources, the version of the library is specified.
22680
- */
22681
- version_added: nonEmptyStringSchema.nullable().default(null),
22682
- /**
22683
- * The version of the adblocker when the entity was removed.
22684
- */
22685
- version_removed: nonEmptyStringSchema.nullable().default(null),
22686
- /**
22687
- * Describes whether the entity is deprecated.
22688
- */
22689
- deprecated: booleanSchema.default(false),
22690
- /**
22691
- * Message that describes why the entity is deprecated.
22692
- * If not specified or it's value is `null`, then the message is not available.
22693
- * It's value is omitted if the entity is not marked as deprecated.
22694
- */
22695
- deprecation_message: nonEmptyStringSchema.nullable().default(null),
22582
+ class AdgCosmeticRuleModifierConverter {
22696
22583
  /**
22697
- * Describes whether the entity is removed; for *already removed* features.
22584
+ * Converts a uBO cosmetic rule modifier list to ADG, if possible.
22585
+ *
22586
+ * @param modifierList Cosmetic rule modifier list node to convert
22587
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
22588
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
22589
+ * If the node was not converted, the result will contain the original node with the same object reference
22590
+ * @throws If the modifier list cannot be converted
22591
+ * @see {@link https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#cosmetic-filter-operators}
22698
22592
  */
22699
- removed: booleanSchema.default(false),
22593
+ static convertFromUbo(modifierList) {
22594
+ const conversionMap = new MultiValueMap();
22595
+ modifierList.children.forEach((modifier, index) => {
22596
+ // :matches-path
22597
+ if (modifier.name.value === UBO_MATCHES_PATH_OPERATOR) {
22598
+ if (!modifier.value) {
22599
+ throw new RuleConversionError(`'${UBO_MATCHES_PATH_OPERATOR}' operator requires a value`);
22600
+ }
22601
+ const value = RegExpUtils.isRegexPattern(modifier.value.value) ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS) : modifier.value.value;
22602
+ // Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
22603
+ conversionMap.add(index, createModifierNode(ADG_PATH_MODIFIER,
22604
+ // We should negate the regexp if the modifier is an exception
22605
+ modifier.exception
22606
+ // eslint-disable-next-line max-len
22607
+ ? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(value))}${REGEX_MARKER}` : value));
22608
+ }
22609
+ });
22610
+ // Check if we have any converted modifiers
22611
+ if (conversionMap.size) {
22612
+ const modifierListClone = clone(modifierList);
22613
+ // Replace the original modifiers with the converted ones
22614
+ modifierListClone.children = modifierListClone.children.map((modifier, index) => {
22615
+ const convertedModifier = conversionMap.get(index);
22616
+ return convertedModifier ?? modifier;
22617
+ }).flat();
22618
+ return createConversionResult(modifierListClone, true);
22619
+ }
22620
+ // Otherwise, just return the original modifier list
22621
+ return createConversionResult(modifierList, false);
22622
+ }
22623
+ }
22624
+ const ERROR_MESSAGES$1 = {
22625
+ // eslint-disable-next-line max-len
22626
+ INVALID_ATTRIBUTE_VALUE: `Expected '${getFormattedTokenName(TokenType$1.Ident)}' or '${getFormattedTokenName(TokenType$1.String)}' as attribute value, but got '%s' with value '%s`
22627
+ };
22628
+ var PseudoClasses;
22629
+ (function (PseudoClasses) {
22630
+ PseudoClasses["AbpContains"] = "-abp-contains";
22631
+ PseudoClasses["AbpHas"] = "-abp-has";
22632
+ PseudoClasses["Contains"] = "contains";
22633
+ PseudoClasses["Has"] = "has";
22634
+ PseudoClasses["HasText"] = "has-text";
22635
+ PseudoClasses["MatchesCss"] = "matches-css";
22636
+ PseudoClasses["MatchesCssAfter"] = "matches-css-after";
22637
+ PseudoClasses["MatchesCssBefore"] = "matches-css-before";
22638
+ PseudoClasses["Not"] = "not";
22639
+ })(PseudoClasses || (PseudoClasses = {}));
22640
+ var PseudoElements;
22641
+ (function (PseudoElements) {
22642
+ PseudoElements["After"] = "after";
22643
+ PseudoElements["Before"] = "before";
22644
+ })(PseudoElements || (PseudoElements = {}));
22645
+ const PSEUDO_ELEMENT_NAMES = new Set([PseudoElements.After, PseudoElements.Before]);
22646
+ /**
22647
+ * CSS selector converter
22648
+ *
22649
+ * @todo Implement `convertToUbo` and `convertToAbp`
22650
+ */
22651
+ class CssSelectorConverter extends ConverterBase {
22700
22652
  /**
22701
- * Message that describes why the entity is removed.
22702
- * If not specified or it's value is `null`, then the message is not available.
22703
- * It's value is omitted if the entity is not marked as deprecated.
22653
+ * Converts Extended CSS elements to AdGuard-compatible ones
22654
+ *
22655
+ * @param selectorList Selector list to convert
22656
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
22657
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
22658
+ * If the node was not converted, the result will contain the original node with the same object reference
22659
+ * @throws If the rule is invalid or incompatible
22704
22660
  */
22705
- removal_message: nonEmptyStringSchema.nullable().default(null)
22706
- });
22661
+ static convertToAdg(selectorList) {
22662
+ const stream = selectorList instanceof CssTokenStream ? selectorList : new CssTokenStream(selectorList);
22663
+ const converted = [];
22664
+ const convertAndPushPseudo = pseudo => {
22665
+ switch (pseudo) {
22666
+ case PseudoClasses.AbpContains:
22667
+ case PseudoClasses.HasText:
22668
+ converted.push(PseudoClasses.Contains);
22669
+ converted.push(OPEN_PARENTHESIS);
22670
+ break;
22671
+ case PseudoClasses.AbpHas:
22672
+ converted.push(PseudoClasses.Has);
22673
+ converted.push(OPEN_PARENTHESIS);
22674
+ break;
22675
+ // a bit special case:
22676
+ // - `:matches-css-before(...)` → `:matches-css(before, ...)`
22677
+ // - `:matches-css-after(...)` → `:matches-css(after, ...)`
22678
+ case PseudoClasses.MatchesCssBefore:
22679
+ case PseudoClasses.MatchesCssAfter:
22680
+ converted.push(PseudoClasses.MatchesCss);
22681
+ converted.push(OPEN_PARENTHESIS);
22682
+ converted.push(pseudo.substring(PseudoClasses.MatchesCss.length + 1));
22683
+ converted.push(COMMA);
22684
+ break;
22685
+ default:
22686
+ converted.push(pseudo);
22687
+ converted.push(OPEN_PARENTHESIS);
22688
+ break;
22689
+ }
22690
+ };
22691
+ while (!stream.isEof()) {
22692
+ const token = stream.getOrFail();
22693
+ if (token.type === TokenType$1.Colon) {
22694
+ // Advance colon
22695
+ stream.advance();
22696
+ converted.push(COLON);
22697
+ const tempToken = stream.getOrFail();
22698
+ // Double colon is a pseudo-element
22699
+ if (tempToken.type === TokenType$1.Colon) {
22700
+ stream.advance();
22701
+ converted.push(COLON);
22702
+ continue;
22703
+ }
22704
+ if (tempToken.type === TokenType$1.Ident) {
22705
+ const name = stream.source.slice(tempToken.start, tempToken.end);
22706
+ if (PSEUDO_ELEMENT_NAMES.has(name)) {
22707
+ // Add an extra colon to the name
22708
+ converted.push(COLON);
22709
+ converted.push(name);
22710
+ } else {
22711
+ // Add the name as is
22712
+ converted.push(name);
22713
+ }
22714
+ // Advance the names
22715
+ stream.advance();
22716
+ } else if (tempToken.type === TokenType$1.Function) {
22717
+ const name = stream.source.slice(tempToken.start, tempToken.end - 1); // omit the last parenthesis
22718
+ // :-abp-contains(...) → :contains(...)
22719
+ // :has-text(...) → :contains(...)
22720
+ // :-abp-has(...) → :has(...)
22721
+ convertAndPushPseudo(name);
22722
+ // Advance the function name
22723
+ stream.advance();
22724
+ }
22725
+ } else if (token.type === TokenType$1.OpenSquareBracket) {
22726
+ let tempToken;
22727
+ const {
22728
+ start
22729
+ } = token;
22730
+ stream.advance();
22731
+ // Converts legacy Extended CSS selectors to the modern Extended CSS syntax.
22732
+ // For example:
22733
+ // - `[-ext-has=...]` → `:has(...)`
22734
+ // - `[-ext-contains=...]` → `:contains(...)`
22735
+ // - `[-ext-matches-css-before=...]` → `:matches-css(before, ...)`
22736
+ stream.skipWhitespace();
22737
+ stream.expect(TokenType$1.Ident);
22738
+ tempToken = stream.getOrFail();
22739
+ let attr = stream.source.slice(tempToken.start, tempToken.end);
22740
+ // Skip if the attribute name is not a legacy Extended CSS one
22741
+ if (!(attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX) || attr.startsWith(ABP_EXT_CSS_PREFIX))) {
22742
+ converted.push(stream.source.slice(start, tempToken.end));
22743
+ stream.advance();
22744
+ continue;
22745
+ }
22746
+ if (attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX)) {
22747
+ attr = attr.slice(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX.length);
22748
+ }
22749
+ stream.advance();
22750
+ stream.skipWhitespace();
22751
+ // Next token should be an equality operator (=), because Extended CSS attribute selectors
22752
+ // do not support other operators
22753
+ stream.expect(TokenType$1.Delim, {
22754
+ value: EQUALS
22755
+ });
22756
+ stream.advance();
22757
+ // Skip optional whitespace after the operator
22758
+ stream.skipWhitespace();
22759
+ // Parse attribute value
22760
+ tempToken = stream.getOrFail();
22761
+ // According to the spec, attribute value should be an identifier or a string
22762
+ if (tempToken.type !== TokenType$1.Ident && tempToken.type !== TokenType$1.String) {
22763
+ throw new Error(sprintf(ERROR_MESSAGES$1.INVALID_ATTRIBUTE_VALUE, getFormattedTokenName(tempToken.type), stream.source.slice(tempToken.start, tempToken.end)));
22764
+ }
22765
+ const value = stream.source.slice(tempToken.start, tempToken.end);
22766
+ // Advance the attribute value
22767
+ stream.advance();
22768
+ // Skip optional whitespace after the attribute value
22769
+ stream.skipWhitespace();
22770
+ // Next character should be a closing square bracket
22771
+ // We don't allow flags for Extended CSS attribute selectors
22772
+ stream.expect(TokenType$1.CloseSquareBracket);
22773
+ stream.advance();
22774
+ converted.push(COLON);
22775
+ convertAndPushPseudo(attr);
22776
+ let processedValue = value.slice(1, -1); // omit the quotes
22777
+ if (attr === PseudoClasses.Has) {
22778
+ // TODO: Optimize this to avoid double tokenization
22779
+ processedValue = CssSelectorConverter.convertToAdg(processedValue).result;
22780
+ }
22781
+ converted.push(processedValue);
22782
+ converted.push(CLOSE_PARENTHESIS);
22783
+ } else {
22784
+ converted.push(stream.source.slice(token.start, token.end));
22785
+ // Advance the token
22786
+ stream.advance();
22787
+ }
22788
+ }
22789
+ const convertedSelectorList = converted.join(EMPTY);
22790
+ return createConversionResult(convertedSelectorList, stream.source !== convertedSelectorList);
22791
+ }
22792
+ }
22793
+
22707
22794
  /**
22708
- * Zod schema for base compatibility data with camelCase properties.
22795
+ * @file CSS injection rule converter
22709
22796
  */
22710
- zodToCamelCase(baseCompatibilityDataSchema);
22711
22797
  /**
22712
- * Refinement logic for base compatibility data.
22798
+ * CSS injection rule converter class
22713
22799
  *
22714
- * @param data Base compatibility data.
22715
- * @param ctx Refinement context.
22800
+ * @todo Implement `convertToUbo` and `convertToAbp`
22716
22801
  */
22717
- const baseRefineLogic = (data, ctx) => {
22718
- if (data.deprecated && !data.deprecation_message) {
22719
- ctx.addIssue({
22720
- code: zod.ZodIssueCode.custom,
22721
- message: 'deprecation_message is required for deprecated modifiers'
22722
- });
22723
- }
22724
- if (!data.deprecated && data.deprecation_message) {
22725
- ctx.addIssue({
22726
- code: zod.ZodIssueCode.custom,
22727
- message: 'deprecation_message is only allowed for deprecated modifiers'
22728
- });
22729
- }
22730
- if (data.aliases && data.aliases.length !== new Set(data.aliases).size) {
22731
- ctx.addIssue({
22732
- code: zod.ZodIssueCode.custom,
22733
- message: 'Aliases must be unique'
22734
- });
22802
+ class CssInjectionRuleConverter extends RuleConverterBase {
22803
+ /**
22804
+ * Converts a CSS injection rule to AdGuard format, if possible.
22805
+ *
22806
+ * @param rule Rule node to convert
22807
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22808
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22809
+ * If the rule was not converted, the result array will contain the original node with the same object reference
22810
+ * @throws If the rule is invalid or cannot be converted
22811
+ */
22812
+ static convertToAdg(rule) {
22813
+ const separator = rule.separator.value;
22814
+ let convertedSeparator = separator;
22815
+ const stream = new CssTokenStream(rule.body.selectorList.value);
22816
+ const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
22817
+ // Change the separator if the rule contains ExtendedCSS elements,
22818
+ // but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
22819
+ // because sometimes we use it to force executing ExtendedCSS library.
22820
+ if (stream.hasAnySelectorExtendedCssNodeStrict() || rule.body.remove) {
22821
+ convertedSeparator = rule.exception ? CosmeticRuleSeparator.AdgExtendedCssInjectionException : CosmeticRuleSeparator.AdgExtendedCssInjection;
22822
+ } else if (rule.syntax !== AdblockSyntax.Adg) {
22823
+ // If the original rule syntax is not AdGuard, use the default separator
22824
+ // e.g. if the input rule is from uBO, we need to convert ## to #$#.
22825
+ convertedSeparator = rule.exception ? CosmeticRuleSeparator.AdgCssInjectionException : CosmeticRuleSeparator.AdgCssInjection;
22826
+ }
22827
+ // Check if the rule needs to be converted
22828
+ if (!(rule.syntax === AdblockSyntax.Common || rule.syntax === AdblockSyntax.Adg) || separator !== convertedSeparator || convertedSelectorList.isConverted) {
22829
+ // TODO: Replace with custom clone method
22830
+ const ruleClone = clone(rule);
22831
+ ruleClone.syntax = AdblockSyntax.Adg;
22832
+ ruleClone.separator.value = convertedSeparator;
22833
+ ruleClone.body.selectorList.value = convertedSelectorList.result;
22834
+ return createNodeConversionResult([ruleClone], true);
22835
+ }
22836
+ // Otherwise, return the original rule
22837
+ return createNodeConversionResult([rule], false);
22735
22838
  }
22736
- };
22839
+ }
22737
22840
 
22738
22841
  /**
22739
- * Checks if error has message.
22740
- *
22741
- * @param error Error object.
22742
- * @returns If param is error.
22842
+ * @file Element hiding rule converter
22743
22843
  */
22744
- function isErrorWithMessage(error) {
22745
- return typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string';
22746
- }
22747
22844
  /**
22748
- * Converts error to the error with message.
22845
+ * Element hiding rule converter class
22749
22846
  *
22750
- * @param maybeError Possible error.
22751
- * @returns Error with message.
22847
+ * @todo Implement `convertToUbo` and `convertToAbp`
22752
22848
  */
22753
- function toErrorWithMessage(maybeError) {
22754
- if (isErrorWithMessage(maybeError)) {
22755
- return maybeError;
22756
- }
22757
- try {
22758
- return new Error(JSON.stringify(maybeError));
22759
- } catch {
22760
- // fallback in case there's an error stringifying the maybeError
22761
- // like with circular references for example.
22762
- return new Error(String(maybeError));
22849
+ class ElementHidingRuleConverter extends RuleConverterBase {
22850
+ /**
22851
+ * Converts an element hiding rule to AdGuard format, if possible.
22852
+ *
22853
+ * @param rule Rule node to convert
22854
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22855
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22856
+ * If the rule was not converted, the result array will contain the original node with the same object reference
22857
+ * @throws If the rule is invalid or cannot be converted
22858
+ */
22859
+ static convertToAdg(rule) {
22860
+ const separator = rule.separator.value;
22861
+ let convertedSeparator = separator;
22862
+ const stream = new CssTokenStream(rule.body.selectorList.value);
22863
+ const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
22864
+ // Change the separator if the rule contains ExtendedCSS elements,
22865
+ // but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
22866
+ // because sometimes we use it to force executing ExtendedCSS library.
22867
+ if (stream.hasAnySelectorExtendedCssNodeStrict()) {
22868
+ convertedSeparator = rule.exception ? CosmeticRuleSeparator.ExtendedElementHidingException : CosmeticRuleSeparator.ExtendedElementHiding;
22869
+ }
22870
+ // Check if the rule needs to be converted
22871
+ if (!(rule.syntax === AdblockSyntax.Common || rule.syntax === AdblockSyntax.Adg) || separator !== convertedSeparator || convertedSelectorList.isConverted) {
22872
+ // TODO: Replace with custom clone method
22873
+ const ruleClone = clone(rule);
22874
+ ruleClone.syntax = AdblockSyntax.Adg;
22875
+ ruleClone.separator.value = convertedSeparator;
22876
+ ruleClone.body.selectorList.value = convertedSelectorList.result;
22877
+ return createNodeConversionResult([ruleClone], true);
22878
+ }
22879
+ // Otherwise, return the original rule
22880
+ return createNodeConversionResult([rule], false);
22763
22881
  }
22764
22882
  }
22765
- /**
22766
- * Converts error object to error with message. This method might be helpful to handle thrown errors.
22767
- *
22768
- * @param error Error object.
22769
- *
22770
- * @returns Message of the error.
22771
- */
22772
- function getErrorMessage(error) {
22773
- return toErrorWithMessage(error).message;
22774
- }
22775
22883
 
22776
22884
  /**
22777
- * @file Schema for modifier data.
22778
- */
22779
- /**
22780
- * Known validators that don't need to be validated as regex.
22885
+ * @file Utility functions for working with network rule nodes
22781
22886
  */
22782
- const KNOWN_VALIDATORS = new Set(['domain', 'pipe_separated_domains', 'regexp', 'url']);
22783
22887
  /**
22784
- * Zod schema for modifier data.
22888
+ * Creates a network rule node
22889
+ *
22890
+ * @param pattern Rule pattern
22891
+ * @param modifiers Rule modifiers (optional, default: undefined)
22892
+ * @param exception Exception rule flag (optional, default: false)
22893
+ * @param syntax Adblock syntax (optional, default: Common)
22894
+ * @returns Network rule node
22785
22895
  */
22786
- zodToCamelCase(baseCompatibilityDataSchema.extend({
22787
- /**
22788
- * List of modifiers that are incompatible with the actual one.
22789
- */
22790
- conflicts: zod.array(nonEmptyStringSchema).nullable().default(null),
22791
- /**
22792
- * The actual modifier is incompatible with all other modifiers, except the ones listed in `conflicts`.
22793
- */
22794
- inverse_conflicts: booleanSchema.default(false),
22795
- /**
22796
- * Describes whether the actual modifier supports value assignment. For example, `$domain` is assignable,
22797
- * so it can be used like this: `$domain=domain.com\|~subdomain.domain.com`, where `=` is the assignment operator
22798
- * and `domain.com\|~subdomain.domain.com` is the value.
22799
- */
22800
- assignable: booleanSchema.default(false),
22801
- /**
22802
- * Describes whether the actual modifier can be negated. For example, `$third-party` is negatable,
22803
- * so it can be used like this: `$~third-party`.
22804
- */
22805
- negatable: booleanSchema.default(true),
22806
- /**
22807
- * The actual modifier can only be used in blocking rules, it cannot be used in exceptions.
22808
- * If it's value is `true`, then the modifier can be used only in blocking rules.
22809
- * `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
22810
- */
22811
- block_only: booleanSchema.default(false),
22812
- /**
22813
- * The actual modifier can only be used in exceptions, it cannot be used in blocking rules.
22814
- * If it's value is `true`, then the modifier can be used only in exceptions.
22815
- * `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
22816
- */
22817
- exception_only: booleanSchema.default(false),
22818
- /**
22819
- * Describes whether the *assignable* modifier value is required.
22820
- * For example, `$cookie` is assignable but it can be used without a value in exception rules:
22821
- * `@@\|\|example.com^$cookie`.
22822
- * If `false`, the `value_format` is required, e.g. the value of `$app` should always be specified
22823
- */
22824
- value_optional: booleanSchema.default(false),
22825
- /**
22826
- * Describes the format of the value for the *assignable* modifier.
22827
- * Its value can be a regex pattern or a known validator name (e.g. `domain`, `pipe_separated_domains`, etc.).
22828
- */
22829
- value_format: nonEmptyStringSchema.nullable().default(null)
22830
- }).superRefine((data, ctx) => {
22831
- // TODO: find something better, for now we can't add refine logic to the base schema:
22832
- // https://github.com/colinhacks/zod/issues/454#issuecomment-848370721
22833
- baseRefineLogic(data, ctx);
22834
- if (data.block_only && data.exception_only) {
22835
- ctx.addIssue({
22836
- code: zod.ZodIssueCode.custom,
22837
- message: 'block_only and exception_only are mutually exclusive'
22838
- });
22839
- }
22840
- if (data.assignable && !data.value_format) {
22841
- ctx.addIssue({
22842
- code: zod.ZodIssueCode.custom,
22843
- message: 'value_format is required for assignable modifiers'
22844
- });
22845
- }
22846
- if (data.value_format) {
22847
- const valueFormat = data.value_format.trim();
22848
- // if it is a known validator, we don't need to validate it further
22849
- if (KNOWN_VALIDATORS.has(valueFormat)) {
22850
- return;
22851
- }
22852
- // otherwise, we need to validate it as a regex
22853
- try {
22854
- XRegExp(valueFormat);
22855
- } catch (error) {
22856
- ctx.addIssue({
22857
- code: zod.ZodIssueCode.custom,
22858
- message: getErrorMessage(error)
22859
- });
22896
+ function createNetworkRuleNode(pattern, modifiers = undefined, exception = false, syntax = AdblockSyntax.Common) {
22897
+ const result = {
22898
+ category: RuleCategory.Network,
22899
+ type: NetworkRuleType.NetworkRule,
22900
+ syntax,
22901
+ exception,
22902
+ pattern: {
22903
+ type: 'Value',
22904
+ value: pattern
22860
22905
  }
22906
+ };
22907
+ if (!isUndefined(modifiers)) {
22908
+ result.modifiers = clone(modifiers);
22861
22909
  }
22862
- }));
22910
+ return result;
22911
+ }
22863
22912
 
22864
22913
  /**
22865
- * @file Schema for redirect data.
22914
+ * @file Converter for request header removal rules
22866
22915
  */
22916
+ const UBO_RESPONSEHEADER_FN = 'responseheader';
22917
+ const ADG_REMOVEHEADER_MODIFIER = 'removeheader';
22918
+ const ERROR_MESSAGES = {
22919
+ EMPTY_PARAMETER: `Empty parameter for '${UBO_RESPONSEHEADER_FN}' function`,
22920
+ EXPECTED_END_OF_RULE: "Expected end of rule, but got '%s'",
22921
+ MULTIPLE_DOMAINS_NOT_SUPPORTED: 'Multiple domains are not supported yet'
22922
+ };
22867
22923
  /**
22868
- * Zod schema for redirect data.
22924
+ * Converter for request header removal rules
22925
+ *
22926
+ * @todo Implement `convertToUbo` (ABP currently doesn't support header removal rules)
22869
22927
  */
22870
- zodToCamelCase(baseCompatibilityDataSchema.extend({
22928
+ class HeaderRemovalRuleConverter extends RuleConverterBase {
22871
22929
  /**
22872
- * Whether the redirect is blocking.
22930
+ * Converts a header removal rule to AdGuard syntax, if possible.
22931
+ *
22932
+ * @param rule Rule node to convert
22933
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22934
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22935
+ * If the rule was not converted, the result array will contain the original node with the same object reference
22936
+ * @throws If the rule is invalid or cannot be converted
22937
+ * @example
22938
+ * If the input rule is:
22939
+ * ```adblock
22940
+ * example.com##^responseheader(header-name)
22941
+ * ```
22942
+ * The output will be:
22943
+ * ```adblock
22944
+ * ||example.com^$removeheader=header-name
22945
+ * ```
22873
22946
  */
22874
- is_blocking: booleanSchema.default(false)
22875
- }).superRefine(baseRefineLogic));
22947
+ static convertToAdg(rule) {
22948
+ // TODO: Add support for ABP syntax once it starts supporting header removal rules
22949
+ // Leave the rule as is if it's not a header removal rule
22950
+ if (rule.category !== RuleCategory.Cosmetic || rule.type !== CosmeticRuleType.HtmlFilteringRule) {
22951
+ return createNodeConversionResult([rule], false);
22952
+ }
22953
+ const stream = new CssTokenStream(rule.body.value);
22954
+ let token;
22955
+ // Skip leading whitespace
22956
+ stream.skipWhitespace();
22957
+ // Next token should be the `^` followed by a `responseheader` function
22958
+ token = stream.get();
22959
+ if (!token || token.type !== TokenType$1.Delim || rule.body.value[token.start] !== UBO_HTML_MASK) {
22960
+ return createNodeConversionResult([rule], false);
22961
+ }
22962
+ stream.advance();
22963
+ token = stream.get();
22964
+ if (!token) {
22965
+ return createNodeConversionResult([rule], false);
22966
+ }
22967
+ const functionName = rule.body.value.slice(token.start, token.end - 1);
22968
+ if (functionName !== UBO_RESPONSEHEADER_FN) {
22969
+ return createNodeConversionResult([rule], false);
22970
+ }
22971
+ // Parse the parameter
22972
+ const paramStart = token.end;
22973
+ stream.skipUntilBalanced();
22974
+ const paramEnd = stream.getOrFail().end;
22975
+ const param = rule.body.value.slice(paramStart, paramEnd - 1).trim();
22976
+ // Do not allow empty parameter
22977
+ if (param.length === 0) {
22978
+ throw new RuleConversionError(ERROR_MESSAGES.EMPTY_PARAMETER);
22979
+ }
22980
+ stream.expect(TokenType$1.CloseParenthesis);
22981
+ stream.advance();
22982
+ // Skip trailing whitespace after the function call
22983
+ stream.skipWhitespace();
22984
+ // Expect the end of the rule - so nothing should be left in the stream
22985
+ if (!stream.isEof()) {
22986
+ token = stream.getOrFail();
22987
+ throw new RuleConversionError(sprintf(ERROR_MESSAGES.EXPECTED_END_OF_RULE, getFormattedTokenName(token.type)));
22988
+ }
22989
+ // Prepare network rule pattern
22990
+ const pattern = [];
22991
+ if (rule.domains.children.length === 1) {
22992
+ // If the rule has only one domain, we can use a simple network rule pattern:
22993
+ // ||single-domain-from-the-rule^
22994
+ pattern.push(ADBLOCK_URL_START, rule.domains.children[0].value, ADBLOCK_URL_SEPARATOR);
22995
+ } else if (rule.domains.children.length > 1) {
22996
+ // TODO: Add support for multiple domains, for example:
22997
+ // example.com,example.org,example.net##^responseheader(header-name)
22998
+ // We should consider allowing $domain with $removeheader modifier,
22999
+ // for example:
23000
+ // $removeheader=header-name,domain=example.com|example.org|example.net
23001
+ throw new RuleConversionError(ERROR_MESSAGES.MULTIPLE_DOMAINS_NOT_SUPPORTED);
23002
+ }
23003
+ // Prepare network rule modifiers
23004
+ const modifiers = createModifierListNode();
23005
+ modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, param));
23006
+ // Construct the network rule
23007
+ return createNodeConversionResult([createNetworkRuleNode(pattern.join(EMPTY), modifiers,
23008
+ // Copy the exception flag
23009
+ rule.exception, AdblockSyntax.Adg)], true);
23010
+ }
23011
+ }
22876
23012
 
22877
23013
  /**
22878
- * @file Schema for scriptlet data.
22879
- */
22880
- /**
22881
- * Zod schema for scriptlet parameter data.
22882
- */
22883
- const scriptletParameterSchema = zod.object({
22884
- /**
22885
- * Name of the actual parameter.
22886
- */
22887
- name: nonEmptyStringSchema,
22888
- /**
22889
- * Describes whether the parameter is required. Empty parameters are not allowed.
22890
- */
22891
- required: booleanSchema,
22892
- /**
22893
- * Short description of the parameter.
22894
- * If not specified or it's value is `null`,then the description is not available.
22895
- */
22896
- description: nonEmptyStringSchema.nullable().default(null),
22897
- /**
22898
- * Regular expression that matches the value of the parameter.
22899
- * If it's value is `null`, then the parameter value is not checked.
22900
- */
22901
- pattern: nonEmptyStringSchema.nullable().default(null),
22902
- /**
22903
- * Default value of the parameter (if any).
22904
- */
22905
- default: nonEmptyStringSchema.nullable().default(null),
22906
- /**
22907
- * Describes whether the parameter is used only for debugging purposes.
22908
- */
22909
- debug: booleanSchema.default(false)
22910
- });
22911
- /**
22912
- * Zod schema for scriptlet parameters.
23014
+ * @file Cosmetic rule converter
22913
23015
  */
22914
- const scriptletParametersSchema = zod.array(scriptletParameterSchema);
22915
23016
  /**
22916
- * Zod schema for scriptlet data.
23017
+ * Cosmetic rule converter class (also known as "non-basic rule converter")
23018
+ *
23019
+ * @todo Implement `convertToUbo` and `convertToAbp`
22917
23020
  */
22918
- zodToCamelCase(baseCompatibilityDataSchema.extend({
23021
+ class CosmeticRuleConverter extends RuleConverterBase {
22919
23022
  /**
22920
- * List of parameters that the scriptlet accepts.
22921
- * **Every** parameter should be listed here, because we check that the scriptlet is used correctly
22922
- * (e.g. that the number of parameters is correct).
23023
+ * Converts a cosmetic rule to AdGuard syntax, if possible.
23024
+ *
23025
+ * @param rule Rule node to convert
23026
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
23027
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
23028
+ * If the rule was not converted, the result array will contain the original node with the same object reference
23029
+ * @throws If the rule is invalid or cannot be converted
22923
23030
  */
22924
- parameters: scriptletParametersSchema.optional()
22925
- }).superRefine((data, ctx) => {
22926
- // TODO: find something better, for now we can't add refine logic to the base schema:
22927
- // https://github.com/colinhacks/zod/issues/454#issuecomment-848370721
22928
- baseRefineLogic(data, ctx);
22929
- // we don't allow required parameters after optional ones
22930
- if (!data.parameters) {
22931
- return;
22932
- }
22933
- let optionalFound = false;
22934
- for (const parameter of data.parameters) {
22935
- if (optionalFound && parameter.required) {
22936
- ctx.addIssue({
22937
- code: zod.ZodIssueCode.custom,
22938
- message: 'Required parameters must be before optional ones'
23031
+ static convertToAdg(rule) {
23032
+ let subconverterResult;
23033
+ // Convert cosmetic rule based on its type
23034
+ switch (rule.type) {
23035
+ case CosmeticRuleType.ElementHidingRule:
23036
+ subconverterResult = ElementHidingRuleConverter.convertToAdg(rule);
23037
+ break;
23038
+ case CosmeticRuleType.ScriptletInjectionRule:
23039
+ subconverterResult = ScriptletRuleConverter.convertToAdg(rule);
23040
+ break;
23041
+ case CosmeticRuleType.CssInjectionRule:
23042
+ subconverterResult = CssInjectionRuleConverter.convertToAdg(rule);
23043
+ break;
23044
+ case CosmeticRuleType.HtmlFilteringRule:
23045
+ // Handle special case: uBO response header filtering rule
23046
+ // TODO: Optimize double CSS tokenization here
23047
+ subconverterResult = HeaderRemovalRuleConverter.convertToAdg(rule);
23048
+ if (subconverterResult.isConverted) {
23049
+ break;
23050
+ }
23051
+ subconverterResult = HtmlRuleConverter.convertToAdg(rule);
23052
+ break;
23053
+ // Note: Currently, only ADG supports JS injection rules, so we don't need to convert them
23054
+ case CosmeticRuleType.JsInjectionRule:
23055
+ subconverterResult = createNodeConversionResult([rule], false);
23056
+ break;
23057
+ default:
23058
+ throw new RuleConversionError('Unsupported cosmetic rule type');
23059
+ }
23060
+ let convertedModifiers;
23061
+ // Convert cosmetic rule modifiers, if any
23062
+ if (rule.modifiers) {
23063
+ if (rule.syntax === AdblockSyntax.Ubo) {
23064
+ // uBO doesn't support this rule:
23065
+ // example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
23066
+ if (rule.type === CosmeticRuleType.ScriptletInjectionRule) {
23067
+ throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
23068
+ }
23069
+ convertedModifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(rule.modifiers);
23070
+ } else if (rule.syntax === AdblockSyntax.Abp) {
23071
+ // TODO: Implement once ABP starts supporting cosmetic rule modifiers
23072
+ throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
23073
+ }
23074
+ }
23075
+ if (subconverterResult.result.length > 1 || subconverterResult.isConverted || convertedModifiers && convertedModifiers.isConverted) {
23076
+ // Add modifier list to the subconverter result rules
23077
+ subconverterResult.result.forEach(subconverterRule => {
23078
+ if (convertedModifiers && subconverterRule.category === RuleCategory.Cosmetic) {
23079
+ // eslint-disable-next-line no-param-reassign
23080
+ subconverterRule.modifiers = convertedModifiers.result;
23081
+ }
22939
23082
  });
23083
+ return subconverterResult;
22940
23084
  }
22941
- if (!parameter.required) {
22942
- optionalFound = true;
23085
+ return createNodeConversionResult([rule], false);
23086
+ }
23087
+ /**
23088
+ * Converts a cosmetic rule to uBlock Origin syntax, if possible.
23089
+ *
23090
+ * @param rule Rule node to convert
23091
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
23092
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
23093
+ * If the rule was not converted, the result array will contain the original node with the same object reference
23094
+ * @throws If the rule is invalid or cannot be converted
23095
+ */
23096
+ // TODO: Add support for other cosmetic rule types
23097
+ static convertToUbo(rule) {
23098
+ // Convert cosmetic rule based on its type
23099
+ if (rule.type === CosmeticRuleType.ScriptletInjectionRule) {
23100
+ if (rule.syntax === AdblockSyntax.Adg && rule.modifiers?.children.length) {
23101
+ // e.g. example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
23102
+ throw new RuleConversionError('uBO scriptlet injection rules do not support cosmetic rule modifiers');
23103
+ }
23104
+ return ScriptletRuleConverter.convertToUbo(rule);
22943
23105
  }
23106
+ return createNodeConversionResult([rule], false);
22944
23107
  }
22945
- }));
23108
+ }
22946
23109
 
22947
23110
  /**
22948
23111
  * @file Network rule modifier list converter.
@@ -23214,6 +23377,22 @@ class RuleConverter extends RuleConverterBase {
23214
23377
  throw new RuleConversionError('Unknown rule category');
23215
23378
  }
23216
23379
  }
23380
+ /**
23381
+ * Converts an adblock filtering rule to uBlock Origin format, if possible.
23382
+ *
23383
+ * @param rule Rule node to convert
23384
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
23385
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
23386
+ * If the rule was not converted, the result array will contain the original node with the same object reference
23387
+ * @throws If the rule is invalid or cannot be converted
23388
+ */
23389
+ // TODO: Add support for other rule types
23390
+ static convertToUbo(rule) {
23391
+ if (rule.category === RuleCategory.Cosmetic) {
23392
+ return CosmeticRuleConverter.convertToUbo(rule);
23393
+ }
23394
+ return createConversionResult([rule], false);
23395
+ }
23217
23396
  }
23218
23397
 
23219
23398
  /**
@@ -23607,7 +23786,7 @@ class ByteBuffer {
23607
23786
  * @see {@link https://stackoverflow.com/a/62797156}
23608
23787
  */
23609
23788
  const isChromium = () => {
23610
- return typeof window !== 'undefined' && (Object.prototype.hasOwnProperty.call(window, 'chrome') || typeof window.navigator !== 'undefined' && /chrome/i.test(window.navigator.userAgent || ''));
23789
+ return typeof window !== 'undefined' && (Object.prototype.hasOwnProperty.call(window, 'chrome') || typeof window.navigator !== 'undefined' && /chrome/i.test(window.navigator.userAgent));
23611
23790
  };
23612
23791
 
23613
23792
  /* eslint-disable no-param-reassign */
@@ -23698,16 +23877,23 @@ class OutputByteBuffer extends ByteBuffer {
23698
23877
  */
23699
23878
  offset;
23700
23879
  /**
23701
- * Size of the shared buffer for encoding strings.
23880
+ * Size of the shared buffer for encoding strings in bytes.
23881
+ * This is a divisor of ByteBuffer.CHUNK_SIZE and experience shows that this value works optimally.
23882
+ * This is sufficient for most strings that occur in filter lists (we checked average string length in popular
23883
+ * filter lists).
23702
23884
  */
23703
23885
  static ENCODER_BUFFER_SIZE = 8192;
23704
23886
  /**
23705
- * Threshold for using a shared buffer for encoding strings.
23887
+ * Length threshold for using a shared buffer for encoding strings.
23888
+ * This temp buffer is needed because we write the short strings in it
23889
+ * (so there is no need to constantly allocate a new buffer).
23890
+ * The reason for dividing ENCODER_BUFFER_SIZE by 4 is to ensure that the encoded string fits in the buffer,
23891
+ * if we also take into account the worst possible case (each character is encoded with 4 bytes).
23706
23892
  */
23707
23893
  static SHORT_STRING_THRESHOLD = 2048; // 8192 / 4
23708
23894
  /**
23709
23895
  * Represents the maximum value that can be written as a 'storage optimized' unsigned integer.
23710
- * 0x1FFFFFFF means 32 bits minus 3 bits, because the last bit in each byte is a flag indicating
23896
+ * 0x1FFFFFFF means 29 bits — 32 bits minus 3 bits because the last bit in each byte is a flag indicating
23711
23897
  * if there are more bytes (except for the last byte).
23712
23898
  */
23713
23899
  static MAX_OPTIMIZED_UINT = 0x1FFFFFFF;
@@ -24254,7 +24440,7 @@ class RuleCategorizer {
24254
24440
  }
24255
24441
  }
24256
24442
  }
24257
- const version = "2.0.0-alpha.0";
24443
+ const version = "2.0.0";
24258
24444
 
24259
24445
  /**
24260
24446
  * @file AGTree version