@adguard/agtree 1.1.4 → 1.1.6

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 v1.1.4 (build date: Wed, 30 Aug 2023 10:02:46 GMT)
2
+ * AGTree v1.1.6 (build date: Fri, 22 Sep 2023 13:09:45 GMT)
3
3
  * (c) 2023 AdGuard Software Ltd.
4
4
  * Released under the MIT license
5
5
  * https://github.com/AdguardTeam/tsurlfilter/tree/master/packages/agtree#readme
@@ -7,13 +7,13 @@
7
7
  import valid from 'semver/functions/valid.js';
8
8
  import coerce from 'semver/functions/coerce.js';
9
9
  import JSON5 from 'json5';
10
- import { walk, parse, toPlainObject, find, generate, fromPlainObject, List } from '@adguard/ecss-tree';
10
+ import { walk, parse, toPlainObject, find, generate, List, fromPlainObject } from '@adguard/ecss-tree';
11
11
  import * as ecssTree from '@adguard/ecss-tree';
12
12
  export { ecssTree as ECSSTree };
13
13
  import cloneDeep from 'clone-deep';
14
14
  import XRegExp from 'xregexp';
15
15
  import { parse as parse$1 } from 'tldts';
16
- import { redirects } from '@adguard/scriptlets';
16
+ import scriptlets from '@adguard/scriptlets';
17
17
 
18
18
  /**
19
19
  * @file Possible adblock syntaxes are listed here.
@@ -69,6 +69,9 @@ var AdblockSyntax;
69
69
  * @file Constant values used by all parts of the library
70
70
  */
71
71
  // General
72
+ /**
73
+ * Empty string.
74
+ */
72
75
  const EMPTY = '';
73
76
  const SPACE = ' ';
74
77
  const TAB = '\t';
@@ -259,7 +262,7 @@ const NEGATION_MARKER = '~';
259
262
  /**
260
263
  * The wildcard symbol — `*`.
261
264
  */
262
- const WILDCARD$1 = ASTERISK;
265
+ const WILDCARD = ASTERISK;
263
266
  /**
264
267
  * Classic domain separator.
265
268
  *
@@ -2858,7 +2861,7 @@ class ModifierParser {
2858
2861
  const modifierEnd = Math.max(StringUtils.skipWSBack(raw) + 1, modifierNameStart);
2859
2862
  // Modifier name can't be empty
2860
2863
  if (modifierNameStart === modifierEnd) {
2861
- throw new AdblockSyntaxError('Modifier name can\'t be empty', locRange(loc, 0, raw.length));
2864
+ throw new AdblockSyntaxError('Modifier name cannot be empty', locRange(loc, 0, raw.length));
2862
2865
  }
2863
2866
  let modifier;
2864
2867
  let value;
@@ -2882,7 +2885,7 @@ class ModifierParser {
2882
2885
  };
2883
2886
  // Value can't be empty
2884
2887
  if (assignmentIndex + 1 === modifierEnd) {
2885
- throw new AdblockSyntaxError('Modifier value can\'t be empty', locRange(loc, 0, raw.length));
2888
+ throw new AdblockSyntaxError('Modifier value cannot be empty', locRange(loc, 0, raw.length));
2886
2889
  }
2887
2890
  // Skip whitespace after the assignment operator
2888
2891
  const valueStart = StringUtils.skipWS(raw, assignmentIndex + MODIFIER_ASSIGN_OPERATOR.length);
@@ -3180,8 +3183,29 @@ const FORBIDDEN_CSS_FUNCTIONS = new Set([
3180
3183
  'url',
3181
3184
  ]);
3182
3185
 
3186
+ /**
3187
+ * @file Clone related utilities
3188
+ *
3189
+ * We should keep clone related functions in this file. Thus, we just provide
3190
+ * a simple interface for cloning values, we use it across the AGTree project,
3191
+ * and the implementation "under the hood" can be improved later, if needed.
3192
+ */
3193
+ /**
3194
+ * Clones an input value to avoid side effects. Use it only in justified cases,
3195
+ * because it can impact performance negatively.
3196
+ *
3197
+ * @param value Value to clone
3198
+ * @returns Cloned value
3199
+ */
3200
+ function clone(value) {
3201
+ // TODO: Replace cloneDeep with a more efficient implementation
3202
+ return cloneDeep(value);
3203
+ }
3204
+
3183
3205
  /**
3184
3206
  * @file Additional / helper functions for ECSSTree / CSSTree.
3207
+ *
3208
+ * @note There are no tests for some functions, but during the AGTree optimization we remove them anyway.
3185
3209
  */
3186
3210
  /**
3187
3211
  * Common CSSTree parsing options.
@@ -3317,10 +3341,10 @@ class CssTree {
3317
3341
  ast = CssTree.parse(selectorList, CssTreeParserContext.selectorList);
3318
3342
  }
3319
3343
  else {
3320
- ast = cloneDeep(selectorList);
3344
+ ast = clone(selectorList);
3321
3345
  }
3322
3346
  const nodes = [];
3323
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3347
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3324
3348
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3325
3349
  walk(ast, (node) => {
3326
3350
  if (CssTree.isExtendedCssNode(node, pseudoClasses, attributeSelectors)) {
@@ -3349,9 +3373,9 @@ class CssTree {
3349
3373
  ast = CssTree.parse(selectorList, CssTreeParserContext.selectorList);
3350
3374
  }
3351
3375
  else {
3352
- ast = cloneDeep(selectorList);
3376
+ ast = selectorList;
3353
3377
  }
3354
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3378
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3355
3379
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3356
3380
  return find(ast, (node) => CssTree.isExtendedCssNode(node, pseudoClasses, attributeSelectors)) !== null;
3357
3381
  }
@@ -3388,14 +3412,14 @@ class CssTree {
3388
3412
  ast = CssTree.parse(declarationList, CssTreeParserContext.declarationList);
3389
3413
  }
3390
3414
  else {
3391
- ast = cloneDeep(declarationList);
3415
+ ast = clone(declarationList);
3392
3416
  }
3393
3417
  const nodes = [];
3394
3418
  // While walking the AST we should skip the nested functions,
3395
3419
  // for example skip url()s in cross-fade(url(), url()), since
3396
3420
  // cross-fade() itself is already a forbidden function
3397
3421
  let inForbiddenFunction = false;
3398
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3422
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3399
3423
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3400
3424
  walk(ast, {
3401
3425
  enter: (node) => {
@@ -3433,9 +3457,9 @@ class CssTree {
3433
3457
  ast = CssTree.parse(declarationList, CssTreeParserContext.declarationList);
3434
3458
  }
3435
3459
  else {
3436
- ast = cloneDeep(declarationList);
3460
+ ast = clone(declarationList);
3437
3461
  }
3438
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3462
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3439
3463
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3440
3464
  return find(ast, (node) => CssTree.isForbiddenFunction(node, forbiddenFunctions)) !== null;
3441
3465
  }
@@ -3667,6 +3691,180 @@ class CssTree {
3667
3691
  });
3668
3692
  return result.trim();
3669
3693
  }
3694
+ /**
3695
+ * Generates string representation of the selector list.
3696
+ *
3697
+ * @param ast SelectorList AST
3698
+ * @returns String representation of the selector list
3699
+ */
3700
+ static generateSelectorListPlain(ast) {
3701
+ const result = [];
3702
+ if (!ast.children || ast.children.length === 0) {
3703
+ throw new Error('Selector list cannot be empty');
3704
+ }
3705
+ ast.children.forEach((selector, index, nodeList) => {
3706
+ if (selector.type !== CssTreeNodeType.Selector) {
3707
+ throw new Error(`Unexpected node type: ${selector.type}`);
3708
+ }
3709
+ result.push(this.generateSelectorPlain(selector));
3710
+ // If there is a next node, add a comma and a space after the selector
3711
+ if (nodeList[index + 1]) {
3712
+ result.push(COMMA, SPACE);
3713
+ }
3714
+ });
3715
+ return result.join(EMPTY);
3716
+ }
3717
+ /**
3718
+ * Selector generation based on CSSTree's AST. This is necessary because CSSTree
3719
+ * only adds spaces in some edge cases.
3720
+ *
3721
+ * @param ast CSS Tree AST
3722
+ * @returns CSS selector as string
3723
+ */
3724
+ static generateSelectorPlain(ast) {
3725
+ let result = EMPTY;
3726
+ let inAttributeSelector = false;
3727
+ let depth = 0;
3728
+ let selectorListDepth = -1;
3729
+ let prevNode = ast;
3730
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3731
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3732
+ walk(ast, {
3733
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3734
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3735
+ enter: (node) => {
3736
+ depth += 1;
3737
+ // Skip attribute selector / selector list children
3738
+ if (inAttributeSelector || selectorListDepth > -1) {
3739
+ return;
3740
+ }
3741
+ switch (node.type) {
3742
+ // "Trivial" nodes
3743
+ case CssTreeNodeType.TypeSelector:
3744
+ result += node.name;
3745
+ break;
3746
+ case CssTreeNodeType.ClassSelector:
3747
+ result += DOT;
3748
+ result += node.name;
3749
+ break;
3750
+ case CssTreeNodeType.IdSelector:
3751
+ result += HASHMARK;
3752
+ result += node.name;
3753
+ break;
3754
+ case CssTreeNodeType.Identifier:
3755
+ result += node.name;
3756
+ break;
3757
+ case CssTreeNodeType.Raw:
3758
+ result += node.value;
3759
+ break;
3760
+ // "Advanced" nodes
3761
+ case CssTreeNodeType.Nth:
3762
+ // Default generation enough
3763
+ result += generate(node);
3764
+ break;
3765
+ // For example :not([id], [name])
3766
+ case CssTreeNodeType.SelectorList:
3767
+ // eslint-disable-next-line no-case-declarations
3768
+ const selectors = [];
3769
+ node.children.forEach((selector) => {
3770
+ if (selector.type === CssTreeNodeType.Selector) {
3771
+ selectors.push(CssTree.generateSelectorPlain(selector));
3772
+ }
3773
+ else if (selector.type === CssTreeNodeType.Raw) {
3774
+ selectors.push(selector.value);
3775
+ }
3776
+ });
3777
+ // Join selector lists
3778
+ result += selectors.join(COMMA + SPACE);
3779
+ // Skip nodes here
3780
+ selectorListDepth = depth;
3781
+ break;
3782
+ case CssTreeNodeType.Combinator:
3783
+ if (node.name === SPACE) {
3784
+ result += node.name;
3785
+ break;
3786
+ }
3787
+ // Prevent this case (unnecessary space): has( > .something)
3788
+ if (prevNode.type !== CssTreeNodeType.Selector) {
3789
+ result += SPACE;
3790
+ }
3791
+ result += node.name;
3792
+ result += SPACE;
3793
+ break;
3794
+ case CssTreeNodeType.AttributeSelector:
3795
+ result += OPEN_SQUARE_BRACKET;
3796
+ // Identifier name
3797
+ if (node.name) {
3798
+ result += node.name.name;
3799
+ }
3800
+ // Matcher operator, eg =
3801
+ if (node.matcher) {
3802
+ result += node.matcher;
3803
+ // Value can be String, Identifier or null
3804
+ if (node.value !== null) {
3805
+ // String node
3806
+ if (node.value.type === CssTreeNodeType.String) {
3807
+ result += generate(node.value);
3808
+ }
3809
+ else if (node.value.type === CssTreeNodeType.Identifier) {
3810
+ // Identifier node
3811
+ result += node.value.name;
3812
+ }
3813
+ }
3814
+ }
3815
+ // Flags
3816
+ if (node.flags) {
3817
+ // Space before flags
3818
+ result += SPACE;
3819
+ result += node.flags;
3820
+ }
3821
+ result += CLOSE_SQUARE_BRACKET;
3822
+ inAttributeSelector = true;
3823
+ break;
3824
+ case CssTreeNodeType.PseudoElementSelector:
3825
+ result += COLON;
3826
+ result += COLON;
3827
+ result += node.name;
3828
+ if (node.children !== null) {
3829
+ result += OPEN_PARENTHESIS;
3830
+ }
3831
+ break;
3832
+ case CssTreeNodeType.PseudoClassSelector:
3833
+ result += COLON;
3834
+ result += node.name;
3835
+ if (node.children !== null) {
3836
+ result += OPEN_PARENTHESIS;
3837
+ }
3838
+ break;
3839
+ }
3840
+ prevNode = node;
3841
+ },
3842
+ leave: (node) => {
3843
+ depth -= 1;
3844
+ if (node.type === CssTreeNodeType.SelectorList && depth + 1 === selectorListDepth) {
3845
+ selectorListDepth = -1;
3846
+ }
3847
+ if (selectorListDepth > -1) {
3848
+ return;
3849
+ }
3850
+ if (node.type === CssTreeNodeType.AttributeSelector) {
3851
+ inAttributeSelector = false;
3852
+ }
3853
+ if (inAttributeSelector) {
3854
+ return;
3855
+ }
3856
+ switch (node.type) {
3857
+ case CssTreeNodeType.PseudoElementSelector:
3858
+ case CssTreeNodeType.PseudoClassSelector:
3859
+ if (node.children) {
3860
+ result += CLOSE_PARENTHESIS;
3861
+ }
3862
+ break;
3863
+ }
3864
+ },
3865
+ });
3866
+ return result.trim();
3867
+ }
3670
3868
  /**
3671
3869
  * Block generation based on CSSTree's AST. This is necessary because CSSTree only adds spaces in some edge cases.
3672
3870
  *
@@ -3850,6 +4048,29 @@ class CssTree {
3850
4048
  });
3851
4049
  return result;
3852
4050
  }
4051
+ /**
4052
+ * Helper function to generate a raw string from a function selector's children
4053
+ *
4054
+ * @param node Function node
4055
+ * @returns Generated function value
4056
+ * @example `responseheader(name)` -> `name`
4057
+ */
4058
+ static generateFunctionPlainValue(node) {
4059
+ const result = [];
4060
+ node.children?.forEach((child) => {
4061
+ switch (child.type) {
4062
+ case CssTreeNodeType.Raw:
4063
+ result.push(child.value);
4064
+ break;
4065
+ default:
4066
+ // Fallback to CSSTree's default generate function
4067
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
4068
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4069
+ result.push(generate(child));
4070
+ }
4071
+ });
4072
+ return result.join(EMPTY);
4073
+ }
3853
4074
  }
3854
4075
 
3855
4076
  /**
@@ -3897,7 +4118,7 @@ class ElementHidingBodyParser {
3897
4118
  * @throws If the AST is invalid
3898
4119
  */
3899
4120
  static generate(ast) {
3900
- return CssTree.generateSelectorList(fromPlainObject(ast.selectorList));
4121
+ return CssTree.generateSelectorListPlain(ast.selectorList);
3901
4122
  }
3902
4123
  }
3903
4124
 
@@ -4141,7 +4362,7 @@ class CssInjectionBodyParser {
4141
4362
  if (mediaQueryList || declarationList || remove) {
4142
4363
  throw new AdblockSyntaxError(
4143
4364
  // eslint-disable-next-line max-len
4144
- 'Invalid selector, regular selector elements can\'t be used after special pseudo-classes', {
4365
+ 'Invalid selector, regular selector elements cannot be used after special pseudo-classes', {
4145
4366
  start: node.loc?.start ?? loc,
4146
4367
  end: shiftLoc(loc, raw.length),
4147
4368
  });
@@ -4830,7 +5051,7 @@ function createModifierListNode(modifiers = []) {
4830
5051
  const result = {
4831
5052
  type: 'ModifierList',
4832
5053
  // We need to clone the modifiers to avoid side effects
4833
- children: cloneDeep(modifiers),
5054
+ children: modifiers.length ? clone(modifiers) : [],
4834
5055
  };
4835
5056
  return result;
4836
5057
  }
@@ -4870,8 +5091,9 @@ function hasUboModifierIndicator(rawSelectorList) {
4870
5091
  * @returns Linked list based selector
4871
5092
  */
4872
5093
  function convertSelectorToLinkedList(selector) {
5094
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
4873
5095
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4874
- return fromPlainObject(cloneDeep(selector));
5096
+ return fromPlainObject(clone(selector));
4875
5097
  }
4876
5098
  /**
4877
5099
  * Helper function that always returns the linked list version of the
@@ -4881,8 +5103,9 @@ function convertSelectorToLinkedList(selector) {
4881
5103
  * @returns Linked list based selector list
4882
5104
  */
4883
5105
  function convertSelectorListToLinkedList(selectorList) {
5106
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
4884
5107
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4885
- return fromPlainObject(cloneDeep(selectorList));
5108
+ return fromPlainObject(clone(selectorList));
4886
5109
  }
4887
5110
  /**
4888
5111
  * Helper function for checking and removing bounding combinators
@@ -5967,7 +6190,8 @@ class FilterListParser {
5967
6190
  */
5968
6191
  static generate(ast, preferRaw = false) {
5969
6192
  let result = EMPTY;
5970
- for (const rule of ast.children) {
6193
+ for (let i = 0; i < ast.children.length; i += 1) {
6194
+ const rule = ast.children[i];
5971
6195
  if (preferRaw && rule.raws?.text) {
5972
6196
  result += rule.raws.text;
5973
6197
  }
@@ -5984,6 +6208,11 @@ class FilterListParser {
5984
6208
  case 'lf':
5985
6209
  result += LF;
5986
6210
  break;
6211
+ default:
6212
+ if (i !== ast.children.length - 1) {
6213
+ result += LF;
6214
+ }
6215
+ break;
5987
6216
  }
5988
6217
  }
5989
6218
  return result;
@@ -6125,7 +6354,7 @@ var data$N = { adg_os_any:{ name:"csp",
6125
6354
  assignable:true,
6126
6355
  negatable:false,
6127
6356
  value_optional:true,
6128
- value_format:"(?xi)\n ^(\n base-uri|\n child-src|\n connect-src|\n default-src|\n font-src|\n form-action|\n frame-ancestors|\n frame-src|\n img-src|\n manifest-src|\n media-src|\n navigate-to|\n object-src|\n plugin-types|\n prefetch-src|\n report-to|\n report-uri|\n sandbox|\n script-src|\n style-src|\n upgrade-insecure-requests|\n worker-src|\n )\n \\s+\n \\S{1,}" },
6357
+ value_format:"csp_value" },
6129
6358
  adg_ext_any:{ name:"csp",
6130
6359
  description:"This modifier completely changes the rule behavior.\nIf it is applied to a rule, it will not block the matching request.\nThe response headers are going to be modified instead.",
6131
6360
  docs:"https://adguard.app/kb/general/ad-filtering/create-own-filters/#csp-modifier",
@@ -6137,7 +6366,7 @@ var data$N = { adg_os_any:{ name:"csp",
6137
6366
  assignable:true,
6138
6367
  negatable:false,
6139
6368
  value_optional:true,
6140
- value_format:"(?xi)\n ^(\n base-uri|\n child-src|\n connect-src|\n default-src|\n font-src|\n form-action|\n frame-ancestors|\n frame-src|\n img-src|\n manifest-src|\n media-src|\n navigate-to|\n object-src|\n plugin-types|\n prefetch-src|\n report-to|\n report-uri|\n sandbox|\n script-src|\n style-src|\n upgrade-insecure-requests|\n worker-src|\n )\n \\s+\n \\S{1,}" },
6369
+ value_format:"csp_value" },
6141
6370
  abp_ext_any:{ name:"csp",
6142
6371
  description:"This modifier completely changes the rule behavior.\nIf it is applied to a rule, it will not block the matching request.\nThe response headers are going to be modified instead.",
6143
6372
  docs:"https://help.adblockplus.org/hc/en-us/articles/360062733293-How-to-write-filters#content-security-policies",
@@ -6147,7 +6376,7 @@ var data$N = { adg_os_any:{ name:"csp",
6147
6376
  assignable:true,
6148
6377
  negatable:false,
6149
6378
  value_optional:true,
6150
- value_format:"(?xi)\n ^(\n base-uri|\n child-src|\n connect-src|\n default-src|\n font-src|\n form-action|\n frame-ancestors|\n frame-src|\n img-src|\n manifest-src|\n media-src|\n navigate-to|\n object-src|\n plugin-types|\n prefetch-src|\n report-to|\n report-uri|\n sandbox|\n script-src|\n style-src|\n upgrade-insecure-requests|\n worker-src|\n )\n \\s+\n \\S{1,}" },
6379
+ value_format:"csp_value" },
6151
6380
  ubo_ext_any:{ name:"csp",
6152
6381
  description:"This modifier completely changes the rule behavior.\nIf it is applied to a rule, it will not block the matching request.\nThe response headers are going to be modified instead.",
6153
6382
  docs:"https://github.com/gorhill/uBlock/wiki/Static-filter-syntax#csp",
@@ -6159,7 +6388,7 @@ var data$N = { adg_os_any:{ name:"csp",
6159
6388
  assignable:true,
6160
6389
  negatable:false,
6161
6390
  value_optional:true,
6162
- value_format:"(?xi)\n ^(\n base-uri|\n child-src|\n connect-src|\n default-src|\n font-src|\n form-action|\n frame-ancestors|\n frame-src|\n img-src|\n manifest-src|\n media-src|\n navigate-to|\n object-src|\n plugin-types|\n prefetch-src|\n report-to|\n report-uri|\n sandbox|\n script-src|\n style-src|\n upgrade-insecure-requests|\n worker-src|\n )\n \\s+\n \\S{1,}" } };
6391
+ value_format:"csp_value" } };
6163
6392
 
6164
6393
  var data$M = { adg_os_any:{ name:"denyallow",
6165
6394
  description:"The `$denyallow` modifier allows to avoid creating additional rules\nwhen it is needed to disable a certain rule for specific domains.\n`$denyallow` matches only target domains and not referrer domains.",
@@ -6666,7 +6895,8 @@ var data$l = { adg_os_any:{ name:"permissions",
6666
6895
  inverse_conflicts:true,
6667
6896
  assignable:true,
6668
6897
  negatable:false,
6669
- value_format:"(?x)\n ^\n (\n ?:(\n accelerometer|\n ambient-light-sensor|\n autoplay|\n battery|\n camera|\n display-capture|\n document-domain|\n encrypted-media|\n execution-while-not-rendered|\n execution-while-out-of-viewport|\n fullscreen|\n gamepad|\n geolocation|\n gyroscope|\n hid|\n identity-credentials-get|\n idle-detection|\n local-fonts|\n magnetometer|\n microphone|\n midi|\n payment|\n picture-in-picture|\n publickey-credentials-create|\n publickey-credentials-get|\n screen-wake-lock|\n serial|\n speaker-selection|\n storage-access|\n usb|\n web-share|\n xr-spatial-tracking\n )\n =\\(\\)\n # optional escaped comma for multiple permissions\n (\\\\,(\\s+)?)?\n )+\n $" } };
6898
+ value_optional:true,
6899
+ value_format:"permissions_value" } };
6670
6900
 
6671
6901
  var data$k = { adg_any:{ name:"ping",
6672
6902
  description:"The rule corresponds to requests caused by either navigator.sendBeacon() or the ping attribute on links.",
@@ -7327,15 +7557,104 @@ const ALLOWED_STEALTH_OPTIONS = new Set([
7327
7557
  'xclientdata',
7328
7558
  'dpi',
7329
7559
  ]);
7560
+ /**
7561
+ * Allowed CSP directives for $csp modifier.
7562
+ *
7563
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives}
7564
+ */
7565
+ const ALLOWED_CSP_DIRECTIVES = new Set([
7566
+ 'base-uri',
7567
+ 'child-src',
7568
+ 'connect-src',
7569
+ 'default-src',
7570
+ 'font-src',
7571
+ 'form-action',
7572
+ 'frame-ancestors',
7573
+ 'frame-src',
7574
+ 'img-src',
7575
+ 'manifest-src',
7576
+ 'media-src',
7577
+ 'navigate-to',
7578
+ 'object-src',
7579
+ 'plugin-types',
7580
+ 'prefetch-src',
7581
+ 'report-to',
7582
+ 'report-uri',
7583
+ 'sandbox',
7584
+ 'script-src',
7585
+ 'style-src',
7586
+ 'upgrade-insecure-requests',
7587
+ 'worker-src',
7588
+ ]);
7589
+ /**
7590
+ * Allowed stealth options for $permissions modifier.
7591
+ *
7592
+ * @see {@link https://adguard.app/kb/general/ad-filtering/create-own-filters/#permissions-modifier}
7593
+ */
7594
+ const ALLOWED_PERMISSION_DIRECTIVES = new Set([
7595
+ 'accelerometer',
7596
+ 'ambient-light-sensor',
7597
+ 'autoplay',
7598
+ 'battery',
7599
+ 'camera',
7600
+ 'display-capture',
7601
+ 'document-domain',
7602
+ 'encrypted-media',
7603
+ 'execution-while-not-rendered',
7604
+ 'execution-while-out-of-viewport',
7605
+ 'fullscreen',
7606
+ 'gamepad',
7607
+ 'geolocation',
7608
+ 'gyroscope',
7609
+ 'hid',
7610
+ 'identity-credentials-get',
7611
+ 'idle-detection',
7612
+ 'local-fonts',
7613
+ 'magnetometer',
7614
+ 'microphone',
7615
+ 'midi',
7616
+ 'payment',
7617
+ 'picture-in-picture',
7618
+ 'publickey-credentials-create',
7619
+ 'publickey-credentials-get',
7620
+ 'screen-wake-lock',
7621
+ 'serial',
7622
+ 'speaker-selection',
7623
+ 'storage-access',
7624
+ 'usb',
7625
+ 'web-share',
7626
+ 'xr-spatial-tracking',
7627
+ ]);
7628
+ /**
7629
+ * One of available tokens for $permission modifier value.
7630
+ *
7631
+ * @see {@link https://w3c.github.io/webappsec-permissions-policy/#structured-header-serialization}
7632
+ */
7633
+ const PERMISSIONS_TOKEN_SELF = 'self';
7634
+ /**
7635
+ * One of allowlist values for $permissions modifier.
7636
+ *
7637
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy#allowlists}
7638
+ */
7639
+ const EMPTY_PERMISSIONS_ALLOWLIST = `${OPEN_PARENTHESIS}${CLOSE_PARENTHESIS}`;
7330
7640
  /**
7331
7641
  * Prefixes for error messages used in modifier validation.
7332
7642
  */
7333
7643
  const VALIDATION_ERROR_PREFIX = {
7334
7644
  BLOCK_ONLY: 'Only blocking rules may contain the modifier',
7335
7645
  EXCEPTION_ONLY: 'Only exception rules may contain the modifier',
7646
+ INVALID_CSP_DIRECTIVES: 'Invalid CSP directives for the modifier',
7336
7647
  INVALID_LIST_VALUES: 'Invalid values for the modifier',
7337
7648
  INVALID_NOOP: 'Invalid noop modifier',
7649
+ INVALID_PERMISSION_DIRECTIVE: 'Invalid Permissions-Policy directive for the modifier',
7650
+ INVALID_PERMISSION_ORIGINS: 'Origins in the value is invalid for the modifier and the directive',
7651
+ INVALID_PERMISSION_ORIGIN_QUOTES: 'Double quotes should be used for origins in the value of the modifier',
7338
7652
  MIXED_NEGATIONS: 'Simultaneous usage of negated and not negated values is forbidden for the modifier',
7653
+ NO_CSP_VALUE: 'No CSP value for the modifier and the directive',
7654
+ NO_CSP_DIRECTIVE_QUOTE: 'CSP directives should no be quoted for the modifier',
7655
+ NO_UNESCAPED_PERMISSION_COMMA: 'Unescaped comma in the value is not allowed for the modifier',
7656
+ // TODO: implement later for $scp and $permissions
7657
+ // NO_VALUE_ONLY_FOR_EXCEPTION: 'Modifier without value can be used only in exception rules',
7339
7658
  NOT_EXISTENT: 'Non-existent modifier',
7340
7659
  NOT_NEGATABLE_MODIFIER: 'Non-negatable modifier',
7341
7660
  NOT_NEGATABLE_VALUE: 'Values cannot be negated for the modifier',
@@ -7448,14 +7767,14 @@ const getSpecificBlockerData = (modifiersData, blockerPrefix, modifierName) => {
7448
7767
  * @example
7449
7768
  * `example.*` — matches with any TLD, e.g. `example.org`, `example.com`, etc.
7450
7769
  */
7451
- const WILDCARD_TLD = DOT + WILDCARD$1;
7770
+ const WILDCARD_TLD = DOT + WILDCARD;
7452
7771
  /**
7453
7772
  * Marker for a wildcard subdomain — `*.`.
7454
7773
  *
7455
7774
  * @example
7456
7775
  * `*.example.org` — matches with any subdomain, e.g. `foo.example.org` or `bar.example.org`
7457
7776
  */
7458
- const WILDCARD_SUBDOMAIN = WILDCARD$1 + DOT;
7777
+ const WILDCARD_SUBDOMAIN = WILDCARD + DOT;
7459
7778
  class DomainUtils {
7460
7779
  /**
7461
7780
  * Check if the input is a valid domain or hostname.
@@ -7466,7 +7785,7 @@ class DomainUtils {
7466
7785
  static isValidDomainOrHostname(domain) {
7467
7786
  let domainToCheck = domain;
7468
7787
  // Wildcard-only domain, typically a generic rule
7469
- if (domainToCheck === WILDCARD$1) {
7788
+ if (domainToCheck === WILDCARD) {
7470
7789
  return true;
7471
7790
  }
7472
7791
  // https://adguard.com/kb/general/ad-filtering/create-own-filters/#wildcard-for-tld
@@ -7647,10 +7966,12 @@ class QuoteUtils {
7647
7966
  var CustomValueFormatValidatorName;
7648
7967
  (function (CustomValueFormatValidatorName) {
7649
7968
  CustomValueFormatValidatorName["App"] = "pipe_separated_apps";
7969
+ CustomValueFormatValidatorName["Csp"] = "csp_value";
7650
7970
  // there are some differences between $domain and $denyallow
7651
7971
  CustomValueFormatValidatorName["DenyAllow"] = "pipe_separated_denyallow_domains";
7652
7972
  CustomValueFormatValidatorName["Domain"] = "pipe_separated_domains";
7653
7973
  CustomValueFormatValidatorName["Method"] = "pipe_separated_methods";
7974
+ CustomValueFormatValidatorName["Permissions"] = "permissions_value";
7654
7975
  CustomValueFormatValidatorName["StealthOption"] = "pipe_separated_stealth_options";
7655
7976
  })(CustomValueFormatValidatorName || (CustomValueFormatValidatorName = {}));
7656
7977
  /**
@@ -7684,7 +8005,7 @@ const isValidAppNameChunk = (chunk) => {
7684
8005
  const isValidAppModifierValue = (value) => {
7685
8006
  // $app modifier does not support wildcard tld
7686
8007
  // https://adguard.app/kb/general/ad-filtering/create-own-filters/#app-modifier
7687
- if (value.includes(WILDCARD$1)) {
8008
+ if (value.includes(WILDCARD)) {
7688
8009
  return false;
7689
8010
  }
7690
8011
  return value
@@ -7711,6 +8032,32 @@ const isValidMethodModifierValue = (value) => {
7711
8032
  const isValidStealthModifierValue = (value) => {
7712
8033
  return ALLOWED_STEALTH_OPTIONS.has(value);
7713
8034
  };
8035
+ /**
8036
+ * Checks whether the given `rawOrigin` is valid as Permissions Allowlist origin.
8037
+ *
8038
+ * @see {@link https://w3c.github.io/webappsec-permissions-policy/#allowlists}
8039
+ *
8040
+ * @param rawOrigin The raw origin.
8041
+ *
8042
+ * @returns True if the origin is valid, false otherwise.
8043
+ */
8044
+ const isValidPermissionsOrigin = (rawOrigin) => {
8045
+ // origins should be quoted by double quote
8046
+ const actualQuoteType = QuoteUtils.getStringQuoteType(rawOrigin);
8047
+ if (actualQuoteType !== QuoteType.Double) {
8048
+ return false;
8049
+ }
8050
+ const origin = QuoteUtils.removeQuotes(rawOrigin);
8051
+ try {
8052
+ // validate the origin by URL constructor
8053
+ // https://w3c.github.io/webappsec-permissions-policy/#algo-parse-policy-directive
8054
+ new URL(origin);
8055
+ }
8056
+ catch (e) {
8057
+ return false;
8058
+ }
8059
+ return true;
8060
+ };
7714
8061
  /**
7715
8062
  * Checks whether the given `value` is valid domain as $denyallow modifier value.
7716
8063
  * Important: wildcard tld are not supported, compared to $domain.
@@ -7723,7 +8070,7 @@ const isValidDenyAllowModifierValue = (value) => {
7723
8070
  // $denyallow modifier does not support wildcard tld
7724
8071
  // https://adguard.app/kb/general/ad-filtering/create-own-filters/#denyallow-modifier
7725
8072
  // but here we are simply checking whether the value contains wildcard `*`, not ends with `.*`
7726
- if (value.includes(WILDCARD$1)) {
8073
+ if (value.includes(WILDCARD)) {
7727
8074
  return false;
7728
8075
  }
7729
8076
  // TODO: add cache for domains validation
@@ -7902,59 +8249,241 @@ const validatePipeSeparatedStealthOptions = (modifier) => {
7902
8249
  return validateListItemsModifier(modifier, (raw) => StealthOptionListParser.parse(raw), isValidStealthModifierValue, customNoNegatedListItemsValidator);
7903
8250
  };
7904
8251
  /**
7905
- * Map of all available pre-defined validators for modifiers with custom `value_format`.
7906
- */
7907
- const CUSTOM_VALUE_FORMAT_MAP = {
7908
- [CustomValueFormatValidatorName.App]: validatePipeSeparatedApps,
7909
- [CustomValueFormatValidatorName.DenyAllow]: validatePipeSeparatedDenyAllowDomains,
7910
- [CustomValueFormatValidatorName.Domain]: validatePipeSeparatedDomains,
7911
- [CustomValueFormatValidatorName.Method]: validatePipeSeparatedMethods,
7912
- [CustomValueFormatValidatorName.StealthOption]: validatePipeSeparatedStealthOptions,
7913
- };
7914
- /**
7915
- * Returns whether the given `valueFormat` is a valid custom value format validator name.
7916
- *
7917
- * @param valueFormat Value format for the modifier.
7918
- *
7919
- * @returns True if `valueFormat` is a supported pre-defined value format validator name, false otherwise.
7920
- */
7921
- const isCustomValueFormatValidator = (valueFormat) => {
7922
- return Object.keys(CUSTOM_VALUE_FORMAT_MAP).includes(valueFormat);
7923
- };
7924
- /**
7925
- * Checks whether the value for given `modifier` is valid.
8252
+ * Validates `csp_value` custom value format.
8253
+ * Used for $csp modifier.
7926
8254
  *
7927
8255
  * @param modifier Modifier AST node.
7928
- * @param valueFormat Value format for the modifier.
7929
8256
  *
7930
8257
  * @returns Validation result.
7931
8258
  */
7932
- const validateValue = (modifier, valueFormat) => {
7933
- if (isCustomValueFormatValidator(valueFormat)) {
7934
- const validator = CUSTOM_VALUE_FORMAT_MAP[valueFormat];
7935
- return validator(modifier);
7936
- }
8259
+ const validateCspValue = (modifier) => {
7937
8260
  const modifierName = modifier.modifier.value;
7938
8261
  if (!modifier.value?.value) {
7939
8262
  return getValueRequiredValidationResult(modifierName);
7940
8263
  }
7941
- let xRegExp;
7942
- try {
7943
- xRegExp = XRegExp(valueFormat);
7944
- }
7945
- catch (e) {
7946
- throw new Error(`${SOURCE_DATA_ERROR_PREFIX.INVALID_VALUE_FORMAT_REGEXP}: '${modifierName}'`);
7947
- }
7948
- const isValid = xRegExp.test(modifier.value?.value);
7949
- if (!isValid) {
7950
- return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}'`);
8264
+ // $csp modifier value may contain multiple directives
8265
+ // e.g. "csp=child-src 'none'; frame-src 'self' *; worker-src 'none'"
8266
+ const policyDirectives = modifier.value.value
8267
+ .split(SEMICOLON)
8268
+ // rule with $csp modifier may end with semicolon
8269
+ // e.g. "$csp=sandbox allow-same-origin;"
8270
+ // TODO: add predicate helper for `(i) => !!i`
8271
+ .filter((i) => !!i);
8272
+ const invalidValueValidationResult = getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}': "${modifier.value.value}"`);
8273
+ if (policyDirectives.length === 0) {
8274
+ return invalidValueValidationResult;
8275
+ }
8276
+ const invalidDirectives = [];
8277
+ for (let i = 0; i < policyDirectives.length; i += 1) {
8278
+ const policyDirective = policyDirectives[i].trim();
8279
+ if (!policyDirective) {
8280
+ return invalidValueValidationResult;
8281
+ }
8282
+ const chunks = policyDirective.split(SPACE);
8283
+ const [directive, ...valueChunks] = chunks;
8284
+ // e.g. "csp=child-src 'none'; ; worker-src 'none'"
8285
+ // validator it here ↑
8286
+ if (!directive) {
8287
+ return invalidValueValidationResult;
8288
+ }
8289
+ if (!ALLOWED_CSP_DIRECTIVES.has(directive)) {
8290
+ // e.g. "csp='child-src' 'none'"
8291
+ if (ALLOWED_CSP_DIRECTIVES.has(QuoteUtils.removeQuotes(directive))) {
8292
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NO_CSP_DIRECTIVE_QUOTE}: '${modifierName}': ${directive}`);
8293
+ }
8294
+ invalidDirectives.push(directive);
8295
+ continue;
8296
+ }
8297
+ if (valueChunks.length === 0) {
8298
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NO_CSP_VALUE}: '${modifierName}': '${directive}'`);
8299
+ }
8300
+ }
8301
+ if (invalidDirectives.length > 0) {
8302
+ const directivesToStr = QuoteUtils.quoteAndJoinStrings(invalidDirectives, QuoteType.Double);
8303
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.INVALID_CSP_DIRECTIVES}: '${modifierName}': ${directivesToStr}`);
7951
8304
  }
7952
8305
  return { valid: true };
7953
8306
  };
7954
-
7955
8307
  /**
7956
- * @file Validator for modifiers.
7957
- */
8308
+ * Validates permission allowlist origins in the value of $permissions modifier.
8309
+ *
8310
+ * @see {@link https://w3c.github.io/webappsec-permissions-policy/#allowlists}
8311
+ *
8312
+ * @param allowlistChunks Array of allowlist chunks.
8313
+ * @param directive Permission directive name.
8314
+ * @param modifierName Modifier name.
8315
+ *
8316
+ * @returns Validation result.
8317
+ */
8318
+ const validatePermissionAllowlistOrigins = (allowlistChunks, directive, modifierName) => {
8319
+ const invalidOrigins = [];
8320
+ for (let i = 0; i < allowlistChunks.length; i += 1) {
8321
+ const chunk = allowlistChunks[i].trim();
8322
+ // skip few spaces between origins (they were splitted by space)
8323
+ // e.g. 'geolocation=("https://example.com" "https://*.example.com")'
8324
+ if (chunk.length === 0) {
8325
+ continue;
8326
+ }
8327
+ /**
8328
+ * 'self' should be checked case-insensitively
8329
+ *
8330
+ * @see {@link https://w3c.github.io/webappsec-permissions-policy/#algo-parse-policy-directive}
8331
+ *
8332
+ * @example 'geolocation=(self)'
8333
+ */
8334
+ if (chunk.toLowerCase() === PERMISSIONS_TOKEN_SELF) {
8335
+ continue;
8336
+ }
8337
+ if (QuoteUtils.getStringQuoteType(chunk) !== QuoteType.Double) {
8338
+ return getInvalidValidationResult(
8339
+ // eslint-disable-next-line max-len
8340
+ `${VALIDATION_ERROR_PREFIX.INVALID_PERMISSION_ORIGIN_QUOTES}: '${modifierName}': '${directive}': '${QuoteUtils.removeQuotes(chunk)}'`);
8341
+ }
8342
+ if (!isValidPermissionsOrigin(chunk)) {
8343
+ invalidOrigins.push(chunk);
8344
+ }
8345
+ }
8346
+ if (invalidOrigins.length > 0) {
8347
+ const originsToStr = QuoteUtils.quoteAndJoinStrings(invalidOrigins);
8348
+ return getInvalidValidationResult(
8349
+ // eslint-disable-next-line max-len
8350
+ `${VALIDATION_ERROR_PREFIX.INVALID_PERMISSION_ORIGINS}: '${modifierName}': '${directive}': ${originsToStr}`);
8351
+ }
8352
+ return { valid: true };
8353
+ };
8354
+ /**
8355
+ * Validates permission allowlist in the modifier value.
8356
+ *
8357
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy#allowlists}
8358
+ * @see {@link https://w3c.github.io/webappsec-permissions-policy/#allowlists}
8359
+ *
8360
+ * @param allowlist Allowlist value.
8361
+ * @param directive Permission directive name.
8362
+ * @param modifierName Modifier name.
8363
+ *
8364
+ * @returns Validation result.
8365
+ */
8366
+ const validatePermissionAllowlist = (allowlist, directive, modifierName) => {
8367
+ // `*` is one of available permissions tokens
8368
+ // e.g. 'fullscreen=*'
8369
+ // https://w3c.github.io/webappsec-permissions-policy/#structured-header-serialization
8370
+ if (allowlist === WILDCARD
8371
+ // e.g. 'autoplay=()'
8372
+ || allowlist === EMPTY_PERMISSIONS_ALLOWLIST) {
8373
+ return { valid: true };
8374
+ }
8375
+ if (!(allowlist.startsWith(OPEN_PARENTHESIS) && allowlist.endsWith(CLOSE_PARENTHESIS))) {
8376
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}'`);
8377
+ }
8378
+ const allowlistChunks = allowlist.slice(1, -1).split(SPACE);
8379
+ return validatePermissionAllowlistOrigins(allowlistChunks, directive, modifierName);
8380
+ };
8381
+ /**
8382
+ * Validates single permission in the modifier value.
8383
+ *
8384
+ * @param permission Single permission value.
8385
+ * @param modifierName Modifier name.
8386
+ * @param modifierValue Modifier value.
8387
+ *
8388
+ * @returns Validation result.
8389
+ */
8390
+ const validateSinglePermission = (permission, modifierName, modifierValue) => {
8391
+ // empty permission in the rule
8392
+ // e.g. 'permissions=storage-access=()\\, \\, camera=()'
8393
+ // the validator is here ↑
8394
+ if (!permission) {
8395
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}'`);
8396
+ }
8397
+ if (permission.includes(COMMA)) {
8398
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.NO_UNESCAPED_PERMISSION_COMMA}: '${modifierName}': '${modifierValue}'`);
8399
+ }
8400
+ const [directive, allowlist] = permission.split(EQUALS);
8401
+ if (!ALLOWED_PERMISSION_DIRECTIVES.has(directive)) {
8402
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.INVALID_PERMISSION_DIRECTIVE}: '${modifierName}': '${directive}'`);
8403
+ }
8404
+ return validatePermissionAllowlist(allowlist, directive, modifierName);
8405
+ };
8406
+ /**
8407
+ * Validates `permissions_value` custom value format.
8408
+ * Used for $permissions modifier.
8409
+ *
8410
+ * @param modifier Modifier AST node.
8411
+ *
8412
+ * @returns Validation result.
8413
+ */
8414
+ const validatePermissions = (modifier) => {
8415
+ if (!modifier.value?.value) {
8416
+ return getValueRequiredValidationResult(modifier.modifier.value);
8417
+ }
8418
+ const modifierName = modifier.modifier.value;
8419
+ const modifierValue = modifier.value.value;
8420
+ // multiple permissions may be separated by escaped commas
8421
+ const permissions = modifier.value.value.split(`${BACKSLASH}${COMMA}`);
8422
+ for (let i = 0; i < permissions.length; i += 1) {
8423
+ const permission = permissions[i].trim();
8424
+ const singlePermissionValidationResult = validateSinglePermission(permission, modifierName, modifierValue);
8425
+ if (!singlePermissionValidationResult.valid) {
8426
+ return singlePermissionValidationResult;
8427
+ }
8428
+ }
8429
+ return { valid: true };
8430
+ };
8431
+ /**
8432
+ * Map of all available pre-defined validators for modifiers with custom `value_format`.
8433
+ */
8434
+ const CUSTOM_VALUE_FORMAT_MAP = {
8435
+ [CustomValueFormatValidatorName.App]: validatePipeSeparatedApps,
8436
+ [CustomValueFormatValidatorName.Csp]: validateCspValue,
8437
+ [CustomValueFormatValidatorName.DenyAllow]: validatePipeSeparatedDenyAllowDomains,
8438
+ [CustomValueFormatValidatorName.Domain]: validatePipeSeparatedDomains,
8439
+ [CustomValueFormatValidatorName.Method]: validatePipeSeparatedMethods,
8440
+ [CustomValueFormatValidatorName.Permissions]: validatePermissions,
8441
+ [CustomValueFormatValidatorName.StealthOption]: validatePipeSeparatedStealthOptions,
8442
+ };
8443
+ /**
8444
+ * Returns whether the given `valueFormat` is a valid custom value format validator name.
8445
+ *
8446
+ * @param valueFormat Value format for the modifier.
8447
+ *
8448
+ * @returns True if `valueFormat` is a supported pre-defined value format validator name, false otherwise.
8449
+ */
8450
+ const isCustomValueFormatValidator = (valueFormat) => {
8451
+ return Object.keys(CUSTOM_VALUE_FORMAT_MAP).includes(valueFormat);
8452
+ };
8453
+ /**
8454
+ * Checks whether the value for given `modifier` is valid.
8455
+ *
8456
+ * @param modifier Modifier AST node.
8457
+ * @param valueFormat Value format for the modifier.
8458
+ *
8459
+ * @returns Validation result.
8460
+ */
8461
+ const validateValue = (modifier, valueFormat) => {
8462
+ if (isCustomValueFormatValidator(valueFormat)) {
8463
+ const validator = CUSTOM_VALUE_FORMAT_MAP[valueFormat];
8464
+ return validator(modifier);
8465
+ }
8466
+ const modifierName = modifier.modifier.value;
8467
+ if (!modifier.value?.value) {
8468
+ return getValueRequiredValidationResult(modifierName);
8469
+ }
8470
+ let xRegExp;
8471
+ try {
8472
+ xRegExp = XRegExp(valueFormat);
8473
+ }
8474
+ catch (e) {
8475
+ throw new Error(`${SOURCE_DATA_ERROR_PREFIX.INVALID_VALUE_FORMAT_REGEXP}: '${modifierName}'`);
8476
+ }
8477
+ const isValid = xRegExp.test(modifier.value?.value);
8478
+ if (!isValid) {
8479
+ return getInvalidValidationResult(`${VALIDATION_ERROR_PREFIX.VALUE_INVALID}: '${modifierName}'`);
8480
+ }
8481
+ return { valid: true };
8482
+ };
8483
+
8484
+ /**
8485
+ * @file Validator for modifiers.
8486
+ */
7958
8487
  /**
7959
8488
  * Fully checks whether the given `modifier` valid for given blocker `syntax`:
7960
8489
  * is it supported by the blocker, deprecated, assignable, negatable, etc.
@@ -8011,6 +8540,10 @@ const validateForSpecificSyntax = (modifiersData, syntax, modifier, isException)
8011
8540
  // e.g. 'domain'
8012
8541
  if (specificBlockerData[SpecificKey.Assignable]) {
8013
8542
  if (!modifier.value) {
8543
+ // TODO: ditch value_optional after custom validators are implemented for value_format for all modifiers.
8544
+ // This checking should be done in each separate custom validator,
8545
+ // because $csp and $permissions without value can be used only in extension rules,
8546
+ // but $cookie with no value can be used in both blocking and exception rules.
8014
8547
  /**
8015
8548
  * Some assignable modifiers can be used without a value,
8016
8549
  * e.g. '@@||example.com^$cookie'.
@@ -8098,7 +8631,7 @@ class ModifierValidator {
8098
8631
  * @returns Result of modifier validation.
8099
8632
  */
8100
8633
  validate = (syntax, rawModifier, isException = false) => {
8101
- const modifier = cloneDeep(rawModifier);
8634
+ const modifier = clone(rawModifier);
8102
8635
  // special case: handle noop modifier which may be used as multiple underscores (not just one)
8103
8636
  // https://adguard.com/kb/general/ad-filtering/create-own-filters/#noop-modifier
8104
8637
  if (modifier.modifier.value.startsWith(UNDERSCORE)) {
@@ -8177,7 +8710,9 @@ class ConverterBase {
8177
8710
  * Converts some data to AdGuard format
8178
8711
  *
8179
8712
  * @param data Data to convert
8180
- * @returns Converted data
8713
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
8714
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
8715
+ * If the node was not converted, the result will contain the original node with the same object reference
8181
8716
  * @throws If the data is invalid or incompatible
8182
8717
  */
8183
8718
  static convertToAdg(data) {
@@ -8187,7 +8722,9 @@ class ConverterBase {
8187
8722
  * Converts some data to Adblock Plus format
8188
8723
  *
8189
8724
  * @param data Data to convert
8190
- * @returns Converted data
8725
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
8726
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
8727
+ * If the node was not converted, the result will contain the original node with the same object reference
8191
8728
  * @throws If the data is invalid or incompatible
8192
8729
  */
8193
8730
  static convertToAbp(data) {
@@ -8197,7 +8734,9 @@ class ConverterBase {
8197
8734
  * Converts some data to uBlock Origin format
8198
8735
  *
8199
8736
  * @param data Data to convert
8200
- * @returns Converted data
8737
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
8738
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
8739
+ * If the node was not converted, the result will contain the original node with the same object reference
8201
8740
  * @throws If the data is invalid or incompatible
8202
8741
  */
8203
8742
  static convertToUbo(data) {
@@ -8221,7 +8760,9 @@ class RuleConverterBase extends ConverterBase {
8221
8760
  * Converts an adblock filtering rule to AdGuard format, if possible.
8222
8761
  *
8223
8762
  * @param rule Rule node to convert
8224
- * @returns Array of converted rule nodes
8763
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8764
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8765
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8225
8766
  * @throws If the rule is invalid or cannot be converted
8226
8767
  */
8227
8768
  static convertToAdg(rule) {
@@ -8231,7 +8772,9 @@ class RuleConverterBase extends ConverterBase {
8231
8772
  * Converts an adblock filtering rule to Adblock Plus format, if possible.
8232
8773
  *
8233
8774
  * @param rule Rule node to convert
8234
- * @returns Array of converted rule nodes
8775
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8776
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8777
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8235
8778
  * @throws If the rule is invalid or cannot be converted
8236
8779
  */
8237
8780
  static convertToAbp(rule) {
@@ -8241,7 +8784,9 @@ class RuleConverterBase extends ConverterBase {
8241
8784
  * Converts an adblock filtering rule to uBlock Origin format, if possible.
8242
8785
  *
8243
8786
  * @param rule Rule node to convert
8244
- * @returns Array of converted rule nodes
8787
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8788
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8789
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8245
8790
  * @throws If the rule is invalid or cannot be converted
8246
8791
  */
8247
8792
  static convertToUbo(rule) {
@@ -8249,6 +8794,37 @@ class RuleConverterBase extends ConverterBase {
8249
8794
  }
8250
8795
  }
8251
8796
 
8797
+ /**
8798
+ * @file Conversion result interface and helper functions
8799
+ */
8800
+ /**
8801
+ * Helper function to create a generic conversion result.
8802
+ *
8803
+ * @param result Conversion result
8804
+ * @param isConverted Indicates whether the input item was converted
8805
+ * @template T Type of the item to convert
8806
+ * @template U Type of the conversion result (defaults to `T`, but can be `T[]` as well)
8807
+ * @returns Generic conversion result
8808
+ */
8809
+ // eslint-disable-next-line max-len
8810
+ function createConversionResult(result, isConverted) {
8811
+ return {
8812
+ result,
8813
+ isConverted,
8814
+ };
8815
+ }
8816
+ /**
8817
+ * Helper function to create a node conversion result.
8818
+ *
8819
+ * @param nodes Array of nodes
8820
+ * @param isConverted Indicates whether the input item was converted
8821
+ * @template T Type of the node (extends `Node`)
8822
+ * @returns Node conversion result
8823
+ */
8824
+ function createNodeConversionResult(nodes, isConverted) {
8825
+ return createConversionResult(nodes, isConverted);
8826
+ }
8827
+
8252
8828
  /**
8253
8829
  * @file Comment rule converter
8254
8830
  */
@@ -8262,27 +8838,30 @@ class CommentRuleConverter extends RuleConverterBase {
8262
8838
  * Converts a comment rule to AdGuard format, if possible.
8263
8839
  *
8264
8840
  * @param rule Rule node to convert
8265
- * @returns Array of converted rule nodes
8841
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8842
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8843
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8266
8844
  * @throws If the rule is invalid or cannot be converted
8267
8845
  */
8268
8846
  static convertToAdg(rule) {
8269
- // Clone the provided AST node to avoid side effects
8270
- const ruleNode = cloneDeep(rule);
8271
8847
  // TODO: Add support for other comment types, if needed
8272
8848
  // Main task is # -> ! conversion
8273
- switch (ruleNode.type) {
8849
+ switch (rule.type) {
8274
8850
  case CommentRuleType.CommentRule:
8275
- // 'Comment' uBO style comments
8276
- if (ruleNode.type === CommentRuleType.CommentRule
8277
- && ruleNode.marker.value === CommentMarker.Hashmark) {
8278
- ruleNode.marker.value = CommentMarker.Regular;
8279
- // Add the hashmark to the beginning of the comment
8280
- ruleNode.text.value = `${SPACE}${CommentMarker.Hashmark}${ruleNode.text.value}`;
8851
+ // Check if the rule needs to be converted
8852
+ if (rule.type === CommentRuleType.CommentRule && rule.marker.value === CommentMarker.Hashmark) {
8853
+ // Add a ! to the beginning of the comment
8854
+ // TODO: Replace with custom clone method
8855
+ const ruleClone = clone(rule);
8856
+ ruleClone.marker.value = CommentMarker.Regular;
8857
+ // Add the hashmark to the beginning of the comment text
8858
+ ruleClone.text.value = `${SPACE}${CommentMarker.Hashmark}${ruleClone.text.value}`;
8859
+ return createNodeConversionResult([ruleClone], true);
8281
8860
  }
8282
- return [ruleNode];
8861
+ return createNodeConversionResult([rule], false);
8283
8862
  // Leave any other comment rule as is
8284
8863
  default:
8285
- return [ruleNode];
8864
+ return createNodeConversionResult([rule], false);
8286
8865
  }
8287
8866
  }
8288
8867
  }
@@ -8452,6 +9031,58 @@ class RegExpUtils {
8452
9031
  }
8453
9032
  }
8454
9033
 
9034
+ /**
9035
+ * @file Custom clone functions for AST nodes, this is probably the most efficient way to clone AST nodes.
9036
+ * @todo Maybe move them to parser classes as 'clone' methods
9037
+ */
9038
+ /**
9039
+ * Clones a scriptlet rule node.
9040
+ *
9041
+ * @param node Node to clone
9042
+ * @returns Cloned node
9043
+ */
9044
+ function cloneScriptletRuleNode(node) {
9045
+ return {
9046
+ type: node.type,
9047
+ children: node.children.map((child) => ({ ...child })),
9048
+ };
9049
+ }
9050
+ /**
9051
+ * Clones a domain list node.
9052
+ *
9053
+ * @param node Node to clone
9054
+ * @returns Cloned node
9055
+ */
9056
+ function cloneDomainListNode(node) {
9057
+ return {
9058
+ type: node.type,
9059
+ separator: node.separator,
9060
+ children: node.children.map((domain) => ({ ...domain })),
9061
+ };
9062
+ }
9063
+ /**
9064
+ * Clones a modifier list node.
9065
+ *
9066
+ * @param node Node to clone
9067
+ * @returns Cloned node
9068
+ */
9069
+ function cloneModifierListNode(node) {
9070
+ return {
9071
+ type: node.type,
9072
+ children: node.children.map((modifier) => {
9073
+ const res = {
9074
+ type: modifier.type,
9075
+ exception: modifier.exception,
9076
+ modifier: { ...modifier.modifier },
9077
+ };
9078
+ if (modifier.value) {
9079
+ res.value = { ...modifier.value };
9080
+ }
9081
+ return res;
9082
+ }),
9083
+ };
9084
+ }
9085
+
8455
9086
  /**
8456
9087
  * @file HTML filtering rule converter
8457
9088
  */
@@ -8464,16 +9095,22 @@ class RegExpUtils {
8464
9095
  *
8465
9096
  * @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#html-filtering-rules}
8466
9097
  */
8467
- const ADGUARD_HTML_DEFAULT_MAX_LENGTH = 8192;
8468
- const ADGUARD_HTML_CONVERSION_MAX_LENGTH = ADGUARD_HTML_DEFAULT_MAX_LENGTH * 32;
9098
+ const ADG_HTML_DEFAULT_MAX_LENGTH = 8192;
9099
+ const ADG_HTML_CONVERSION_MAX_LENGTH = ADG_HTML_DEFAULT_MAX_LENGTH * 32;
8469
9100
  const NOT_SPECIFIED = -1;
8470
- const CONTAINS$1 = 'contains';
8471
- const HAS_TEXT$1 = 'has-text';
8472
- const MAX_LENGTH = 'max-length';
8473
- const MIN_LENGTH = 'min-length';
8474
- const MIN_TEXT_LENGTH = 'min-text-length';
8475
- const TAG_CONTENT = 'tag-content';
8476
- const WILDCARD = 'wildcard';
9101
+ var PseudoClasses$1;
9102
+ (function (PseudoClasses) {
9103
+ PseudoClasses["Contains"] = "contains";
9104
+ PseudoClasses["HasText"] = "has-text";
9105
+ PseudoClasses["MinTextLength"] = "min-text-length";
9106
+ })(PseudoClasses$1 || (PseudoClasses$1 = {}));
9107
+ var AttributeSelectors;
9108
+ (function (AttributeSelectors) {
9109
+ AttributeSelectors["MaxLength"] = "max-length";
9110
+ AttributeSelectors["MinLength"] = "min-length";
9111
+ AttributeSelectors["TagContent"] = "tag-content";
9112
+ AttributeSelectors["Wildcard"] = "wildcard";
9113
+ })(AttributeSelectors || (AttributeSelectors = {}));
8477
9114
  /**
8478
9115
  * HTML filtering rule converter class
8479
9116
  *
@@ -8496,16 +9133,23 @@ class HtmlRuleConverter extends RuleConverterBase {
8496
9133
  * ```
8497
9134
  *
8498
9135
  * @param rule Rule node to convert
8499
- * @returns Array of converted rule nodes
9136
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9137
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9138
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8500
9139
  * @throws If the rule is invalid or cannot be converted
8501
9140
  */
8502
9141
  static convertToAdg(rule) {
8503
- // Clone the provided AST node to avoid side effects
8504
- const ruleNode = cloneDeep(rule);
9142
+ // Ignore AdGuard rules
9143
+ if (rule.syntax === AdblockSyntax.Adg) {
9144
+ return createNodeConversionResult([rule], false);
9145
+ }
9146
+ if (rule.syntax === AdblockSyntax.Abp) {
9147
+ throw new RuleConversionError('Invalid rule, ABP does not support HTML filtering rules');
9148
+ }
8505
9149
  // Prepare the conversion result
8506
9150
  const conversionResult = [];
8507
9151
  // Iterate over selector list
8508
- for (const selector of ruleNode.body.body.children) {
9152
+ for (const selector of rule.body.body.children) {
8509
9153
  // Check selector, just in case
8510
9154
  if (selector.type !== CssTreeNodeType.Selector) {
8511
9155
  throw new RuleConversionError(`Expected selector, got '${selector.type}'`);
@@ -8532,24 +9176,24 @@ class HtmlRuleConverter extends RuleConverterBase {
8532
9176
  throw new RuleConversionError('Tag selector should be the first child, if present');
8533
9177
  }
8534
9178
  // Simply store the tag selector
8535
- convertedSelector.children.push(cloneDeep(node));
9179
+ convertedSelector.children.push(clone(node));
8536
9180
  break;
8537
9181
  case CssTreeNodeType.AttributeSelector:
8538
9182
  // Check if the attribute selector is a special AdGuard attribute
8539
9183
  switch (node.name.name) {
8540
- case MIN_LENGTH:
9184
+ case AttributeSelectors.MinLength:
8541
9185
  minLength = CssTree.parseAttributeSelectorValueAsNumber(node);
8542
9186
  break;
8543
- case MAX_LENGTH:
9187
+ case AttributeSelectors.MaxLength:
8544
9188
  maxLength = CssTree.parseAttributeSelectorValueAsNumber(node);
8545
9189
  break;
8546
- case TAG_CONTENT:
8547
- case WILDCARD:
9190
+ case AttributeSelectors.TagContent:
9191
+ case AttributeSelectors.Wildcard:
8548
9192
  CssTree.assertAttributeSelectorHasStringValue(node);
8549
- convertedSelector.children.push(cloneDeep(node));
9193
+ convertedSelector.children.push(clone(node));
8550
9194
  break;
8551
9195
  default:
8552
- convertedSelector.children.push(cloneDeep(node));
9196
+ convertedSelector.children.push(clone(node));
8553
9197
  }
8554
9198
  break;
8555
9199
  case CssTreeNodeType.PseudoClassSelector:
@@ -8563,18 +9207,18 @@ class HtmlRuleConverter extends RuleConverterBase {
8563
9207
  }
8564
9208
  // Process the pseudo class based on its name
8565
9209
  switch (node.name) {
8566
- case HAS_TEXT$1:
8567
- case CONTAINS$1:
9210
+ case PseudoClasses$1.HasText:
9211
+ case PseudoClasses$1.Contains:
8568
9212
  // Check if the argument is a RegExp
8569
9213
  if (RegExpUtils.isRegexPattern(arg.value)) {
8570
9214
  // TODO: Add some support for RegExp patterns later
8571
9215
  // Need to find a way to convert some RegExp patterns to glob patterns
8572
9216
  throw new RuleConversionError('Conversion of RegExp patterns is not yet supported');
8573
9217
  }
8574
- convertedSelector.children.push(CssTree.createAttributeSelectorNode(TAG_CONTENT, arg.value));
9218
+ convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.TagContent, arg.value));
8575
9219
  break;
8576
9220
  // https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#subjectmin-text-lengthn
8577
- case MIN_TEXT_LENGTH:
9221
+ case PseudoClasses$1.MinTextLength:
8578
9222
  minLength = CssTree.parsePseudoClassArgumentAsNumber(node);
8579
9223
  break;
8580
9224
  default:
@@ -8586,10 +9230,10 @@ class HtmlRuleConverter extends RuleConverterBase {
8586
9230
  }
8587
9231
  }
8588
9232
  if (minLength !== NOT_SPECIFIED) {
8589
- convertedSelector.children.push(CssTree.createAttributeSelectorNode(MIN_LENGTH, String(minLength)));
9233
+ convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.MinLength, String(minLength)));
8590
9234
  }
8591
- convertedSelector.children.push(CssTree.createAttributeSelectorNode(MAX_LENGTH, String(maxLength === NOT_SPECIFIED
8592
- ? ADGUARD_HTML_CONVERSION_MAX_LENGTH
9235
+ convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.MaxLength, String(maxLength === NOT_SPECIFIED
9236
+ ? ADG_HTML_CONVERSION_MAX_LENGTH
8593
9237
  : maxLength)));
8594
9238
  // Create the converted rule
8595
9239
  conversionResult.push({
@@ -8599,7 +9243,7 @@ class HtmlRuleConverter extends RuleConverterBase {
8599
9243
  // Convert the separator based on the exception status
8600
9244
  separator: {
8601
9245
  type: 'Value',
8602
- value: ruleNode.exception
9246
+ value: rule.exception
8603
9247
  ? CosmeticRuleSeparator.AdgHtmlFilteringException
8604
9248
  : CosmeticRuleSeparator.AdgHtmlFiltering,
8605
9249
  },
@@ -8614,11 +9258,11 @@ class HtmlRuleConverter extends RuleConverterBase {
8614
9258
  }],
8615
9259
  },
8616
9260
  },
8617
- exception: ruleNode.exception,
8618
- domains: ruleNode.domains,
9261
+ exception: rule.exception,
9262
+ domains: cloneDomainListNode(rule.domains),
8619
9263
  });
8620
9264
  }
8621
- return conversionResult;
9265
+ return createNodeConversionResult(conversionResult, true);
8622
9266
  }
8623
9267
  }
8624
9268
 
@@ -8639,96 +9283,38 @@ function getScriptletName(scriptletNode) {
8639
9283
  return scriptletNode.children[0].value;
8640
9284
  }
8641
9285
  /**
8642
- * Set name of the scriptlet
9286
+ * Set name of the scriptlet.
9287
+ * Modifies input `scriptletNode` if needed.
8643
9288
  *
8644
9289
  * @param scriptletNode Scriptlet node to set name of
8645
9290
  * @param name Name to set
8646
- * @returns Scriptlet node with the specified name
8647
- * @throws If the scriptlet is empty
8648
9291
  */
8649
9292
  function setScriptletName(scriptletNode, name) {
8650
- if (scriptletNode.children.length === 0) {
8651
- throw new Error('Empty scriptlet');
9293
+ if (scriptletNode.children.length > 0) {
9294
+ // eslint-disable-next-line no-param-reassign
9295
+ scriptletNode.children[0].value = name;
8652
9296
  }
8653
- const scriptletNodeClone = cloneDeep(scriptletNode);
8654
- scriptletNodeClone.children[0].value = name;
8655
- return scriptletNodeClone;
8656
9297
  }
8657
9298
  /**
8658
9299
  * Set quote type of the scriptlet parameters
8659
9300
  *
8660
9301
  * @param scriptletNode Scriptlet node to set quote type of
8661
9302
  * @param quoteType Preferred quote type
8662
- * @returns Scriptlet node with the specified quote type
8663
9303
  */
8664
9304
  function setScriptletQuoteType(scriptletNode, quoteType) {
8665
- if (scriptletNode.children.length === 0) {
8666
- throw new Error('Empty scriptlet');
8667
- }
8668
- const scriptletNodeClone = cloneDeep(scriptletNode);
8669
- for (let i = 0; i < scriptletNodeClone.children.length; i += 1) {
8670
- scriptletNodeClone.children[i].value = QuoteUtils.setStringQuoteType(scriptletNodeClone.children[i].value, quoteType);
8671
- }
8672
- return scriptletNodeClone;
8673
- }
8674
-
8675
- /**
8676
- * @file Scriptlet conversions from ABP and uBO to ADG
8677
- */
8678
- const ABP_SCRIPTLET_PREFIX = 'abp-';
8679
- const UBO_SCRIPTLET_PREFIX = 'ubo-';
8680
- /**
8681
- * Helper class for converting scriptlets from ABP and uBO to ADG
8682
- */
8683
- class AdgScriptletConverter {
8684
- /**
8685
- * Helper function to convert scriptlets to ADG. We implement the core
8686
- * logic here to avoid code duplication.
8687
- *
8688
- * @param scriptletNode Scriptlet parameter list node to convert
8689
- * @param prefix Prefix to add to the scriptlet name
8690
- * @returns Converted scriptlet parameter list node
8691
- */
8692
- static convertToAdg(scriptletNode, prefix) {
8693
- // Remove possible quotes just to make it easier to work with the scriptlet name
8694
- const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletNode), QuoteType.None);
8695
- // Clone the node to avoid any side effects
8696
- let result = cloneDeep(scriptletNode);
8697
- // Only add prefix if it's not already there
8698
- if (!scriptletName.startsWith(prefix)) {
8699
- result = setScriptletName(scriptletNode, `${prefix}${scriptletName}`);
9305
+ if (scriptletNode.children.length > 0) {
9306
+ for (let i = 0; i < scriptletNode.children.length; i += 1) {
9307
+ // eslint-disable-next-line no-param-reassign
9308
+ scriptletNode.children[i].value = QuoteUtils.setStringQuoteType(scriptletNode.children[i].value, quoteType);
8700
9309
  }
8701
- // ADG scriptlet parameters should be quoted, and single quoted are preferred
8702
- result = setScriptletQuoteType(result, QuoteType.Single);
8703
- return result;
8704
9310
  }
8705
- /**
8706
- * Converts an ABP snippet node to ADG scriptlet node, if possible.
8707
- *
8708
- * @param scriptletNode Scriptlet node to convert
8709
- * @returns Converted scriptlet node
8710
- * @throws If the scriptlet isn't supported by ADG or is invalid
8711
- * @see {@link https://help.adblockplus.org/hc/en-us/articles/1500002338501#snippets-ref}
8712
- */
8713
- static convertFromAbp = (scriptletNode) => {
8714
- return AdgScriptletConverter.convertToAdg(scriptletNode, ABP_SCRIPTLET_PREFIX);
8715
- };
8716
- /**
8717
- * Convert a uBO scriptlet node to ADG scriptlet node, if possible.
8718
- *
8719
- * @param scriptletNode Scriptlet node to convert
8720
- * @returns Converted scriptlet node
8721
- * @throws If the scriptlet isn't supported by ADG or is invalid
8722
- * @see {@link https://github.com/gorhill/uBlock/wiki/Resources-Library#available-general-purpose-scriptlets}
8723
- */
8724
- static convertFromUbo = (scriptletNode) => {
8725
- return AdgScriptletConverter.convertToAdg(scriptletNode, UBO_SCRIPTLET_PREFIX);
8726
- };
8727
9311
  }
8728
9312
 
8729
9313
  /**
8730
9314
  * @file Scriptlet injection rule converter
8731
9315
  */
9316
+ const ABP_SCRIPTLET_PREFIX = 'abp-';
9317
+ const UBO_SCRIPTLET_PREFIX = 'ubo-';
8732
9318
  /**
8733
9319
  * Scriptlet injection rule converter class
8734
9320
  *
@@ -8739,38 +9325,91 @@ class ScriptletRuleConverter extends RuleConverterBase {
8739
9325
  * Converts a scriptlet injection rule to AdGuard format, if possible.
8740
9326
  *
8741
9327
  * @param rule Rule node to convert
8742
- * @returns Array of converted rule nodes
9328
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9329
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9330
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8743
9331
  * @throws If the rule is invalid or cannot be converted
8744
9332
  */
8745
9333
  static convertToAdg(rule) {
8746
- // Clone the provided AST node to avoid side effects
8747
- const ruleNode = cloneDeep(rule);
9334
+ // Ignore AdGuard rules
9335
+ if (rule.syntax === AdblockSyntax.Adg) {
9336
+ return createNodeConversionResult([rule], false);
9337
+ }
9338
+ const separator = rule.separator.value;
9339
+ let convertedSeparator = separator;
9340
+ convertedSeparator = rule.exception
9341
+ ? CosmeticRuleSeparator.AdgJsInjectionException
9342
+ : CosmeticRuleSeparator.AdgJsInjection;
8748
9343
  const convertedScriptlets = [];
8749
- for (const scriptlet of ruleNode.body.children) {
8750
- if (ruleNode.syntax === AdblockSyntax.Abp) {
8751
- convertedScriptlets.push(AdgScriptletConverter.convertFromAbp(scriptlet));
8752
- }
8753
- else if (ruleNode.syntax === AdblockSyntax.Ubo) {
8754
- convertedScriptlets.push(AdgScriptletConverter.convertFromUbo(scriptlet));
9344
+ for (const scriptlet of rule.body.children) {
9345
+ // Clone the node to avoid any side effects
9346
+ const scriptletClone = cloneScriptletRuleNode(scriptlet);
9347
+ // Remove possible quotes just to make it easier to work with the scriptlet name
9348
+ const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), QuoteType.None);
9349
+ // Add prefix if it's not already there
9350
+ let prefix;
9351
+ switch (rule.syntax) {
9352
+ case AdblockSyntax.Abp:
9353
+ prefix = ABP_SCRIPTLET_PREFIX;
9354
+ break;
9355
+ case AdblockSyntax.Ubo:
9356
+ prefix = UBO_SCRIPTLET_PREFIX;
9357
+ break;
9358
+ default:
9359
+ prefix = EMPTY;
8755
9360
  }
8756
- else if (ruleNode.syntax === AdblockSyntax.Adg) {
8757
- convertedScriptlets.push(scriptlet);
9361
+ if (!scriptletName.startsWith(prefix)) {
9362
+ setScriptletName(scriptletClone, `${prefix}${scriptletName}`);
8758
9363
  }
9364
+ // ADG scriptlet parameters should be quoted, and single quoted are preferred
9365
+ setScriptletQuoteType(scriptletClone, QuoteType.Single);
9366
+ convertedScriptlets.push(scriptletClone);
8759
9367
  }
8760
- ruleNode.separator.value = ruleNode.exception
8761
- ? CosmeticRuleSeparator.AdgJsInjectionException
8762
- : CosmeticRuleSeparator.AdgJsInjection;
8763
- // ADG doesn't support multiple scriptlets in one rule, so we should split them
8764
- return convertedScriptlets.map((scriptlet) => {
8765
- return {
8766
- ...ruleNode,
9368
+ return createNodeConversionResult(convertedScriptlets.map((scriptlet) => {
9369
+ const res = {
9370
+ category: rule.category,
9371
+ type: rule.type,
8767
9372
  syntax: AdblockSyntax.Adg,
9373
+ exception: rule.exception,
9374
+ domains: cloneDomainListNode(rule.domains),
9375
+ separator: {
9376
+ type: 'Value',
9377
+ value: convertedSeparator,
9378
+ },
8768
9379
  body: {
8769
- ...ruleNode.body,
9380
+ type: rule.body.type,
8770
9381
  children: [scriptlet],
8771
9382
  },
8772
9383
  };
8773
- });
9384
+ if (rule.modifiers) {
9385
+ res.modifiers = cloneModifierListNode(rule.modifiers);
9386
+ }
9387
+ return res;
9388
+ }), true);
9389
+ }
9390
+ }
9391
+
9392
+ /**
9393
+ * A very simple map extension that allows to store multiple values for the same key
9394
+ * by storing them in an array.
9395
+ *
9396
+ * @todo Add more methods if needed
9397
+ */
9398
+ class MultiValueMap extends Map {
9399
+ /**
9400
+ * Adds a value to the map. If the key already exists, the value will be appended to the existing array,
9401
+ * otherwise a new array will be created for the key.
9402
+ *
9403
+ * @param key Key to add
9404
+ * @param values Value(s) to add
9405
+ */
9406
+ add(key, ...values) {
9407
+ let currentValues = super.get(key);
9408
+ if (isUndefined(currentValues)) {
9409
+ currentValues = [];
9410
+ super.set(key, values);
9411
+ }
9412
+ currentValues.push(...values);
8774
9413
  }
8775
9414
  }
8776
9415
 
@@ -8796,69 +9435,115 @@ class AdgCosmeticRuleModifierConverter {
8796
9435
  * Converts a uBO cosmetic rule modifier list to ADG, if possible.
8797
9436
  *
8798
9437
  * @param modifierList Cosmetic rule modifier list node to convert
8799
- * @returns Converted cosmetic rule modifier list node
9438
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9439
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9440
+ * If the node was not converted, the result will contain the original node with the same object reference
8800
9441
  * @throws If the modifier list cannot be converted
8801
9442
  * @see {@link https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#cosmetic-filter-operators}
8802
9443
  */
8803
- static convertFromUbo = (modifierList) => {
8804
- const convertedModifierList = createModifierListNode();
8805
- for (const modifier of modifierList.children) {
8806
- let modifierValue;
8807
- switch (modifier.modifier.value) {
8808
- case UBO_MATCHES_PATH_OPERATOR:
8809
- // :matches-path() should have a value
8810
- if (!modifier.value) {
8811
- throw new RuleConversionError('Missing value for :matches-path(...)');
8812
- }
8813
- modifierValue = RegExpUtils.isRegexPattern(modifier.value.value)
8814
- ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS)
8815
- : modifier.value.value;
8816
- // Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
8817
- convertedModifierList.children.push(createModifierNode(ADG_PATH_MODIFIER,
8818
- // We should negate the regexp if the modifier is an exception
8819
- modifier.exception
8820
- // eslint-disable-next-line max-len
8821
- ? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(modifierValue))}${REGEX_MARKER}`
8822
- : modifierValue));
8823
- break;
8824
- default:
8825
- // Leave the modifier as-is
8826
- convertedModifierList.children.push(modifier);
9444
+ static convertFromUbo(modifierList) {
9445
+ const conversionMap = new MultiValueMap();
9446
+ modifierList.children.forEach((modifier, index) => {
9447
+ // :matches-path
9448
+ if (modifier.modifier.value === UBO_MATCHES_PATH_OPERATOR) {
9449
+ if (!modifier.value) {
9450
+ throw new RuleConversionError(`'${UBO_MATCHES_PATH_OPERATOR}' operator requires a value`);
9451
+ }
9452
+ const value = RegExpUtils.isRegexPattern(modifier.value.value)
9453
+ ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS)
9454
+ : modifier.value.value;
9455
+ // Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
9456
+ conversionMap.add(index, createModifierNode(ADG_PATH_MODIFIER,
9457
+ // We should negate the regexp if the modifier is an exception
9458
+ modifier.exception
9459
+ // eslint-disable-next-line max-len
9460
+ ? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(value))}${REGEX_MARKER}`
9461
+ : value));
8827
9462
  }
8828
- }
8829
- return convertedModifierList;
8830
- };
9463
+ });
9464
+ // Check if we have any converted modifiers
9465
+ if (conversionMap.size) {
9466
+ const modifierListClone = clone(modifierList);
9467
+ // Replace the original modifiers with the converted ones
9468
+ modifierListClone.children = modifierListClone.children.map((modifier, index) => {
9469
+ const convertedModifier = conversionMap.get(index);
9470
+ return convertedModifier ?? modifier;
9471
+ }).flat();
9472
+ return createConversionResult(modifierListClone, true);
9473
+ }
9474
+ // Otherwise, just return the original modifier list
9475
+ return createConversionResult(modifierList, false);
9476
+ }
8831
9477
  }
8832
9478
 
8833
- // Constants for pseudo-classes (please keep them sorted alphabetically)
8834
- const ABP_CONTAINS = '-abp-contains';
8835
- const ABP_HAS = '-abp-has';
8836
- const CONTAINS = 'contains';
8837
- const HAS = 'has';
8838
- const HAS_TEXT = 'has-text';
8839
- const MATCHES_CSS = 'matches-css';
8840
- const MATCHES_CSS_AFTER = 'matches-css-after';
8841
- const MATCHES_CSS_BEFORE = 'matches-css-before';
8842
- const NOT = 'not';
8843
- // Constants for pseudo-elements (please keep them sorted alphabetically)
8844
- const AFTER = 'after';
8845
- const BEFORE = 'before';
9479
+ var PseudoClasses;
9480
+ (function (PseudoClasses) {
9481
+ PseudoClasses["AbpContains"] = "-abp-contains";
9482
+ PseudoClasses["AbpHas"] = "-abp-has";
9483
+ PseudoClasses["Contains"] = "contains";
9484
+ PseudoClasses["Has"] = "has";
9485
+ PseudoClasses["HasText"] = "has-text";
9486
+ PseudoClasses["MatchesCss"] = "matches-css";
9487
+ PseudoClasses["MatchesCssAfter"] = "matches-css-after";
9488
+ PseudoClasses["MatchesCssBefore"] = "matches-css-before";
9489
+ PseudoClasses["Not"] = "not";
9490
+ })(PseudoClasses || (PseudoClasses = {}));
9491
+ var PseudoElements;
9492
+ (function (PseudoElements) {
9493
+ PseudoElements["After"] = "after";
9494
+ PseudoElements["Before"] = "before";
9495
+ })(PseudoElements || (PseudoElements = {}));
9496
+ const PSEUDO_ELEMENT_NAMES = new Set([
9497
+ PseudoElements.After,
9498
+ PseudoElements.Before,
9499
+ ]);
9500
+ const LEGACY_MATCHES_CSS_NAMES = new Set([
9501
+ PseudoClasses.MatchesCssAfter,
9502
+ PseudoClasses.MatchesCssBefore,
9503
+ ]);
9504
+ const LEGACY_EXT_CSS_INDICATOR_PSEUDO_NAMES = new Set([
9505
+ PseudoClasses.Not,
9506
+ PseudoClasses.MatchesCssBefore,
9507
+ PseudoClasses.MatchesCssAfter,
9508
+ ]);
9509
+ const CSS_CONVERSION_INDICATOR_PSEUDO_NAMES = new Set([
9510
+ PseudoClasses.AbpContains,
9511
+ PseudoClasses.AbpHas,
9512
+ PseudoClasses.HasText,
9513
+ ]);
8846
9514
  /**
8847
9515
  * Converts some pseudo-classes to pseudo-elements. For example:
8848
9516
  * - `:before` → `::before`
8849
9517
  *
8850
9518
  * @param selectorList Selector list to convert
8851
- * @returns Converted selector list
9519
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9520
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9521
+ * If the node was not converted, the result will contain the original node with the same object reference
8852
9522
  */
8853
9523
  function convertToPseudoElements(selectorList) {
8854
- // Prepare conversion result
8855
- const selectorListClone = cloneDeep(selectorList);
9524
+ // Check conversion indications before doing any heavy work
9525
+ const hasIndicator = find(
9526
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9527
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9528
+ selectorList, (node) => node.type === CssTreeNodeType.PseudoClassSelector && PSEUDO_ELEMENT_NAMES.has(node.name));
9529
+ if (!hasIndicator) {
9530
+ return createConversionResult(selectorList, false);
9531
+ }
9532
+ // Make a clone of the selector list to avoid modifying the original one,
9533
+ // then convert & return the cloned version
9534
+ const selectorListClone = clone(selectorList);
9535
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9536
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8856
9537
  walk(selectorListClone, {
9538
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9539
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8857
9540
  leave: (node) => {
8858
9541
  if (node.type === CssTreeNodeType.PseudoClassSelector) {
8859
- // :after ::after
8860
- // :before ::before
8861
- if (node.name === AFTER || node.name === BEFORE) {
9542
+ // If the pseudo-class is `:before` or `:after`, then we should
9543
+ // convert the node type to pseudo-element:
9544
+ // :after → ::after
9545
+ // :before → ::before
9546
+ if (PSEUDO_ELEMENT_NAMES.has(node.name)) {
8862
9547
  Object.assign(node, {
8863
9548
  ...node,
8864
9549
  type: CssTreeNodeType.PseudoElementSelector,
@@ -8867,7 +9552,7 @@ function convertToPseudoElements(selectorList) {
8867
9552
  }
8868
9553
  },
8869
9554
  });
8870
- return selectorListClone;
9555
+ return createConversionResult(selectorListClone, true);
8871
9556
  }
8872
9557
  /**
8873
9558
  * Converts legacy Extended CSS `matches-css-before` and `matches-css-after`
@@ -8876,33 +9561,36 @@ function convertToPseudoElements(selectorList) {
8876
9561
  * - `:matches-css-after(...)` → `:matches-css(after, ...)`
8877
9562
  *
8878
9563
  * @param node Node to convert
9564
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9565
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9566
+ * If the node was not converted, the result will contain the original node with the same object reference
8879
9567
  * @throws If the node is invalid
8880
9568
  */
8881
9569
  function convertLegacyMatchesCss(node) {
8882
- const nodeClone = cloneDeep(node);
8883
- if (nodeClone.type === CssTreeNodeType.PseudoClassSelector
8884
- && [MATCHES_CSS_BEFORE, MATCHES_CSS_AFTER].includes(nodeClone.name)) {
8885
- if (!nodeClone.children || nodeClone.children.size < 1) {
8886
- throw new Error(`Invalid ${nodeClone.name} pseudo-class: missing argument`);
8887
- }
8888
- // Remove the 'matches-css-' prefix to get the direction
8889
- const direction = nodeClone.name.substring(MATCHES_CSS.length + 1);
8890
- // Rename the pseudo-class
8891
- nodeClone.name = MATCHES_CSS;
8892
- // Add the direction to the first raw argument
8893
- const arg = nodeClone.children.first;
8894
- // Check argument
8895
- if (!arg) {
8896
- throw new Error(`Invalid ${nodeClone.name} pseudo-class: argument shouldn't be null`);
8897
- }
8898
- if (arg.type !== CssTreeNodeType.Raw) {
8899
- throw new Error(`Invalid ${nodeClone.name} pseudo-class: unexpected argument type`);
8900
- }
8901
- // Add the direction as the first argument
8902
- arg.value = `${direction},${arg.value}`;
8903
- // Replace the original node with the converted one
8904
- Object.assign(node, nodeClone);
8905
- }
9570
+ // Check conversion indications before doing any heavy work
9571
+ if (node.type !== CssTreeNodeType.PseudoClassSelector || !LEGACY_MATCHES_CSS_NAMES.has(node.name)) {
9572
+ return createConversionResult(node, false);
9573
+ }
9574
+ const nodeClone = clone(node);
9575
+ if (!nodeClone.children || nodeClone.children.length < 1) {
9576
+ throw new Error(`Invalid ${node.name} pseudo-class: missing argument`);
9577
+ }
9578
+ // Rename the pseudo-class
9579
+ nodeClone.name = PseudoClasses.MatchesCss;
9580
+ // Remove the 'matches-css-' prefix to get the direction
9581
+ const direction = node.name.substring(PseudoClasses.MatchesCss.length + 1);
9582
+ // Add the direction to the first raw argument
9583
+ const arg = nodeClone.children[0];
9584
+ // Check argument
9585
+ if (!arg) {
9586
+ throw new Error(`Invalid ${node.name} pseudo-class: argument shouldn't be null`);
9587
+ }
9588
+ if (arg.type !== CssTreeNodeType.Raw) {
9589
+ throw new Error(`Invalid ${node.name} pseudo-class: unexpected argument type`);
9590
+ }
9591
+ // Add the direction as the first argument
9592
+ arg.value = `${direction},${arg.value}`;
9593
+ return createConversionResult(nodeClone, true);
8906
9594
  }
8907
9595
  /**
8908
9596
  * Converts legacy Extended CSS selectors to the modern Extended CSS syntax.
@@ -8912,16 +9600,40 @@ function convertLegacyMatchesCss(node) {
8912
9600
  * - `[-ext-matches-css-before=...]` → `:matches-css(before, ...)`
8913
9601
  *
8914
9602
  * @param selectorList Selector list AST to convert
8915
- * @returns Converted selector list
9603
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9604
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9605
+ * If the node was not converted, the result will contain the original node with the same object reference
8916
9606
  */
8917
9607
  function convertFromLegacyExtendedCss(selectorList) {
8918
- // Prepare conversion result
8919
- const selectorListClone = cloneDeep(selectorList);
9608
+ // Check conversion indications before doing any heavy work
9609
+ const hasIndicator = find(
9610
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9611
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9612
+ selectorList, (node) => {
9613
+ if (node.type === CssTreeNodeType.PseudoClassSelector) {
9614
+ return LEGACY_EXT_CSS_INDICATOR_PSEUDO_NAMES.has(node.name);
9615
+ }
9616
+ if (node.type === CssTreeNodeType.AttributeSelector) {
9617
+ return node.name.name.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX);
9618
+ }
9619
+ return false;
9620
+ });
9621
+ if (!hasIndicator) {
9622
+ return createConversionResult(selectorList, false);
9623
+ }
9624
+ const selectorListClone = clone(selectorList);
9625
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9626
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8920
9627
  walk(selectorListClone, {
9628
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9629
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8921
9630
  leave: (node) => {
8922
9631
  // :matches-css-before(arg) → :matches-css(before,arg)
8923
9632
  // :matches-css-after(arg) → :matches-css(after,arg)
8924
- convertLegacyMatchesCss(node);
9633
+ const convertedLegacyExtCss = convertLegacyMatchesCss(node);
9634
+ if (convertedLegacyExtCss.isConverted) {
9635
+ Object.assign(node, convertedLegacyExtCss.result);
9636
+ }
8925
9637
  // [-ext-name=...] → :name(...)
8926
9638
  // [-ext-name='...'] → :name(...)
8927
9639
  // [-ext-name="..."] → :name(...)
@@ -8935,7 +9647,7 @@ function convertFromLegacyExtendedCss(selectorList) {
8935
9647
  // Remove the '-ext-' prefix to get the pseudo-class name
8936
9648
  const name = node.name.name.substring(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX.length);
8937
9649
  // Prepare the children list for the pseudo-class node
8938
- const children = new List();
9650
+ const children = [];
8939
9651
  // TODO: Change String node to Raw node to drop the quotes.
8940
9652
  // The structure of the node is the same, just the type
8941
9653
  // is different and generate() will generate the quotes
@@ -8948,7 +9660,7 @@ function convertFromLegacyExtendedCss(selectorList) {
8948
9660
  // For example, if the input is [-ext-has="> .selector"], then
8949
9661
  // we need to parse "> .selector" as a selector instead of string
8950
9662
  // it as a raw value
8951
- if ([HAS, NOT].includes(name)) {
9663
+ if ([PseudoClasses.Has, PseudoClasses.Not].includes(name)) {
8952
9664
  // Get the value of the attribute selector
8953
9665
  const { value } = node;
8954
9666
  // If the value is an identifier, then simply push it to the
@@ -8958,10 +9670,12 @@ function convertFromLegacyExtendedCss(selectorList) {
8958
9670
  }
8959
9671
  else if (value.type === CssTreeNodeType.String) {
8960
9672
  // Parse the value as a selector
8961
- const parsedChildren = CssTree.parse(value.value, CssTreeParserContext.selectorList);
9673
+ const parsedChildren = CssTree.parsePlain(value.value, CssTreeParserContext.selectorList);
8962
9674
  // Don't forget convert the parsed AST again, because
8963
9675
  // it was a raw string before
8964
- children.push(convertFromLegacyExtendedCss(parsedChildren));
9676
+ const convertedChildren = convertFromLegacyExtendedCss(parsedChildren);
9677
+ // Push the converted children to the list
9678
+ children.push(convertedChildren.result);
8965
9679
  }
8966
9680
  }
8967
9681
  else {
@@ -8988,14 +9702,12 @@ function convertFromLegacyExtendedCss(selectorList) {
8988
9702
  children,
8989
9703
  };
8990
9704
  // Handle this case: [-ext-matches-css-before=...] → :matches-css(before,...)
8991
- convertLegacyMatchesCss(pseudoNode);
8992
- // Convert attribute selector to pseudo-class selector, but
8993
- // keep the reference to the original node
8994
- Object.assign(node, pseudoNode);
9705
+ const convertedPseudoNode = convertLegacyMatchesCss(pseudoNode);
9706
+ Object.assign(node, convertedPseudoNode.isConverted ? convertedPseudoNode.result : pseudoNode);
8995
9707
  }
8996
9708
  },
8997
9709
  });
8998
- return selectorListClone;
9710
+ return createConversionResult(selectorListClone, true);
8999
9711
  }
9000
9712
  /**
9001
9713
  * CSS selector converter
@@ -9007,32 +9719,51 @@ class CssSelectorConverter extends ConverterBase {
9007
9719
  * Converts Extended CSS elements to AdGuard-compatible ones
9008
9720
  *
9009
9721
  * @param selectorList Selector list to convert
9010
- * @returns Converted selector list
9722
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9723
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9724
+ * If the node was not converted, the result will contain the original node with the same object reference
9011
9725
  * @throws If the rule is invalid or incompatible
9012
9726
  */
9013
9727
  static convertToAdg(selectorList) {
9014
9728
  // First, convert
9015
9729
  // - legacy Extended CSS selectors to the modern Extended CSS syntax and
9016
9730
  // - some pseudo-classes to pseudo-elements
9017
- const selectorListClone = convertToPseudoElements(convertFromLegacyExtendedCss(cloneDeep(selectorList)));
9731
+ const legacyExtCssConverted = convertFromLegacyExtendedCss(selectorList);
9732
+ const pseudoElementsConverted = convertToPseudoElements(legacyExtCssConverted.result);
9733
+ const hasIndicator = legacyExtCssConverted.isConverted || pseudoElementsConverted.isConverted || find(
9734
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9735
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9736
+ selectorList,
9737
+ // eslint-disable-next-line max-len
9738
+ (node) => node.type === CssTreeNodeType.PseudoClassSelector && CSS_CONVERSION_INDICATOR_PSEUDO_NAMES.has(node.name));
9739
+ if (!hasIndicator) {
9740
+ return createConversionResult(selectorList, false);
9741
+ }
9742
+ const selectorListClone = legacyExtCssConverted.isConverted || pseudoElementsConverted.isConverted
9743
+ ? pseudoElementsConverted.result
9744
+ : clone(selectorList);
9018
9745
  // Then, convert some Extended CSS pseudo-classes to AdGuard-compatible ones
9746
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9747
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9019
9748
  walk(selectorListClone, {
9749
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9750
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9020
9751
  leave: (node) => {
9021
9752
  if (node.type === CssTreeNodeType.PseudoClassSelector) {
9022
9753
  // :-abp-contains(...) → :contains(...)
9023
9754
  // :has-text(...) → :contains(...)
9024
- if (node.name === ABP_CONTAINS || node.name === HAS_TEXT) {
9025
- CssTree.renamePseudoClass(node, CONTAINS);
9755
+ if (node.name === PseudoClasses.AbpContains || node.name === PseudoClasses.HasText) {
9756
+ CssTree.renamePseudoClass(node, PseudoClasses.Contains);
9026
9757
  }
9027
9758
  // :-abp-has(...) → :has(...)
9028
- if (node.name === ABP_HAS) {
9029
- CssTree.renamePseudoClass(node, HAS);
9759
+ if (node.name === PseudoClasses.AbpHas) {
9760
+ CssTree.renamePseudoClass(node, PseudoClasses.Has);
9030
9761
  }
9031
9762
  // TODO: check uBO's `:others()` and `:watch-attr()` pseudo-classes
9032
9763
  }
9033
9764
  },
9034
9765
  });
9035
- return selectorListClone;
9766
+ return createConversionResult(selectorListClone, true);
9036
9767
  }
9037
9768
  }
9038
9769
 
@@ -9049,27 +9780,39 @@ class CssInjectionRuleConverter extends RuleConverterBase {
9049
9780
  * Converts a CSS injection rule to AdGuard format, if possible.
9050
9781
  *
9051
9782
  * @param rule Rule node to convert
9052
- * @returns Array of converted rule nodes
9783
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9784
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9785
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9053
9786
  * @throws If the rule is invalid or cannot be converted
9054
9787
  */
9055
9788
  static convertToAdg(rule) {
9056
- // Clone the provided AST node to avoid side effects
9057
- const ruleNode = cloneDeep(rule);
9789
+ const separator = rule.separator.value;
9790
+ let convertedSeparator = separator;
9058
9791
  // Change the separator if the rule contains ExtendedCSS selectors
9059
- if (CssTree.hasAnySelectorExtendedCssNode(ruleNode.body.selectorList) || ruleNode.body.remove) {
9060
- ruleNode.separator.value = ruleNode.exception
9792
+ if (CssTree.hasAnySelectorExtendedCssNode(rule.body.selectorList) || rule.body.remove) {
9793
+ convertedSeparator = rule.exception
9061
9794
  ? CosmeticRuleSeparator.AdgExtendedCssInjectionException
9062
9795
  : CosmeticRuleSeparator.AdgExtendedCssInjection;
9063
9796
  }
9064
9797
  else {
9065
- ruleNode.separator.value = ruleNode.exception
9798
+ convertedSeparator = rule.exception
9066
9799
  ? CosmeticRuleSeparator.AdgCssInjectionException
9067
9800
  : CosmeticRuleSeparator.AdgCssInjection;
9068
9801
  }
9069
- // Convert CSS selector list
9070
- Object.assign(ruleNode.body.selectorList, CssSelectorConverter.convertToAdg(fromPlainObject(ruleNode.body.selectorList)));
9071
- ruleNode.syntax = AdblockSyntax.Adg;
9072
- return [ruleNode];
9802
+ const convertedSelectorList = CssSelectorConverter.convertToAdg(rule.body.selectorList);
9803
+ // Check if the rule needs to be converted
9804
+ if (!(rule.syntax === AdblockSyntax.Common || rule.syntax === AdblockSyntax.Adg)
9805
+ || separator !== convertedSeparator
9806
+ || convertedSelectorList.isConverted) {
9807
+ // TODO: Replace with custom clone method
9808
+ const ruleClone = clone(rule);
9809
+ ruleClone.syntax = AdblockSyntax.Adg;
9810
+ ruleClone.separator.value = convertedSeparator;
9811
+ ruleClone.body.selectorList = convertedSelectorList.result;
9812
+ return createNodeConversionResult([ruleClone], true);
9813
+ }
9814
+ // Otherwise, return the original rule
9815
+ return createNodeConversionResult([rule], false);
9073
9816
  }
9074
9817
  }
9075
9818
 
@@ -9086,27 +9829,39 @@ class ElementHidingRuleConverter extends RuleConverterBase {
9086
9829
  * Converts an element hiding rule to AdGuard format, if possible.
9087
9830
  *
9088
9831
  * @param rule Rule node to convert
9089
- * @returns Array of converted rule nodes
9832
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9833
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9834
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9090
9835
  * @throws If the rule is invalid or cannot be converted
9091
9836
  */
9092
9837
  static convertToAdg(rule) {
9093
- // Clone the provided AST node to avoid side effects
9094
- const ruleNode = cloneDeep(rule);
9838
+ const separator = rule.separator.value;
9839
+ let convertedSeparator = separator;
9095
9840
  // Change the separator if the rule contains ExtendedCSS selectors
9096
- if (CssTree.hasAnySelectorExtendedCssNode(ruleNode.body.selectorList)) {
9097
- ruleNode.separator.value = ruleNode.exception
9841
+ if (CssTree.hasAnySelectorExtendedCssNode(rule.body.selectorList)) {
9842
+ convertedSeparator = rule.exception
9098
9843
  ? CosmeticRuleSeparator.ExtendedElementHidingException
9099
9844
  : CosmeticRuleSeparator.ExtendedElementHiding;
9100
9845
  }
9101
9846
  else {
9102
- ruleNode.separator.value = ruleNode.exception
9847
+ convertedSeparator = rule.exception
9103
9848
  ? CosmeticRuleSeparator.ElementHidingException
9104
9849
  : CosmeticRuleSeparator.ElementHiding;
9105
9850
  }
9106
- // Convert CSS selector list
9107
- Object.assign(ruleNode.body.selectorList, CssSelectorConverter.convertToAdg(fromPlainObject(ruleNode.body.selectorList)));
9108
- ruleNode.syntax = AdblockSyntax.Adg;
9109
- return [ruleNode];
9851
+ const convertedSelectorList = CssSelectorConverter.convertToAdg(rule.body.selectorList);
9852
+ // Check if the rule needs to be converted
9853
+ if (!(rule.syntax === AdblockSyntax.Common || rule.syntax === AdblockSyntax.Adg)
9854
+ || separator !== convertedSeparator
9855
+ || convertedSelectorList.isConverted) {
9856
+ // TODO: Replace with custom clone method
9857
+ const ruleClone = clone(rule);
9858
+ ruleClone.syntax = AdblockSyntax.Adg;
9859
+ ruleClone.separator.value = convertedSeparator;
9860
+ ruleClone.body.selectorList = convertedSelectorList.result;
9861
+ return createNodeConversionResult([ruleClone], true);
9862
+ }
9863
+ // Otherwise, return the original rule
9864
+ return createNodeConversionResult([rule], false);
9110
9865
  }
9111
9866
  }
9112
9867
 
@@ -9134,7 +9889,7 @@ function createNetworkRuleNode(pattern, modifiers = undefined, exception = false
9134
9889
  },
9135
9890
  };
9136
9891
  if (!isUndefined(modifiers)) {
9137
- result.modifiers = cloneDeep(modifiers);
9892
+ result.modifiers = clone(modifiers);
9138
9893
  }
9139
9894
  return result;
9140
9895
  }
@@ -9154,32 +9909,37 @@ class HeaderRemovalRuleConverter extends RuleConverterBase {
9154
9909
  * Converts a header removal rule to AdGuard syntax, if possible.
9155
9910
  *
9156
9911
  * @param rule Rule node to convert
9157
- * @returns Array of converted rule nodes
9912
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9913
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9914
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9158
9915
  * @throws If the rule is invalid or cannot be converted
9916
+ * @example
9917
+ * If the input rule is:
9918
+ * ```adblock
9919
+ * example.com##^responseheader(header-name)
9920
+ * ```
9921
+ * The output will be:
9922
+ * ```adblock
9923
+ * ||example.com^$removeheader=header-name
9924
+ * ```
9159
9925
  */
9160
9926
  static convertToAdg(rule) {
9161
- // Clone the provided AST node to avoid side effects
9162
- const ruleNode = cloneDeep(rule);
9163
9927
  // TODO: Add support for ABP syntax once it starts supporting header removal rules
9164
- // Check the input rule
9165
- if (ruleNode.category !== RuleCategory.Cosmetic
9166
- || ruleNode.type !== CosmeticRuleType.HtmlFilteringRule
9167
- || ruleNode.body.body.type !== CssTreeNodeType.Function
9168
- || ruleNode.body.body.name !== UBO_RESPONSEHEADER_MARKER) {
9169
- throw new RuleConversionError('Not a response header rule');
9928
+ // Leave the rule as is if it's not a header removal rule
9929
+ if (rule.category !== RuleCategory.Cosmetic
9930
+ || rule.type !== CosmeticRuleType.HtmlFilteringRule
9931
+ || rule.body.body.type !== CssTreeNodeType.Function
9932
+ || rule.body.body.name !== UBO_RESPONSEHEADER_MARKER) {
9933
+ return createNodeConversionResult([rule], false);
9170
9934
  }
9171
9935
  // Prepare network rule pattern
9172
- let pattern = EMPTY;
9173
- if (ruleNode.domains.children.length === 1) {
9936
+ const pattern = [];
9937
+ if (rule.domains.children.length === 1) {
9174
9938
  // If the rule has only one domain, we can use a simple network rule pattern:
9175
9939
  // ||single-domain-from-the-rule^
9176
- pattern = [
9177
- ADBLOCK_URL_START,
9178
- ruleNode.domains.children[0].value,
9179
- ADBLOCK_URL_SEPARATOR,
9180
- ].join(EMPTY);
9940
+ pattern.push(ADBLOCK_URL_START, rule.domains.children[0].value, ADBLOCK_URL_SEPARATOR);
9181
9941
  }
9182
- else if (ruleNode.domains.children.length > 1) {
9942
+ else if (rule.domains.children.length > 1) {
9183
9943
  // TODO: Add support for multiple domains, for example:
9184
9944
  // example.com,example.org,example.net##^responseheader(header-name)
9185
9945
  // We should consider allowing $domain with $removeheader modifier,
@@ -9189,13 +9949,13 @@ class HeaderRemovalRuleConverter extends RuleConverterBase {
9189
9949
  }
9190
9950
  // Prepare network rule modifiers
9191
9951
  const modifiers = createModifierListNode();
9192
- modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, CssTree.generateFunctionValue(fromPlainObject(ruleNode.body.body))));
9952
+ modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, CssTree.generateFunctionPlainValue(rule.body.body)));
9193
9953
  // Construct the network rule
9194
- return [
9195
- createNetworkRuleNode(pattern, modifiers,
9954
+ return createNodeConversionResult([
9955
+ createNetworkRuleNode(pattern.join(EMPTY), modifiers,
9196
9956
  // Copy the exception flag
9197
- ruleNode.exception, AdblockSyntax.Adg),
9198
- ];
9957
+ rule.exception, AdblockSyntax.Adg),
9958
+ ], true);
9199
9959
  }
9200
9960
  }
9201
9961
 
@@ -9212,54 +9972,80 @@ class CosmeticRuleConverter extends RuleConverterBase {
9212
9972
  * Converts a cosmetic rule to AdGuard syntax, if possible.
9213
9973
  *
9214
9974
  * @param rule Rule node to convert
9215
- * @returns Array of converted rule nodes
9975
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9976
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9977
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9216
9978
  * @throws If the rule is invalid or cannot be converted
9217
9979
  */
9218
9980
  static convertToAdg(rule) {
9219
- // Clone the provided AST node to avoid side effects
9220
- const ruleNode = cloneDeep(rule);
9221
- // Convert cosmetic rule modifiers
9222
- if (ruleNode.modifiers) {
9223
- if (ruleNode.syntax === AdblockSyntax.Ubo) {
9224
- // uBO doesn't support this rule:
9225
- // example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
9226
- if (ruleNode.type === CosmeticRuleType.ScriptletInjectionRule) {
9227
- throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
9228
- }
9229
- ruleNode.modifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(ruleNode.modifiers);
9230
- }
9231
- else if (ruleNode.syntax === AdblockSyntax.Abp) {
9232
- // TODO: Implement once ABP starts supporting cosmetic rule modifiers
9233
- throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
9234
- }
9235
- }
9981
+ let subconverterResult;
9236
9982
  // Convert cosmetic rule based on its type
9237
- switch (ruleNode.type) {
9983
+ switch (rule.type) {
9238
9984
  case CosmeticRuleType.ElementHidingRule:
9239
- return ElementHidingRuleConverter.convertToAdg(ruleNode);
9985
+ subconverterResult = ElementHidingRuleConverter.convertToAdg(rule);
9986
+ break;
9240
9987
  case CosmeticRuleType.ScriptletInjectionRule:
9241
- return ScriptletRuleConverter.convertToAdg(ruleNode);
9988
+ subconverterResult = ScriptletRuleConverter.convertToAdg(rule);
9989
+ break;
9242
9990
  case CosmeticRuleType.CssInjectionRule:
9243
- return CssInjectionRuleConverter.convertToAdg(ruleNode);
9991
+ subconverterResult = CssInjectionRuleConverter.convertToAdg(rule);
9992
+ break;
9244
9993
  case CosmeticRuleType.HtmlFilteringRule:
9245
9994
  // Handle special case: uBO response header filtering rule
9246
- if (ruleNode.body.body.type === CssTreeNodeType.Function
9247
- && ruleNode.body.body.name === UBO_RESPONSEHEADER_MARKER) {
9248
- return HeaderRemovalRuleConverter.convertToAdg(ruleNode);
9995
+ if (rule.body.body.type === CssTreeNodeType.Function
9996
+ && rule.body.body.name === UBO_RESPONSEHEADER_MARKER) {
9997
+ subconverterResult = HeaderRemovalRuleConverter.convertToAdg(rule);
9249
9998
  }
9250
- return HtmlRuleConverter.convertToAdg(ruleNode);
9251
- // Note: Currently, only ADG supports JS injection rules
9999
+ else {
10000
+ subconverterResult = HtmlRuleConverter.convertToAdg(rule);
10001
+ }
10002
+ break;
10003
+ // Note: Currently, only ADG supports JS injection rules, so we don't need to convert them
9252
10004
  case CosmeticRuleType.JsInjectionRule:
9253
- return [ruleNode];
10005
+ subconverterResult = createNodeConversionResult([rule], false);
10006
+ break;
9254
10007
  default:
9255
10008
  throw new RuleConversionError('Unsupported cosmetic rule type');
9256
10009
  }
10010
+ let convertedModifiers;
10011
+ // Convert cosmetic rule modifiers, if any
10012
+ if (rule.modifiers) {
10013
+ if (rule.syntax === AdblockSyntax.Ubo) {
10014
+ // uBO doesn't support this rule:
10015
+ // example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
10016
+ if (rule.type === CosmeticRuleType.ScriptletInjectionRule) {
10017
+ throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
10018
+ }
10019
+ convertedModifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(rule.modifiers);
10020
+ }
10021
+ else if (rule.syntax === AdblockSyntax.Abp) {
10022
+ // TODO: Implement once ABP starts supporting cosmetic rule modifiers
10023
+ throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
10024
+ }
10025
+ }
10026
+ if ((subconverterResult.result.length > 1 || subconverterResult.isConverted)
10027
+ || (convertedModifiers && convertedModifiers.isConverted)) {
10028
+ // Add modifier list to the subconverter result rules
10029
+ subconverterResult.result.forEach((subconverterRule) => {
10030
+ if (convertedModifiers && subconverterRule.category === RuleCategory.Cosmetic) {
10031
+ // eslint-disable-next-line no-param-reassign
10032
+ subconverterRule.modifiers = convertedModifiers.result;
10033
+ }
10034
+ });
10035
+ return subconverterResult;
10036
+ }
10037
+ return createNodeConversionResult([rule], false);
9257
10038
  }
9258
10039
  }
9259
10040
 
9260
10041
  /**
9261
10042
  * @file Network rule modifier list converter.
9262
10043
  */
10044
+ // Since scriptlets library doesn't have ESM exports, we should import
10045
+ // the whole module and then extract the required functions from it here.
10046
+ // Otherwise importing AGTree will cause an error in ESM environment,
10047
+ // because scriptlets library doesn't support named exports.
10048
+ const { redirects } = scriptlets;
9263
10049
  /**
9264
10050
  * @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#csp-modifier}
9265
10051
  */
@@ -9320,17 +10106,16 @@ class NetworkRuleModifierListConverter extends ConverterBase {
9320
10106
  * Converts a network rule modifier list to AdGuard format, if possible.
9321
10107
  *
9322
10108
  * @param modifierList Network rule modifier list node to convert
9323
- * @returns Converted modifier list node
10109
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10110
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
10111
+ * If the node was not converted, the result will contain the original node with the same object reference
9324
10112
  * @throws If the conversion is not possible
9325
10113
  */
9326
10114
  static convertToAdg(modifierList) {
9327
- // Clone the provided AST node to avoid side effects
9328
- const modifierListNode = cloneDeep(modifierList);
9329
- const convertedModifierList = createModifierListNode();
9330
- // We should merge $csp modifiers into one
9331
- const cspValues = [];
9332
- modifierListNode.children.forEach((modifierNode) => {
9333
- // Handle regular modifiers conversion and $csp modifiers collection
10115
+ const conversionMap = new MultiValueMap();
10116
+ // Special case: $csp modifier
10117
+ let cspCount = 0;
10118
+ modifierList.children.forEach((modifierNode, index) => {
9334
10119
  const modifierConversions = ADG_CONVERSION_MAP.get(modifierNode.modifier.value);
9335
10120
  if (modifierConversions) {
9336
10121
  for (const modifierConversion of modifierConversions) {
@@ -9343,17 +10128,14 @@ class NetworkRuleModifierListConverter extends ConverterBase {
9343
10128
  const value = modifierConversion.value
9344
10129
  ? modifierConversion.value(modifierNode.value?.value)
9345
10130
  : modifierNode.value?.value;
9346
- if (name === CSP_MODIFIER && value) {
9347
- // Special case: collect $csp values
9348
- cspValues.push(value);
10131
+ // Check if the name or the value is different from the original modifier
10132
+ // If so, add the converted modifier to the list
10133
+ if (name !== modifierNode.modifier.value || value !== modifierNode.value?.value) {
10134
+ conversionMap.add(index, createModifierNode(name, value, exception));
9349
10135
  }
9350
- else {
9351
- // Regular case: collect the converted modifiers, if the modifier list
9352
- // not already contains the same modifier
9353
- const existingModifier = convertedModifierList.children.find((m) => m.modifier.value === name && m.exception === exception && m.value?.value === value);
9354
- if (!existingModifier) {
9355
- convertedModifierList.children.push(createModifierNode(name, value, exception));
9356
- }
10136
+ // Special case: $csp modifier
10137
+ if (name === CSP_MODIFIER) {
10138
+ cspCount += 1;
9357
10139
  }
9358
10140
  }
9359
10141
  return;
@@ -9376,26 +10158,52 @@ class NetworkRuleModifierListConverter extends ConverterBase {
9376
10158
  // Try to convert the redirect resource name to ADG format
9377
10159
  // This function returns undefined if the resource name is unknown
9378
10160
  const convertedRedirectResource = redirects.convertRedirectNameToAdg(redirectResource);
9379
- convertedModifierList.children.push(createModifierNode(modifierName,
9380
- // If the redirect resource name is unknown, fall back to the original one
9381
- // Later, the validator will throw an error if the resource name is invalid
9382
- convertedRedirectResource || redirectResource, modifierNode.exception));
9383
- return;
9384
- }
9385
- // In all other cases, just copy the modifier as is, if the modifier list
9386
- // not already contains the same modifier
9387
- const existingModifier = convertedModifierList.children.find((m) => m.modifier.value === modifierNode.modifier.value
9388
- && m.exception === modifierNode.exception
9389
- && m.value?.value === modifierNode.value?.value);
9390
- if (!existingModifier) {
9391
- convertedModifierList.children.push(modifierNode);
10161
+ // Check if the modifier name or the redirect resource name is different from the original modifier
10162
+ // If so, add the converted modifier to the list
10163
+ if (modifierName !== modifierNode.modifier.value
10164
+ || (convertedRedirectResource !== undefined && convertedRedirectResource !== redirectResource)) {
10165
+ conversionMap.add(index, createModifierNode(modifierName,
10166
+ // If the redirect resource name is unknown, fall back to the original one
10167
+ // Later, the validator will throw an error if the resource name is invalid
10168
+ convertedRedirectResource || redirectResource, modifierNode.exception));
10169
+ }
9392
10170
  }
9393
10171
  });
9394
- // Merge $csp modifiers into one, then add it to the converted modifier list
9395
- if (cspValues.length > 0) {
9396
- convertedModifierList.children.push(createModifierNode(CSP_MODIFIER, cspValues.join(CSP_SEPARATOR)));
10172
+ // Prepare the result if there are any converted modifiers or $csp modifiers
10173
+ if (conversionMap.size || cspCount) {
10174
+ const modifierListClone = cloneModifierListNode(modifierList);
10175
+ // Replace the original modifiers with the converted ones
10176
+ // One modifier may be replaced with multiple modifiers, so we need to flatten the array
10177
+ modifierListClone.children = modifierListClone.children.map((modifierNode, index) => {
10178
+ const conversionRecord = conversionMap.get(index);
10179
+ if (conversionRecord) {
10180
+ return conversionRecord;
10181
+ }
10182
+ return modifierNode;
10183
+ }).flat();
10184
+ // Special case: $csp modifier: merge multiple $csp modifiers into one
10185
+ // and put it at the end of the modifier list
10186
+ if (cspCount) {
10187
+ const cspValues = [];
10188
+ modifierListClone.children = modifierListClone.children.filter((modifierNode) => {
10189
+ if (modifierNode.modifier.value === CSP_MODIFIER) {
10190
+ if (!modifierNode.value?.value) {
10191
+ throw new RuleConversionError('$csp modifier value is missing');
10192
+ }
10193
+ cspValues.push(modifierNode.value?.value);
10194
+ return false;
10195
+ }
10196
+ return true;
10197
+ });
10198
+ modifierListClone.children.push(createModifierNode(CSP_MODIFIER, cspValues.join(CSP_SEPARATOR)));
10199
+ }
10200
+ // Before returning the result, remove duplicated modifiers
10201
+ modifierListClone.children = modifierListClone.children.filter((modifierNode, index, self) => self.findIndex((m) => m.modifier.value === modifierNode.modifier.value
10202
+ && m.exception === modifierNode.exception
10203
+ && m.value?.value === modifierNode.value?.value) === index);
10204
+ return createConversionResult(modifierListClone, true);
9397
10205
  }
9398
- return convertedModifierList;
10206
+ return createConversionResult(modifierList, false);
9399
10207
  }
9400
10208
  }
9401
10209
 
@@ -9412,17 +10220,35 @@ class NetworkRuleConverter extends RuleConverterBase {
9412
10220
  * Converts a network rule to AdGuard format, if possible.
9413
10221
  *
9414
10222
  * @param rule Rule node to convert
9415
- * @returns Array of converted rule nodes
10223
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
10224
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
10225
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9416
10226
  * @throws If the rule is invalid or cannot be converted
9417
10227
  */
9418
10228
  static convertToAdg(rule) {
9419
- // Clone the provided AST node to avoid side effects
9420
- const ruleNode = cloneDeep(rule);
9421
- // Convert modifiers
9422
- if (ruleNode.modifiers) {
9423
- Object.assign(ruleNode.modifiers, NetworkRuleModifierListConverter.convertToAdg(ruleNode.modifiers));
10229
+ if (rule.modifiers) {
10230
+ const modifiers = NetworkRuleModifierListConverter.convertToAdg(rule.modifiers);
10231
+ // If the object reference is different, it means that the modifiers were converted
10232
+ // In this case, we should clone the entire rule and replace the modifiers with the converted ones
10233
+ if (modifiers.isConverted) {
10234
+ return {
10235
+ result: [{
10236
+ category: RuleCategory.Network,
10237
+ type: 'NetworkRule',
10238
+ syntax: rule.syntax,
10239
+ exception: rule.exception,
10240
+ pattern: {
10241
+ type: 'Value',
10242
+ value: rule.pattern.value,
10243
+ },
10244
+ modifiers: modifiers.result,
10245
+ }],
10246
+ isConverted: true,
10247
+ };
10248
+ }
9424
10249
  }
9425
- return [ruleNode];
10250
+ // If the modifiers were not converted, return the original rule
10251
+ return createNodeConversionResult([rule], false);
9426
10252
  }
9427
10253
  }
9428
10254
 
@@ -9443,48 +10269,27 @@ class RuleConverter extends RuleConverterBase {
9443
10269
  * Converts an adblock filtering rule to AdGuard format, if possible.
9444
10270
  *
9445
10271
  * @param rule Rule node to convert
9446
- * @returns Array of converted rule nodes
10272
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
10273
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
10274
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9447
10275
  * @throws If the rule is invalid or cannot be converted
9448
10276
  */
9449
10277
  static convertToAdg(rule) {
9450
- // Clone the provided AST node to avoid side effects
9451
- const ruleNode = cloneDeep(rule);
9452
10278
  // Delegate conversion to the corresponding sub-converter
9453
10279
  // based on the rule category
9454
- switch (ruleNode.category) {
10280
+ switch (rule.category) {
9455
10281
  case RuleCategory.Comment:
9456
- return CommentRuleConverter.convertToAdg(ruleNode);
10282
+ return CommentRuleConverter.convertToAdg(rule);
9457
10283
  case RuleCategory.Cosmetic:
9458
- return CosmeticRuleConverter.convertToAdg(ruleNode);
10284
+ return CosmeticRuleConverter.convertToAdg(rule);
9459
10285
  case RuleCategory.Network:
9460
- return NetworkRuleConverter.convertToAdg(ruleNode);
10286
+ return NetworkRuleConverter.convertToAdg(rule);
9461
10287
  default:
9462
- throw new RuleConversionError(`Unknown rule category: ${ruleNode.category}`);
10288
+ throw new RuleConversionError(`Unknown rule category: ${rule.category}`);
9463
10289
  }
9464
10290
  }
9465
10291
  }
9466
10292
 
9467
- /**
9468
- * @file Utility functions for working with filter list nodes
9469
- */
9470
- /**
9471
- * Creates a filter list node
9472
- *
9473
- * @param rules Rules to put in the list (optional, defaults to an empty list)
9474
- * @returns Filter list node
9475
- */
9476
- function createFilterListNode(rules = []) {
9477
- const result = {
9478
- type: 'FilterList',
9479
- children: [],
9480
- };
9481
- // We need to clone the rules to avoid side effects
9482
- if (rules.length > 0) {
9483
- result.children = cloneDeep(rules);
9484
- }
9485
- return result;
9486
- }
9487
-
9488
10293
  /**
9489
10294
  * @file Adblock filter list converter
9490
10295
  */
@@ -9503,18 +10308,133 @@ class FilterListConverter extends ConverterBase {
9503
10308
  * Converts an adblock filter list to AdGuard format, if possible.
9504
10309
  *
9505
10310
  * @param filterListNode Filter list node to convert
9506
- * @returns Converted filter list node
9507
- * @throws If the filter list is invalid or cannot be converted
10311
+ * @param tolerant Indicates whether the converter should be tolerant to invalid rules. If enabled and a rule is
10312
+ * invalid, it will be left as is. If disabled and a rule is invalid, the whole filter list will be failed.
10313
+ * Defaults to `true`.
10314
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10315
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
10316
+ * If the node was not converted, the result will contain the original node with the same object reference
10317
+ * @throws If the filter list is invalid or cannot be converted (if the tolerant mode is disabled)
10318
+ */
10319
+ static convertToAdg(filterListNode, tolerant = true) {
10320
+ // Prepare a map to store the converted rules by their index in the filter list
10321
+ const conversionMap = new MultiValueMap();
10322
+ // Iterate over the filtering rules and convert them one by one, then add them to the result (one conversion may
10323
+ // result in multiple rules)
10324
+ for (let i = 0; i < filterListNode.children.length; i += 1) {
10325
+ try {
10326
+ const convertedRules = RuleConverter.convertToAdg(filterListNode.children[i]);
10327
+ // Add the converted rules to the map if they were converted
10328
+ if (convertedRules.isConverted) {
10329
+ conversionMap.add(i, ...convertedRules.result);
10330
+ }
10331
+ }
10332
+ catch (error) {
10333
+ // If the tolerant mode is disabled, we should throw an error, this will fail the whole filter list
10334
+ // conversion.
10335
+ // Otherwise, we just ignore the error and leave the rule as is
10336
+ if (!tolerant) {
10337
+ throw error;
10338
+ }
10339
+ }
10340
+ }
10341
+ // If the conversion map is empty, it means that no rules were converted, so we can return the original filter
10342
+ // list
10343
+ if (conversionMap.size === 0) {
10344
+ return createConversionResult(filterListNode, false);
10345
+ }
10346
+ // Otherwise, create a new filter list node with the converted rules
10347
+ const convertedFilterList = {
10348
+ type: 'FilterList',
10349
+ children: [],
10350
+ };
10351
+ // Iterate over the original rules again and add them to the converted filter list, replacing the converted
10352
+ // rules with the new ones at the specified indexes
10353
+ for (let i = 0; i < filterListNode.children.length; i += 1) {
10354
+ const rules = conversionMap.get(i);
10355
+ if (rules) {
10356
+ convertedFilterList.children.push(...rules);
10357
+ }
10358
+ else {
10359
+ // We clone the unconverted rules to avoid mutating the original filter list if we return the converted
10360
+ // one
10361
+ convertedFilterList.children.push(clone(filterListNode.children[i]));
10362
+ }
10363
+ }
10364
+ return createConversionResult(convertedFilterList, true);
10365
+ }
10366
+ }
10367
+
10368
+ /**
10369
+ * @file Filter list converter for raw filter lists
10370
+ *
10371
+ * Technically, this is a wrapper around `FilterListConverter` that works with nodes instead of strings.
10372
+ */
10373
+ /**
10374
+ * Adblock filter list converter class.
10375
+ *
10376
+ * You can use this class to convert string-based filter lists, since most of the converters work with nodes.
10377
+ * This class just provides an extra layer on top of the {@link FilterListConverter} and calls the parser/serializer
10378
+ * before/after the conversion internally.
10379
+ *
10380
+ * @todo Implement `convertToUbo` and `convertToAbp`
10381
+ */
10382
+ class RawFilterListConverter extends ConverterBase {
10383
+ /**
10384
+ * Converts an adblock filter list text to AdGuard format, if possible.
10385
+ *
10386
+ * @param rawFilterList Raw filter list text to convert
10387
+ * @param tolerant Indicates whether the converter should be tolerant to invalid rules. If enabled and a rule is
10388
+ * invalid, it will be left as is. If disabled and a rule is invalid, the whole filter list will be failed.
10389
+ * Defaults to `true`.
10390
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10391
+ * the array of converted filter list text, and its `isConverted` flag indicates whether the original rule was
10392
+ * converted. If the rule was not converted, the original filter list text will be returned
10393
+ * @throws If the filter list is invalid or cannot be converted (if the tolerant mode is disabled)
9508
10394
  */
9509
- static convertToAdg(filterListNode) {
9510
- const result = createFilterListNode();
9511
- // Iterate over the filtering rules and convert them one by one,
9512
- // then add them to the result (one conversion may result in multiple rules)
9513
- for (const ruleNode of filterListNode.children) {
9514
- const convertedRules = RuleConverter.convertToAdg(ruleNode);
9515
- result.children.push(...convertedRules);
10395
+ static convertToAdg(rawFilterList, tolerant = true) {
10396
+ const conversionResult = FilterListConverter.convertToAdg(FilterListParser.parse(rawFilterList, tolerant), tolerant);
10397
+ // If the filter list was not converted, return the original text
10398
+ if (!conversionResult.isConverted) {
10399
+ return createConversionResult(rawFilterList, false);
9516
10400
  }
9517
- return result;
10401
+ // Otherwise, serialize the filter list and return the result
10402
+ return createConversionResult(FilterListParser.generate(conversionResult.result), true);
10403
+ }
10404
+ }
10405
+
10406
+ /**
10407
+ * @file Rule converter for raw rules
10408
+ *
10409
+ * Technically, this is a wrapper around `RuleConverter` that works with nodes instead of strings.
10410
+ */
10411
+ /**
10412
+ * Adblock filtering rule converter class.
10413
+ *
10414
+ * You can use this class to convert string-based adblock rules, since most of the converters work with nodes.
10415
+ * This class just provides an extra layer on top of the {@link RuleConverter} and calls the parser/serializer
10416
+ * before/after the conversion internally.
10417
+ *
10418
+ * @todo Implement `convertToUbo` and `convertToAbp`
10419
+ */
10420
+ class RawRuleConverter extends ConverterBase {
10421
+ /**
10422
+ * Converts an adblock filtering rule to AdGuard format, if possible.
10423
+ *
10424
+ * @param rawRule Raw rule text to convert
10425
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10426
+ * the array of converted rule texts, and its `isConverted` flag indicates whether the original rule was converted.
10427
+ * If the rule was not converted, the original rule text will be returned
10428
+ * @throws If the rule is invalid or cannot be converted
10429
+ */
10430
+ static convertToAdg(rawRule) {
10431
+ const conversionResult = RuleConverter.convertToAdg(RuleParser.parse(rawRule));
10432
+ // If the rule was not converted, return the original rule text
10433
+ if (!conversionResult.isConverted) {
10434
+ return createConversionResult([rawRule], false);
10435
+ }
10436
+ // Otherwise, serialize the converted rule nodes
10437
+ return createConversionResult(conversionResult.result.map(RuleParser.generate), true);
9518
10438
  }
9519
10439
  }
9520
10440
 
@@ -9596,7 +10516,7 @@ class LogicalExpressionUtils {
9596
10516
  }
9597
10517
  }
9598
10518
 
9599
- const version$1 = "1.1.4";
10519
+ const version$1 = "1.1.6";
9600
10520
 
9601
10521
  /**
9602
10522
  * @file AGTree version
@@ -9607,4 +10527,4 @@ const version$1 = "1.1.4";
9607
10527
  // with wrong relative path to `package.json`. So we need this little "hack"
9608
10528
  const version = version$1;
9609
10529
 
9610
- export { ADBLOCK_URL_SEPARATOR, ADBLOCK_URL_SEPARATOR_REGEX, ADBLOCK_URL_START, ADBLOCK_URL_START_REGEX, ADBLOCK_WILDCARD, ADBLOCK_WILDCARD_REGEX, ADG_SCRIPTLET_MASK, AGLINT_COMMAND_PREFIX, AdblockSyntax, AdblockSyntaxError, AgentCommentRuleParser, AgentParser, AppListParser, COMMA_DOMAIN_LIST_SEPARATOR, CommentMarker, CommentRuleParser, CommentRuleType, ConfigCommentRuleParser, CosmeticRuleParser, CosmeticRuleSeparator, CosmeticRuleSeparatorUtils, CosmeticRuleType, CssTree, CssTreeNodeType, CssTreeParserContext, DomainListParser, DomainUtils, EXT_CSS_LEGACY_ATTRIBUTES, EXT_CSS_PSEUDO_CLASSES, FORBIDDEN_CSS_FUNCTIONS, FilterListConverter, FilterListParser, HINT_MARKER, HintCommentRuleParser, HintParser, IF, INCLUDE, LogicalExpressionParser, LogicalExpressionUtils, METADATA_HEADERS, MODIFIERS_SEPARATOR, MODIFIER_ASSIGN_OPERATOR, MetadataCommentRuleParser, MethodListParser, ModifierListParser, ModifierParser, NEGATION_MARKER, NETWORK_RULE_EXCEPTION_MARKER, NETWORK_RULE_SEPARATOR, NetworkRuleParser, NotImplementedError, PIPE_MODIFIER_SEPARATOR, PREPROCESSOR_MARKER, ParameterListParser, PreProcessorCommentRuleParser, QuoteType, QuoteUtils, RegExpUtils, RuleCategory, RuleConversionError, RuleConverter, RuleParser, SAFARI_CB_AFFINITY, SPECIAL_REGEX_SYMBOLS, StealthOptionListParser, UBO_SCRIPTLET_MASK, locRange, modifierValidator, shiftLoc, version };
10530
+ export { ADBLOCK_URL_SEPARATOR, ADBLOCK_URL_SEPARATOR_REGEX, ADBLOCK_URL_START, ADBLOCK_URL_START_REGEX, ADBLOCK_WILDCARD, ADBLOCK_WILDCARD_REGEX, ADG_SCRIPTLET_MASK, AGLINT_COMMAND_PREFIX, AdblockSyntax, AdblockSyntaxError, AgentCommentRuleParser, AgentParser, AppListParser, COMMA_DOMAIN_LIST_SEPARATOR, CommentMarker, CommentRuleParser, CommentRuleType, ConfigCommentRuleParser, CosmeticRuleParser, CosmeticRuleSeparator, CosmeticRuleSeparatorUtils, CosmeticRuleType, CssTree, CssTreeNodeType, CssTreeParserContext, DomainListParser, DomainUtils, EXT_CSS_LEGACY_ATTRIBUTES, EXT_CSS_PSEUDO_CLASSES, FORBIDDEN_CSS_FUNCTIONS, FilterListConverter, FilterListParser, HINT_MARKER, HintCommentRuleParser, HintParser, IF, INCLUDE, LogicalExpressionParser, LogicalExpressionUtils, METADATA_HEADERS, MODIFIERS_SEPARATOR, MODIFIER_ASSIGN_OPERATOR, MetadataCommentRuleParser, MethodListParser, ModifierListParser, ModifierParser, NEGATION_MARKER, NETWORK_RULE_EXCEPTION_MARKER, NETWORK_RULE_SEPARATOR, NetworkRuleParser, NotImplementedError, PIPE_MODIFIER_SEPARATOR, PREPROCESSOR_MARKER, ParameterListParser, PreProcessorCommentRuleParser, QuoteType, QuoteUtils, RawFilterListConverter, RawRuleConverter, RegExpUtils, RuleCategory, RuleConversionError, RuleConverter, RuleParser, SAFARI_CB_AFFINITY, SPECIAL_REGEX_SYMBOLS, StealthOptionListParser, UBO_SCRIPTLET_MASK, locRange, modifierValidator, shiftLoc, version };