@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.
@@ -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
@@ -7,7 +7,7 @@
7
7
  import valid from 'semver/functions/valid.js';
8
8
  import coerce from 'semver/functions/coerce.js';
9
9
  import JSON5 from 'json5';
10
- import { walk, parse, toPlainObject, find, generate, fromPlainObject, List } from '@adguard/ecss-tree';
10
+ import { walk, parse, toPlainObject, find, generate, List, fromPlainObject } from '@adguard/ecss-tree';
11
11
  import * as ecssTree from '@adguard/ecss-tree';
12
12
  export { ecssTree as ECSSTree };
13
13
  import cloneDeep from 'clone-deep';
@@ -262,7 +262,7 @@ const NEGATION_MARKER = '~';
262
262
  /**
263
263
  * The wildcard symbol — `*`.
264
264
  */
265
- const WILDCARD$1 = ASTERISK;
265
+ const WILDCARD = ASTERISK;
266
266
  /**
267
267
  * Classic domain separator.
268
268
  *
@@ -2861,7 +2861,7 @@ class ModifierParser {
2861
2861
  const modifierEnd = Math.max(StringUtils.skipWSBack(raw) + 1, modifierNameStart);
2862
2862
  // Modifier name can't be empty
2863
2863
  if (modifierNameStart === modifierEnd) {
2864
- throw new AdblockSyntaxError('Modifier name can\'t be empty', locRange(loc, 0, raw.length));
2864
+ throw new AdblockSyntaxError('Modifier name cannot be empty', locRange(loc, 0, raw.length));
2865
2865
  }
2866
2866
  let modifier;
2867
2867
  let value;
@@ -2885,7 +2885,7 @@ class ModifierParser {
2885
2885
  };
2886
2886
  // Value can't be empty
2887
2887
  if (assignmentIndex + 1 === modifierEnd) {
2888
- throw new AdblockSyntaxError('Modifier value can\'t be empty', locRange(loc, 0, raw.length));
2888
+ throw new AdblockSyntaxError('Modifier value cannot be empty', locRange(loc, 0, raw.length));
2889
2889
  }
2890
2890
  // Skip whitespace after the assignment operator
2891
2891
  const valueStart = StringUtils.skipWS(raw, assignmentIndex + MODIFIER_ASSIGN_OPERATOR.length);
@@ -3183,8 +3183,29 @@ const FORBIDDEN_CSS_FUNCTIONS = new Set([
3183
3183
  'url',
3184
3184
  ]);
3185
3185
 
3186
+ /**
3187
+ * @file Clone related utilities
3188
+ *
3189
+ * We should keep clone related functions in this file. Thus, we just provide
3190
+ * a simple interface for cloning values, we use it across the AGTree project,
3191
+ * and the implementation "under the hood" can be improved later, if needed.
3192
+ */
3193
+ /**
3194
+ * Clones an input value to avoid side effects. Use it only in justified cases,
3195
+ * because it can impact performance negatively.
3196
+ *
3197
+ * @param value Value to clone
3198
+ * @returns Cloned value
3199
+ */
3200
+ function clone(value) {
3201
+ // TODO: Replace cloneDeep with a more efficient implementation
3202
+ return cloneDeep(value);
3203
+ }
3204
+
3186
3205
  /**
3187
3206
  * @file Additional / helper functions for ECSSTree / CSSTree.
3207
+ *
3208
+ * @note There are no tests for some functions, but during the AGTree optimization we remove them anyway.
3188
3209
  */
3189
3210
  /**
3190
3211
  * Common CSSTree parsing options.
@@ -3320,10 +3341,10 @@ class CssTree {
3320
3341
  ast = CssTree.parse(selectorList, CssTreeParserContext.selectorList);
3321
3342
  }
3322
3343
  else {
3323
- ast = cloneDeep(selectorList);
3344
+ ast = clone(selectorList);
3324
3345
  }
3325
3346
  const nodes = [];
3326
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3347
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3327
3348
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3328
3349
  walk(ast, (node) => {
3329
3350
  if (CssTree.isExtendedCssNode(node, pseudoClasses, attributeSelectors)) {
@@ -3352,9 +3373,9 @@ class CssTree {
3352
3373
  ast = CssTree.parse(selectorList, CssTreeParserContext.selectorList);
3353
3374
  }
3354
3375
  else {
3355
- ast = cloneDeep(selectorList);
3376
+ ast = selectorList;
3356
3377
  }
3357
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3378
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3358
3379
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3359
3380
  return find(ast, (node) => CssTree.isExtendedCssNode(node, pseudoClasses, attributeSelectors)) !== null;
3360
3381
  }
@@ -3391,14 +3412,14 @@ class CssTree {
3391
3412
  ast = CssTree.parse(declarationList, CssTreeParserContext.declarationList);
3392
3413
  }
3393
3414
  else {
3394
- ast = cloneDeep(declarationList);
3415
+ ast = clone(declarationList);
3395
3416
  }
3396
3417
  const nodes = [];
3397
3418
  // While walking the AST we should skip the nested functions,
3398
3419
  // for example skip url()s in cross-fade(url(), url()), since
3399
3420
  // cross-fade() itself is already a forbidden function
3400
3421
  let inForbiddenFunction = false;
3401
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3422
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3402
3423
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3403
3424
  walk(ast, {
3404
3425
  enter: (node) => {
@@ -3436,9 +3457,9 @@ class CssTree {
3436
3457
  ast = CssTree.parse(declarationList, CssTreeParserContext.declarationList);
3437
3458
  }
3438
3459
  else {
3439
- ast = cloneDeep(declarationList);
3460
+ ast = clone(declarationList);
3440
3461
  }
3441
- // TODO: CSSTree types should be improved, as a workaround we use `any` here
3462
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3442
3463
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3443
3464
  return find(ast, (node) => CssTree.isForbiddenFunction(node, forbiddenFunctions)) !== null;
3444
3465
  }
@@ -3670,6 +3691,180 @@ class CssTree {
3670
3691
  });
3671
3692
  return result.trim();
3672
3693
  }
3694
+ /**
3695
+ * Generates string representation of the selector list.
3696
+ *
3697
+ * @param ast SelectorList AST
3698
+ * @returns String representation of the selector list
3699
+ */
3700
+ static generateSelectorListPlain(ast) {
3701
+ const result = [];
3702
+ if (!ast.children || ast.children.length === 0) {
3703
+ throw new Error('Selector list cannot be empty');
3704
+ }
3705
+ ast.children.forEach((selector, index, nodeList) => {
3706
+ if (selector.type !== CssTreeNodeType.Selector) {
3707
+ throw new Error(`Unexpected node type: ${selector.type}`);
3708
+ }
3709
+ result.push(this.generateSelectorPlain(selector));
3710
+ // If there is a next node, add a comma and a space after the selector
3711
+ if (nodeList[index + 1]) {
3712
+ result.push(COMMA, SPACE);
3713
+ }
3714
+ });
3715
+ return result.join(EMPTY);
3716
+ }
3717
+ /**
3718
+ * Selector generation based on CSSTree's AST. This is necessary because CSSTree
3719
+ * only adds spaces in some edge cases.
3720
+ *
3721
+ * @param ast CSS Tree AST
3722
+ * @returns CSS selector as string
3723
+ */
3724
+ static generateSelectorPlain(ast) {
3725
+ let result = EMPTY;
3726
+ let inAttributeSelector = false;
3727
+ let depth = 0;
3728
+ let selectorListDepth = -1;
3729
+ let prevNode = ast;
3730
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3731
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3732
+ walk(ast, {
3733
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
3734
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3735
+ enter: (node) => {
3736
+ depth += 1;
3737
+ // Skip attribute selector / selector list children
3738
+ if (inAttributeSelector || selectorListDepth > -1) {
3739
+ return;
3740
+ }
3741
+ switch (node.type) {
3742
+ // "Trivial" nodes
3743
+ case CssTreeNodeType.TypeSelector:
3744
+ result += node.name;
3745
+ break;
3746
+ case CssTreeNodeType.ClassSelector:
3747
+ result += DOT;
3748
+ result += node.name;
3749
+ break;
3750
+ case CssTreeNodeType.IdSelector:
3751
+ result += HASHMARK;
3752
+ result += node.name;
3753
+ break;
3754
+ case CssTreeNodeType.Identifier:
3755
+ result += node.name;
3756
+ break;
3757
+ case CssTreeNodeType.Raw:
3758
+ result += node.value;
3759
+ break;
3760
+ // "Advanced" nodes
3761
+ case CssTreeNodeType.Nth:
3762
+ // Default generation enough
3763
+ result += generate(node);
3764
+ break;
3765
+ // For example :not([id], [name])
3766
+ case CssTreeNodeType.SelectorList:
3767
+ // eslint-disable-next-line no-case-declarations
3768
+ const selectors = [];
3769
+ node.children.forEach((selector) => {
3770
+ if (selector.type === CssTreeNodeType.Selector) {
3771
+ selectors.push(CssTree.generateSelectorPlain(selector));
3772
+ }
3773
+ else if (selector.type === CssTreeNodeType.Raw) {
3774
+ selectors.push(selector.value);
3775
+ }
3776
+ });
3777
+ // Join selector lists
3778
+ result += selectors.join(COMMA + SPACE);
3779
+ // Skip nodes here
3780
+ selectorListDepth = depth;
3781
+ break;
3782
+ case CssTreeNodeType.Combinator:
3783
+ if (node.name === SPACE) {
3784
+ result += node.name;
3785
+ break;
3786
+ }
3787
+ // Prevent this case (unnecessary space): has( > .something)
3788
+ if (prevNode.type !== CssTreeNodeType.Selector) {
3789
+ result += SPACE;
3790
+ }
3791
+ result += node.name;
3792
+ result += SPACE;
3793
+ break;
3794
+ case CssTreeNodeType.AttributeSelector:
3795
+ result += OPEN_SQUARE_BRACKET;
3796
+ // Identifier name
3797
+ if (node.name) {
3798
+ result += node.name.name;
3799
+ }
3800
+ // Matcher operator, eg =
3801
+ if (node.matcher) {
3802
+ result += node.matcher;
3803
+ // Value can be String, Identifier or null
3804
+ if (node.value !== null) {
3805
+ // String node
3806
+ if (node.value.type === CssTreeNodeType.String) {
3807
+ result += generate(node.value);
3808
+ }
3809
+ else if (node.value.type === CssTreeNodeType.Identifier) {
3810
+ // Identifier node
3811
+ result += node.value.name;
3812
+ }
3813
+ }
3814
+ }
3815
+ // Flags
3816
+ if (node.flags) {
3817
+ // Space before flags
3818
+ result += SPACE;
3819
+ result += node.flags;
3820
+ }
3821
+ result += CLOSE_SQUARE_BRACKET;
3822
+ inAttributeSelector = true;
3823
+ break;
3824
+ case CssTreeNodeType.PseudoElementSelector:
3825
+ result += COLON;
3826
+ result += COLON;
3827
+ result += node.name;
3828
+ if (node.children !== null) {
3829
+ result += OPEN_PARENTHESIS;
3830
+ }
3831
+ break;
3832
+ case CssTreeNodeType.PseudoClassSelector:
3833
+ result += COLON;
3834
+ result += node.name;
3835
+ if (node.children !== null) {
3836
+ result += OPEN_PARENTHESIS;
3837
+ }
3838
+ break;
3839
+ }
3840
+ prevNode = node;
3841
+ },
3842
+ leave: (node) => {
3843
+ depth -= 1;
3844
+ if (node.type === CssTreeNodeType.SelectorList && depth + 1 === selectorListDepth) {
3845
+ selectorListDepth = -1;
3846
+ }
3847
+ if (selectorListDepth > -1) {
3848
+ return;
3849
+ }
3850
+ if (node.type === CssTreeNodeType.AttributeSelector) {
3851
+ inAttributeSelector = false;
3852
+ }
3853
+ if (inAttributeSelector) {
3854
+ return;
3855
+ }
3856
+ switch (node.type) {
3857
+ case CssTreeNodeType.PseudoElementSelector:
3858
+ case CssTreeNodeType.PseudoClassSelector:
3859
+ if (node.children) {
3860
+ result += CLOSE_PARENTHESIS;
3861
+ }
3862
+ break;
3863
+ }
3864
+ },
3865
+ });
3866
+ return result.trim();
3867
+ }
3673
3868
  /**
3674
3869
  * Block generation based on CSSTree's AST. This is necessary because CSSTree only adds spaces in some edge cases.
3675
3870
  *
@@ -3853,6 +4048,29 @@ class CssTree {
3853
4048
  });
3854
4049
  return result;
3855
4050
  }
4051
+ /**
4052
+ * Helper function to generate a raw string from a function selector's children
4053
+ *
4054
+ * @param node Function node
4055
+ * @returns Generated function value
4056
+ * @example `responseheader(name)` -> `name`
4057
+ */
4058
+ static generateFunctionPlainValue(node) {
4059
+ const result = [];
4060
+ node.children?.forEach((child) => {
4061
+ switch (child.type) {
4062
+ case CssTreeNodeType.Raw:
4063
+ result.push(child.value);
4064
+ break;
4065
+ default:
4066
+ // Fallback to CSSTree's default generate function
4067
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
4068
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4069
+ result.push(generate(child));
4070
+ }
4071
+ });
4072
+ return result.join(EMPTY);
4073
+ }
3856
4074
  }
3857
4075
 
3858
4076
  /**
@@ -3900,7 +4118,7 @@ class ElementHidingBodyParser {
3900
4118
  * @throws If the AST is invalid
3901
4119
  */
3902
4120
  static generate(ast) {
3903
- return CssTree.generateSelectorList(fromPlainObject(ast.selectorList));
4121
+ return CssTree.generateSelectorListPlain(ast.selectorList);
3904
4122
  }
3905
4123
  }
3906
4124
 
@@ -4144,7 +4362,7 @@ class CssInjectionBodyParser {
4144
4362
  if (mediaQueryList || declarationList || remove) {
4145
4363
  throw new AdblockSyntaxError(
4146
4364
  // eslint-disable-next-line max-len
4147
- 'Invalid selector, regular selector elements can\'t be used after special pseudo-classes', {
4365
+ 'Invalid selector, regular selector elements cannot be used after special pseudo-classes', {
4148
4366
  start: node.loc?.start ?? loc,
4149
4367
  end: shiftLoc(loc, raw.length),
4150
4368
  });
@@ -4833,7 +5051,7 @@ function createModifierListNode(modifiers = []) {
4833
5051
  const result = {
4834
5052
  type: 'ModifierList',
4835
5053
  // We need to clone the modifiers to avoid side effects
4836
- children: cloneDeep(modifiers),
5054
+ children: modifiers.length ? clone(modifiers) : [],
4837
5055
  };
4838
5056
  return result;
4839
5057
  }
@@ -4873,8 +5091,9 @@ function hasUboModifierIndicator(rawSelectorList) {
4873
5091
  * @returns Linked list based selector
4874
5092
  */
4875
5093
  function convertSelectorToLinkedList(selector) {
5094
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
4876
5095
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4877
- return fromPlainObject(cloneDeep(selector));
5096
+ return fromPlainObject(clone(selector));
4878
5097
  }
4879
5098
  /**
4880
5099
  * Helper function that always returns the linked list version of the
@@ -4884,8 +5103,9 @@ function convertSelectorToLinkedList(selector) {
4884
5103
  * @returns Linked list based selector list
4885
5104
  */
4886
5105
  function convertSelectorListToLinkedList(selectorList) {
5106
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
4887
5107
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4888
- return fromPlainObject(cloneDeep(selectorList));
5108
+ return fromPlainObject(clone(selectorList));
4889
5109
  }
4890
5110
  /**
4891
5111
  * Helper function for checking and removing bounding combinators
@@ -5970,7 +6190,8 @@ class FilterListParser {
5970
6190
  */
5971
6191
  static generate(ast, preferRaw = false) {
5972
6192
  let result = EMPTY;
5973
- for (const rule of ast.children) {
6193
+ for (let i = 0; i < ast.children.length; i += 1) {
6194
+ const rule = ast.children[i];
5974
6195
  if (preferRaw && rule.raws?.text) {
5975
6196
  result += rule.raws.text;
5976
6197
  }
@@ -5987,6 +6208,11 @@ class FilterListParser {
5987
6208
  case 'lf':
5988
6209
  result += LF;
5989
6210
  break;
6211
+ default:
6212
+ if (i !== ast.children.length - 1) {
6213
+ result += LF;
6214
+ }
6215
+ break;
5990
6216
  }
5991
6217
  }
5992
6218
  return result;
@@ -7541,14 +7767,14 @@ const getSpecificBlockerData = (modifiersData, blockerPrefix, modifierName) => {
7541
7767
  * @example
7542
7768
  * `example.*` — matches with any TLD, e.g. `example.org`, `example.com`, etc.
7543
7769
  */
7544
- const WILDCARD_TLD = DOT + WILDCARD$1;
7770
+ const WILDCARD_TLD = DOT + WILDCARD;
7545
7771
  /**
7546
7772
  * Marker for a wildcard subdomain — `*.`.
7547
7773
  *
7548
7774
  * @example
7549
7775
  * `*.example.org` — matches with any subdomain, e.g. `foo.example.org` or `bar.example.org`
7550
7776
  */
7551
- const WILDCARD_SUBDOMAIN = WILDCARD$1 + DOT;
7777
+ const WILDCARD_SUBDOMAIN = WILDCARD + DOT;
7552
7778
  class DomainUtils {
7553
7779
  /**
7554
7780
  * Check if the input is a valid domain or hostname.
@@ -7559,7 +7785,7 @@ class DomainUtils {
7559
7785
  static isValidDomainOrHostname(domain) {
7560
7786
  let domainToCheck = domain;
7561
7787
  // Wildcard-only domain, typically a generic rule
7562
- if (domainToCheck === WILDCARD$1) {
7788
+ if (domainToCheck === WILDCARD) {
7563
7789
  return true;
7564
7790
  }
7565
7791
  // https://adguard.com/kb/general/ad-filtering/create-own-filters/#wildcard-for-tld
@@ -7779,7 +8005,7 @@ const isValidAppNameChunk = (chunk) => {
7779
8005
  const isValidAppModifierValue = (value) => {
7780
8006
  // $app modifier does not support wildcard tld
7781
8007
  // https://adguard.app/kb/general/ad-filtering/create-own-filters/#app-modifier
7782
- if (value.includes(WILDCARD$1)) {
8008
+ if (value.includes(WILDCARD)) {
7783
8009
  return false;
7784
8010
  }
7785
8011
  return value
@@ -7844,7 +8070,7 @@ const isValidDenyAllowModifierValue = (value) => {
7844
8070
  // $denyallow modifier does not support wildcard tld
7845
8071
  // https://adguard.app/kb/general/ad-filtering/create-own-filters/#denyallow-modifier
7846
8072
  // but here we are simply checking whether the value contains wildcard `*`, not ends with `.*`
7847
- if (value.includes(WILDCARD$1)) {
8073
+ if (value.includes(WILDCARD)) {
7848
8074
  return false;
7849
8075
  }
7850
8076
  // TODO: add cache for domains validation
@@ -8141,7 +8367,7 @@ const validatePermissionAllowlist = (allowlist, directive, modifierName) => {
8141
8367
  // `*` is one of available permissions tokens
8142
8368
  // e.g. 'fullscreen=*'
8143
8369
  // https://w3c.github.io/webappsec-permissions-policy/#structured-header-serialization
8144
- if (allowlist === WILDCARD$1
8370
+ if (allowlist === WILDCARD
8145
8371
  // e.g. 'autoplay=()'
8146
8372
  || allowlist === EMPTY_PERMISSIONS_ALLOWLIST) {
8147
8373
  return { valid: true };
@@ -8405,7 +8631,7 @@ class ModifierValidator {
8405
8631
  * @returns Result of modifier validation.
8406
8632
  */
8407
8633
  validate = (syntax, rawModifier, isException = false) => {
8408
- const modifier = cloneDeep(rawModifier);
8634
+ const modifier = clone(rawModifier);
8409
8635
  // special case: handle noop modifier which may be used as multiple underscores (not just one)
8410
8636
  // https://adguard.com/kb/general/ad-filtering/create-own-filters/#noop-modifier
8411
8637
  if (modifier.modifier.value.startsWith(UNDERSCORE)) {
@@ -8484,7 +8710,9 @@ class ConverterBase {
8484
8710
  * Converts some data to AdGuard format
8485
8711
  *
8486
8712
  * @param data Data to convert
8487
- * @returns Converted data
8713
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
8714
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
8715
+ * If the node was not converted, the result will contain the original node with the same object reference
8488
8716
  * @throws If the data is invalid or incompatible
8489
8717
  */
8490
8718
  static convertToAdg(data) {
@@ -8494,7 +8722,9 @@ class ConverterBase {
8494
8722
  * Converts some data to Adblock Plus format
8495
8723
  *
8496
8724
  * @param data Data to convert
8497
- * @returns Converted data
8725
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
8726
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
8727
+ * If the node was not converted, the result will contain the original node with the same object reference
8498
8728
  * @throws If the data is invalid or incompatible
8499
8729
  */
8500
8730
  static convertToAbp(data) {
@@ -8504,7 +8734,9 @@ class ConverterBase {
8504
8734
  * Converts some data to uBlock Origin format
8505
8735
  *
8506
8736
  * @param data Data to convert
8507
- * @returns Converted data
8737
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
8738
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
8739
+ * If the node was not converted, the result will contain the original node with the same object reference
8508
8740
  * @throws If the data is invalid or incompatible
8509
8741
  */
8510
8742
  static convertToUbo(data) {
@@ -8528,7 +8760,9 @@ class RuleConverterBase extends ConverterBase {
8528
8760
  * Converts an adblock filtering rule to AdGuard format, if possible.
8529
8761
  *
8530
8762
  * @param rule Rule node to convert
8531
- * @returns Array of converted rule nodes
8763
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8764
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8765
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8532
8766
  * @throws If the rule is invalid or cannot be converted
8533
8767
  */
8534
8768
  static convertToAdg(rule) {
@@ -8538,7 +8772,9 @@ class RuleConverterBase extends ConverterBase {
8538
8772
  * Converts an adblock filtering rule to Adblock Plus format, if possible.
8539
8773
  *
8540
8774
  * @param rule Rule node to convert
8541
- * @returns Array of converted rule nodes
8775
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8776
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8777
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8542
8778
  * @throws If the rule is invalid or cannot be converted
8543
8779
  */
8544
8780
  static convertToAbp(rule) {
@@ -8548,7 +8784,9 @@ class RuleConverterBase extends ConverterBase {
8548
8784
  * Converts an adblock filtering rule to uBlock Origin format, if possible.
8549
8785
  *
8550
8786
  * @param rule Rule node to convert
8551
- * @returns Array of converted rule nodes
8787
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8788
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8789
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8552
8790
  * @throws If the rule is invalid or cannot be converted
8553
8791
  */
8554
8792
  static convertToUbo(rule) {
@@ -8556,6 +8794,37 @@ class RuleConverterBase extends ConverterBase {
8556
8794
  }
8557
8795
  }
8558
8796
 
8797
+ /**
8798
+ * @file Conversion result interface and helper functions
8799
+ */
8800
+ /**
8801
+ * Helper function to create a generic conversion result.
8802
+ *
8803
+ * @param result Conversion result
8804
+ * @param isConverted Indicates whether the input item was converted
8805
+ * @template T Type of the item to convert
8806
+ * @template U Type of the conversion result (defaults to `T`, but can be `T[]` as well)
8807
+ * @returns Generic conversion result
8808
+ */
8809
+ // eslint-disable-next-line max-len
8810
+ function createConversionResult(result, isConverted) {
8811
+ return {
8812
+ result,
8813
+ isConverted,
8814
+ };
8815
+ }
8816
+ /**
8817
+ * Helper function to create a node conversion result.
8818
+ *
8819
+ * @param nodes Array of nodes
8820
+ * @param isConverted Indicates whether the input item was converted
8821
+ * @template T Type of the node (extends `Node`)
8822
+ * @returns Node conversion result
8823
+ */
8824
+ function createNodeConversionResult(nodes, isConverted) {
8825
+ return createConversionResult(nodes, isConverted);
8826
+ }
8827
+
8559
8828
  /**
8560
8829
  * @file Comment rule converter
8561
8830
  */
@@ -8569,27 +8838,30 @@ class CommentRuleConverter extends RuleConverterBase {
8569
8838
  * Converts a comment rule to AdGuard format, if possible.
8570
8839
  *
8571
8840
  * @param rule Rule node to convert
8572
- * @returns Array of converted rule nodes
8841
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
8842
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
8843
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8573
8844
  * @throws If the rule is invalid or cannot be converted
8574
8845
  */
8575
8846
  static convertToAdg(rule) {
8576
- // Clone the provided AST node to avoid side effects
8577
- const ruleNode = cloneDeep(rule);
8578
8847
  // TODO: Add support for other comment types, if needed
8579
8848
  // Main task is # -> ! conversion
8580
- switch (ruleNode.type) {
8849
+ switch (rule.type) {
8581
8850
  case CommentRuleType.CommentRule:
8582
- // 'Comment' uBO style comments
8583
- if (ruleNode.type === CommentRuleType.CommentRule
8584
- && ruleNode.marker.value === CommentMarker.Hashmark) {
8585
- ruleNode.marker.value = CommentMarker.Regular;
8586
- // Add the hashmark to the beginning of the comment
8587
- ruleNode.text.value = `${SPACE}${CommentMarker.Hashmark}${ruleNode.text.value}`;
8851
+ // Check if the rule needs to be converted
8852
+ if (rule.type === CommentRuleType.CommentRule && rule.marker.value === CommentMarker.Hashmark) {
8853
+ // Add a ! to the beginning of the comment
8854
+ // TODO: Replace with custom clone method
8855
+ const ruleClone = clone(rule);
8856
+ ruleClone.marker.value = CommentMarker.Regular;
8857
+ // Add the hashmark to the beginning of the comment text
8858
+ ruleClone.text.value = `${SPACE}${CommentMarker.Hashmark}${ruleClone.text.value}`;
8859
+ return createNodeConversionResult([ruleClone], true);
8588
8860
  }
8589
- return [ruleNode];
8861
+ return createNodeConversionResult([rule], false);
8590
8862
  // Leave any other comment rule as is
8591
8863
  default:
8592
- return [ruleNode];
8864
+ return createNodeConversionResult([rule], false);
8593
8865
  }
8594
8866
  }
8595
8867
  }
@@ -8759,6 +9031,58 @@ class RegExpUtils {
8759
9031
  }
8760
9032
  }
8761
9033
 
9034
+ /**
9035
+ * @file Custom clone functions for AST nodes, this is probably the most efficient way to clone AST nodes.
9036
+ * @todo Maybe move them to parser classes as 'clone' methods
9037
+ */
9038
+ /**
9039
+ * Clones a scriptlet rule node.
9040
+ *
9041
+ * @param node Node to clone
9042
+ * @returns Cloned node
9043
+ */
9044
+ function cloneScriptletRuleNode(node) {
9045
+ return {
9046
+ type: node.type,
9047
+ children: node.children.map((child) => ({ ...child })),
9048
+ };
9049
+ }
9050
+ /**
9051
+ * Clones a domain list node.
9052
+ *
9053
+ * @param node Node to clone
9054
+ * @returns Cloned node
9055
+ */
9056
+ function cloneDomainListNode(node) {
9057
+ return {
9058
+ type: node.type,
9059
+ separator: node.separator,
9060
+ children: node.children.map((domain) => ({ ...domain })),
9061
+ };
9062
+ }
9063
+ /**
9064
+ * Clones a modifier list node.
9065
+ *
9066
+ * @param node Node to clone
9067
+ * @returns Cloned node
9068
+ */
9069
+ function cloneModifierListNode(node) {
9070
+ return {
9071
+ type: node.type,
9072
+ children: node.children.map((modifier) => {
9073
+ const res = {
9074
+ type: modifier.type,
9075
+ exception: modifier.exception,
9076
+ modifier: { ...modifier.modifier },
9077
+ };
9078
+ if (modifier.value) {
9079
+ res.value = { ...modifier.value };
9080
+ }
9081
+ return res;
9082
+ }),
9083
+ };
9084
+ }
9085
+
8762
9086
  /**
8763
9087
  * @file HTML filtering rule converter
8764
9088
  */
@@ -8771,16 +9095,22 @@ class RegExpUtils {
8771
9095
  *
8772
9096
  * @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#html-filtering-rules}
8773
9097
  */
8774
- const ADGUARD_HTML_DEFAULT_MAX_LENGTH = 8192;
8775
- const ADGUARD_HTML_CONVERSION_MAX_LENGTH = ADGUARD_HTML_DEFAULT_MAX_LENGTH * 32;
9098
+ const ADG_HTML_DEFAULT_MAX_LENGTH = 8192;
9099
+ const ADG_HTML_CONVERSION_MAX_LENGTH = ADG_HTML_DEFAULT_MAX_LENGTH * 32;
8776
9100
  const NOT_SPECIFIED = -1;
8777
- const CONTAINS$1 = 'contains';
8778
- const HAS_TEXT$1 = 'has-text';
8779
- const MAX_LENGTH = 'max-length';
8780
- const MIN_LENGTH = 'min-length';
8781
- const MIN_TEXT_LENGTH = 'min-text-length';
8782
- const TAG_CONTENT = 'tag-content';
8783
- const WILDCARD = 'wildcard';
9101
+ var PseudoClasses$1;
9102
+ (function (PseudoClasses) {
9103
+ PseudoClasses["Contains"] = "contains";
9104
+ PseudoClasses["HasText"] = "has-text";
9105
+ PseudoClasses["MinTextLength"] = "min-text-length";
9106
+ })(PseudoClasses$1 || (PseudoClasses$1 = {}));
9107
+ var AttributeSelectors;
9108
+ (function (AttributeSelectors) {
9109
+ AttributeSelectors["MaxLength"] = "max-length";
9110
+ AttributeSelectors["MinLength"] = "min-length";
9111
+ AttributeSelectors["TagContent"] = "tag-content";
9112
+ AttributeSelectors["Wildcard"] = "wildcard";
9113
+ })(AttributeSelectors || (AttributeSelectors = {}));
8784
9114
  /**
8785
9115
  * HTML filtering rule converter class
8786
9116
  *
@@ -8803,16 +9133,23 @@ class HtmlRuleConverter extends RuleConverterBase {
8803
9133
  * ```
8804
9134
  *
8805
9135
  * @param rule Rule node to convert
8806
- * @returns Array of converted rule nodes
9136
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9137
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9138
+ * If the rule was not converted, the result array will contain the original node with the same object reference
8807
9139
  * @throws If the rule is invalid or cannot be converted
8808
9140
  */
8809
9141
  static convertToAdg(rule) {
8810
- // Clone the provided AST node to avoid side effects
8811
- const ruleNode = cloneDeep(rule);
9142
+ // Ignore AdGuard rules
9143
+ if (rule.syntax === AdblockSyntax.Adg) {
9144
+ return createNodeConversionResult([rule], false);
9145
+ }
9146
+ if (rule.syntax === AdblockSyntax.Abp) {
9147
+ throw new RuleConversionError('Invalid rule, ABP does not support HTML filtering rules');
9148
+ }
8812
9149
  // Prepare the conversion result
8813
9150
  const conversionResult = [];
8814
9151
  // Iterate over selector list
8815
- for (const selector of ruleNode.body.body.children) {
9152
+ for (const selector of rule.body.body.children) {
8816
9153
  // Check selector, just in case
8817
9154
  if (selector.type !== CssTreeNodeType.Selector) {
8818
9155
  throw new RuleConversionError(`Expected selector, got '${selector.type}'`);
@@ -8839,24 +9176,24 @@ class HtmlRuleConverter extends RuleConverterBase {
8839
9176
  throw new RuleConversionError('Tag selector should be the first child, if present');
8840
9177
  }
8841
9178
  // Simply store the tag selector
8842
- convertedSelector.children.push(cloneDeep(node));
9179
+ convertedSelector.children.push(clone(node));
8843
9180
  break;
8844
9181
  case CssTreeNodeType.AttributeSelector:
8845
9182
  // Check if the attribute selector is a special AdGuard attribute
8846
9183
  switch (node.name.name) {
8847
- case MIN_LENGTH:
9184
+ case AttributeSelectors.MinLength:
8848
9185
  minLength = CssTree.parseAttributeSelectorValueAsNumber(node);
8849
9186
  break;
8850
- case MAX_LENGTH:
9187
+ case AttributeSelectors.MaxLength:
8851
9188
  maxLength = CssTree.parseAttributeSelectorValueAsNumber(node);
8852
9189
  break;
8853
- case TAG_CONTENT:
8854
- case WILDCARD:
9190
+ case AttributeSelectors.TagContent:
9191
+ case AttributeSelectors.Wildcard:
8855
9192
  CssTree.assertAttributeSelectorHasStringValue(node);
8856
- convertedSelector.children.push(cloneDeep(node));
9193
+ convertedSelector.children.push(clone(node));
8857
9194
  break;
8858
9195
  default:
8859
- convertedSelector.children.push(cloneDeep(node));
9196
+ convertedSelector.children.push(clone(node));
8860
9197
  }
8861
9198
  break;
8862
9199
  case CssTreeNodeType.PseudoClassSelector:
@@ -8870,18 +9207,18 @@ class HtmlRuleConverter extends RuleConverterBase {
8870
9207
  }
8871
9208
  // Process the pseudo class based on its name
8872
9209
  switch (node.name) {
8873
- case HAS_TEXT$1:
8874
- case CONTAINS$1:
9210
+ case PseudoClasses$1.HasText:
9211
+ case PseudoClasses$1.Contains:
8875
9212
  // Check if the argument is a RegExp
8876
9213
  if (RegExpUtils.isRegexPattern(arg.value)) {
8877
9214
  // TODO: Add some support for RegExp patterns later
8878
9215
  // Need to find a way to convert some RegExp patterns to glob patterns
8879
9216
  throw new RuleConversionError('Conversion of RegExp patterns is not yet supported');
8880
9217
  }
8881
- convertedSelector.children.push(CssTree.createAttributeSelectorNode(TAG_CONTENT, arg.value));
9218
+ convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.TagContent, arg.value));
8882
9219
  break;
8883
9220
  // https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#subjectmin-text-lengthn
8884
- case MIN_TEXT_LENGTH:
9221
+ case PseudoClasses$1.MinTextLength:
8885
9222
  minLength = CssTree.parsePseudoClassArgumentAsNumber(node);
8886
9223
  break;
8887
9224
  default:
@@ -8893,10 +9230,10 @@ class HtmlRuleConverter extends RuleConverterBase {
8893
9230
  }
8894
9231
  }
8895
9232
  if (minLength !== NOT_SPECIFIED) {
8896
- convertedSelector.children.push(CssTree.createAttributeSelectorNode(MIN_LENGTH, String(minLength)));
9233
+ convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.MinLength, String(minLength)));
8897
9234
  }
8898
- convertedSelector.children.push(CssTree.createAttributeSelectorNode(MAX_LENGTH, String(maxLength === NOT_SPECIFIED
8899
- ? ADGUARD_HTML_CONVERSION_MAX_LENGTH
9235
+ convertedSelector.children.push(CssTree.createAttributeSelectorNode(AttributeSelectors.MaxLength, String(maxLength === NOT_SPECIFIED
9236
+ ? ADG_HTML_CONVERSION_MAX_LENGTH
8900
9237
  : maxLength)));
8901
9238
  // Create the converted rule
8902
9239
  conversionResult.push({
@@ -8906,7 +9243,7 @@ class HtmlRuleConverter extends RuleConverterBase {
8906
9243
  // Convert the separator based on the exception status
8907
9244
  separator: {
8908
9245
  type: 'Value',
8909
- value: ruleNode.exception
9246
+ value: rule.exception
8910
9247
  ? CosmeticRuleSeparator.AdgHtmlFilteringException
8911
9248
  : CosmeticRuleSeparator.AdgHtmlFiltering,
8912
9249
  },
@@ -8921,11 +9258,11 @@ class HtmlRuleConverter extends RuleConverterBase {
8921
9258
  }],
8922
9259
  },
8923
9260
  },
8924
- exception: ruleNode.exception,
8925
- domains: ruleNode.domains,
9261
+ exception: rule.exception,
9262
+ domains: cloneDomainListNode(rule.domains),
8926
9263
  });
8927
9264
  }
8928
- return conversionResult;
9265
+ return createNodeConversionResult(conversionResult, true);
8929
9266
  }
8930
9267
  }
8931
9268
 
@@ -8946,96 +9283,38 @@ function getScriptletName(scriptletNode) {
8946
9283
  return scriptletNode.children[0].value;
8947
9284
  }
8948
9285
  /**
8949
- * Set name of the scriptlet
9286
+ * Set name of the scriptlet.
9287
+ * Modifies input `scriptletNode` if needed.
8950
9288
  *
8951
9289
  * @param scriptletNode Scriptlet node to set name of
8952
9290
  * @param name Name to set
8953
- * @returns Scriptlet node with the specified name
8954
- * @throws If the scriptlet is empty
8955
9291
  */
8956
9292
  function setScriptletName(scriptletNode, name) {
8957
- if (scriptletNode.children.length === 0) {
8958
- throw new Error('Empty scriptlet');
9293
+ if (scriptletNode.children.length > 0) {
9294
+ // eslint-disable-next-line no-param-reassign
9295
+ scriptletNode.children[0].value = name;
8959
9296
  }
8960
- const scriptletNodeClone = cloneDeep(scriptletNode);
8961
- scriptletNodeClone.children[0].value = name;
8962
- return scriptletNodeClone;
8963
9297
  }
8964
9298
  /**
8965
9299
  * Set quote type of the scriptlet parameters
8966
9300
  *
8967
9301
  * @param scriptletNode Scriptlet node to set quote type of
8968
9302
  * @param quoteType Preferred quote type
8969
- * @returns Scriptlet node with the specified quote type
8970
9303
  */
8971
9304
  function setScriptletQuoteType(scriptletNode, quoteType) {
8972
- if (scriptletNode.children.length === 0) {
8973
- throw new Error('Empty scriptlet');
8974
- }
8975
- const scriptletNodeClone = cloneDeep(scriptletNode);
8976
- for (let i = 0; i < scriptletNodeClone.children.length; i += 1) {
8977
- scriptletNodeClone.children[i].value = QuoteUtils.setStringQuoteType(scriptletNodeClone.children[i].value, quoteType);
8978
- }
8979
- return scriptletNodeClone;
8980
- }
8981
-
8982
- /**
8983
- * @file Scriptlet conversions from ABP and uBO to ADG
8984
- */
8985
- const ABP_SCRIPTLET_PREFIX = 'abp-';
8986
- const UBO_SCRIPTLET_PREFIX = 'ubo-';
8987
- /**
8988
- * Helper class for converting scriptlets from ABP and uBO to ADG
8989
- */
8990
- class AdgScriptletConverter {
8991
- /**
8992
- * Helper function to convert scriptlets to ADG. We implement the core
8993
- * logic here to avoid code duplication.
8994
- *
8995
- * @param scriptletNode Scriptlet parameter list node to convert
8996
- * @param prefix Prefix to add to the scriptlet name
8997
- * @returns Converted scriptlet parameter list node
8998
- */
8999
- static convertToAdg(scriptletNode, prefix) {
9000
- // Remove possible quotes just to make it easier to work with the scriptlet name
9001
- const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletNode), QuoteType.None);
9002
- // Clone the node to avoid any side effects
9003
- let result = cloneDeep(scriptletNode);
9004
- // Only add prefix if it's not already there
9005
- if (!scriptletName.startsWith(prefix)) {
9006
- result = setScriptletName(scriptletNode, `${prefix}${scriptletName}`);
9305
+ if (scriptletNode.children.length > 0) {
9306
+ for (let i = 0; i < scriptletNode.children.length; i += 1) {
9307
+ // eslint-disable-next-line no-param-reassign
9308
+ scriptletNode.children[i].value = QuoteUtils.setStringQuoteType(scriptletNode.children[i].value, quoteType);
9007
9309
  }
9008
- // ADG scriptlet parameters should be quoted, and single quoted are preferred
9009
- result = setScriptletQuoteType(result, QuoteType.Single);
9010
- return result;
9011
9310
  }
9012
- /**
9013
- * Converts an ABP snippet node to ADG scriptlet node, if possible.
9014
- *
9015
- * @param scriptletNode Scriptlet node to convert
9016
- * @returns Converted scriptlet node
9017
- * @throws If the scriptlet isn't supported by ADG or is invalid
9018
- * @see {@link https://help.adblockplus.org/hc/en-us/articles/1500002338501#snippets-ref}
9019
- */
9020
- static convertFromAbp = (scriptletNode) => {
9021
- return AdgScriptletConverter.convertToAdg(scriptletNode, ABP_SCRIPTLET_PREFIX);
9022
- };
9023
- /**
9024
- * Convert a uBO scriptlet node to ADG scriptlet node, if possible.
9025
- *
9026
- * @param scriptletNode Scriptlet node to convert
9027
- * @returns Converted scriptlet node
9028
- * @throws If the scriptlet isn't supported by ADG or is invalid
9029
- * @see {@link https://github.com/gorhill/uBlock/wiki/Resources-Library#available-general-purpose-scriptlets}
9030
- */
9031
- static convertFromUbo = (scriptletNode) => {
9032
- return AdgScriptletConverter.convertToAdg(scriptletNode, UBO_SCRIPTLET_PREFIX);
9033
- };
9034
9311
  }
9035
9312
 
9036
9313
  /**
9037
9314
  * @file Scriptlet injection rule converter
9038
9315
  */
9316
+ const ABP_SCRIPTLET_PREFIX = 'abp-';
9317
+ const UBO_SCRIPTLET_PREFIX = 'ubo-';
9039
9318
  /**
9040
9319
  * Scriptlet injection rule converter class
9041
9320
  *
@@ -9046,38 +9325,91 @@ class ScriptletRuleConverter extends RuleConverterBase {
9046
9325
  * Converts a scriptlet injection rule to AdGuard format, if possible.
9047
9326
  *
9048
9327
  * @param rule Rule node to convert
9049
- * @returns Array of converted rule nodes
9328
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9329
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9330
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9050
9331
  * @throws If the rule is invalid or cannot be converted
9051
9332
  */
9052
9333
  static convertToAdg(rule) {
9053
- // Clone the provided AST node to avoid side effects
9054
- const ruleNode = cloneDeep(rule);
9334
+ // Ignore AdGuard rules
9335
+ if (rule.syntax === AdblockSyntax.Adg) {
9336
+ return createNodeConversionResult([rule], false);
9337
+ }
9338
+ const separator = rule.separator.value;
9339
+ let convertedSeparator = separator;
9340
+ convertedSeparator = rule.exception
9341
+ ? CosmeticRuleSeparator.AdgJsInjectionException
9342
+ : CosmeticRuleSeparator.AdgJsInjection;
9055
9343
  const convertedScriptlets = [];
9056
- for (const scriptlet of ruleNode.body.children) {
9057
- if (ruleNode.syntax === AdblockSyntax.Abp) {
9058
- convertedScriptlets.push(AdgScriptletConverter.convertFromAbp(scriptlet));
9059
- }
9060
- else if (ruleNode.syntax === AdblockSyntax.Ubo) {
9061
- convertedScriptlets.push(AdgScriptletConverter.convertFromUbo(scriptlet));
9344
+ for (const scriptlet of rule.body.children) {
9345
+ // Clone the node to avoid any side effects
9346
+ const scriptletClone = cloneScriptletRuleNode(scriptlet);
9347
+ // Remove possible quotes just to make it easier to work with the scriptlet name
9348
+ const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), QuoteType.None);
9349
+ // Add prefix if it's not already there
9350
+ let prefix;
9351
+ switch (rule.syntax) {
9352
+ case AdblockSyntax.Abp:
9353
+ prefix = ABP_SCRIPTLET_PREFIX;
9354
+ break;
9355
+ case AdblockSyntax.Ubo:
9356
+ prefix = UBO_SCRIPTLET_PREFIX;
9357
+ break;
9358
+ default:
9359
+ prefix = EMPTY;
9062
9360
  }
9063
- else if (ruleNode.syntax === AdblockSyntax.Adg) {
9064
- convertedScriptlets.push(scriptlet);
9361
+ if (!scriptletName.startsWith(prefix)) {
9362
+ setScriptletName(scriptletClone, `${prefix}${scriptletName}`);
9065
9363
  }
9364
+ // ADG scriptlet parameters should be quoted, and single quoted are preferred
9365
+ setScriptletQuoteType(scriptletClone, QuoteType.Single);
9366
+ convertedScriptlets.push(scriptletClone);
9066
9367
  }
9067
- ruleNode.separator.value = ruleNode.exception
9068
- ? CosmeticRuleSeparator.AdgJsInjectionException
9069
- : CosmeticRuleSeparator.AdgJsInjection;
9070
- // ADG doesn't support multiple scriptlets in one rule, so we should split them
9071
- return convertedScriptlets.map((scriptlet) => {
9072
- return {
9073
- ...ruleNode,
9368
+ return createNodeConversionResult(convertedScriptlets.map((scriptlet) => {
9369
+ const res = {
9370
+ category: rule.category,
9371
+ type: rule.type,
9074
9372
  syntax: AdblockSyntax.Adg,
9373
+ exception: rule.exception,
9374
+ domains: cloneDomainListNode(rule.domains),
9375
+ separator: {
9376
+ type: 'Value',
9377
+ value: convertedSeparator,
9378
+ },
9075
9379
  body: {
9076
- ...ruleNode.body,
9380
+ type: rule.body.type,
9077
9381
  children: [scriptlet],
9078
9382
  },
9079
9383
  };
9080
- });
9384
+ if (rule.modifiers) {
9385
+ res.modifiers = cloneModifierListNode(rule.modifiers);
9386
+ }
9387
+ return res;
9388
+ }), true);
9389
+ }
9390
+ }
9391
+
9392
+ /**
9393
+ * A very simple map extension that allows to store multiple values for the same key
9394
+ * by storing them in an array.
9395
+ *
9396
+ * @todo Add more methods if needed
9397
+ */
9398
+ class MultiValueMap extends Map {
9399
+ /**
9400
+ * Adds a value to the map. If the key already exists, the value will be appended to the existing array,
9401
+ * otherwise a new array will be created for the key.
9402
+ *
9403
+ * @param key Key to add
9404
+ * @param values Value(s) to add
9405
+ */
9406
+ add(key, ...values) {
9407
+ let currentValues = super.get(key);
9408
+ if (isUndefined(currentValues)) {
9409
+ currentValues = [];
9410
+ super.set(key, values);
9411
+ }
9412
+ currentValues.push(...values);
9081
9413
  }
9082
9414
  }
9083
9415
 
@@ -9103,69 +9435,115 @@ class AdgCosmeticRuleModifierConverter {
9103
9435
  * Converts a uBO cosmetic rule modifier list to ADG, if possible.
9104
9436
  *
9105
9437
  * @param modifierList Cosmetic rule modifier list node to convert
9106
- * @returns Converted cosmetic rule modifier list node
9438
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9439
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9440
+ * If the node was not converted, the result will contain the original node with the same object reference
9107
9441
  * @throws If the modifier list cannot be converted
9108
9442
  * @see {@link https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#cosmetic-filter-operators}
9109
9443
  */
9110
- static convertFromUbo = (modifierList) => {
9111
- const convertedModifierList = createModifierListNode();
9112
- for (const modifier of modifierList.children) {
9113
- let modifierValue;
9114
- switch (modifier.modifier.value) {
9115
- case UBO_MATCHES_PATH_OPERATOR:
9116
- // :matches-path() should have a value
9117
- if (!modifier.value) {
9118
- throw new RuleConversionError('Missing value for :matches-path(...)');
9119
- }
9120
- modifierValue = RegExpUtils.isRegexPattern(modifier.value.value)
9121
- ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS)
9122
- : modifier.value.value;
9123
- // Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
9124
- convertedModifierList.children.push(createModifierNode(ADG_PATH_MODIFIER,
9125
- // We should negate the regexp if the modifier is an exception
9126
- modifier.exception
9127
- // eslint-disable-next-line max-len
9128
- ? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(modifierValue))}${REGEX_MARKER}`
9129
- : modifierValue));
9130
- break;
9131
- default:
9132
- // Leave the modifier as-is
9133
- convertedModifierList.children.push(modifier);
9444
+ static convertFromUbo(modifierList) {
9445
+ const conversionMap = new MultiValueMap();
9446
+ modifierList.children.forEach((modifier, index) => {
9447
+ // :matches-path
9448
+ if (modifier.modifier.value === UBO_MATCHES_PATH_OPERATOR) {
9449
+ if (!modifier.value) {
9450
+ throw new RuleConversionError(`'${UBO_MATCHES_PATH_OPERATOR}' operator requires a value`);
9451
+ }
9452
+ const value = RegExpUtils.isRegexPattern(modifier.value.value)
9453
+ ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS)
9454
+ : modifier.value.value;
9455
+ // Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
9456
+ conversionMap.add(index, createModifierNode(ADG_PATH_MODIFIER,
9457
+ // We should negate the regexp if the modifier is an exception
9458
+ modifier.exception
9459
+ // eslint-disable-next-line max-len
9460
+ ? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(value))}${REGEX_MARKER}`
9461
+ : value));
9134
9462
  }
9135
- }
9136
- return convertedModifierList;
9137
- };
9463
+ });
9464
+ // Check if we have any converted modifiers
9465
+ if (conversionMap.size) {
9466
+ const modifierListClone = clone(modifierList);
9467
+ // Replace the original modifiers with the converted ones
9468
+ modifierListClone.children = modifierListClone.children.map((modifier, index) => {
9469
+ const convertedModifier = conversionMap.get(index);
9470
+ return convertedModifier ?? modifier;
9471
+ }).flat();
9472
+ return createConversionResult(modifierListClone, true);
9473
+ }
9474
+ // Otherwise, just return the original modifier list
9475
+ return createConversionResult(modifierList, false);
9476
+ }
9138
9477
  }
9139
9478
 
9140
- // Constants for pseudo-classes (please keep them sorted alphabetically)
9141
- const ABP_CONTAINS = '-abp-contains';
9142
- const ABP_HAS = '-abp-has';
9143
- const CONTAINS = 'contains';
9144
- const HAS = 'has';
9145
- const HAS_TEXT = 'has-text';
9146
- const MATCHES_CSS = 'matches-css';
9147
- const MATCHES_CSS_AFTER = 'matches-css-after';
9148
- const MATCHES_CSS_BEFORE = 'matches-css-before';
9149
- const NOT = 'not';
9150
- // Constants for pseudo-elements (please keep them sorted alphabetically)
9151
- const AFTER = 'after';
9152
- const BEFORE = 'before';
9479
+ var PseudoClasses;
9480
+ (function (PseudoClasses) {
9481
+ PseudoClasses["AbpContains"] = "-abp-contains";
9482
+ PseudoClasses["AbpHas"] = "-abp-has";
9483
+ PseudoClasses["Contains"] = "contains";
9484
+ PseudoClasses["Has"] = "has";
9485
+ PseudoClasses["HasText"] = "has-text";
9486
+ PseudoClasses["MatchesCss"] = "matches-css";
9487
+ PseudoClasses["MatchesCssAfter"] = "matches-css-after";
9488
+ PseudoClasses["MatchesCssBefore"] = "matches-css-before";
9489
+ PseudoClasses["Not"] = "not";
9490
+ })(PseudoClasses || (PseudoClasses = {}));
9491
+ var PseudoElements;
9492
+ (function (PseudoElements) {
9493
+ PseudoElements["After"] = "after";
9494
+ PseudoElements["Before"] = "before";
9495
+ })(PseudoElements || (PseudoElements = {}));
9496
+ const PSEUDO_ELEMENT_NAMES = new Set([
9497
+ PseudoElements.After,
9498
+ PseudoElements.Before,
9499
+ ]);
9500
+ const LEGACY_MATCHES_CSS_NAMES = new Set([
9501
+ PseudoClasses.MatchesCssAfter,
9502
+ PseudoClasses.MatchesCssBefore,
9503
+ ]);
9504
+ const LEGACY_EXT_CSS_INDICATOR_PSEUDO_NAMES = new Set([
9505
+ PseudoClasses.Not,
9506
+ PseudoClasses.MatchesCssBefore,
9507
+ PseudoClasses.MatchesCssAfter,
9508
+ ]);
9509
+ const CSS_CONVERSION_INDICATOR_PSEUDO_NAMES = new Set([
9510
+ PseudoClasses.AbpContains,
9511
+ PseudoClasses.AbpHas,
9512
+ PseudoClasses.HasText,
9513
+ ]);
9153
9514
  /**
9154
9515
  * Converts some pseudo-classes to pseudo-elements. For example:
9155
9516
  * - `:before` → `::before`
9156
9517
  *
9157
9518
  * @param selectorList Selector list to convert
9158
- * @returns Converted selector list
9519
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9520
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9521
+ * If the node was not converted, the result will contain the original node with the same object reference
9159
9522
  */
9160
9523
  function convertToPseudoElements(selectorList) {
9161
- // Prepare conversion result
9162
- const selectorListClone = cloneDeep(selectorList);
9524
+ // Check conversion indications before doing any heavy work
9525
+ const hasIndicator = find(
9526
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9527
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9528
+ selectorList, (node) => node.type === CssTreeNodeType.PseudoClassSelector && PSEUDO_ELEMENT_NAMES.has(node.name));
9529
+ if (!hasIndicator) {
9530
+ return createConversionResult(selectorList, false);
9531
+ }
9532
+ // Make a clone of the selector list to avoid modifying the original one,
9533
+ // then convert & return the cloned version
9534
+ const selectorListClone = clone(selectorList);
9535
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9536
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9163
9537
  walk(selectorListClone, {
9538
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9539
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9164
9540
  leave: (node) => {
9165
9541
  if (node.type === CssTreeNodeType.PseudoClassSelector) {
9166
- // :after ::after
9167
- // :before ::before
9168
- if (node.name === AFTER || node.name === BEFORE) {
9542
+ // If the pseudo-class is `:before` or `:after`, then we should
9543
+ // convert the node type to pseudo-element:
9544
+ // :after → ::after
9545
+ // :before → ::before
9546
+ if (PSEUDO_ELEMENT_NAMES.has(node.name)) {
9169
9547
  Object.assign(node, {
9170
9548
  ...node,
9171
9549
  type: CssTreeNodeType.PseudoElementSelector,
@@ -9174,7 +9552,7 @@ function convertToPseudoElements(selectorList) {
9174
9552
  }
9175
9553
  },
9176
9554
  });
9177
- return selectorListClone;
9555
+ return createConversionResult(selectorListClone, true);
9178
9556
  }
9179
9557
  /**
9180
9558
  * Converts legacy Extended CSS `matches-css-before` and `matches-css-after`
@@ -9183,33 +9561,36 @@ function convertToPseudoElements(selectorList) {
9183
9561
  * - `:matches-css-after(...)` → `:matches-css(after, ...)`
9184
9562
  *
9185
9563
  * @param node Node to convert
9564
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9565
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9566
+ * If the node was not converted, the result will contain the original node with the same object reference
9186
9567
  * @throws If the node is invalid
9187
9568
  */
9188
9569
  function convertLegacyMatchesCss(node) {
9189
- const nodeClone = cloneDeep(node);
9190
- if (nodeClone.type === CssTreeNodeType.PseudoClassSelector
9191
- && [MATCHES_CSS_BEFORE, MATCHES_CSS_AFTER].includes(nodeClone.name)) {
9192
- if (!nodeClone.children || nodeClone.children.size < 1) {
9193
- throw new Error(`Invalid ${nodeClone.name} pseudo-class: missing argument`);
9194
- }
9195
- // Remove the 'matches-css-' prefix to get the direction
9196
- const direction = nodeClone.name.substring(MATCHES_CSS.length + 1);
9197
- // Rename the pseudo-class
9198
- nodeClone.name = MATCHES_CSS;
9199
- // Add the direction to the first raw argument
9200
- const arg = nodeClone.children.first;
9201
- // Check argument
9202
- if (!arg) {
9203
- throw new Error(`Invalid ${nodeClone.name} pseudo-class: argument shouldn't be null`);
9204
- }
9205
- if (arg.type !== CssTreeNodeType.Raw) {
9206
- throw new Error(`Invalid ${nodeClone.name} pseudo-class: unexpected argument type`);
9207
- }
9208
- // Add the direction as the first argument
9209
- arg.value = `${direction},${arg.value}`;
9210
- // Replace the original node with the converted one
9211
- Object.assign(node, nodeClone);
9212
- }
9570
+ // Check conversion indications before doing any heavy work
9571
+ if (node.type !== CssTreeNodeType.PseudoClassSelector || !LEGACY_MATCHES_CSS_NAMES.has(node.name)) {
9572
+ return createConversionResult(node, false);
9573
+ }
9574
+ const nodeClone = clone(node);
9575
+ if (!nodeClone.children || nodeClone.children.length < 1) {
9576
+ throw new Error(`Invalid ${node.name} pseudo-class: missing argument`);
9577
+ }
9578
+ // Rename the pseudo-class
9579
+ nodeClone.name = PseudoClasses.MatchesCss;
9580
+ // Remove the 'matches-css-' prefix to get the direction
9581
+ const direction = node.name.substring(PseudoClasses.MatchesCss.length + 1);
9582
+ // Add the direction to the first raw argument
9583
+ const arg = nodeClone.children[0];
9584
+ // Check argument
9585
+ if (!arg) {
9586
+ throw new Error(`Invalid ${node.name} pseudo-class: argument shouldn't be null`);
9587
+ }
9588
+ if (arg.type !== CssTreeNodeType.Raw) {
9589
+ throw new Error(`Invalid ${node.name} pseudo-class: unexpected argument type`);
9590
+ }
9591
+ // Add the direction as the first argument
9592
+ arg.value = `${direction},${arg.value}`;
9593
+ return createConversionResult(nodeClone, true);
9213
9594
  }
9214
9595
  /**
9215
9596
  * Converts legacy Extended CSS selectors to the modern Extended CSS syntax.
@@ -9219,16 +9600,40 @@ function convertLegacyMatchesCss(node) {
9219
9600
  * - `[-ext-matches-css-before=...]` → `:matches-css(before, ...)`
9220
9601
  *
9221
9602
  * @param selectorList Selector list AST to convert
9222
- * @returns Converted selector list
9603
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9604
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9605
+ * If the node was not converted, the result will contain the original node with the same object reference
9223
9606
  */
9224
9607
  function convertFromLegacyExtendedCss(selectorList) {
9225
- // Prepare conversion result
9226
- const selectorListClone = cloneDeep(selectorList);
9608
+ // Check conversion indications before doing any heavy work
9609
+ const hasIndicator = find(
9610
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9611
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9612
+ selectorList, (node) => {
9613
+ if (node.type === CssTreeNodeType.PseudoClassSelector) {
9614
+ return LEGACY_EXT_CSS_INDICATOR_PSEUDO_NAMES.has(node.name);
9615
+ }
9616
+ if (node.type === CssTreeNodeType.AttributeSelector) {
9617
+ return node.name.name.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX);
9618
+ }
9619
+ return false;
9620
+ });
9621
+ if (!hasIndicator) {
9622
+ return createConversionResult(selectorList, false);
9623
+ }
9624
+ const selectorListClone = clone(selectorList);
9625
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9626
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9227
9627
  walk(selectorListClone, {
9628
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9629
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9228
9630
  leave: (node) => {
9229
9631
  // :matches-css-before(arg) → :matches-css(before,arg)
9230
9632
  // :matches-css-after(arg) → :matches-css(after,arg)
9231
- convertLegacyMatchesCss(node);
9633
+ const convertedLegacyExtCss = convertLegacyMatchesCss(node);
9634
+ if (convertedLegacyExtCss.isConverted) {
9635
+ Object.assign(node, convertedLegacyExtCss.result);
9636
+ }
9232
9637
  // [-ext-name=...] → :name(...)
9233
9638
  // [-ext-name='...'] → :name(...)
9234
9639
  // [-ext-name="..."] → :name(...)
@@ -9242,7 +9647,7 @@ function convertFromLegacyExtendedCss(selectorList) {
9242
9647
  // Remove the '-ext-' prefix to get the pseudo-class name
9243
9648
  const name = node.name.name.substring(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX.length);
9244
9649
  // Prepare the children list for the pseudo-class node
9245
- const children = new List();
9650
+ const children = [];
9246
9651
  // TODO: Change String node to Raw node to drop the quotes.
9247
9652
  // The structure of the node is the same, just the type
9248
9653
  // is different and generate() will generate the quotes
@@ -9255,7 +9660,7 @@ function convertFromLegacyExtendedCss(selectorList) {
9255
9660
  // For example, if the input is [-ext-has="> .selector"], then
9256
9661
  // we need to parse "> .selector" as a selector instead of string
9257
9662
  // it as a raw value
9258
- if ([HAS, NOT].includes(name)) {
9663
+ if ([PseudoClasses.Has, PseudoClasses.Not].includes(name)) {
9259
9664
  // Get the value of the attribute selector
9260
9665
  const { value } = node;
9261
9666
  // If the value is an identifier, then simply push it to the
@@ -9265,10 +9670,12 @@ function convertFromLegacyExtendedCss(selectorList) {
9265
9670
  }
9266
9671
  else if (value.type === CssTreeNodeType.String) {
9267
9672
  // Parse the value as a selector
9268
- const parsedChildren = CssTree.parse(value.value, CssTreeParserContext.selectorList);
9673
+ const parsedChildren = CssTree.parsePlain(value.value, CssTreeParserContext.selectorList);
9269
9674
  // Don't forget convert the parsed AST again, because
9270
9675
  // it was a raw string before
9271
- children.push(convertFromLegacyExtendedCss(parsedChildren));
9676
+ const convertedChildren = convertFromLegacyExtendedCss(parsedChildren);
9677
+ // Push the converted children to the list
9678
+ children.push(convertedChildren.result);
9272
9679
  }
9273
9680
  }
9274
9681
  else {
@@ -9295,14 +9702,12 @@ function convertFromLegacyExtendedCss(selectorList) {
9295
9702
  children,
9296
9703
  };
9297
9704
  // Handle this case: [-ext-matches-css-before=...] → :matches-css(before,...)
9298
- convertLegacyMatchesCss(pseudoNode);
9299
- // Convert attribute selector to pseudo-class selector, but
9300
- // keep the reference to the original node
9301
- Object.assign(node, pseudoNode);
9705
+ const convertedPseudoNode = convertLegacyMatchesCss(pseudoNode);
9706
+ Object.assign(node, convertedPseudoNode.isConverted ? convertedPseudoNode.result : pseudoNode);
9302
9707
  }
9303
9708
  },
9304
9709
  });
9305
- return selectorListClone;
9710
+ return createConversionResult(selectorListClone, true);
9306
9711
  }
9307
9712
  /**
9308
9713
  * CSS selector converter
@@ -9314,32 +9719,51 @@ class CssSelectorConverter extends ConverterBase {
9314
9719
  * Converts Extended CSS elements to AdGuard-compatible ones
9315
9720
  *
9316
9721
  * @param selectorList Selector list to convert
9317
- * @returns Converted selector list
9722
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
9723
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
9724
+ * If the node was not converted, the result will contain the original node with the same object reference
9318
9725
  * @throws If the rule is invalid or incompatible
9319
9726
  */
9320
9727
  static convertToAdg(selectorList) {
9321
9728
  // First, convert
9322
9729
  // - legacy Extended CSS selectors to the modern Extended CSS syntax and
9323
9730
  // - some pseudo-classes to pseudo-elements
9324
- const selectorListClone = convertToPseudoElements(convertFromLegacyExtendedCss(cloneDeep(selectorList)));
9731
+ const legacyExtCssConverted = convertFromLegacyExtendedCss(selectorList);
9732
+ const pseudoElementsConverted = convertToPseudoElements(legacyExtCssConverted.result);
9733
+ const hasIndicator = legacyExtCssConverted.isConverted || pseudoElementsConverted.isConverted || find(
9734
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9735
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9736
+ selectorList,
9737
+ // eslint-disable-next-line max-len
9738
+ (node) => node.type === CssTreeNodeType.PseudoClassSelector && CSS_CONVERSION_INDICATOR_PSEUDO_NAMES.has(node.name));
9739
+ if (!hasIndicator) {
9740
+ return createConversionResult(selectorList, false);
9741
+ }
9742
+ const selectorListClone = legacyExtCssConverted.isConverted || pseudoElementsConverted.isConverted
9743
+ ? pseudoElementsConverted.result
9744
+ : clone(selectorList);
9325
9745
  // Then, convert some Extended CSS pseudo-classes to AdGuard-compatible ones
9746
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9747
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9326
9748
  walk(selectorListClone, {
9749
+ // TODO: Need to improve CSSTree types, until then we need to use any type here
9750
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9327
9751
  leave: (node) => {
9328
9752
  if (node.type === CssTreeNodeType.PseudoClassSelector) {
9329
9753
  // :-abp-contains(...) → :contains(...)
9330
9754
  // :has-text(...) → :contains(...)
9331
- if (node.name === ABP_CONTAINS || node.name === HAS_TEXT) {
9332
- CssTree.renamePseudoClass(node, CONTAINS);
9755
+ if (node.name === PseudoClasses.AbpContains || node.name === PseudoClasses.HasText) {
9756
+ CssTree.renamePseudoClass(node, PseudoClasses.Contains);
9333
9757
  }
9334
9758
  // :-abp-has(...) → :has(...)
9335
- if (node.name === ABP_HAS) {
9336
- CssTree.renamePseudoClass(node, HAS);
9759
+ if (node.name === PseudoClasses.AbpHas) {
9760
+ CssTree.renamePseudoClass(node, PseudoClasses.Has);
9337
9761
  }
9338
9762
  // TODO: check uBO's `:others()` and `:watch-attr()` pseudo-classes
9339
9763
  }
9340
9764
  },
9341
9765
  });
9342
- return selectorListClone;
9766
+ return createConversionResult(selectorListClone, true);
9343
9767
  }
9344
9768
  }
9345
9769
 
@@ -9356,27 +9780,39 @@ class CssInjectionRuleConverter extends RuleConverterBase {
9356
9780
  * Converts a CSS injection rule to AdGuard format, if possible.
9357
9781
  *
9358
9782
  * @param rule Rule node to convert
9359
- * @returns Array of converted rule nodes
9783
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9784
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9785
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9360
9786
  * @throws If the rule is invalid or cannot be converted
9361
9787
  */
9362
9788
  static convertToAdg(rule) {
9363
- // Clone the provided AST node to avoid side effects
9364
- const ruleNode = cloneDeep(rule);
9789
+ const separator = rule.separator.value;
9790
+ let convertedSeparator = separator;
9365
9791
  // Change the separator if the rule contains ExtendedCSS selectors
9366
- if (CssTree.hasAnySelectorExtendedCssNode(ruleNode.body.selectorList) || ruleNode.body.remove) {
9367
- ruleNode.separator.value = ruleNode.exception
9792
+ if (CssTree.hasAnySelectorExtendedCssNode(rule.body.selectorList) || rule.body.remove) {
9793
+ convertedSeparator = rule.exception
9368
9794
  ? CosmeticRuleSeparator.AdgExtendedCssInjectionException
9369
9795
  : CosmeticRuleSeparator.AdgExtendedCssInjection;
9370
9796
  }
9371
9797
  else {
9372
- ruleNode.separator.value = ruleNode.exception
9798
+ convertedSeparator = rule.exception
9373
9799
  ? CosmeticRuleSeparator.AdgCssInjectionException
9374
9800
  : CosmeticRuleSeparator.AdgCssInjection;
9375
9801
  }
9376
- // Convert CSS selector list
9377
- Object.assign(ruleNode.body.selectorList, CssSelectorConverter.convertToAdg(fromPlainObject(ruleNode.body.selectorList)));
9378
- ruleNode.syntax = AdblockSyntax.Adg;
9379
- return [ruleNode];
9802
+ const convertedSelectorList = CssSelectorConverter.convertToAdg(rule.body.selectorList);
9803
+ // Check if the rule needs to be converted
9804
+ if (!(rule.syntax === AdblockSyntax.Common || rule.syntax === AdblockSyntax.Adg)
9805
+ || separator !== convertedSeparator
9806
+ || convertedSelectorList.isConverted) {
9807
+ // TODO: Replace with custom clone method
9808
+ const ruleClone = clone(rule);
9809
+ ruleClone.syntax = AdblockSyntax.Adg;
9810
+ ruleClone.separator.value = convertedSeparator;
9811
+ ruleClone.body.selectorList = convertedSelectorList.result;
9812
+ return createNodeConversionResult([ruleClone], true);
9813
+ }
9814
+ // Otherwise, return the original rule
9815
+ return createNodeConversionResult([rule], false);
9380
9816
  }
9381
9817
  }
9382
9818
 
@@ -9393,27 +9829,39 @@ class ElementHidingRuleConverter extends RuleConverterBase {
9393
9829
  * Converts an element hiding rule to AdGuard format, if possible.
9394
9830
  *
9395
9831
  * @param rule Rule node to convert
9396
- * @returns Array of converted rule nodes
9832
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9833
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9834
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9397
9835
  * @throws If the rule is invalid or cannot be converted
9398
9836
  */
9399
9837
  static convertToAdg(rule) {
9400
- // Clone the provided AST node to avoid side effects
9401
- const ruleNode = cloneDeep(rule);
9838
+ const separator = rule.separator.value;
9839
+ let convertedSeparator = separator;
9402
9840
  // Change the separator if the rule contains ExtendedCSS selectors
9403
- if (CssTree.hasAnySelectorExtendedCssNode(ruleNode.body.selectorList)) {
9404
- ruleNode.separator.value = ruleNode.exception
9841
+ if (CssTree.hasAnySelectorExtendedCssNode(rule.body.selectorList)) {
9842
+ convertedSeparator = rule.exception
9405
9843
  ? CosmeticRuleSeparator.ExtendedElementHidingException
9406
9844
  : CosmeticRuleSeparator.ExtendedElementHiding;
9407
9845
  }
9408
9846
  else {
9409
- ruleNode.separator.value = ruleNode.exception
9847
+ convertedSeparator = rule.exception
9410
9848
  ? CosmeticRuleSeparator.ElementHidingException
9411
9849
  : CosmeticRuleSeparator.ElementHiding;
9412
9850
  }
9413
- // Convert CSS selector list
9414
- Object.assign(ruleNode.body.selectorList, CssSelectorConverter.convertToAdg(fromPlainObject(ruleNode.body.selectorList)));
9415
- ruleNode.syntax = AdblockSyntax.Adg;
9416
- return [ruleNode];
9851
+ const convertedSelectorList = CssSelectorConverter.convertToAdg(rule.body.selectorList);
9852
+ // Check if the rule needs to be converted
9853
+ if (!(rule.syntax === AdblockSyntax.Common || rule.syntax === AdblockSyntax.Adg)
9854
+ || separator !== convertedSeparator
9855
+ || convertedSelectorList.isConverted) {
9856
+ // TODO: Replace with custom clone method
9857
+ const ruleClone = clone(rule);
9858
+ ruleClone.syntax = AdblockSyntax.Adg;
9859
+ ruleClone.separator.value = convertedSeparator;
9860
+ ruleClone.body.selectorList = convertedSelectorList.result;
9861
+ return createNodeConversionResult([ruleClone], true);
9862
+ }
9863
+ // Otherwise, return the original rule
9864
+ return createNodeConversionResult([rule], false);
9417
9865
  }
9418
9866
  }
9419
9867
 
@@ -9441,7 +9889,7 @@ function createNetworkRuleNode(pattern, modifiers = undefined, exception = false
9441
9889
  },
9442
9890
  };
9443
9891
  if (!isUndefined(modifiers)) {
9444
- result.modifiers = cloneDeep(modifiers);
9892
+ result.modifiers = clone(modifiers);
9445
9893
  }
9446
9894
  return result;
9447
9895
  }
@@ -9461,32 +9909,37 @@ class HeaderRemovalRuleConverter extends RuleConverterBase {
9461
9909
  * Converts a header removal rule to AdGuard syntax, if possible.
9462
9910
  *
9463
9911
  * @param rule Rule node to convert
9464
- * @returns Array of converted rule nodes
9912
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9913
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9914
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9465
9915
  * @throws If the rule is invalid or cannot be converted
9916
+ * @example
9917
+ * If the input rule is:
9918
+ * ```adblock
9919
+ * example.com##^responseheader(header-name)
9920
+ * ```
9921
+ * The output will be:
9922
+ * ```adblock
9923
+ * ||example.com^$removeheader=header-name
9924
+ * ```
9466
9925
  */
9467
9926
  static convertToAdg(rule) {
9468
- // Clone the provided AST node to avoid side effects
9469
- const ruleNode = cloneDeep(rule);
9470
9927
  // TODO: Add support for ABP syntax once it starts supporting header removal rules
9471
- // Check the input rule
9472
- if (ruleNode.category !== RuleCategory.Cosmetic
9473
- || ruleNode.type !== CosmeticRuleType.HtmlFilteringRule
9474
- || ruleNode.body.body.type !== CssTreeNodeType.Function
9475
- || ruleNode.body.body.name !== UBO_RESPONSEHEADER_MARKER) {
9476
- throw new RuleConversionError('Not a response header rule');
9928
+ // Leave the rule as is if it's not a header removal rule
9929
+ if (rule.category !== RuleCategory.Cosmetic
9930
+ || rule.type !== CosmeticRuleType.HtmlFilteringRule
9931
+ || rule.body.body.type !== CssTreeNodeType.Function
9932
+ || rule.body.body.name !== UBO_RESPONSEHEADER_MARKER) {
9933
+ return createNodeConversionResult([rule], false);
9477
9934
  }
9478
9935
  // Prepare network rule pattern
9479
- let pattern = EMPTY;
9480
- if (ruleNode.domains.children.length === 1) {
9936
+ const pattern = [];
9937
+ if (rule.domains.children.length === 1) {
9481
9938
  // If the rule has only one domain, we can use a simple network rule pattern:
9482
9939
  // ||single-domain-from-the-rule^
9483
- pattern = [
9484
- ADBLOCK_URL_START,
9485
- ruleNode.domains.children[0].value,
9486
- ADBLOCK_URL_SEPARATOR,
9487
- ].join(EMPTY);
9940
+ pattern.push(ADBLOCK_URL_START, rule.domains.children[0].value, ADBLOCK_URL_SEPARATOR);
9488
9941
  }
9489
- else if (ruleNode.domains.children.length > 1) {
9942
+ else if (rule.domains.children.length > 1) {
9490
9943
  // TODO: Add support for multiple domains, for example:
9491
9944
  // example.com,example.org,example.net##^responseheader(header-name)
9492
9945
  // We should consider allowing $domain with $removeheader modifier,
@@ -9496,13 +9949,13 @@ class HeaderRemovalRuleConverter extends RuleConverterBase {
9496
9949
  }
9497
9950
  // Prepare network rule modifiers
9498
9951
  const modifiers = createModifierListNode();
9499
- modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, CssTree.generateFunctionValue(fromPlainObject(ruleNode.body.body))));
9952
+ modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, CssTree.generateFunctionPlainValue(rule.body.body)));
9500
9953
  // Construct the network rule
9501
- return [
9502
- createNetworkRuleNode(pattern, modifiers,
9954
+ return createNodeConversionResult([
9955
+ createNetworkRuleNode(pattern.join(EMPTY), modifiers,
9503
9956
  // Copy the exception flag
9504
- ruleNode.exception, AdblockSyntax.Adg),
9505
- ];
9957
+ rule.exception, AdblockSyntax.Adg),
9958
+ ], true);
9506
9959
  }
9507
9960
  }
9508
9961
 
@@ -9519,48 +9972,69 @@ class CosmeticRuleConverter extends RuleConverterBase {
9519
9972
  * Converts a cosmetic rule to AdGuard syntax, if possible.
9520
9973
  *
9521
9974
  * @param rule Rule node to convert
9522
- * @returns Array of converted rule nodes
9975
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
9976
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
9977
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9523
9978
  * @throws If the rule is invalid or cannot be converted
9524
9979
  */
9525
9980
  static convertToAdg(rule) {
9526
- // Clone the provided AST node to avoid side effects
9527
- const ruleNode = cloneDeep(rule);
9528
- // Convert cosmetic rule modifiers
9529
- if (ruleNode.modifiers) {
9530
- if (ruleNode.syntax === AdblockSyntax.Ubo) {
9531
- // uBO doesn't support this rule:
9532
- // example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
9533
- if (ruleNode.type === CosmeticRuleType.ScriptletInjectionRule) {
9534
- throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
9535
- }
9536
- ruleNode.modifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(ruleNode.modifiers);
9537
- }
9538
- else if (ruleNode.syntax === AdblockSyntax.Abp) {
9539
- // TODO: Implement once ABP starts supporting cosmetic rule modifiers
9540
- throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
9541
- }
9542
- }
9981
+ let subconverterResult;
9543
9982
  // Convert cosmetic rule based on its type
9544
- switch (ruleNode.type) {
9983
+ switch (rule.type) {
9545
9984
  case CosmeticRuleType.ElementHidingRule:
9546
- return ElementHidingRuleConverter.convertToAdg(ruleNode);
9985
+ subconverterResult = ElementHidingRuleConverter.convertToAdg(rule);
9986
+ break;
9547
9987
  case CosmeticRuleType.ScriptletInjectionRule:
9548
- return ScriptletRuleConverter.convertToAdg(ruleNode);
9988
+ subconverterResult = ScriptletRuleConverter.convertToAdg(rule);
9989
+ break;
9549
9990
  case CosmeticRuleType.CssInjectionRule:
9550
- return CssInjectionRuleConverter.convertToAdg(ruleNode);
9991
+ subconverterResult = CssInjectionRuleConverter.convertToAdg(rule);
9992
+ break;
9551
9993
  case CosmeticRuleType.HtmlFilteringRule:
9552
9994
  // Handle special case: uBO response header filtering rule
9553
- if (ruleNode.body.body.type === CssTreeNodeType.Function
9554
- && ruleNode.body.body.name === UBO_RESPONSEHEADER_MARKER) {
9555
- return HeaderRemovalRuleConverter.convertToAdg(ruleNode);
9995
+ if (rule.body.body.type === CssTreeNodeType.Function
9996
+ && rule.body.body.name === UBO_RESPONSEHEADER_MARKER) {
9997
+ subconverterResult = HeaderRemovalRuleConverter.convertToAdg(rule);
9556
9998
  }
9557
- return HtmlRuleConverter.convertToAdg(ruleNode);
9558
- // Note: Currently, only ADG supports JS injection rules
9999
+ else {
10000
+ subconverterResult = HtmlRuleConverter.convertToAdg(rule);
10001
+ }
10002
+ break;
10003
+ // Note: Currently, only ADG supports JS injection rules, so we don't need to convert them
9559
10004
  case CosmeticRuleType.JsInjectionRule:
9560
- return [ruleNode];
10005
+ subconverterResult = createNodeConversionResult([rule], false);
10006
+ break;
9561
10007
  default:
9562
10008
  throw new RuleConversionError('Unsupported cosmetic rule type');
9563
10009
  }
10010
+ let convertedModifiers;
10011
+ // Convert cosmetic rule modifiers, if any
10012
+ if (rule.modifiers) {
10013
+ if (rule.syntax === AdblockSyntax.Ubo) {
10014
+ // uBO doesn't support this rule:
10015
+ // example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
10016
+ if (rule.type === CosmeticRuleType.ScriptletInjectionRule) {
10017
+ throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
10018
+ }
10019
+ convertedModifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(rule.modifiers);
10020
+ }
10021
+ else if (rule.syntax === AdblockSyntax.Abp) {
10022
+ // TODO: Implement once ABP starts supporting cosmetic rule modifiers
10023
+ throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
10024
+ }
10025
+ }
10026
+ if ((subconverterResult.result.length > 1 || subconverterResult.isConverted)
10027
+ || (convertedModifiers && convertedModifiers.isConverted)) {
10028
+ // Add modifier list to the subconverter result rules
10029
+ subconverterResult.result.forEach((subconverterRule) => {
10030
+ if (convertedModifiers && subconverterRule.category === RuleCategory.Cosmetic) {
10031
+ // eslint-disable-next-line no-param-reassign
10032
+ subconverterRule.modifiers = convertedModifiers.result;
10033
+ }
10034
+ });
10035
+ return subconverterResult;
10036
+ }
10037
+ return createNodeConversionResult([rule], false);
9564
10038
  }
9565
10039
  }
9566
10040
 
@@ -9632,17 +10106,16 @@ class NetworkRuleModifierListConverter extends ConverterBase {
9632
10106
  * Converts a network rule modifier list to AdGuard format, if possible.
9633
10107
  *
9634
10108
  * @param modifierList Network rule modifier list node to convert
9635
- * @returns Converted modifier list node
10109
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10110
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
10111
+ * If the node was not converted, the result will contain the original node with the same object reference
9636
10112
  * @throws If the conversion is not possible
9637
10113
  */
9638
10114
  static convertToAdg(modifierList) {
9639
- // Clone the provided AST node to avoid side effects
9640
- const modifierListNode = cloneDeep(modifierList);
9641
- const convertedModifierList = createModifierListNode();
9642
- // We should merge $csp modifiers into one
9643
- const cspValues = [];
9644
- modifierListNode.children.forEach((modifierNode) => {
9645
- // Handle regular modifiers conversion and $csp modifiers collection
10115
+ const conversionMap = new MultiValueMap();
10116
+ // Special case: $csp modifier
10117
+ let cspCount = 0;
10118
+ modifierList.children.forEach((modifierNode, index) => {
9646
10119
  const modifierConversions = ADG_CONVERSION_MAP.get(modifierNode.modifier.value);
9647
10120
  if (modifierConversions) {
9648
10121
  for (const modifierConversion of modifierConversions) {
@@ -9655,17 +10128,14 @@ class NetworkRuleModifierListConverter extends ConverterBase {
9655
10128
  const value = modifierConversion.value
9656
10129
  ? modifierConversion.value(modifierNode.value?.value)
9657
10130
  : modifierNode.value?.value;
9658
- if (name === CSP_MODIFIER && value) {
9659
- // Special case: collect $csp values
9660
- cspValues.push(value);
10131
+ // Check if the name or the value is different from the original modifier
10132
+ // If so, add the converted modifier to the list
10133
+ if (name !== modifierNode.modifier.value || value !== modifierNode.value?.value) {
10134
+ conversionMap.add(index, createModifierNode(name, value, exception));
9661
10135
  }
9662
- else {
9663
- // Regular case: collect the converted modifiers, if the modifier list
9664
- // not already contains the same modifier
9665
- const existingModifier = convertedModifierList.children.find((m) => m.modifier.value === name && m.exception === exception && m.value?.value === value);
9666
- if (!existingModifier) {
9667
- convertedModifierList.children.push(createModifierNode(name, value, exception));
9668
- }
10136
+ // Special case: $csp modifier
10137
+ if (name === CSP_MODIFIER) {
10138
+ cspCount += 1;
9669
10139
  }
9670
10140
  }
9671
10141
  return;
@@ -9688,26 +10158,52 @@ class NetworkRuleModifierListConverter extends ConverterBase {
9688
10158
  // Try to convert the redirect resource name to ADG format
9689
10159
  // This function returns undefined if the resource name is unknown
9690
10160
  const convertedRedirectResource = redirects.convertRedirectNameToAdg(redirectResource);
9691
- convertedModifierList.children.push(createModifierNode(modifierName,
9692
- // If the redirect resource name is unknown, fall back to the original one
9693
- // Later, the validator will throw an error if the resource name is invalid
9694
- convertedRedirectResource || redirectResource, modifierNode.exception));
9695
- return;
9696
- }
9697
- // In all other cases, just copy the modifier as is, if the modifier list
9698
- // not already contains the same modifier
9699
- const existingModifier = convertedModifierList.children.find((m) => m.modifier.value === modifierNode.modifier.value
9700
- && m.exception === modifierNode.exception
9701
- && m.value?.value === modifierNode.value?.value);
9702
- if (!existingModifier) {
9703
- convertedModifierList.children.push(modifierNode);
10161
+ // Check if the modifier name or the redirect resource name is different from the original modifier
10162
+ // If so, add the converted modifier to the list
10163
+ if (modifierName !== modifierNode.modifier.value
10164
+ || (convertedRedirectResource !== undefined && convertedRedirectResource !== redirectResource)) {
10165
+ conversionMap.add(index, createModifierNode(modifierName,
10166
+ // If the redirect resource name is unknown, fall back to the original one
10167
+ // Later, the validator will throw an error if the resource name is invalid
10168
+ convertedRedirectResource || redirectResource, modifierNode.exception));
10169
+ }
9704
10170
  }
9705
10171
  });
9706
- // Merge $csp modifiers into one, then add it to the converted modifier list
9707
- if (cspValues.length > 0) {
9708
- convertedModifierList.children.push(createModifierNode(CSP_MODIFIER, cspValues.join(CSP_SEPARATOR)));
10172
+ // Prepare the result if there are any converted modifiers or $csp modifiers
10173
+ if (conversionMap.size || cspCount) {
10174
+ const modifierListClone = cloneModifierListNode(modifierList);
10175
+ // Replace the original modifiers with the converted ones
10176
+ // One modifier may be replaced with multiple modifiers, so we need to flatten the array
10177
+ modifierListClone.children = modifierListClone.children.map((modifierNode, index) => {
10178
+ const conversionRecord = conversionMap.get(index);
10179
+ if (conversionRecord) {
10180
+ return conversionRecord;
10181
+ }
10182
+ return modifierNode;
10183
+ }).flat();
10184
+ // Special case: $csp modifier: merge multiple $csp modifiers into one
10185
+ // and put it at the end of the modifier list
10186
+ if (cspCount) {
10187
+ const cspValues = [];
10188
+ modifierListClone.children = modifierListClone.children.filter((modifierNode) => {
10189
+ if (modifierNode.modifier.value === CSP_MODIFIER) {
10190
+ if (!modifierNode.value?.value) {
10191
+ throw new RuleConversionError('$csp modifier value is missing');
10192
+ }
10193
+ cspValues.push(modifierNode.value?.value);
10194
+ return false;
10195
+ }
10196
+ return true;
10197
+ });
10198
+ modifierListClone.children.push(createModifierNode(CSP_MODIFIER, cspValues.join(CSP_SEPARATOR)));
10199
+ }
10200
+ // Before returning the result, remove duplicated modifiers
10201
+ modifierListClone.children = modifierListClone.children.filter((modifierNode, index, self) => self.findIndex((m) => m.modifier.value === modifierNode.modifier.value
10202
+ && m.exception === modifierNode.exception
10203
+ && m.value?.value === modifierNode.value?.value) === index);
10204
+ return createConversionResult(modifierListClone, true);
9709
10205
  }
9710
- return convertedModifierList;
10206
+ return createConversionResult(modifierList, false);
9711
10207
  }
9712
10208
  }
9713
10209
 
@@ -9724,17 +10220,35 @@ class NetworkRuleConverter extends RuleConverterBase {
9724
10220
  * Converts a network rule to AdGuard format, if possible.
9725
10221
  *
9726
10222
  * @param rule Rule node to convert
9727
- * @returns Array of converted rule nodes
10223
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
10224
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
10225
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9728
10226
  * @throws If the rule is invalid or cannot be converted
9729
10227
  */
9730
10228
  static convertToAdg(rule) {
9731
- // Clone the provided AST node to avoid side effects
9732
- const ruleNode = cloneDeep(rule);
9733
- // Convert modifiers
9734
- if (ruleNode.modifiers) {
9735
- Object.assign(ruleNode.modifiers, NetworkRuleModifierListConverter.convertToAdg(ruleNode.modifiers));
10229
+ if (rule.modifiers) {
10230
+ const modifiers = NetworkRuleModifierListConverter.convertToAdg(rule.modifiers);
10231
+ // If the object reference is different, it means that the modifiers were converted
10232
+ // In this case, we should clone the entire rule and replace the modifiers with the converted ones
10233
+ if (modifiers.isConverted) {
10234
+ return {
10235
+ result: [{
10236
+ category: RuleCategory.Network,
10237
+ type: 'NetworkRule',
10238
+ syntax: rule.syntax,
10239
+ exception: rule.exception,
10240
+ pattern: {
10241
+ type: 'Value',
10242
+ value: rule.pattern.value,
10243
+ },
10244
+ modifiers: modifiers.result,
10245
+ }],
10246
+ isConverted: true,
10247
+ };
10248
+ }
9736
10249
  }
9737
- return [ruleNode];
10250
+ // If the modifiers were not converted, return the original rule
10251
+ return createNodeConversionResult([rule], false);
9738
10252
  }
9739
10253
  }
9740
10254
 
@@ -9755,48 +10269,27 @@ class RuleConverter extends RuleConverterBase {
9755
10269
  * Converts an adblock filtering rule to AdGuard format, if possible.
9756
10270
  *
9757
10271
  * @param rule Rule node to convert
9758
- * @returns Array of converted rule nodes
10272
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
10273
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
10274
+ * If the rule was not converted, the result array will contain the original node with the same object reference
9759
10275
  * @throws If the rule is invalid or cannot be converted
9760
10276
  */
9761
10277
  static convertToAdg(rule) {
9762
- // Clone the provided AST node to avoid side effects
9763
- const ruleNode = cloneDeep(rule);
9764
10278
  // Delegate conversion to the corresponding sub-converter
9765
10279
  // based on the rule category
9766
- switch (ruleNode.category) {
10280
+ switch (rule.category) {
9767
10281
  case RuleCategory.Comment:
9768
- return CommentRuleConverter.convertToAdg(ruleNode);
10282
+ return CommentRuleConverter.convertToAdg(rule);
9769
10283
  case RuleCategory.Cosmetic:
9770
- return CosmeticRuleConverter.convertToAdg(ruleNode);
10284
+ return CosmeticRuleConverter.convertToAdg(rule);
9771
10285
  case RuleCategory.Network:
9772
- return NetworkRuleConverter.convertToAdg(ruleNode);
10286
+ return NetworkRuleConverter.convertToAdg(rule);
9773
10287
  default:
9774
- throw new RuleConversionError(`Unknown rule category: ${ruleNode.category}`);
10288
+ throw new RuleConversionError(`Unknown rule category: ${rule.category}`);
9775
10289
  }
9776
10290
  }
9777
10291
  }
9778
10292
 
9779
- /**
9780
- * @file Utility functions for working with filter list nodes
9781
- */
9782
- /**
9783
- * Creates a filter list node
9784
- *
9785
- * @param rules Rules to put in the list (optional, defaults to an empty list)
9786
- * @returns Filter list node
9787
- */
9788
- function createFilterListNode(rules = []) {
9789
- const result = {
9790
- type: 'FilterList',
9791
- children: [],
9792
- };
9793
- // We need to clone the rules to avoid side effects
9794
- if (rules.length > 0) {
9795
- result.children = cloneDeep(rules);
9796
- }
9797
- return result;
9798
- }
9799
-
9800
10293
  /**
9801
10294
  * @file Adblock filter list converter
9802
10295
  */
@@ -9815,18 +10308,133 @@ class FilterListConverter extends ConverterBase {
9815
10308
  * Converts an adblock filter list to AdGuard format, if possible.
9816
10309
  *
9817
10310
  * @param filterListNode Filter list node to convert
9818
- * @returns Converted filter list node
9819
- * @throws If the filter list is invalid or cannot be converted
10311
+ * @param tolerant Indicates whether the converter should be tolerant to invalid rules. If enabled and a rule is
10312
+ * invalid, it will be left as is. If disabled and a rule is invalid, the whole filter list will be failed.
10313
+ * Defaults to `true`.
10314
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10315
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
10316
+ * If the node was not converted, the result will contain the original node with the same object reference
10317
+ * @throws If the filter list is invalid or cannot be converted (if the tolerant mode is disabled)
10318
+ */
10319
+ static convertToAdg(filterListNode, tolerant = true) {
10320
+ // Prepare a map to store the converted rules by their index in the filter list
10321
+ const conversionMap = new MultiValueMap();
10322
+ // Iterate over the filtering rules and convert them one by one, then add them to the result (one conversion may
10323
+ // result in multiple rules)
10324
+ for (let i = 0; i < filterListNode.children.length; i += 1) {
10325
+ try {
10326
+ const convertedRules = RuleConverter.convertToAdg(filterListNode.children[i]);
10327
+ // Add the converted rules to the map if they were converted
10328
+ if (convertedRules.isConverted) {
10329
+ conversionMap.add(i, ...convertedRules.result);
10330
+ }
10331
+ }
10332
+ catch (error) {
10333
+ // If the tolerant mode is disabled, we should throw an error, this will fail the whole filter list
10334
+ // conversion.
10335
+ // Otherwise, we just ignore the error and leave the rule as is
10336
+ if (!tolerant) {
10337
+ throw error;
10338
+ }
10339
+ }
10340
+ }
10341
+ // If the conversion map is empty, it means that no rules were converted, so we can return the original filter
10342
+ // list
10343
+ if (conversionMap.size === 0) {
10344
+ return createConversionResult(filterListNode, false);
10345
+ }
10346
+ // Otherwise, create a new filter list node with the converted rules
10347
+ const convertedFilterList = {
10348
+ type: 'FilterList',
10349
+ children: [],
10350
+ };
10351
+ // Iterate over the original rules again and add them to the converted filter list, replacing the converted
10352
+ // rules with the new ones at the specified indexes
10353
+ for (let i = 0; i < filterListNode.children.length; i += 1) {
10354
+ const rules = conversionMap.get(i);
10355
+ if (rules) {
10356
+ convertedFilterList.children.push(...rules);
10357
+ }
10358
+ else {
10359
+ // We clone the unconverted rules to avoid mutating the original filter list if we return the converted
10360
+ // one
10361
+ convertedFilterList.children.push(clone(filterListNode.children[i]));
10362
+ }
10363
+ }
10364
+ return createConversionResult(convertedFilterList, true);
10365
+ }
10366
+ }
10367
+
10368
+ /**
10369
+ * @file Filter list converter for raw filter lists
10370
+ *
10371
+ * Technically, this is a wrapper around `FilterListConverter` that works with nodes instead of strings.
10372
+ */
10373
+ /**
10374
+ * Adblock filter list converter class.
10375
+ *
10376
+ * You can use this class to convert string-based filter lists, since most of the converters work with nodes.
10377
+ * This class just provides an extra layer on top of the {@link FilterListConverter} and calls the parser/serializer
10378
+ * before/after the conversion internally.
10379
+ *
10380
+ * @todo Implement `convertToUbo` and `convertToAbp`
10381
+ */
10382
+ class RawFilterListConverter extends ConverterBase {
10383
+ /**
10384
+ * Converts an adblock filter list text to AdGuard format, if possible.
10385
+ *
10386
+ * @param rawFilterList Raw filter list text to convert
10387
+ * @param tolerant Indicates whether the converter should be tolerant to invalid rules. If enabled and a rule is
10388
+ * invalid, it will be left as is. If disabled and a rule is invalid, the whole filter list will be failed.
10389
+ * Defaults to `true`.
10390
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10391
+ * the array of converted filter list text, and its `isConverted` flag indicates whether the original rule was
10392
+ * converted. If the rule was not converted, the original filter list text will be returned
10393
+ * @throws If the filter list is invalid or cannot be converted (if the tolerant mode is disabled)
9820
10394
  */
9821
- static convertToAdg(filterListNode) {
9822
- const result = createFilterListNode();
9823
- // Iterate over the filtering rules and convert them one by one,
9824
- // then add them to the result (one conversion may result in multiple rules)
9825
- for (const ruleNode of filterListNode.children) {
9826
- const convertedRules = RuleConverter.convertToAdg(ruleNode);
9827
- result.children.push(...convertedRules);
10395
+ static convertToAdg(rawFilterList, tolerant = true) {
10396
+ const conversionResult = FilterListConverter.convertToAdg(FilterListParser.parse(rawFilterList, tolerant), tolerant);
10397
+ // If the filter list was not converted, return the original text
10398
+ if (!conversionResult.isConverted) {
10399
+ return createConversionResult(rawFilterList, false);
9828
10400
  }
9829
- return result;
10401
+ // Otherwise, serialize the filter list and return the result
10402
+ return createConversionResult(FilterListParser.generate(conversionResult.result), true);
10403
+ }
10404
+ }
10405
+
10406
+ /**
10407
+ * @file Rule converter for raw rules
10408
+ *
10409
+ * Technically, this is a wrapper around `RuleConverter` that works with nodes instead of strings.
10410
+ */
10411
+ /**
10412
+ * Adblock filtering rule converter class.
10413
+ *
10414
+ * You can use this class to convert string-based adblock rules, since most of the converters work with nodes.
10415
+ * This class just provides an extra layer on top of the {@link RuleConverter} and calls the parser/serializer
10416
+ * before/after the conversion internally.
10417
+ *
10418
+ * @todo Implement `convertToUbo` and `convertToAbp`
10419
+ */
10420
+ class RawRuleConverter extends ConverterBase {
10421
+ /**
10422
+ * Converts an adblock filtering rule to AdGuard format, if possible.
10423
+ *
10424
+ * @param rawRule Raw rule text to convert
10425
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
10426
+ * the array of converted rule texts, and its `isConverted` flag indicates whether the original rule was converted.
10427
+ * If the rule was not converted, the original rule text will be returned
10428
+ * @throws If the rule is invalid or cannot be converted
10429
+ */
10430
+ static convertToAdg(rawRule) {
10431
+ const conversionResult = RuleConverter.convertToAdg(RuleParser.parse(rawRule));
10432
+ // If the rule was not converted, return the original rule text
10433
+ if (!conversionResult.isConverted) {
10434
+ return createConversionResult([rawRule], false);
10435
+ }
10436
+ // Otherwise, serialize the converted rule nodes
10437
+ return createConversionResult(conversionResult.result.map(RuleParser.generate), true);
9830
10438
  }
9831
10439
  }
9832
10440
 
@@ -9908,7 +10516,7 @@ class LogicalExpressionUtils {
9908
10516
  }
9909
10517
  }
9910
10518
 
9911
- const version$1 = "1.1.5";
10519
+ const version$1 = "1.1.6";
9912
10520
 
9913
10521
  /**
9914
10522
  * @file AGTree version
@@ -9919,4 +10527,4 @@ const version$1 = "1.1.5";
9919
10527
  // with wrong relative path to `package.json`. So we need this little "hack"
9920
10528
  const version = version$1;
9921
10529
 
9922
- export { ADBLOCK_URL_SEPARATOR, ADBLOCK_URL_SEPARATOR_REGEX, ADBLOCK_URL_START, ADBLOCK_URL_START_REGEX, ADBLOCK_WILDCARD, ADBLOCK_WILDCARD_REGEX, ADG_SCRIPTLET_MASK, AGLINT_COMMAND_PREFIX, AdblockSyntax, AdblockSyntaxError, AgentCommentRuleParser, AgentParser, AppListParser, COMMA_DOMAIN_LIST_SEPARATOR, CommentMarker, CommentRuleParser, CommentRuleType, ConfigCommentRuleParser, CosmeticRuleParser, CosmeticRuleSeparator, CosmeticRuleSeparatorUtils, CosmeticRuleType, CssTree, CssTreeNodeType, CssTreeParserContext, DomainListParser, DomainUtils, EXT_CSS_LEGACY_ATTRIBUTES, EXT_CSS_PSEUDO_CLASSES, FORBIDDEN_CSS_FUNCTIONS, FilterListConverter, FilterListParser, HINT_MARKER, HintCommentRuleParser, HintParser, IF, INCLUDE, LogicalExpressionParser, LogicalExpressionUtils, METADATA_HEADERS, MODIFIERS_SEPARATOR, MODIFIER_ASSIGN_OPERATOR, MetadataCommentRuleParser, MethodListParser, ModifierListParser, ModifierParser, NEGATION_MARKER, NETWORK_RULE_EXCEPTION_MARKER, NETWORK_RULE_SEPARATOR, NetworkRuleParser, NotImplementedError, PIPE_MODIFIER_SEPARATOR, PREPROCESSOR_MARKER, ParameterListParser, PreProcessorCommentRuleParser, QuoteType, QuoteUtils, RegExpUtils, RuleCategory, RuleConversionError, RuleConverter, RuleParser, SAFARI_CB_AFFINITY, SPECIAL_REGEX_SYMBOLS, StealthOptionListParser, UBO_SCRIPTLET_MASK, locRange, modifierValidator, shiftLoc, version };
10530
+ export { ADBLOCK_URL_SEPARATOR, ADBLOCK_URL_SEPARATOR_REGEX, ADBLOCK_URL_START, ADBLOCK_URL_START_REGEX, ADBLOCK_WILDCARD, ADBLOCK_WILDCARD_REGEX, ADG_SCRIPTLET_MASK, AGLINT_COMMAND_PREFIX, AdblockSyntax, AdblockSyntaxError, AgentCommentRuleParser, AgentParser, AppListParser, COMMA_DOMAIN_LIST_SEPARATOR, CommentMarker, CommentRuleParser, CommentRuleType, ConfigCommentRuleParser, CosmeticRuleParser, CosmeticRuleSeparator, CosmeticRuleSeparatorUtils, CosmeticRuleType, CssTree, CssTreeNodeType, CssTreeParserContext, DomainListParser, DomainUtils, EXT_CSS_LEGACY_ATTRIBUTES, EXT_CSS_PSEUDO_CLASSES, FORBIDDEN_CSS_FUNCTIONS, FilterListConverter, FilterListParser, HINT_MARKER, HintCommentRuleParser, HintParser, IF, INCLUDE, LogicalExpressionParser, LogicalExpressionUtils, METADATA_HEADERS, MODIFIERS_SEPARATOR, MODIFIER_ASSIGN_OPERATOR, MetadataCommentRuleParser, MethodListParser, ModifierListParser, ModifierParser, NEGATION_MARKER, NETWORK_RULE_EXCEPTION_MARKER, NETWORK_RULE_SEPARATOR, NetworkRuleParser, NotImplementedError, PIPE_MODIFIER_SEPARATOR, PREPROCESSOR_MARKER, ParameterListParser, PreProcessorCommentRuleParser, QuoteType, QuoteUtils, RawFilterListConverter, RawRuleConverter, RegExpUtils, RuleCategory, RuleConversionError, RuleConverter, RuleParser, SAFARI_CB_AFFINITY, SPECIAL_REGEX_SYMBOLS, StealthOptionListParser, UBO_SCRIPTLET_MASK, locRange, modifierValidator, shiftLoc, version };