@adguard/agtree 1.1.5 → 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.
package/dist/agtree.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * AGTree v1.1.5 (build date: Tue, 05 Sep 2023 15:10:53 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
@@ -281,7 +281,7 @@ const NEGATION_MARKER = '~';
281
281
  /**
282
282
  * The wildcard symbol — `*`.
283
283
  */
284
- const WILDCARD$1 = ASTERISK;
284
+ const WILDCARD = ASTERISK;
285
285
  /**
286
286
  * Classic domain separator.
287
287
  *
@@ -2880,7 +2880,7 @@ class ModifierParser {
2880
2880
  const modifierEnd = Math.max(StringUtils.skipWSBack(raw) + 1, modifierNameStart);
2881
2881
  // Modifier name can't be empty
2882
2882
  if (modifierNameStart === modifierEnd) {
2883
- throw new AdblockSyntaxError('Modifier name can\'t be empty', locRange(loc, 0, raw.length));
2883
+ throw new AdblockSyntaxError('Modifier name cannot be empty', locRange(loc, 0, raw.length));
2884
2884
  }
2885
2885
  let modifier;
2886
2886
  let value;
@@ -2904,7 +2904,7 @@ class ModifierParser {
2904
2904
  };
2905
2905
  // Value can't be empty
2906
2906
  if (assignmentIndex + 1 === modifierEnd) {
2907
- throw new AdblockSyntaxError('Modifier value can\'t be empty', locRange(loc, 0, raw.length));
2907
+ throw new AdblockSyntaxError('Modifier value cannot be empty', locRange(loc, 0, raw.length));
2908
2908
  }
2909
2909
  // Skip whitespace after the assignment operator
2910
2910
  const valueStart = StringUtils.skipWS(raw, assignmentIndex + MODIFIER_ASSIGN_OPERATOR.length);
@@ -3202,8 +3202,29 @@ const FORBIDDEN_CSS_FUNCTIONS = new Set([
3202
3202
  'url',
3203
3203
  ]);
3204
3204
 
3205
+ /**
3206
+ * @file Clone related utilities
3207
+ *
3208
+ * We should keep clone related functions in this file. Thus, we just provide
3209
+ * a simple interface for cloning values, we use it across the AGTree project,
3210
+ * and the implementation "under the hood" can be improved later, if needed.
3211
+ */
3212
+ /**
3213
+ * Clones an input value to avoid side effects. Use it only in justified cases,
3214
+ * because it can impact performance negatively.
3215
+ *
3216
+ * @param value Value to clone
3217
+ * @returns Cloned value
3218
+ */
3219
+ function clone(value) {
3220
+ // TODO: Replace cloneDeep with a more efficient implementation
3221
+ return cloneDeep(value);
3222
+ }
3223
+
3205
3224
  /**
3206
3225
  * @file Additional / helper functions for ECSSTree / CSSTree.
3226
+ *
3227
+ * @note There are no tests for some functions, but during the AGTree optimization we remove them anyway.
3207
3228
  */
3208
3229
  /**
3209
3230
  * Common CSSTree parsing options.
@@ -3339,10 +3360,10 @@ class CssTree {
3339
3360
  ast = CssTree.parse(selectorList, exports.CssTreeParserContext.selectorList);
3340
3361
  }
3341
3362
  else {
3342
- ast = cloneDeep(selectorList);
3363
+ ast = clone(selectorList);
3343
3364
  }
3344
3365
  const nodes = [];
3345
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3366
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3346
3367
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3347
3368
  ecssTree.walk(ast, (node) => {
3348
3369
  if (CssTree.isExtendedCssNode(node, pseudoClasses, attributeSelectors)) {
@@ -3371,9 +3392,9 @@ class CssTree {
3371
3392
  ast = CssTree.parse(selectorList, exports.CssTreeParserContext.selectorList);
3372
3393
  }
3373
3394
  else {
3374
- ast = cloneDeep(selectorList);
3395
+ ast = selectorList;
3375
3396
  }
3376
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3397
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3377
3398
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3378
3399
  return ecssTree.find(ast, (node) => CssTree.isExtendedCssNode(node, pseudoClasses, attributeSelectors)) !== null;
3379
3400
  }
@@ -3410,14 +3431,14 @@ class CssTree {
3410
3431
  ast = CssTree.parse(declarationList, exports.CssTreeParserContext.declarationList);
3411
3432
  }
3412
3433
  else {
3413
- ast = cloneDeep(declarationList);
3434
+ ast = clone(declarationList);
3414
3435
  }
3415
3436
  const nodes = [];
3416
3437
  // While walking the AST we should skip the nested functions,
3417
3438
  // for example skip url()s in cross-fade(url(), url()), since
3418
3439
  // cross-fade() itself is already a forbidden function
3419
3440
  let inForbiddenFunction = false;
3420
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3441
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3421
3442
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3422
3443
  ecssTree.walk(ast, {
3423
3444
  enter: (node) => {
@@ -3455,9 +3476,9 @@ class CssTree {
3455
3476
  ast = CssTree.parse(declarationList, exports.CssTreeParserContext.declarationList);
3456
3477
  }
3457
3478
  else {
3458
- ast = cloneDeep(declarationList);
3479
+ ast = clone(declarationList);
3459
3480
  }
3460
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3481
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3461
3482
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3462
3483
  return ecssTree.find(ast, (node) => CssTree.isForbiddenFunction(node, forbiddenFunctions)) !== null;
3463
3484
  }
@@ -3689,6 +3710,180 @@ class CssTree {
3689
3710
  });
3690
3711
  return result.trim();
3691
3712
  }
3713
+ /**
3714
+ * Generates string representation of the selector list.
3715
+ *
3716
+ * @param ast SelectorList AST
3717
+ * @returns String representation of the selector list
3718
+ */
3719
+ static generateSelectorListPlain(ast) {
3720
+ const result = [];
3721
+ if (!ast.children || ast.children.length === 0) {
3722
+ throw new Error('Selector list cannot be empty');
3723
+ }
3724
+ ast.children.forEach((selector, index, nodeList) => {
3725
+ if (selector.type !== exports.CssTreeNodeType.Selector) {
3726
+ throw new Error(`Unexpected node type: ${selector.type}`);
3727
+ }
3728
+ result.push(this.generateSelectorPlain(selector));
3729
+ // If there is a next node, add a comma and a space after the selector
3730
+ if (nodeList[index + 1]) {
3731
+ result.push(COMMA, SPACE);
3732
+ }
3733
+ });
3734
+ return result.join(EMPTY);
3735
+ }
3736
+ /**
3737
+ * Selector generation based on CSSTree's AST. This is necessary because CSSTree
3738
+ * only adds spaces in some edge cases.
3739
+ *
3740
+ * @param ast CSS Tree AST
3741
+ * @returns CSS selector as string
3742
+ */
3743
+ static generateSelectorPlain(ast) {
3744
+ let result = EMPTY;
3745
+ let inAttributeSelector = false;
3746
+ let depth = 0;
3747
+ let selectorListDepth = -1;
3748
+ let prevNode = ast;
3749
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3750
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3751
+ ecssTree.walk(ast, {
3752
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3753
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3754
+ enter: (node) => {
3755
+ depth += 1;
3756
+ // Skip attribute selector / selector list children
3757
+ if (inAttributeSelector || selectorListDepth > -1) {
3758
+ return;
3759
+ }
3760
+ switch (node.type) {
3761
+ // "Trivial" nodes
3762
+ case exports.CssTreeNodeType.TypeSelector:
3763
+ result += node.name;
3764
+ break;
3765
+ case exports.CssTreeNodeType.ClassSelector:
3766
+ result += DOT;
3767
+ result += node.name;
3768
+ break;
3769
+ case exports.CssTreeNodeType.IdSelector:
3770
+ result += HASHMARK;
3771
+ result += node.name;
3772
+ break;
3773
+ case exports.CssTreeNodeType.Identifier:
3774
+ result += node.name;
3775
+ break;
3776
+ case exports.CssTreeNodeType.Raw:
3777
+ result += node.value;
3778
+ break;
3779
+ // "Advanced" nodes
3780
+ case exports.CssTreeNodeType.Nth:
3781
+ // Default generation enough
3782
+ result += ecssTree.generate(node);
3783
+ break;
3784
+ // For example :not([id], [name])
3785
+ case exports.CssTreeNodeType.SelectorList:
3786
+ // eslint-disable-next-line no-case-declarations
3787
+ const selectors = [];
3788
+ node.children.forEach((selector) => {
3789
+ if (selector.type === exports.CssTreeNodeType.Selector) {
3790
+ selectors.push(CssTree.generateSelectorPlain(selector));
3791
+ }
3792
+ else if (selector.type === exports.CssTreeNodeType.Raw) {
3793
+ selectors.push(selector.value);
3794
+ }
3795
+ });
3796
+ // Join selector lists
3797
+ result += selectors.join(COMMA + SPACE);
3798
+ // Skip nodes here
3799
+ selectorListDepth = depth;
3800
+ break;
3801
+ case exports.CssTreeNodeType.Combinator:
3802
+ if (node.name === SPACE) {
3803
+ result += node.name;
3804
+ break;
3805
+ }
3806
+ // Prevent this case (unnecessary space): has( > .something)
3807
+ if (prevNode.type !== exports.CssTreeNodeType.Selector) {
3808
+ result += SPACE;
3809
+ }
3810
+ result += node.name;
3811
+ result += SPACE;
3812
+ break;
3813
+ case exports.CssTreeNodeType.AttributeSelector:
3814
+ result += OPEN_SQUARE_BRACKET;
3815
+ // Identifier name
3816
+ if (node.name) {
3817
+ result += node.name.name;
3818
+ }
3819
+ // Matcher operator, eg =
3820
+ if (node.matcher) {
3821
+ result += node.matcher;
3822
+ // Value can be String, Identifier or null
3823
+ if (node.value !== null) {
3824
+ // String node
3825
+ if (node.value.type === exports.CssTreeNodeType.String) {
3826
+ result += ecssTree.generate(node.value);
3827
+ }
3828
+ else if (node.value.type === exports.CssTreeNodeType.Identifier) {
3829
+ // Identifier node
3830
+ result += node.value.name;
3831
+ }
3832
+ }
3833
+ }
3834
+ // Flags
3835
+ if (node.flags) {
3836
+ // Space before flags
3837
+ result += SPACE;
3838
+ result += node.flags;
3839
+ }
3840
+ result += CLOSE_SQUARE_BRACKET;
3841
+ inAttributeSelector = true;
3842
+ break;
3843
+ case exports.CssTreeNodeType.PseudoElementSelector:
3844
+ result += COLON;
3845
+ result += COLON;
3846
+ result += node.name;
3847
+ if (node.children !== null) {
3848
+ result += OPEN_PARENTHESIS;
3849
+ }
3850
+ break;
3851
+ case exports.CssTreeNodeType.PseudoClassSelector:
3852
+ result += COLON;
3853
+ result += node.name;
3854
+ if (node.children !== null) {
3855
+ result += OPEN_PARENTHESIS;
3856
+ }
3857
+ break;
3858
+ }
3859
+ prevNode = node;
3860
+ },
3861
+ leave: (node) => {
3862
+ depth -= 1;
3863
+ if (node.type === exports.CssTreeNodeType.SelectorList && depth + 1 === selectorListDepth) {
3864
+ selectorListDepth = -1;
3865
+ }
3866
+ if (selectorListDepth > -1) {
3867
+ return;
3868
+ }
3869
+ if (node.type === exports.CssTreeNodeType.AttributeSelector) {
3870
+ inAttributeSelector = false;
3871
+ }
3872
+ if (inAttributeSelector) {
3873
+ return;
3874
+ }
3875
+ switch (node.type) {
3876
+ case exports.CssTreeNodeType.PseudoElementSelector:
3877
+ case exports.CssTreeNodeType.PseudoClassSelector:
3878
+ if (node.children) {
3879
+ result += CLOSE_PARENTHESIS;
3880
+ }
3881
+ break;
3882
+ }
3883
+ },
3884
+ });
3885
+ return result.trim();
3886
+ }
3692
3887
  /**
3693
3888
  * Block generation based on CSSTree's AST. This is necessary because CSSTree only adds spaces in some edge cases.
3694
3889
  *
@@ -3872,6 +4067,29 @@ class CssTree {
3872
4067
  });
3873
4068
  return result;
3874
4069
  }
4070
+ /**
4071
+ * Helper function to generate a raw string from a function selector's children
4072
+ *
4073
+ * @param node Function node
4074
+ * @returns Generated function value
4075
+ * @example `responseheader(name)` -> `name`
4076
+ */
4077
+ static generateFunctionPlainValue(node) {
4078
+ const result = [];
4079
+ node.children?.forEach((child) => {
4080
+ switch (child.type) {
4081
+ case exports.CssTreeNodeType.Raw:
4082
+ result.push(child.value);
4083
+ break;
4084
+ default:
4085
+ // Fallback to CSSTree's default generate function
4086
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
4087
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4088
+ result.push(ecssTree.generate(child));
4089
+ }
4090
+ });
4091
+ return result.join(EMPTY);
4092
+ }
3875
4093
  }
3876
4094
 
3877
4095
  /**
@@ -3919,7 +4137,7 @@ class ElementHidingBodyParser {
3919
4137
  * @throws If the AST is invalid
3920
4138
  */
3921
4139
  static generate(ast) {
3922
- return CssTree.generateSelectorList(ecssTree.fromPlainObject(ast.selectorList));
4140
+ return CssTree.generateSelectorListPlain(ast.selectorList);
3923
4141
  }
3924
4142
  }
3925
4143
 
@@ -4163,7 +4381,7 @@ class CssInjectionBodyParser {
4163
4381
  if (mediaQueryList || declarationList || remove) {
4164
4382
  throw new AdblockSyntaxError(
4165
4383
  // eslint-disable-next-line max-len
4166
- 'Invalid selector, regular selector elements can\'t be used after special pseudo-classes', {
4384
+ 'Invalid selector, regular selector elements cannot be used after special pseudo-classes', {
4167
4385
  start: node.loc?.start ?? loc,
4168
4386
  end: shiftLoc(loc, raw.length),
4169
4387
  });
@@ -4852,7 +5070,7 @@ function createModifierListNode(modifiers = []) {
4852
5070
  const result = {
4853
5071
  type: 'ModifierList',
4854
5072
  // We need to clone the modifiers to avoid side effects
4855
- children: cloneDeep(modifiers),
5073
+ children: modifiers.length ? clone(modifiers) : [],
4856
5074
  };
4857
5075
  return result;
4858
5076
  }
@@ -4892,8 +5110,9 @@ function hasUboModifierIndicator(rawSelectorList) {
4892
5110
  * @returns Linked list based selector
4893
5111
  */
4894
5112
  function convertSelectorToLinkedList(selector) {
5113
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
4895
5114
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4896
- return ecssTree.fromPlainObject(cloneDeep(selector));
5115
+ return ecssTree.fromPlainObject(clone(selector));
4897
5116
  }
4898
5117
  /**
4899
5118
  * Helper function that always returns the linked list version of the
@@ -4903,8 +5122,9 @@ function convertSelectorToLinkedList(selector) {
4903
5122
  * @returns Linked list based selector list
4904
5123
  */
4905
5124
  function convertSelectorListToLinkedList(selectorList) {
5125
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
4906
5126
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4907
- return ecssTree.fromPlainObject(cloneDeep(selectorList));
5127
+ return ecssTree.fromPlainObject(clone(selectorList));
4908
5128
  }
4909
5129
  /**
4910
5130
  * Helper function for checking and removing bounding combinators
@@ -5989,7 +6209,8 @@ class FilterListParser {
5989
6209
  */
5990
6210
  static generate(ast, preferRaw = false) {
5991
6211
  let result = EMPTY;
5992
- for (const rule of ast.children) {
6212
+ for (let i = 0; i < ast.children.length; i += 1) {
6213
+ const rule = ast.children[i];
5993
6214
  if (preferRaw && rule.raws?.text) {
5994
6215
  result += rule.raws.text;
5995
6216
  }
@@ -6006,6 +6227,11 @@ class FilterListParser {
6006
6227
  case 'lf':
6007
6228
  result += LF;
6008
6229
  break;
6230
+ default:
6231
+ if (i !== ast.children.length - 1) {
6232
+ result += LF;
6233
+ }
6234
+ break;
6009
6235
  }
6010
6236
  }
6011
6237
  return result;
@@ -7560,14 +7786,14 @@ const getSpecificBlockerData = (modifiersData, blockerPrefix, modifierName) => {
7560
7786
  * @example
7561
7787
  * `example.*` — matches with any TLD, e.g. `example.org`, `example.com`, etc.
7562
7788
  */
7563
- const WILDCARD_TLD = DOT + WILDCARD$1;
7789
+ const WILDCARD_TLD = DOT + WILDCARD;
7564
7790
  /**
7565
7791
  * Marker for a wildcard subdomain — `*.`.
7566
7792
  *
7567
7793
  * @example
7568
7794
  * `*.example.org` — matches with any subdomain, e.g. `foo.example.org` or `bar.example.org`
7569
7795
  */
7570
- const WILDCARD_SUBDOMAIN = WILDCARD$1 + DOT;
7796
+ const WILDCARD_SUBDOMAIN = WILDCARD + DOT;
7571
7797
  class DomainUtils {
7572
7798
  /**
7573
7799
  * Check if the input is a valid domain or hostname.
@@ -7578,7 +7804,7 @@ class DomainUtils {
7578
7804
  static isValidDomainOrHostname(domain) {
7579
7805
  let domainToCheck = domain;
7580
7806
  // Wildcard-only domain, typically a generic rule
7581
- if (domainToCheck === WILDCARD$1) {
7807
+ if (domainToCheck === WILDCARD) {
7582
7808
  return true;
7583
7809
  }
7584
7810
  // https://adguard.com/kb/general/ad-filtering/create-own-filters/#wildcard-for-tld
@@ -7798,7 +8024,7 @@ const isValidAppNameChunk = (chunk) => {
7798
8024
  const isValidAppModifierValue = (value) => {
7799
8025
  // $app modifier does not support wildcard tld
7800
8026
  // https://adguard.app/kb/general/ad-filtering/create-own-filters/#app-modifier
7801
- if (value.includes(WILDCARD$1)) {
8027
+ if (value.includes(WILDCARD)) {
7802
8028
  return false;
7803
8029
  }
7804
8030
  return value
@@ -7863,7 +8089,7 @@ const isValidDenyAllowModifierValue = (value) => {
7863
8089
  // $denyallow modifier does not support wildcard tld
7864
8090
  // https://adguard.app/kb/general/ad-filtering/create-own-filters/#denyallow-modifier
7865
8091
  // but here we are simply checking whether the value contains wildcard `*`, not ends with `.*`
7866
- if (value.includes(WILDCARD$1)) {
8092
+ if (value.includes(WILDCARD)) {
7867
8093
  return false;
7868
8094
  }
7869
8095
  // TODO: add cache for domains validation
@@ -8160,7 +8386,7 @@ const validatePermissionAllowlist = (allowlist, directive, modifierName) => {
8160
8386
  // `*` is one of available permissions tokens
8161
8387
  // e.g. 'fullscreen=*'
8162
8388
  // https://w3c.github.io/webappsec-permissions-policy/#structured-header-serialization
8163
- if (allowlist === WILDCARD$1
8389
+ if (allowlist === WILDCARD
8164
8390
  // e.g. 'autoplay=()'
8165
8391
  || allowlist === EMPTY_PERMISSIONS_ALLOWLIST) {
8166
8392
  return { valid: true };
@@ -8424,7 +8650,7 @@ class ModifierValidator {
8424
8650
  * @returns Result of modifier validation.
8425
8651
  */
8426
8652
  validate = (syntax, rawModifier, isException = false) => {
8427
- const modifier = cloneDeep(rawModifier);
8653
+ const modifier = clone(rawModifier);
8428
8654
  // special case: handle noop modifier which may be used as multiple underscores (not just one)
8429
8655
  // https://adguard.com/kb/general/ad-filtering/create-own-filters/#noop-modifier
8430
8656
  if (modifier.modifier.value.startsWith(UNDERSCORE)) {
@@ -8503,7 +8729,9 @@ class ConverterBase {
8503
8729
  * Converts some data to AdGuard format
8504
8730
  *
8505
8731
  * @param data Data to convert
8506
- * @returns Converted data
8732
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
8733
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
8734
+ * If the node was not converted, the result will contain the original node with the same object reference
8507
8735
  * @throws If the data is invalid or incompatible
8508
8736
  */
8509
8737
  static convertToAdg(data) {
@@ -8513,7 +8741,9 @@ class ConverterBase {
8513
8741
  * Converts some data to Adblock Plus format
8514
8742
  *
8515
8743
  * @param data Data to convert
8516
- * @returns Converted data
8744
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
8745
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
8746
+ * If the node was not converted, the result will contain the original node with the same object reference
8517
8747
  * @throws If the data is invalid or incompatible
8518
8748
  */
8519
8749
  static convertToAbp(data) {
@@ -8523,7 +8753,9 @@ class ConverterBase {
8523
8753
  * Converts some data to uBlock Origin format
8524
8754
  *
8525
8755
  * @param data Data to convert
8526
- * @returns Converted data
8756
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
8757
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
8758
+ * If the node was not converted, the result will contain the original node with the same object reference
8527
8759
  * @throws If the data is invalid or incompatible
8528
8760
  */
8529
8761
  static convertToUbo(data) {
@@ -8547,7 +8779,9 @@ class RuleConverterBase extends ConverterBase {
8547
8779
  * Converts an adblock filtering rule to AdGuard format, if possible.
8548
8780
  *
8549
8781
  * @param rule Rule node to convert
8550
- * @returns Array of converted rule nodes
8782
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8783
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8784
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8551
8785
  * @throws If the rule is invalid or cannot be converted
8552
8786
  */
8553
8787
  static convertToAdg(rule) {
@@ -8557,7 +8791,9 @@ class RuleConverterBase extends ConverterBase {
8557
8791
  * Converts an adblock filtering rule to Adblock Plus format, if possible.
8558
8792
  *
8559
8793
  * @param rule Rule node to convert
8560
- * @returns Array of converted rule nodes
8794
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8795
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8796
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8561
8797
  * @throws If the rule is invalid or cannot be converted
8562
8798
  */
8563
8799
  static convertToAbp(rule) {
@@ -8567,7 +8803,9 @@ class RuleConverterBase extends ConverterBase {
8567
8803
  * Converts an adblock filtering rule to uBlock Origin format, if possible.
8568
8804
  *
8569
8805
  * @param rule Rule node to convert
8570
- * @returns Array of converted rule nodes
8806
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8807
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8808
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8571
8809
  * @throws If the rule is invalid or cannot be converted
8572
8810
  */
8573
8811
  static convertToUbo(rule) {
@@ -8575,6 +8813,37 @@ class RuleConverterBase extends ConverterBase {
8575
8813
  }
8576
8814
  }
8577
8815
 
8816
+ /**
8817
+ * @file Conversion result interface and helper functions
8818
+ */
8819
+ /**
8820
+ * Helper function to create a generic conversion result.
8821
+ *
8822
+ * @param result Conversion result
8823
+ * @param isConverted Indicates whether the input item was converted
8824
+ * @template T Type of the item to convert
8825
+ * @template U Type of the conversion result (defaults to `T`, but can be `T[]` as well)
8826
+ * @returns Generic conversion result
8827
+ */
8828
+ // eslint-disable-next-line max-len
8829
+ function createConversionResult(result, isConverted) {
8830
+ return {
8831
+ result,
8832
+ isConverted,
8833
+ };
8834
+ }
8835
+ /**
8836
+ * Helper function to create a node conversion result.
8837
+ *
8838
+ * @param nodes Array of nodes
8839
+ * @param isConverted Indicates whether the input item was converted
8840
+ * @template T Type of the node (extends `Node`)
8841
+ * @returns Node conversion result
8842
+ */
8843
+ function createNodeConversionResult(nodes, isConverted) {
8844
+ return createConversionResult(nodes, isConverted);
8845
+ }
8846
+
8578
8847
  /**
8579
8848
  * @file Comment rule converter
8580
8849
  */
@@ -8588,27 +8857,30 @@ class CommentRuleConverter extends RuleConverterBase {
8588
8857
  * Converts a comment rule to AdGuard format, if possible.
8589
8858
  *
8590
8859
  * @param rule Rule node to convert
8591
- * @returns Array of converted rule nodes
8860
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8861
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8862
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8592
8863
  * @throws If the rule is invalid or cannot be converted
8593
8864
  */
8594
8865
  static convertToAdg(rule) {
8595
- // Clone the provided AST node to avoid side effects
8596
- const ruleNode = cloneDeep(rule);
8597
8866
  // TODO: Add support for other comment types, if needed
8598
8867
  // Main task is # -> ! conversion
8599
- switch (ruleNode.type) {
8868
+ switch (rule.type) {
8600
8869
  case exports.CommentRuleType.CommentRule:
8601
- // 'Comment' uBO style comments
8602
- if (ruleNode.type === exports.CommentRuleType.CommentRule
8603
- && ruleNode.marker.value === exports.CommentMarker.Hashmark) {
8604
- ruleNode.marker.value = exports.CommentMarker.Regular;
8605
- // Add the hashmark to the beginning of the comment
8606
- ruleNode.text.value = `${SPACE}${exports.CommentMarker.Hashmark}${ruleNode.text.value}`;
8870
+ // Check if the rule needs to be converted
8871
+ if (rule.type === exports.CommentRuleType.CommentRule && rule.marker.value === exports.CommentMarker.Hashmark) {
8872
+ // Add a ! to the beginning of the comment
8873
+ // TODO: Replace with custom clone method
8874
+ const ruleClone = clone(rule);
8875
+ ruleClone.marker.value = exports.CommentMarker.Regular;
8876
+ // Add the hashmark to the beginning of the comment text
8877
+ ruleClone.text.value = `${SPACE}${exports.CommentMarker.Hashmark}${ruleClone.text.value}`;
8878
+ return createNodeConversionResult([ruleClone], true);
8607
8879
  }
8608
- return [ruleNode];
8880
+ return createNodeConversionResult([rule], false);
8609
8881
  // Leave any other comment rule as is
8610
8882
  default:
8611
- return [ruleNode];
8883
+ return createNodeConversionResult([rule], false);
8612
8884
  }
8613
8885
  }
8614
8886
  }
@@ -8778,6 +9050,58 @@ class RegExpUtils {
8778
9050
  }
8779
9051
  }
8780
9052
 
9053
+ /**
9054
+ * @file Custom clone functions for AST nodes, this is probably the most efficient way to clone AST nodes.
9055
+ * @todo Maybe move them to parser classes as 'clone' methods
9056
+ */
9057
+ /**
9058
+ * Clones a scriptlet rule node.
9059
+ *
9060
+ * @param node Node to clone
9061
+ * @returns Cloned node
9062
+ */
9063
+ function cloneScriptletRuleNode(node) {
9064
+ return {
9065
+ type: node.type,
9066
+ children: node.children.map((child) => ({ ...child })),
9067
+ };
9068
+ }
9069
+ /**
9070
+ * Clones a domain list node.
9071
+ *
9072
+ * @param node Node to clone
9073
+ * @returns Cloned node
9074
+ */
9075
+ function cloneDomainListNode(node) {
9076
+ return {
9077
+ type: node.type,
9078
+ separator: node.separator,
9079
+ children: node.children.map((domain) => ({ ...domain })),
9080
+ };
9081
+ }
9082
+ /**
9083
+ * Clones a modifier list node.
9084
+ *
9085
+ * @param node Node to clone
9086
+ * @returns Cloned node
9087
+ */
9088
+ function cloneModifierListNode(node) {
9089
+ return {
9090
+ type: node.type,
9091
+ children: node.children.map((modifier) => {
9092
+ const res = {
9093
+ type: modifier.type,
9094
+ exception: modifier.exception,
9095
+ modifier: { ...modifier.modifier },
9096
+ };
9097
+ if (modifier.value) {
9098
+ res.value = { ...modifier.value };
9099
+ }
9100
+ return res;
9101
+ }),
9102
+ };
9103
+ }
9104
+
8781
9105
  /**
8782
9106
  * @file HTML filtering rule converter
8783
9107
  */
@@ -8790,16 +9114,22 @@ class RegExpUtils {
8790
9114
  *
8791
9115
  * @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#html-filtering-rules}
8792
9116
  */
8793
- const ADGUARD_HTML_DEFAULT_MAX_LENGTH = 8192;
8794
- const ADGUARD_HTML_CONVERSION_MAX_LENGTH = ADGUARD_HTML_DEFAULT_MAX_LENGTH * 32;
9117
+ const ADG_HTML_DEFAULT_MAX_LENGTH = 8192;
9118
+ const ADG_HTML_CONVERSION_MAX_LENGTH = ADG_HTML_DEFAULT_MAX_LENGTH * 32;
8795
9119
  const NOT_SPECIFIED = -1;
8796
- const CONTAINS$1 = 'contains';
8797
- const HAS_TEXT$1 = 'has-text';
8798
- const MAX_LENGTH = 'max-length';
8799
- const MIN_LENGTH = 'min-length';
8800
- const MIN_TEXT_LENGTH = 'min-text-length';
8801
- const TAG_CONTENT = 'tag-content';
8802
- const WILDCARD = 'wildcard';
9120
+ var PseudoClasses$1;
9121
+ (function (PseudoClasses) {
9122
+ PseudoClasses["Contains"] = "contains";
9123
+ PseudoClasses["HasText"] = "has-text";
9124
+ PseudoClasses["MinTextLength"] = "min-text-length";
9125
+ })(PseudoClasses$1 || (PseudoClasses$1 = {}));
9126
+ var AttributeSelectors;
9127
+ (function (AttributeSelectors) {
9128
+ AttributeSelectors["MaxLength"] = "max-length";
9129
+ AttributeSelectors["MinLength"] = "min-length";
9130
+ AttributeSelectors["TagContent"] = "tag-content";
9131
+ AttributeSelectors["Wildcard"] = "wildcard";
9132
+ })(AttributeSelectors || (AttributeSelectors = {}));
8803
9133
  /**
8804
9134
  * HTML filtering rule converter class
8805
9135
  *
@@ -8822,16 +9152,23 @@ class HtmlRuleConverter extends RuleConverterBase {
8822
9152
  * ```
8823
9153
  *
8824
9154
  * @param rule Rule node to convert
8825
- * @returns Array of converted rule nodes
9155
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9156
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9157
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8826
9158
  * @throws If the rule is invalid or cannot be converted
8827
9159
  */
8828
9160
  static convertToAdg(rule) {
8829
- // Clone the provided AST node to avoid side effects
8830
- const ruleNode = cloneDeep(rule);
9161
+ // Ignore AdGuard rules
9162
+ if (rule.syntax === exports.AdblockSyntax.Adg) {
9163
+ return createNodeConversionResult([rule], false);
9164
+ }
9165
+ if (rule.syntax === exports.AdblockSyntax.Abp) {
9166
+ throw new RuleConversionError('Invalid rule, ABP does not support HTML filtering rules');
9167
+ }
8831
9168
  // Prepare the conversion result
8832
9169
  const conversionResult = [];
8833
9170
  // Iterate over selector list
8834
- for (const selector of ruleNode.body.body.children) {
9171
+ for (const selector of rule.body.body.children) {
8835
9172
  // Check selector, just in case
8836
9173
  if (selector.type !== exports.CssTreeNodeType.Selector) {
8837
9174
  throw new RuleConversionError(`Expected selector, got '${selector.type}'`);
@@ -8858,24 +9195,24 @@ class HtmlRuleConverter extends RuleConverterBase {
8858
9195
  throw new RuleConversionError('Tag selector should be the first child, if present');
8859
9196
  }
8860
9197
  // Simply store the tag selector
8861
- convertedSelector.children.push(cloneDeep(node));
9198
+ convertedSelector.children.push(clone(node));
8862
9199
  break;
8863
9200
  case exports.CssTreeNodeType.AttributeSelector:
8864
9201
  // Check if the attribute selector is a special AdGuard attribute
8865
9202
  switch (node.name.name) {
8866
- case MIN_LENGTH:
9203
+ case AttributeSelectors.MinLength:
8867
9204
  minLength = CssTree.parseAttributeSelectorValueAsNumber(node);
8868
9205
  break;
8869
- case MAX_LENGTH:
9206
+ case AttributeSelectors.MaxLength:
8870
9207
  maxLength = CssTree.parseAttributeSelectorValueAsNumber(node);
8871
9208
  break;
8872
- case TAG_CONTENT:
8873
- case WILDCARD:
9209
+ case AttributeSelectors.TagContent:
9210
+ case AttributeSelectors.Wildcard:
8874
9211
  CssTree.assertAttributeSelectorHasStringValue(node);
8875
- convertedSelector.children.push(cloneDeep(node));
9212
+ convertedSelector.children.push(clone(node));
8876
9213
  break;
8877
9214
  default:
8878
- convertedSelector.children.push(cloneDeep(node));
9215
+ convertedSelector.children.push(clone(node));
8879
9216
  }
8880
9217
  break;
8881
9218
  case exports.CssTreeNodeType.PseudoClassSelector:
@@ -8889,18 +9226,18 @@ class HtmlRuleConverter extends RuleConverterBase {
8889
9226
  }
8890
9227
  // Process the pseudo class based on its name
8891
9228
  switch (node.name) {
8892
- case HAS_TEXT$1:
8893
- case CONTAINS$1:
9229
+ case PseudoClasses$1.HasText:
9230
+ case PseudoClasses$1.Contains:
8894
9231
  // Check if the argument is a RegExp
8895
9232
  if (RegExpUtils.isRegexPattern(arg.value)) {
8896
9233
  // TODO: Add some support for RegExp patterns later
8897
9234
  // Need to find a way to convert some RegExp patterns to glob patterns
8898
9235
  throw new RuleConversionError('Conversion of RegExp patterns is not yet supported');
8899
9236
  }
8900
- convertedSelector.children.push(CssTree.createAttributeSelectorNode(TAG_CONTENT, arg.value));
9237
+ convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.TagContent, arg.value));
8901
9238
  break;
8902
9239
  // https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#subjectmin-text-lengthn
8903
- case MIN_TEXT_LENGTH:
9240
+ case PseudoClasses$1.MinTextLength:
8904
9241
  minLength = CssTree.parsePseudoClassArgumentAsNumber(node);
8905
9242
  break;
8906
9243
  default:
@@ -8912,10 +9249,10 @@ class HtmlRuleConverter extends RuleConverterBase {
8912
9249
  }
8913
9250
  }
8914
9251
  if (minLength !== NOT_SPECIFIED) {
8915
- convertedSelector.children.push(CssTree.createAttributeSelectorNode(MIN_LENGTH, String(minLength)));
9252
+ convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.MinLength, String(minLength)));
8916
9253
  }
8917
- convertedSelector.children.push(CssTree.createAttributeSelectorNode(MAX_LENGTH, String(maxLength === NOT_SPECIFIED
8918
- ? ADGUARD_HTML_CONVERSION_MAX_LENGTH
9254
+ convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.MaxLength, String(maxLength === NOT_SPECIFIED
9255
+ ? ADG_HTML_CONVERSION_MAX_LENGTH
8919
9256
  : maxLength)));
8920
9257
  // Create the converted rule
8921
9258
  conversionResult.push({
@@ -8925,7 +9262,7 @@ class HtmlRuleConverter extends RuleConverterBase {
8925
9262
  // Convert the separator based on the exception status
8926
9263
  separator: {
8927
9264
  type: 'Value',
8928
- value: ruleNode.exception
9265
+ value: rule.exception
8929
9266
  ? exports.CosmeticRuleSeparator.AdgHtmlFilteringException
8930
9267
  : exports.CosmeticRuleSeparator.AdgHtmlFiltering,
8931
9268
  },
@@ -8940,11 +9277,11 @@ class HtmlRuleConverter extends RuleConverterBase {
8940
9277
  }],
8941
9278
  },
8942
9279
  },
8943
- exception: ruleNode.exception,
8944
- domains: ruleNode.domains,
9280
+ exception: rule.exception,
9281
+ domains: cloneDomainListNode(rule.domains),
8945
9282
  });
8946
9283
  }
8947
- return conversionResult;
9284
+ return createNodeConversionResult(conversionResult, true);
8948
9285
  }
8949
9286
  }
8950
9287
 
@@ -8965,96 +9302,38 @@ function getScriptletName(scriptletNode) {
8965
9302
  return scriptletNode.children[0].value;
8966
9303
  }
8967
9304
  /**
8968
- * Set name of the scriptlet
9305
+ * Set name of the scriptlet.
9306
+ * Modifies input `scriptletNode` if needed.
8969
9307
  *
8970
9308
  * @param scriptletNode Scriptlet node to set name of
8971
9309
  * @param name Name to set
8972
- * @returns Scriptlet node with the specified name
8973
- * @throws If the scriptlet is empty
8974
9310
  */
8975
9311
  function setScriptletName(scriptletNode, name) {
8976
- if (scriptletNode.children.length === 0) {
8977
- throw new Error('Empty scriptlet');
9312
+ if (scriptletNode.children.length > 0) {
9313
+ // eslint-disable-next-line no-param-reassign
9314
+ scriptletNode.children[0].value = name;
8978
9315
  }
8979
- const scriptletNodeClone = cloneDeep(scriptletNode);
8980
- scriptletNodeClone.children[0].value = name;
8981
- return scriptletNodeClone;
8982
9316
  }
8983
9317
  /**
8984
9318
  * Set quote type of the scriptlet parameters
8985
9319
  *
8986
9320
  * @param scriptletNode Scriptlet node to set quote type of
8987
9321
  * @param quoteType Preferred quote type
8988
- * @returns Scriptlet node with the specified quote type
8989
9322
  */
8990
9323
  function setScriptletQuoteType(scriptletNode, quoteType) {
8991
- if (scriptletNode.children.length === 0) {
8992
- throw new Error('Empty scriptlet');
8993
- }
8994
- const scriptletNodeClone = cloneDeep(scriptletNode);
8995
- for (let i = 0; i < scriptletNodeClone.children.length; i += 1) {
8996
- scriptletNodeClone.children[i].value = QuoteUtils.setStringQuoteType(scriptletNodeClone.children[i].value, quoteType);
8997
- }
8998
- return scriptletNodeClone;
8999
- }
9000
-
9001
- /**
9002
- * @file Scriptlet conversions from ABP and uBO to ADG
9003
- */
9004
- const ABP_SCRIPTLET_PREFIX = 'abp-';
9005
- const UBO_SCRIPTLET_PREFIX = 'ubo-';
9006
- /**
9007
- * Helper class for converting scriptlets from ABP and uBO to ADG
9008
- */
9009
- class AdgScriptletConverter {
9010
- /**
9011
- * Helper function to convert scriptlets to ADG. We implement the core
9012
- * logic here to avoid code duplication.
9013
- *
9014
- * @param scriptletNode Scriptlet parameter list node to convert
9015
- * @param prefix Prefix to add to the scriptlet name
9016
- * @returns Converted scriptlet parameter list node
9017
- */
9018
- static convertToAdg(scriptletNode, prefix) {
9019
- // Remove possible quotes just to make it easier to work with the scriptlet name
9020
- const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletNode), exports.QuoteType.None);
9021
- // Clone the node to avoid any side effects
9022
- let result = cloneDeep(scriptletNode);
9023
- // Only add prefix if it's not already there
9024
- if (!scriptletName.startsWith(prefix)) {
9025
- result = setScriptletName(scriptletNode, `${prefix}${scriptletName}`);
9324
+ if (scriptletNode.children.length > 0) {
9325
+ for (let i = 0; i < scriptletNode.children.length; i += 1) {
9326
+ // eslint-disable-next-line no-param-reassign
9327
+ scriptletNode.children[i].value = QuoteUtils.setStringQuoteType(scriptletNode.children[i].value, quoteType);
9026
9328
  }
9027
- // ADG scriptlet parameters should be quoted, and single quoted are preferred
9028
- result = setScriptletQuoteType(result, exports.QuoteType.Single);
9029
- return result;
9030
9329
  }
9031
- /**
9032
- * Converts an ABP snippet node to ADG scriptlet node, if possible.
9033
- *
9034
- * @param scriptletNode Scriptlet node to convert
9035
- * @returns Converted scriptlet node
9036
- * @throws If the scriptlet isn't supported by ADG or is invalid
9037
- * @see {@link https://help.adblockplus.org/hc/en-us/articles/1500002338501#snippets-ref}
9038
- */
9039
- static convertFromAbp = (scriptletNode) => {
9040
- return AdgScriptletConverter.convertToAdg(scriptletNode, ABP_SCRIPTLET_PREFIX);
9041
- };
9042
- /**
9043
- * Convert a uBO scriptlet node to ADG scriptlet node, if possible.
9044
- *
9045
- * @param scriptletNode Scriptlet node to convert
9046
- * @returns Converted scriptlet node
9047
- * @throws If the scriptlet isn't supported by ADG or is invalid
9048
- * @see {@link https://github.com/gorhill/uBlock/wiki/Resources-Library#available-general-purpose-scriptlets}
9049
- */
9050
- static convertFromUbo = (scriptletNode) => {
9051
- return AdgScriptletConverter.convertToAdg(scriptletNode, UBO_SCRIPTLET_PREFIX);
9052
- };
9053
9330
  }
9054
9331
 
9055
9332
  /**
9056
9333
  * @file Scriptlet injection rule converter
9057
9334
  */
9335
+ const ABP_SCRIPTLET_PREFIX = 'abp-';
9336
+ const UBO_SCRIPTLET_PREFIX = 'ubo-';
9058
9337
  /**
9059
9338
  * Scriptlet injection rule converter class
9060
9339
  *
@@ -9065,38 +9344,91 @@ class ScriptletRuleConverter extends RuleConverterBase {
9065
9344
  * Converts a scriptlet injection rule to AdGuard format, if possible.
9066
9345
  *
9067
9346
  * @param rule Rule node to convert
9068
- * @returns Array of converted rule nodes
9347
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9348
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9349
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9069
9350
  * @throws If the rule is invalid or cannot be converted
9070
9351
  */
9071
9352
  static convertToAdg(rule) {
9072
- // Clone the provided AST node to avoid side effects
9073
- const ruleNode = cloneDeep(rule);
9353
+ // Ignore AdGuard rules
9354
+ if (rule.syntax === exports.AdblockSyntax.Adg) {
9355
+ return createNodeConversionResult([rule], false);
9356
+ }
9357
+ const separator = rule.separator.value;
9358
+ let convertedSeparator = separator;
9359
+ convertedSeparator = rule.exception
9360
+ ? exports.CosmeticRuleSeparator.AdgJsInjectionException
9361
+ : exports.CosmeticRuleSeparator.AdgJsInjection;
9074
9362
  const convertedScriptlets = [];
9075
- for (const scriptlet of ruleNode.body.children) {
9076
- if (ruleNode.syntax === exports.AdblockSyntax.Abp) {
9077
- convertedScriptlets.push(AdgScriptletConverter.convertFromAbp(scriptlet));
9078
- }
9079
- else if (ruleNode.syntax === exports.AdblockSyntax.Ubo) {
9080
- convertedScriptlets.push(AdgScriptletConverter.convertFromUbo(scriptlet));
9363
+ for (const scriptlet of rule.body.children) {
9364
+ // Clone the node to avoid any side effects
9365
+ const scriptletClone = cloneScriptletRuleNode(scriptlet);
9366
+ // Remove possible quotes just to make it easier to work with the scriptlet name
9367
+ const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), exports.QuoteType.None);
9368
+ // Add prefix if it's not already there
9369
+ let prefix;
9370
+ switch (rule.syntax) {
9371
+ case exports.AdblockSyntax.Abp:
9372
+ prefix = ABP_SCRIPTLET_PREFIX;
9373
+ break;
9374
+ case exports.AdblockSyntax.Ubo:
9375
+ prefix = UBO_SCRIPTLET_PREFIX;
9376
+ break;
9377
+ default:
9378
+ prefix = EMPTY;
9081
9379
  }
9082
- else if (ruleNode.syntax === exports.AdblockSyntax.Adg) {
9083
- convertedScriptlets.push(scriptlet);
9380
+ if (!scriptletName.startsWith(prefix)) {
9381
+ setScriptletName(scriptletClone, `${prefix}${scriptletName}`);
9084
9382
  }
9383
+ // ADG scriptlet parameters should be quoted, and single quoted are preferred
9384
+ setScriptletQuoteType(scriptletClone, exports.QuoteType.Single);
9385
+ convertedScriptlets.push(scriptletClone);
9085
9386
  }
9086
- ruleNode.separator.value = ruleNode.exception
9087
- ? exports.CosmeticRuleSeparator.AdgJsInjectionException
9088
- : exports.CosmeticRuleSeparator.AdgJsInjection;
9089
- // ADG doesn't support multiple scriptlets in one rule, so we should split them
9090
- return convertedScriptlets.map((scriptlet) => {
9091
- return {
9092
- ...ruleNode,
9387
+ return createNodeConversionResult(convertedScriptlets.map((scriptlet) => {
9388
+ const res = {
9389
+ category: rule.category,
9390
+ type: rule.type,
9093
9391
  syntax: exports.AdblockSyntax.Adg,
9392
+ exception: rule.exception,
9393
+ domains: cloneDomainListNode(rule.domains),
9394
+ separator: {
9395
+ type: 'Value',
9396
+ value: convertedSeparator,
9397
+ },
9094
9398
  body: {
9095
- ...ruleNode.body,
9399
+ type: rule.body.type,
9096
9400
  children: [scriptlet],
9097
9401
  },
9098
9402
  };
9099
- });
9403
+ if (rule.modifiers) {
9404
+ res.modifiers = cloneModifierListNode(rule.modifiers);
9405
+ }
9406
+ return res;
9407
+ }), true);
9408
+ }
9409
+ }
9410
+
9411
+ /**
9412
+ * A very simple map extension that allows to store multiple values for the same key
9413
+ * by storing them in an array.
9414
+ *
9415
+ * @todo Add more methods if needed
9416
+ */
9417
+ class MultiValueMap extends Map {
9418
+ /**
9419
+ * Adds a value to the map. If the key already exists, the value will be appended to the existing array,
9420
+ * otherwise a new array will be created for the key.
9421
+ *
9422
+ * @param key Key to add
9423
+ * @param values Value(s) to add
9424
+ */
9425
+ add(key, ...values) {
9426
+ let currentValues = super.get(key);
9427
+ if (isUndefined(currentValues)) {
9428
+ currentValues = [];
9429
+ super.set(key, values);
9430
+ }
9431
+ currentValues.push(...values);
9100
9432
  }
9101
9433
  }
9102
9434
 
@@ -9122,69 +9454,115 @@ class AdgCosmeticRuleModifierConverter {
9122
9454
  * Converts a uBO cosmetic rule modifier list to ADG, if possible.
9123
9455
  *
9124
9456
  * @param modifierList Cosmetic rule modifier list node to convert
9125
- * @returns Converted cosmetic rule modifier list node
9457
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9458
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9459
+ * If the node was not converted, the result will contain the original node with the same object reference
9126
9460
  * @throws If the modifier list cannot be converted
9127
9461
  * @see {@link https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#cosmetic-filter-operators}
9128
9462
  */
9129
- static convertFromUbo = (modifierList) => {
9130
- const convertedModifierList = createModifierListNode();
9131
- for (const modifier of modifierList.children) {
9132
- let modifierValue;
9133
- switch (modifier.modifier.value) {
9134
- case UBO_MATCHES_PATH_OPERATOR:
9135
- // :matches-path() should have a value
9136
- if (!modifier.value) {
9137
- throw new RuleConversionError('Missing value for :matches-path(...)');
9138
- }
9139
- modifierValue = RegExpUtils.isRegexPattern(modifier.value.value)
9140
- ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS)
9141
- : modifier.value.value;
9142
- // Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
9143
- convertedModifierList.children.push(createModifierNode(ADG_PATH_MODIFIER,
9144
- // We should negate the regexp if the modifier is an exception
9145
- modifier.exception
9146
- // eslint-disable-next-line max-len
9147
- ? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(modifierValue))}${REGEX_MARKER}`
9148
- : modifierValue));
9149
- break;
9150
- default:
9151
- // Leave the modifier as-is
9152
- convertedModifierList.children.push(modifier);
9463
+ static convertFromUbo(modifierList) {
9464
+ const conversionMap = new MultiValueMap();
9465
+ modifierList.children.forEach((modifier, index) => {
9466
+ // :matches-path
9467
+ if (modifier.modifier.value === UBO_MATCHES_PATH_OPERATOR) {
9468
+ if (!modifier.value) {
9469
+ throw new RuleConversionError(`'${UBO_MATCHES_PATH_OPERATOR}' operator requires a value`);
9470
+ }
9471
+ const value = RegExpUtils.isRegexPattern(modifier.value.value)
9472
+ ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS)
9473
+ : modifier.value.value;
9474
+ // Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
9475
+ conversionMap.add(index, createModifierNode(ADG_PATH_MODIFIER,
9476
+ // We should negate the regexp if the modifier is an exception
9477
+ modifier.exception
9478
+ // eslint-disable-next-line max-len
9479
+ ? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(value))}${REGEX_MARKER}`
9480
+ : value));
9153
9481
  }
9154
- }
9155
- return convertedModifierList;
9156
- };
9482
+ });
9483
+ // Check if we have any converted modifiers
9484
+ if (conversionMap.size) {
9485
+ const modifierListClone = clone(modifierList);
9486
+ // Replace the original modifiers with the converted ones
9487
+ modifierListClone.children = modifierListClone.children.map((modifier, index) => {
9488
+ const convertedModifier = conversionMap.get(index);
9489
+ return convertedModifier ?? modifier;
9490
+ }).flat();
9491
+ return createConversionResult(modifierListClone, true);
9492
+ }
9493
+ // Otherwise, just return the original modifier list
9494
+ return createConversionResult(modifierList, false);
9495
+ }
9157
9496
  }
9158
9497
 
9159
- // Constants for pseudo-classes (please keep them sorted alphabetically)
9160
- const ABP_CONTAINS = '-abp-contains';
9161
- const ABP_HAS = '-abp-has';
9162
- const CONTAINS = 'contains';
9163
- const HAS = 'has';
9164
- const HAS_TEXT = 'has-text';
9165
- const MATCHES_CSS = 'matches-css';
9166
- const MATCHES_CSS_AFTER = 'matches-css-after';
9167
- const MATCHES_CSS_BEFORE = 'matches-css-before';
9168
- const NOT = 'not';
9169
- // Constants for pseudo-elements (please keep them sorted alphabetically)
9170
- const AFTER = 'after';
9171
- const BEFORE = 'before';
9498
+ var PseudoClasses;
9499
+ (function (PseudoClasses) {
9500
+ PseudoClasses["AbpContains"] = "-abp-contains";
9501
+ PseudoClasses["AbpHas"] = "-abp-has";
9502
+ PseudoClasses["Contains"] = "contains";
9503
+ PseudoClasses["Has"] = "has";
9504
+ PseudoClasses["HasText"] = "has-text";
9505
+ PseudoClasses["MatchesCss"] = "matches-css";
9506
+ PseudoClasses["MatchesCssAfter"] = "matches-css-after";
9507
+ PseudoClasses["MatchesCssBefore"] = "matches-css-before";
9508
+ PseudoClasses["Not"] = "not";
9509
+ })(PseudoClasses || (PseudoClasses = {}));
9510
+ var PseudoElements;
9511
+ (function (PseudoElements) {
9512
+ PseudoElements["After"] = "after";
9513
+ PseudoElements["Before"] = "before";
9514
+ })(PseudoElements || (PseudoElements = {}));
9515
+ const PSEUDO_ELEMENT_NAMES = new Set([
9516
+ PseudoElements.After,
9517
+ PseudoElements.Before,
9518
+ ]);
9519
+ const LEGACY_MATCHES_CSS_NAMES = new Set([
9520
+ PseudoClasses.MatchesCssAfter,
9521
+ PseudoClasses.MatchesCssBefore,
9522
+ ]);
9523
+ const LEGACY_EXT_CSS_INDICATOR_PSEUDO_NAMES = new Set([
9524
+ PseudoClasses.Not,
9525
+ PseudoClasses.MatchesCssBefore,
9526
+ PseudoClasses.MatchesCssAfter,
9527
+ ]);
9528
+ const CSS_CONVERSION_INDICATOR_PSEUDO_NAMES = new Set([
9529
+ PseudoClasses.AbpContains,
9530
+ PseudoClasses.AbpHas,
9531
+ PseudoClasses.HasText,
9532
+ ]);
9172
9533
  /**
9173
9534
  * Converts some pseudo-classes to pseudo-elements. For example:
9174
9535
  * - `:before` → `::before`
9175
9536
  *
9176
9537
  * @param selectorList Selector list to convert
9177
- * @returns Converted selector list
9538
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9539
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9540
+ * If the node was not converted, the result will contain the original node with the same object reference
9178
9541
  */
9179
9542
  function convertToPseudoElements(selectorList) {
9180
- // Prepare conversion result
9181
- const selectorListClone = cloneDeep(selectorList);
9543
+ // Check conversion indications before doing any heavy work
9544
+ const hasIndicator = ecssTree.find(
9545
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9546
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9547
+ selectorList, (node) => node.type === exports.CssTreeNodeType.PseudoClassSelector && PSEUDO_ELEMENT_NAMES.has(node.name));
9548
+ if (!hasIndicator) {
9549
+ return createConversionResult(selectorList, false);
9550
+ }
9551
+ // Make a clone of the selector list to avoid modifying the original one,
9552
+ // then convert & return the cloned version
9553
+ const selectorListClone = clone(selectorList);
9554
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9555
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9182
9556
  ecssTree.walk(selectorListClone, {
9557
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9558
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9183
9559
  leave: (node) => {
9184
9560
  if (node.type === exports.CssTreeNodeType.PseudoClassSelector) {
9185
- // :after ::after
9186
- // :before ::before
9187
- if (node.name === AFTER || node.name === BEFORE) {
9561
+ // If the pseudo-class is `:before` or `:after`, then we should
9562
+ // convert the node type to pseudo-element:
9563
+ // :after → ::after
9564
+ // :before → ::before
9565
+ if (PSEUDO_ELEMENT_NAMES.has(node.name)) {
9188
9566
  Object.assign(node, {
9189
9567
  ...node,
9190
9568
  type: exports.CssTreeNodeType.PseudoElementSelector,
@@ -9193,7 +9571,7 @@ function convertToPseudoElements(selectorList) {
9193
9571
  }
9194
9572
  },
9195
9573
  });
9196
- return selectorListClone;
9574
+ return createConversionResult(selectorListClone, true);
9197
9575
  }
9198
9576
  /**
9199
9577
  * Converts legacy Extended CSS `matches-css-before` and `matches-css-after`
@@ -9202,33 +9580,36 @@ function convertToPseudoElements(selectorList) {
9202
9580
  * - `:matches-css-after(...)` → `:matches-css(after, ...)`
9203
9581
  *
9204
9582
  * @param node Node to convert
9583
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9584
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9585
+ * If the node was not converted, the result will contain the original node with the same object reference
9205
9586
  * @throws If the node is invalid
9206
9587
  */
9207
9588
  function convertLegacyMatchesCss(node) {
9208
- const nodeClone = cloneDeep(node);
9209
- if (nodeClone.type === exports.CssTreeNodeType.PseudoClassSelector
9210
- && [MATCHES_CSS_BEFORE, MATCHES_CSS_AFTER].includes(nodeClone.name)) {
9211
- if (!nodeClone.children || nodeClone.children.size < 1) {
9212
- throw new Error(`Invalid ${nodeClone.name} pseudo-class: missing argument`);
9213
- }
9214
- // Remove the 'matches-css-' prefix to get the direction
9215
- const direction = nodeClone.name.substring(MATCHES_CSS.length + 1);
9216
- // Rename the pseudo-class
9217
- nodeClone.name = MATCHES_CSS;
9218
- // Add the direction to the first raw argument
9219
- const arg = nodeClone.children.first;
9220
- // Check argument
9221
- if (!arg) {
9222
- throw new Error(`Invalid ${nodeClone.name} pseudo-class: argument shouldn't be null`);
9223
- }
9224
- if (arg.type !== exports.CssTreeNodeType.Raw) {
9225
- throw new Error(`Invalid ${nodeClone.name} pseudo-class: unexpected argument type`);
9226
- }
9227
- // Add the direction as the first argument
9228
- arg.value = `${direction},${arg.value}`;
9229
- // Replace the original node with the converted one
9230
- Object.assign(node, nodeClone);
9231
- }
9589
+ // Check conversion indications before doing any heavy work
9590
+ if (node.type !== exports.CssTreeNodeType.PseudoClassSelector || !LEGACY_MATCHES_CSS_NAMES.has(node.name)) {
9591
+ return createConversionResult(node, false);
9592
+ }
9593
+ const nodeClone = clone(node);
9594
+ if (!nodeClone.children || nodeClone.children.length < 1) {
9595
+ throw new Error(`Invalid ${node.name} pseudo-class: missing argument`);
9596
+ }
9597
+ // Rename the pseudo-class
9598
+ nodeClone.name = PseudoClasses.MatchesCss;
9599
+ // Remove the 'matches-css-' prefix to get the direction
9600
+ const direction = node.name.substring(PseudoClasses.MatchesCss.length + 1);
9601
+ // Add the direction to the first raw argument
9602
+ const arg = nodeClone.children[0];
9603
+ // Check argument
9604
+ if (!arg) {
9605
+ throw new Error(`Invalid ${node.name} pseudo-class: argument shouldn't be null`);
9606
+ }
9607
+ if (arg.type !== exports.CssTreeNodeType.Raw) {
9608
+ throw new Error(`Invalid ${node.name} pseudo-class: unexpected argument type`);
9609
+ }
9610
+ // Add the direction as the first argument
9611
+ arg.value = `${direction},${arg.value}`;
9612
+ return createConversionResult(nodeClone, true);
9232
9613
  }
9233
9614
  /**
9234
9615
  * Converts legacy Extended CSS selectors to the modern Extended CSS syntax.
@@ -9238,16 +9619,40 @@ function convertLegacyMatchesCss(node) {
9238
9619
  * - `[-ext-matches-css-before=...]` → `:matches-css(before, ...)`
9239
9620
  *
9240
9621
  * @param selectorList Selector list AST to convert
9241
- * @returns Converted selector list
9622
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9623
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9624
+ * If the node was not converted, the result will contain the original node with the same object reference
9242
9625
  */
9243
9626
  function convertFromLegacyExtendedCss(selectorList) {
9244
- // Prepare conversion result
9245
- const selectorListClone = cloneDeep(selectorList);
9627
+ // Check conversion indications before doing any heavy work
9628
+ const hasIndicator = ecssTree.find(
9629
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9630
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9631
+ selectorList, (node) => {
9632
+ if (node.type === exports.CssTreeNodeType.PseudoClassSelector) {
9633
+ return LEGACY_EXT_CSS_INDICATOR_PSEUDO_NAMES.has(node.name);
9634
+ }
9635
+ if (node.type === exports.CssTreeNodeType.AttributeSelector) {
9636
+ return node.name.name.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX);
9637
+ }
9638
+ return false;
9639
+ });
9640
+ if (!hasIndicator) {
9641
+ return createConversionResult(selectorList, false);
9642
+ }
9643
+ const selectorListClone = clone(selectorList);
9644
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9645
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9246
9646
  ecssTree.walk(selectorListClone, {
9647
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9648
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9247
9649
  leave: (node) => {
9248
9650
  // :matches-css-before(arg) → :matches-css(before,arg)
9249
9651
  // :matches-css-after(arg) → :matches-css(after,arg)
9250
- convertLegacyMatchesCss(node);
9652
+ const convertedLegacyExtCss = convertLegacyMatchesCss(node);
9653
+ if (convertedLegacyExtCss.isConverted) {
9654
+ Object.assign(node, convertedLegacyExtCss.result);
9655
+ }
9251
9656
  // [-ext-name=...] → :name(...)
9252
9657
  // [-ext-name='...'] → :name(...)
9253
9658
  // [-ext-name="..."] → :name(...)
@@ -9261,7 +9666,7 @@ function convertFromLegacyExtendedCss(selectorList) {
9261
9666
  // Remove the '-ext-' prefix to get the pseudo-class name
9262
9667
  const name = node.name.name.substring(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX.length);
9263
9668
  // Prepare the children list for the pseudo-class node
9264
- const children = new ecssTree.List();
9669
+ const children = [];
9265
9670
  // TODO: Change String node to Raw node to drop the quotes.
9266
9671
  // The structure of the node is the same, just the type
9267
9672
  // is different and generate() will generate the quotes
@@ -9274,7 +9679,7 @@ function convertFromLegacyExtendedCss(selectorList) {
9274
9679
  // For example, if the input is [-ext-has="> .selector"], then
9275
9680
  // we need to parse "> .selector" as a selector instead of string
9276
9681
  // it as a raw value
9277
- if ([HAS, NOT].includes(name)) {
9682
+ if ([PseudoClasses.Has, PseudoClasses.Not].includes(name)) {
9278
9683
  // Get the value of the attribute selector
9279
9684
  const { value } = node;
9280
9685
  // If the value is an identifier, then simply push it to the
@@ -9284,10 +9689,12 @@ function convertFromLegacyExtendedCss(selectorList) {
9284
9689
  }
9285
9690
  else if (value.type === exports.CssTreeNodeType.String) {
9286
9691
  // Parse the value as a selector
9287
- const parsedChildren = CssTree.parse(value.value, exports.CssTreeParserContext.selectorList);
9692
+ const parsedChildren = CssTree.parsePlain(value.value, exports.CssTreeParserContext.selectorList);
9288
9693
  // Don't forget convert the parsed AST again, because
9289
9694
  // it was a raw string before
9290
- children.push(convertFromLegacyExtendedCss(parsedChildren));
9695
+ const convertedChildren = convertFromLegacyExtendedCss(parsedChildren);
9696
+ // Push the converted children to the list
9697
+ children.push(convertedChildren.result);
9291
9698
  }
9292
9699
  }
9293
9700
  else {
@@ -9314,14 +9721,12 @@ function convertFromLegacyExtendedCss(selectorList) {
9314
9721
  children,
9315
9722
  };
9316
9723
  // Handle this case: [-ext-matches-css-before=...] → :matches-css(before,...)
9317
- convertLegacyMatchesCss(pseudoNode);
9318
- // Convert attribute selector to pseudo-class selector, but
9319
- // keep the reference to the original node
9320
- Object.assign(node, pseudoNode);
9724
+ const convertedPseudoNode = convertLegacyMatchesCss(pseudoNode);
9725
+ Object.assign(node, convertedPseudoNode.isConverted ? convertedPseudoNode.result : pseudoNode);
9321
9726
  }
9322
9727
  },
9323
9728
  });
9324
- return selectorListClone;
9729
+ return createConversionResult(selectorListClone, true);
9325
9730
  }
9326
9731
  /**
9327
9732
  * CSS selector converter
@@ -9333,32 +9738,51 @@ class CssSelectorConverter extends ConverterBase {
9333
9738
  * Converts Extended CSS elements to AdGuard-compatible ones
9334
9739
  *
9335
9740
  * @param selectorList Selector list to convert
9336
- * @returns Converted selector list
9741
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9742
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9743
+ * If the node was not converted, the result will contain the original node with the same object reference
9337
9744
  * @throws If the rule is invalid or incompatible
9338
9745
  */
9339
9746
  static convertToAdg(selectorList) {
9340
9747
  // First, convert
9341
9748
  // - legacy Extended CSS selectors to the modern Extended CSS syntax and
9342
9749
  // - some pseudo-classes to pseudo-elements
9343
- const selectorListClone = convertToPseudoElements(convertFromLegacyExtendedCss(cloneDeep(selectorList)));
9750
+ const legacyExtCssConverted = convertFromLegacyExtendedCss(selectorList);
9751
+ const pseudoElementsConverted = convertToPseudoElements(legacyExtCssConverted.result);
9752
+ const hasIndicator = legacyExtCssConverted.isConverted || pseudoElementsConverted.isConverted || ecssTree.find(
9753
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9754
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9755
+ selectorList,
9756
+ // eslint-disable-next-line max-len
9757
+ (node) => node.type === exports.CssTreeNodeType.PseudoClassSelector && CSS_CONVERSION_INDICATOR_PSEUDO_NAMES.has(node.name));
9758
+ if (!hasIndicator) {
9759
+ return createConversionResult(selectorList, false);
9760
+ }
9761
+ const selectorListClone = legacyExtCssConverted.isConverted || pseudoElementsConverted.isConverted
9762
+ ? pseudoElementsConverted.result
9763
+ : clone(selectorList);
9344
9764
  // Then, convert some Extended CSS pseudo-classes to AdGuard-compatible ones
9765
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9766
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9345
9767
  ecssTree.walk(selectorListClone, {
9768
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9769
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9346
9770
  leave: (node) => {
9347
9771
  if (node.type === exports.CssTreeNodeType.PseudoClassSelector) {
9348
9772
  // :-abp-contains(...) → :contains(...)
9349
9773
  // :has-text(...) → :contains(...)
9350
- if (node.name === ABP_CONTAINS || node.name === HAS_TEXT) {
9351
- CssTree.renamePseudoClass(node, CONTAINS);
9774
+ if (node.name === PseudoClasses.AbpContains || node.name === PseudoClasses.HasText) {
9775
+ CssTree.renamePseudoClass(node, PseudoClasses.Contains);
9352
9776
  }
9353
9777
  // :-abp-has(...) → :has(...)
9354
- if (node.name === ABP_HAS) {
9355
- CssTree.renamePseudoClass(node, HAS);
9778
+ if (node.name === PseudoClasses.AbpHas) {
9779
+ CssTree.renamePseudoClass(node, PseudoClasses.Has);
9356
9780
  }
9357
9781
  // TODO: check uBO's `:others()` and `:watch-attr()` pseudo-classes
9358
9782
  }
9359
9783
  },
9360
9784
  });
9361
- return selectorListClone;
9785
+ return createConversionResult(selectorListClone, true);
9362
9786
  }
9363
9787
  }
9364
9788
 
@@ -9375,27 +9799,39 @@ class CssInjectionRuleConverter extends RuleConverterBase {
9375
9799
  * Converts a CSS injection rule to AdGuard format, if possible.
9376
9800
  *
9377
9801
  * @param rule Rule node to convert
9378
- * @returns Array of converted rule nodes
9802
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9803
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9804
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9379
9805
  * @throws If the rule is invalid or cannot be converted
9380
9806
  */
9381
9807
  static convertToAdg(rule) {
9382
- // Clone the provided AST node to avoid side effects
9383
- const ruleNode = cloneDeep(rule);
9808
+ const separator = rule.separator.value;
9809
+ let convertedSeparator = separator;
9384
9810
  // Change the separator if the rule contains ExtendedCSS selectors
9385
- if (CssTree.hasAnySelectorExtendedCssNode(ruleNode.body.selectorList) || ruleNode.body.remove) {
9386
- ruleNode.separator.value = ruleNode.exception
9811
+ if (CssTree.hasAnySelectorExtendedCssNode(rule.body.selectorList) || rule.body.remove) {
9812
+ convertedSeparator = rule.exception
9387
9813
  ? exports.CosmeticRuleSeparator.AdgExtendedCssInjectionException
9388
9814
  : exports.CosmeticRuleSeparator.AdgExtendedCssInjection;
9389
9815
  }
9390
9816
  else {
9391
- ruleNode.separator.value = ruleNode.exception
9817
+ convertedSeparator = rule.exception
9392
9818
  ? exports.CosmeticRuleSeparator.AdgCssInjectionException
9393
9819
  : exports.CosmeticRuleSeparator.AdgCssInjection;
9394
9820
  }
9395
- // Convert CSS selector list
9396
- Object.assign(ruleNode.body.selectorList, CssSelectorConverter.convertToAdg(ecssTree.fromPlainObject(ruleNode.body.selectorList)));
9397
- ruleNode.syntax = exports.AdblockSyntax.Adg;
9398
- return [ruleNode];
9821
+ const convertedSelectorList = CssSelectorConverter.convertToAdg(rule.body.selectorList);
9822
+ // Check if the rule needs to be converted
9823
+ if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg)
9824
+ || separator !== convertedSeparator
9825
+ || convertedSelectorList.isConverted) {
9826
+ // TODO: Replace with custom clone method
9827
+ const ruleClone = clone(rule);
9828
+ ruleClone.syntax = exports.AdblockSyntax.Adg;
9829
+ ruleClone.separator.value = convertedSeparator;
9830
+ ruleClone.body.selectorList = convertedSelectorList.result;
9831
+ return createNodeConversionResult([ruleClone], true);
9832
+ }
9833
+ // Otherwise, return the original rule
9834
+ return createNodeConversionResult([rule], false);
9399
9835
  }
9400
9836
  }
9401
9837
 
@@ -9412,27 +9848,39 @@ class ElementHidingRuleConverter extends RuleConverterBase {
9412
9848
  * Converts an element hiding rule to AdGuard format, if possible.
9413
9849
  *
9414
9850
  * @param rule Rule node to convert
9415
- * @returns Array of converted rule nodes
9851
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9852
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9853
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9416
9854
  * @throws If the rule is invalid or cannot be converted
9417
9855
  */
9418
9856
  static convertToAdg(rule) {
9419
- // Clone the provided AST node to avoid side effects
9420
- const ruleNode = cloneDeep(rule);
9857
+ const separator = rule.separator.value;
9858
+ let convertedSeparator = separator;
9421
9859
  // Change the separator if the rule contains ExtendedCSS selectors
9422
- if (CssTree.hasAnySelectorExtendedCssNode(ruleNode.body.selectorList)) {
9423
- ruleNode.separator.value = ruleNode.exception
9860
+ if (CssTree.hasAnySelectorExtendedCssNode(rule.body.selectorList)) {
9861
+ convertedSeparator = rule.exception
9424
9862
  ? exports.CosmeticRuleSeparator.ExtendedElementHidingException
9425
9863
  : exports.CosmeticRuleSeparator.ExtendedElementHiding;
9426
9864
  }
9427
9865
  else {
9428
- ruleNode.separator.value = ruleNode.exception
9866
+ convertedSeparator = rule.exception
9429
9867
  ? exports.CosmeticRuleSeparator.ElementHidingException
9430
9868
  : exports.CosmeticRuleSeparator.ElementHiding;
9431
9869
  }
9432
- // Convert CSS selector list
9433
- Object.assign(ruleNode.body.selectorList, CssSelectorConverter.convertToAdg(ecssTree.fromPlainObject(ruleNode.body.selectorList)));
9434
- ruleNode.syntax = exports.AdblockSyntax.Adg;
9435
- return [ruleNode];
9870
+ const convertedSelectorList = CssSelectorConverter.convertToAdg(rule.body.selectorList);
9871
+ // Check if the rule needs to be converted
9872
+ if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg)
9873
+ || separator !== convertedSeparator
9874
+ || convertedSelectorList.isConverted) {
9875
+ // TODO: Replace with custom clone method
9876
+ const ruleClone = clone(rule);
9877
+ ruleClone.syntax = exports.AdblockSyntax.Adg;
9878
+ ruleClone.separator.value = convertedSeparator;
9879
+ ruleClone.body.selectorList = convertedSelectorList.result;
9880
+ return createNodeConversionResult([ruleClone], true);
9881
+ }
9882
+ // Otherwise, return the original rule
9883
+ return createNodeConversionResult([rule], false);
9436
9884
  }
9437
9885
  }
9438
9886
 
@@ -9460,7 +9908,7 @@ function createNetworkRuleNode(pattern, modifiers = undefined, exception = false
9460
9908
  },
9461
9909
  };
9462
9910
  if (!isUndefined(modifiers)) {
9463
- result.modifiers = cloneDeep(modifiers);
9911
+ result.modifiers = clone(modifiers);
9464
9912
  }
9465
9913
  return result;
9466
9914
  }
@@ -9480,32 +9928,37 @@ class HeaderRemovalRuleConverter extends RuleConverterBase {
9480
9928
  * Converts a header removal rule to AdGuard syntax, if possible.
9481
9929
  *
9482
9930
  * @param rule Rule node to convert
9483
- * @returns Array of converted rule nodes
9931
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9932
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9933
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9484
9934
  * @throws If the rule is invalid or cannot be converted
9935
+ * @example
9936
+ * If the input rule is:
9937
+ * ```adblock
9938
+ * example.com##^responseheader(header-name)
9939
+ * ```
9940
+ * The output will be:
9941
+ * ```adblock
9942
+ * ||example.com^$removeheader=header-name
9943
+ * ```
9485
9944
  */
9486
9945
  static convertToAdg(rule) {
9487
- // Clone the provided AST node to avoid side effects
9488
- const ruleNode = cloneDeep(rule);
9489
9946
  // TODO: Add support for ABP syntax once it starts supporting header removal rules
9490
- // Check the input rule
9491
- if (ruleNode.category !== exports.RuleCategory.Cosmetic
9492
- || ruleNode.type !== exports.CosmeticRuleType.HtmlFilteringRule
9493
- || ruleNode.body.body.type !== exports.CssTreeNodeType.Function
9494
- || ruleNode.body.body.name !== UBO_RESPONSEHEADER_MARKER) {
9495
- throw new RuleConversionError('Not a response header rule');
9947
+ // Leave the rule as is if it's not a header removal rule
9948
+ if (rule.category !== exports.RuleCategory.Cosmetic
9949
+ || rule.type !== exports.CosmeticRuleType.HtmlFilteringRule
9950
+ || rule.body.body.type !== exports.CssTreeNodeType.Function
9951
+ || rule.body.body.name !== UBO_RESPONSEHEADER_MARKER) {
9952
+ return createNodeConversionResult([rule], false);
9496
9953
  }
9497
9954
  // Prepare network rule pattern
9498
- let pattern = EMPTY;
9499
- if (ruleNode.domains.children.length === 1) {
9955
+ const pattern = [];
9956
+ if (rule.domains.children.length === 1) {
9500
9957
  // If the rule has only one domain, we can use a simple network rule pattern:
9501
9958
  // ||single-domain-from-the-rule^
9502
- pattern = [
9503
- ADBLOCK_URL_START,
9504
- ruleNode.domains.children[0].value,
9505
- ADBLOCK_URL_SEPARATOR,
9506
- ].join(EMPTY);
9959
+ pattern.push(ADBLOCK_URL_START, rule.domains.children[0].value, ADBLOCK_URL_SEPARATOR);
9507
9960
  }
9508
- else if (ruleNode.domains.children.length > 1) {
9961
+ else if (rule.domains.children.length > 1) {
9509
9962
  // TODO: Add support for multiple domains, for example:
9510
9963
  // example.com,example.org,example.net##^responseheader(header-name)
9511
9964
  // We should consider allowing $domain with $removeheader modifier,
@@ -9515,13 +9968,13 @@ class HeaderRemovalRuleConverter extends RuleConverterBase {
9515
9968
  }
9516
9969
  // Prepare network rule modifiers
9517
9970
  const modifiers = createModifierListNode();
9518
- modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, CssTree.generateFunctionValue(ecssTree.fromPlainObject(ruleNode.body.body))));
9971
+ modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, CssTree.generateFunctionPlainValue(rule.body.body)));
9519
9972
  // Construct the network rule
9520
- return [
9521
- createNetworkRuleNode(pattern, modifiers,
9973
+ return createNodeConversionResult([
9974
+ createNetworkRuleNode(pattern.join(EMPTY), modifiers,
9522
9975
  // Copy the exception flag
9523
- ruleNode.exception, exports.AdblockSyntax.Adg),
9524
- ];
9976
+ rule.exception, exports.AdblockSyntax.Adg),
9977
+ ], true);
9525
9978
  }
9526
9979
  }
9527
9980
 
@@ -9538,48 +9991,69 @@ class CosmeticRuleConverter extends RuleConverterBase {
9538
9991
  * Converts a cosmetic rule to AdGuard syntax, if possible.
9539
9992
  *
9540
9993
  * @param rule Rule node to convert
9541
- * @returns Array of converted rule nodes
9994
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9995
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9996
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9542
9997
  * @throws If the rule is invalid or cannot be converted
9543
9998
  */
9544
9999
  static convertToAdg(rule) {
9545
- // Clone the provided AST node to avoid side effects
9546
- const ruleNode = cloneDeep(rule);
9547
- // Convert cosmetic rule modifiers
9548
- if (ruleNode.modifiers) {
9549
- if (ruleNode.syntax === exports.AdblockSyntax.Ubo) {
9550
- // uBO doesn't support this rule:
9551
- // example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
9552
- if (ruleNode.type === exports.CosmeticRuleType.ScriptletInjectionRule) {
9553
- throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
9554
- }
9555
- ruleNode.modifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(ruleNode.modifiers);
9556
- }
9557
- else if (ruleNode.syntax === exports.AdblockSyntax.Abp) {
9558
- // TODO: Implement once ABP starts supporting cosmetic rule modifiers
9559
- throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
9560
- }
9561
- }
10000
+ let subconverterResult;
9562
10001
  // Convert cosmetic rule based on its type
9563
- switch (ruleNode.type) {
10002
+ switch (rule.type) {
9564
10003
  case exports.CosmeticRuleType.ElementHidingRule:
9565
- return ElementHidingRuleConverter.convertToAdg(ruleNode);
10004
+ subconverterResult = ElementHidingRuleConverter.convertToAdg(rule);
10005
+ break;
9566
10006
  case exports.CosmeticRuleType.ScriptletInjectionRule:
9567
- return ScriptletRuleConverter.convertToAdg(ruleNode);
10007
+ subconverterResult = ScriptletRuleConverter.convertToAdg(rule);
10008
+ break;
9568
10009
  case exports.CosmeticRuleType.CssInjectionRule:
9569
- return CssInjectionRuleConverter.convertToAdg(ruleNode);
10010
+ subconverterResult = CssInjectionRuleConverter.convertToAdg(rule);
10011
+ break;
9570
10012
  case exports.CosmeticRuleType.HtmlFilteringRule:
9571
10013
  // Handle special case: uBO response header filtering rule
9572
- if (ruleNode.body.body.type === exports.CssTreeNodeType.Function
9573
- && ruleNode.body.body.name === UBO_RESPONSEHEADER_MARKER) {
9574
- return HeaderRemovalRuleConverter.convertToAdg(ruleNode);
10014
+ if (rule.body.body.type === exports.CssTreeNodeType.Function
10015
+ && rule.body.body.name === UBO_RESPONSEHEADER_MARKER) {
10016
+ subconverterResult = HeaderRemovalRuleConverter.convertToAdg(rule);
9575
10017
  }
9576
- return HtmlRuleConverter.convertToAdg(ruleNode);
9577
- // Note: Currently, only ADG supports JS injection rules
10018
+ else {
10019
+ subconverterResult = HtmlRuleConverter.convertToAdg(rule);
10020
+ }
10021
+ break;
10022
+ // Note: Currently, only ADG supports JS injection rules, so we don't need to convert them
9578
10023
  case exports.CosmeticRuleType.JsInjectionRule:
9579
- return [ruleNode];
10024
+ subconverterResult = createNodeConversionResult([rule], false);
10025
+ break;
9580
10026
  default:
9581
10027
  throw new RuleConversionError('Unsupported cosmetic rule type');
9582
10028
  }
10029
+ let convertedModifiers;
10030
+ // Convert cosmetic rule modifiers, if any
10031
+ if (rule.modifiers) {
10032
+ if (rule.syntax === exports.AdblockSyntax.Ubo) {
10033
+ // uBO doesn't support this rule:
10034
+ // example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
10035
+ if (rule.type === exports.CosmeticRuleType.ScriptletInjectionRule) {
10036
+ throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
10037
+ }
10038
+ convertedModifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(rule.modifiers);
10039
+ }
10040
+ else if (rule.syntax === exports.AdblockSyntax.Abp) {
10041
+ // TODO: Implement once ABP starts supporting cosmetic rule modifiers
10042
+ throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
10043
+ }
10044
+ }
10045
+ if ((subconverterResult.result.length > 1 || subconverterResult.isConverted)
10046
+ || (convertedModifiers && convertedModifiers.isConverted)) {
10047
+ // Add modifier list to the subconverter result rules
10048
+ subconverterResult.result.forEach((subconverterRule) => {
10049
+ if (convertedModifiers && subconverterRule.category === exports.RuleCategory.Cosmetic) {
10050
+ // eslint-disable-next-line no-param-reassign
10051
+ subconverterRule.modifiers = convertedModifiers.result;
10052
+ }
10053
+ });
10054
+ return subconverterResult;
10055
+ }
10056
+ return createNodeConversionResult([rule], false);
9583
10057
  }
9584
10058
  }
9585
10059
 
@@ -9651,17 +10125,16 @@ class NetworkRuleModifierListConverter extends ConverterBase {
9651
10125
  * Converts a network rule modifier list to AdGuard format, if possible.
9652
10126
  *
9653
10127
  * @param modifierList Network rule modifier list node to convert
9654
- * @returns Converted modifier list node
10128
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10129
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
10130
+ * If the node was not converted, the result will contain the original node with the same object reference
9655
10131
  * @throws If the conversion is not possible
9656
10132
  */
9657
10133
  static convertToAdg(modifierList) {
9658
- // Clone the provided AST node to avoid side effects
9659
- const modifierListNode = cloneDeep(modifierList);
9660
- const convertedModifierList = createModifierListNode();
9661
- // We should merge $csp modifiers into one
9662
- const cspValues = [];
9663
- modifierListNode.children.forEach((modifierNode) => {
9664
- // Handle regular modifiers conversion and $csp modifiers collection
10134
+ const conversionMap = new MultiValueMap();
10135
+ // Special case: $csp modifier
10136
+ let cspCount = 0;
10137
+ modifierList.children.forEach((modifierNode, index) => {
9665
10138
  const modifierConversions = ADG_CONVERSION_MAP.get(modifierNode.modifier.value);
9666
10139
  if (modifierConversions) {
9667
10140
  for (const modifierConversion of modifierConversions) {
@@ -9674,17 +10147,14 @@ class NetworkRuleModifierListConverter extends ConverterBase {
9674
10147
  const value = modifierConversion.value
9675
10148
  ? modifierConversion.value(modifierNode.value?.value)
9676
10149
  : modifierNode.value?.value;
9677
- if (name === CSP_MODIFIER && value) {
9678
- // Special case: collect $csp values
9679
- cspValues.push(value);
10150
+ // Check if the name or the value is different from the original modifier
10151
+ // If so, add the converted modifier to the list
10152
+ if (name !== modifierNode.modifier.value || value !== modifierNode.value?.value) {
10153
+ conversionMap.add(index, createModifierNode(name, value, exception));
9680
10154
  }
9681
- else {
9682
- // Regular case: collect the converted modifiers, if the modifier list
9683
- // not already contains the same modifier
9684
- const existingModifier = convertedModifierList.children.find((m) => m.modifier.value === name && m.exception === exception && m.value?.value === value);
9685
- if (!existingModifier) {
9686
- convertedModifierList.children.push(createModifierNode(name, value, exception));
9687
- }
10155
+ // Special case: $csp modifier
10156
+ if (name === CSP_MODIFIER) {
10157
+ cspCount += 1;
9688
10158
  }
9689
10159
  }
9690
10160
  return;
@@ -9707,26 +10177,52 @@ class NetworkRuleModifierListConverter extends ConverterBase {
9707
10177
  // Try to convert the redirect resource name to ADG format
9708
10178
  // This function returns undefined if the resource name is unknown
9709
10179
  const convertedRedirectResource = redirects.convertRedirectNameToAdg(redirectResource);
9710
- convertedModifierList.children.push(createModifierNode(modifierName,
9711
- // If the redirect resource name is unknown, fall back to the original one
9712
- // Later, the validator will throw an error if the resource name is invalid
9713
- convertedRedirectResource || redirectResource, modifierNode.exception));
9714
- return;
9715
- }
9716
- // In all other cases, just copy the modifier as is, if the modifier list
9717
- // not already contains the same modifier
9718
- const existingModifier = convertedModifierList.children.find((m) => m.modifier.value === modifierNode.modifier.value
9719
- && m.exception === modifierNode.exception
9720
- && m.value?.value === modifierNode.value?.value);
9721
- if (!existingModifier) {
9722
- convertedModifierList.children.push(modifierNode);
10180
+ // Check if the modifier name or the redirect resource name is different from the original modifier
10181
+ // If so, add the converted modifier to the list
10182
+ if (modifierName !== modifierNode.modifier.value
10183
+ || (convertedRedirectResource !== undefined && convertedRedirectResource !== redirectResource)) {
10184
+ conversionMap.add(index, createModifierNode(modifierName,
10185
+ // If the redirect resource name is unknown, fall back to the original one
10186
+ // Later, the validator will throw an error if the resource name is invalid
10187
+ convertedRedirectResource || redirectResource, modifierNode.exception));
10188
+ }
9723
10189
  }
9724
10190
  });
9725
- // Merge $csp modifiers into one, then add it to the converted modifier list
9726
- if (cspValues.length > 0) {
9727
- convertedModifierList.children.push(createModifierNode(CSP_MODIFIER, cspValues.join(CSP_SEPARATOR)));
10191
+ // Prepare the result if there are any converted modifiers or $csp modifiers
10192
+ if (conversionMap.size || cspCount) {
10193
+ const modifierListClone = cloneModifierListNode(modifierList);
10194
+ // Replace the original modifiers with the converted ones
10195
+ // One modifier may be replaced with multiple modifiers, so we need to flatten the array
10196
+ modifierListClone.children = modifierListClone.children.map((modifierNode, index) => {
10197
+ const conversionRecord = conversionMap.get(index);
10198
+ if (conversionRecord) {
10199
+ return conversionRecord;
10200
+ }
10201
+ return modifierNode;
10202
+ }).flat();
10203
+ // Special case: $csp modifier: merge multiple $csp modifiers into one
10204
+ // and put it at the end of the modifier list
10205
+ if (cspCount) {
10206
+ const cspValues = [];
10207
+ modifierListClone.children = modifierListClone.children.filter((modifierNode) => {
10208
+ if (modifierNode.modifier.value === CSP_MODIFIER) {
10209
+ if (!modifierNode.value?.value) {
10210
+ throw new RuleConversionError('$csp modifier value is missing');
10211
+ }
10212
+ cspValues.push(modifierNode.value?.value);
10213
+ return false;
10214
+ }
10215
+ return true;
10216
+ });
10217
+ modifierListClone.children.push(createModifierNode(CSP_MODIFIER, cspValues.join(CSP_SEPARATOR)));
10218
+ }
10219
+ // Before returning the result, remove duplicated modifiers
10220
+ modifierListClone.children = modifierListClone.children.filter((modifierNode, index, self) => self.findIndex((m) => m.modifier.value === modifierNode.modifier.value
10221
+ && m.exception === modifierNode.exception
10222
+ && m.value?.value === modifierNode.value?.value) === index);
10223
+ return createConversionResult(modifierListClone, true);
9728
10224
  }
9729
- return convertedModifierList;
10225
+ return createConversionResult(modifierList, false);
9730
10226
  }
9731
10227
  }
9732
10228
 
@@ -9743,17 +10239,35 @@ class NetworkRuleConverter extends RuleConverterBase {
9743
10239
  * Converts a network rule to AdGuard format, if possible.
9744
10240
  *
9745
10241
  * @param rule Rule node to convert
9746
- * @returns Array of converted rule nodes
10242
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
10243
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
10244
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9747
10245
  * @throws If the rule is invalid or cannot be converted
9748
10246
  */
9749
10247
  static convertToAdg(rule) {
9750
- // Clone the provided AST node to avoid side effects
9751
- const ruleNode = cloneDeep(rule);
9752
- // Convert modifiers
9753
- if (ruleNode.modifiers) {
9754
- Object.assign(ruleNode.modifiers, NetworkRuleModifierListConverter.convertToAdg(ruleNode.modifiers));
10248
+ if (rule.modifiers) {
10249
+ const modifiers = NetworkRuleModifierListConverter.convertToAdg(rule.modifiers);
10250
+ // If the object reference is different, it means that the modifiers were converted
10251
+ // In this case, we should clone the entire rule and replace the modifiers with the converted ones
10252
+ if (modifiers.isConverted) {
10253
+ return {
10254
+ result: [{
10255
+ category: exports.RuleCategory.Network,
10256
+ type: 'NetworkRule',
10257
+ syntax: rule.syntax,
10258
+ exception: rule.exception,
10259
+ pattern: {
10260
+ type: 'Value',
10261
+ value: rule.pattern.value,
10262
+ },
10263
+ modifiers: modifiers.result,
10264
+ }],
10265
+ isConverted: true,
10266
+ };
10267
+ }
9755
10268
  }
9756
- return [ruleNode];
10269
+ // If the modifiers were not converted, return the original rule
10270
+ return createNodeConversionResult([rule], false);
9757
10271
  }
9758
10272
  }
9759
10273
 
@@ -9774,48 +10288,27 @@ class RuleConverter extends RuleConverterBase {
9774
10288
  * Converts an adblock filtering rule to AdGuard format, if possible.
9775
10289
  *
9776
10290
  * @param rule Rule node to convert
9777
- * @returns Array of converted rule nodes
10291
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
10292
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
10293
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9778
10294
  * @throws If the rule is invalid or cannot be converted
9779
10295
  */
9780
10296
  static convertToAdg(rule) {
9781
- // Clone the provided AST node to avoid side effects
9782
- const ruleNode = cloneDeep(rule);
9783
10297
  // Delegate conversion to the corresponding sub-converter
9784
10298
  // based on the rule category
9785
- switch (ruleNode.category) {
10299
+ switch (rule.category) {
9786
10300
  case exports.RuleCategory.Comment:
9787
- return CommentRuleConverter.convertToAdg(ruleNode);
10301
+ return CommentRuleConverter.convertToAdg(rule);
9788
10302
  case exports.RuleCategory.Cosmetic:
9789
- return CosmeticRuleConverter.convertToAdg(ruleNode);
10303
+ return CosmeticRuleConverter.convertToAdg(rule);
9790
10304
  case exports.RuleCategory.Network:
9791
- return NetworkRuleConverter.convertToAdg(ruleNode);
10305
+ return NetworkRuleConverter.convertToAdg(rule);
9792
10306
  default:
9793
- throw new RuleConversionError(`Unknown rule category: ${ruleNode.category}`);
10307
+ throw new RuleConversionError(`Unknown rule category: ${rule.category}`);
9794
10308
  }
9795
10309
  }
9796
10310
  }
9797
10311
 
9798
- /**
9799
- * @file Utility functions for working with filter list nodes
9800
- */
9801
- /**
9802
- * Creates a filter list node
9803
- *
9804
- * @param rules Rules to put in the list (optional, defaults to an empty list)
9805
- * @returns Filter list node
9806
- */
9807
- function createFilterListNode(rules = []) {
9808
- const result = {
9809
- type: 'FilterList',
9810
- children: [],
9811
- };
9812
- // We need to clone the rules to avoid side effects
9813
- if (rules.length > 0) {
9814
- result.children = cloneDeep(rules);
9815
- }
9816
- return result;
9817
- }
9818
-
9819
10312
  /**
9820
10313
  * @file Adblock filter list converter
9821
10314
  */
@@ -9834,18 +10327,133 @@ class FilterListConverter extends ConverterBase {
9834
10327
  * Converts an adblock filter list to AdGuard format, if possible.
9835
10328
  *
9836
10329
  * @param filterListNode Filter list node to convert
9837
- * @returns Converted filter list node
9838
- * @throws If the filter list is invalid or cannot be converted
10330
+ * @param tolerant Indicates whether the converter should be tolerant to invalid rules. If enabled and a rule is
10331
+ * invalid, it will be left as is. If disabled and a rule is invalid, the whole filter list will be failed.
10332
+ * Defaults to `true`.
10333
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10334
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
10335
+ * If the node was not converted, the result will contain the original node with the same object reference
10336
+ * @throws If the filter list is invalid or cannot be converted (if the tolerant mode is disabled)
10337
+ */
10338
+ static convertToAdg(filterListNode, tolerant = true) {
10339
+ // Prepare a map to store the converted rules by their index in the filter list
10340
+ const conversionMap = new MultiValueMap();
10341
+ // Iterate over the filtering rules and convert them one by one, then add them to the result (one conversion may
10342
+ // result in multiple rules)
10343
+ for (let i = 0; i < filterListNode.children.length; i += 1) {
10344
+ try {
10345
+ const convertedRules = RuleConverter.convertToAdg(filterListNode.children[i]);
10346
+ // Add the converted rules to the map if they were converted
10347
+ if (convertedRules.isConverted) {
10348
+ conversionMap.add(i, ...convertedRules.result);
10349
+ }
10350
+ }
10351
+ catch (error) {
10352
+ // If the tolerant mode is disabled, we should throw an error, this will fail the whole filter list
10353
+ // conversion.
10354
+ // Otherwise, we just ignore the error and leave the rule as is
10355
+ if (!tolerant) {
10356
+ throw error;
10357
+ }
10358
+ }
10359
+ }
10360
+ // If the conversion map is empty, it means that no rules were converted, so we can return the original filter
10361
+ // list
10362
+ if (conversionMap.size === 0) {
10363
+ return createConversionResult(filterListNode, false);
10364
+ }
10365
+ // Otherwise, create a new filter list node with the converted rules
10366
+ const convertedFilterList = {
10367
+ type: 'FilterList',
10368
+ children: [],
10369
+ };
10370
+ // Iterate over the original rules again and add them to the converted filter list, replacing the converted
10371
+ // rules with the new ones at the specified indexes
10372
+ for (let i = 0; i < filterListNode.children.length; i += 1) {
10373
+ const rules = conversionMap.get(i);
10374
+ if (rules) {
10375
+ convertedFilterList.children.push(...rules);
10376
+ }
10377
+ else {
10378
+ // We clone the unconverted rules to avoid mutating the original filter list if we return the converted
10379
+ // one
10380
+ convertedFilterList.children.push(clone(filterListNode.children[i]));
10381
+ }
10382
+ }
10383
+ return createConversionResult(convertedFilterList, true);
10384
+ }
10385
+ }
10386
+
10387
+ /**
10388
+ * @file Filter list converter for raw filter lists
10389
+ *
10390
+ * Technically, this is a wrapper around `FilterListConverter` that works with nodes instead of strings.
10391
+ */
10392
+ /**
10393
+ * Adblock filter list converter class.
10394
+ *
10395
+ * You can use this class to convert string-based filter lists, since most of the converters work with nodes.
10396
+ * This class just provides an extra layer on top of the {@link FilterListConverter} and calls the parser/serializer
10397
+ * before/after the conversion internally.
10398
+ *
10399
+ * @todo Implement `convertToUbo` and `convertToAbp`
10400
+ */
10401
+ class RawFilterListConverter extends ConverterBase {
10402
+ /**
10403
+ * Converts an adblock filter list text to AdGuard format, if possible.
10404
+ *
10405
+ * @param rawFilterList Raw filter list text to convert
10406
+ * @param tolerant Indicates whether the converter should be tolerant to invalid rules. If enabled and a rule is
10407
+ * invalid, it will be left as is. If disabled and a rule is invalid, the whole filter list will be failed.
10408
+ * Defaults to `true`.
10409
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10410
+ * the array of converted filter list text, and its `isConverted` flag indicates whether the original rule was
10411
+ * converted. If the rule was not converted, the original filter list text will be returned
10412
+ * @throws If the filter list is invalid or cannot be converted (if the tolerant mode is disabled)
9839
10413
  */
9840
- static convertToAdg(filterListNode) {
9841
- const result = createFilterListNode();
9842
- // Iterate over the filtering rules and convert them one by one,
9843
- // then add them to the result (one conversion may result in multiple rules)
9844
- for (const ruleNode of filterListNode.children) {
9845
- const convertedRules = RuleConverter.convertToAdg(ruleNode);
9846
- result.children.push(...convertedRules);
10414
+ static convertToAdg(rawFilterList, tolerant = true) {
10415
+ const conversionResult = FilterListConverter.convertToAdg(FilterListParser.parse(rawFilterList, tolerant), tolerant);
10416
+ // If the filter list was not converted, return the original text
10417
+ if (!conversionResult.isConverted) {
10418
+ return createConversionResult(rawFilterList, false);
9847
10419
  }
9848
- return result;
10420
+ // Otherwise, serialize the filter list and return the result
10421
+ return createConversionResult(FilterListParser.generate(conversionResult.result), true);
10422
+ }
10423
+ }
10424
+
10425
+ /**
10426
+ * @file Rule converter for raw rules
10427
+ *
10428
+ * Technically, this is a wrapper around `RuleConverter` that works with nodes instead of strings.
10429
+ */
10430
+ /**
10431
+ * Adblock filtering rule converter class.
10432
+ *
10433
+ * You can use this class to convert string-based adblock rules, since most of the converters work with nodes.
10434
+ * This class just provides an extra layer on top of the {@link RuleConverter} and calls the parser/serializer
10435
+ * before/after the conversion internally.
10436
+ *
10437
+ * @todo Implement `convertToUbo` and `convertToAbp`
10438
+ */
10439
+ class RawRuleConverter extends ConverterBase {
10440
+ /**
10441
+ * Converts an adblock filtering rule to AdGuard format, if possible.
10442
+ *
10443
+ * @param rawRule Raw rule text to convert
10444
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10445
+ * the array of converted rule texts, and its `isConverted` flag indicates whether the original rule was converted.
10446
+ * If the rule was not converted, the original rule text will be returned
10447
+ * @throws If the rule is invalid or cannot be converted
10448
+ */
10449
+ static convertToAdg(rawRule) {
10450
+ const conversionResult = RuleConverter.convertToAdg(RuleParser.parse(rawRule));
10451
+ // If the rule was not converted, return the original rule text
10452
+ if (!conversionResult.isConverted) {
10453
+ return createConversionResult([rawRule], false);
10454
+ }
10455
+ // Otherwise, serialize the converted rule nodes
10456
+ return createConversionResult(conversionResult.result.map(RuleParser.generate), true);
9849
10457
  }
9850
10458
  }
9851
10459
 
@@ -9927,7 +10535,7 @@ class LogicalExpressionUtils {
9927
10535
  }
9928
10536
  }
9929
10537
 
9930
- const version$1 = "1.1.5";
10538
+ const version$1 = "1.1.6";
9931
10539
 
9932
10540
  /**
9933
10541
  * @file AGTree version
@@ -9988,6 +10596,8 @@ exports.PREPROCESSOR_MARKER = PREPROCESSOR_MARKER;
9988
10596
  exports.ParameterListParser = ParameterListParser;
9989
10597
  exports.PreProcessorCommentRuleParser = PreProcessorCommentRuleParser;
9990
10598
  exports.QuoteUtils = QuoteUtils;
10599
+ exports.RawFilterListConverter = RawFilterListConverter;
10600
+ exports.RawRuleConverter = RawRuleConverter;
9991
10601
  exports.RegExpUtils = RegExpUtils;
9992
10602
  exports.RuleConversionError = RuleConversionError;
9993
10603
  exports.RuleConverter = RuleConverter;