@adguard/agtree 2.0.0-alpha.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agtree.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * AGTree v2.0.0-alpha.0 (build date: Mon, 29 Jul 2024 13:43:09 GMT)
2
+ * AGTree v2.0.1 (build date: Fri, 06 Sep 2024 12:11:38 GMT)
3
3
  * (c) 2024 Adguard Software Ltd.
4
4
  * Released under the MIT license
5
5
  * https://github.com/AdguardTeam/tsurlfilter/tree/master/packages/agtree#readme
@@ -212,6 +212,7 @@ const NUMBERS = new Set([NUMBER_0, NUMBER_1, NUMBER_2, NUMBER_3, NUMBER_4, NUMBE
212
212
  const REGEX_MARKER = '/';
213
213
  const ADG_SCRIPTLET_MASK = '//scriptlet';
214
214
  const UBO_SCRIPTLET_MASK = '+js';
215
+ const UBO_SCRIPTLET_MASK_LEGACY = 'script:inject';
215
216
  const UBO_HTML_MASK = '^';
216
217
  // Modifiers are separated by ",". For example: "script,domain=example.com"
217
218
  const MODIFIERS_SEPARATOR = ',';
@@ -763,6 +764,15 @@ class StringUtils {
763
764
  }
764
765
  }
765
766
 
767
+ /**
768
+ * Possible operators in the logical expression.
769
+ */
770
+ var OperatorValue;
771
+ (function (OperatorValue) {
772
+ OperatorValue["Not"] = "!";
773
+ OperatorValue["And"] = "&&";
774
+ OperatorValue["Or"] = "||";
775
+ })(OperatorValue || (OperatorValue = {}));
766
776
  /**
767
777
  * Represents the different comment markers that can be used in an adblock rule.
768
778
  *
@@ -1139,6 +1149,15 @@ const isUndefined = value => {
1139
1149
  const isNull = value => {
1140
1150
  return value === null;
1141
1151
  };
1152
+ /**
1153
+ * Checks whether the given value is a string.
1154
+ *
1155
+ * @param value Value to check.
1156
+ * @returns `true` if the value is a string, `false` otherwise.
1157
+ */
1158
+ const isString = value => {
1159
+ return typeof value === 'string';
1160
+ };
1142
1161
  /**
1143
1162
  * Checks whether the given value is an array of Uint8Arrays.
1144
1163
  *
@@ -2958,15 +2977,6 @@ var ParenthesisNodeBinaryPropMap;
2958
2977
  ParenthesisNodeBinaryPropMap[ParenthesisNodeBinaryPropMap["Start"] = 2] = "Start";
2959
2978
  ParenthesisNodeBinaryPropMap[ParenthesisNodeBinaryPropMap["End"] = 3] = "End";
2960
2979
  })(ParenthesisNodeBinaryPropMap || (ParenthesisNodeBinaryPropMap = {}));
2961
- /**
2962
- * Possible operators in the logical expression.
2963
- */
2964
- var OperatorValue;
2965
- (function (OperatorValue) {
2966
- OperatorValue["Not"] = "!";
2967
- OperatorValue["And"] = "&&";
2968
- OperatorValue["Or"] = "||";
2969
- })(OperatorValue || (OperatorValue = {}));
2970
2980
  /**
2971
2981
  * Possible token types in the logical expression.
2972
2982
  */
@@ -3012,7 +3022,9 @@ const getOperatorOrFail = binary => {
3012
3022
  /**
3013
3023
  * Serialization map for known variables.
3014
3024
  */
3015
- const KNOWN_VARIABLES_MAP = new Map([['ext_abp', 0], ['ext_ublock', 1], ['ext_ubol', 2], ['ext_devbuild', 3], ['env_chromium', 4], ['env_edge', 5], ['env_firefox', 6], ['env_mobile', 7], ['env_safari', 8], ['env_mv3', 9], ['false', 10], ['cap_html_filtering', 11], ['cap_user_stylesheet', 12], ['adguard', 13], ['adguard_app_windows', 14], ['adguard_app_mac', 15], ['adguard_app_android', 16], ['adguard_app_ios', 17], ['adguard_ext_safari', 18], ['adguard_ext_chromium', 19], ['adguard_ext_firefox', 20], ['adguard_ext_edge', 21], ['adguard_ext_opera', 22], ['adguard_ext_android_cb', 23]]);
3025
+ const KNOWN_VARIABLES_MAP = new Map([['ext_abp', 0], ['ext_ublock', 1], ['ext_ubol', 2], ['ext_devbuild', 3], ['env_chromium', 4], ['env_edge', 5], ['env_firefox', 6], ['env_mobile', 7], ['env_safari', 8], ['env_mv3', 9], ['false', 10], ['cap_html_filtering', 11], ['cap_user_stylesheet', 12], ['adguard', 13], ['adguard_app_windows', 14], ['adguard_app_mac', 15], ['adguard_app_android', 16], ['adguard_app_ios', 17], ['adguard_ext_safari', 18], ['adguard_ext_chromium', 19], ['adguard_ext_firefox', 20], ['adguard_ext_edge', 21], ['adguard_ext_opera', 22], ['adguard_ext_android_cb', 23]
3026
+ // TODO: Add 'adguard_ext_chromium_mv3' to the list
3027
+ ]);
3016
3028
  /**
3017
3029
  * Deserialization map for known variables.
3018
3030
  */
@@ -5716,7 +5728,7 @@ class CssTokenStream {
5716
5728
  * @returns An array containing the number of tokens skipped and the number of tokens skipped without leading and
5717
5729
  * trailing whitespace tokens.
5718
5730
  */
5719
- skipUntilEx(type, balance) {
5731
+ skipUntilExt(type, balance) {
5720
5732
  let i = this.index;
5721
5733
  let firstNonWsToken = -1; // -1 means no non-whitespace token found yet
5722
5734
  let lastNonWsToken = -1; // -1 means no non-whitespace token found yet
@@ -5944,7 +5956,7 @@ class AdgCssInjectionParser extends ParserBase {
5944
5956
  // └ this one
5945
5957
  const {
5946
5958
  skippedTrimmed: selectorTokensLength
5947
- } = stream.skipUntilEx(cssTokenizer.TokenType.OpenCurlyBracket, balanceShift + 1);
5959
+ } = stream.skipUntilExt(cssTokenizer.TokenType.OpenCurlyBracket, balanceShift + 1);
5948
5960
  stream.expect(cssTokenizer.TokenType.OpenCurlyBracket);
5949
5961
  // If the skipped tokens count is 0 without leading and trailing whitespace characters, then the selector list
5950
5962
  // is empty
@@ -6184,6 +6196,7 @@ class AbpSnippetInjectionBodyParser extends ParserBase {
6184
6196
  *
6185
6197
  * @note Only 256 values can be represented this way.
6186
6198
  */
6199
+ // TODO: Update this map with the actual values
6187
6200
  static FREQUENT_ARGS_SERIALIZATION_MAP = new Map([['abort-current-inline-script', 0], ['abort-on-property-read', 1], ['abort-on-property-write', 2], ['json-prune', 3], ['log', 4], ['prevent-listener', 5], ['cookie-remover', 6], ['override-property-read', 7], ['abort-on-iframe-property-read', 8], ['abort-on-iframe-property-write', 9], ['freeze-element', 10], ['json-override', 11], ['simulate-mouse-event', 12], ['strip-fetch-query-parameter', 13], ['hide-if-contains', 14], ['hide-if-contains-image', 15], ['hide-if-contains-image-hash', 16], ['hide-if-contains-similar-text', 17], ['hide-if-contains-visible-text', 18], ['hide-if-contains-and-matches-style', 19], ['hide-if-graph-matches', 20], ['hide-if-has-and-matches-style', 21], ['hide-if-labelled-by', 22], ['hide-if-matches-xpath', 23], ['hide-if-matches-computed-xpath', 24], ['hide-if-shadow-contains', 25], ['debug', 26], ['trace', 27], ['race', 28]]);
6188
6201
  /**
6189
6202
  * Value map for binary deserialization. This helps to reduce the size of the serialized data,
@@ -6311,6 +6324,7 @@ class UboScriptletInjectionBodyParser extends ParserBase {
6311
6324
  *
6312
6325
  * @note Only 256 values can be represented this way.
6313
6326
  */
6327
+ // TODO: Update this map with the actual values
6314
6328
  static FREQUENT_ARGS_SERIALIZATION_MAP = new Map([['abort-current-script.js', 0], ['acs.js', 1], ['abort-current-inline-script.js', 2], ['acis.js', 3], ['abort-on-property-read.js', 4], ['aopr.js', 5], ['abort-on-property-write.js', 6], ['aopw.js', 7], ['abort-on-stack-trace.js', 8], ['aost.js', 9], ['adjust-setInterval.js', 10], ['nano-setInterval-booster.js', 11], ['nano-sib.js', 12], ['adjust-setTimeout.js', 13], ['nano-setTimeout-booster.js', 14], ['nano-stb.js', 15], ['close-window.js', 16], ['window-close-if.js', 17], ['disable-newtab-links.js', 18], ['evaldata-prune.js', 19], ['json-prune.js', 20], ['addEventListener-logger.js', 21], ['aell.js', 22], ['m3u-prune.js', 23], ['nowebrtc.js', 24], ['addEventListener-defuser.js', 25], ['aeld.js', 26], ['prevent-addEventListener.js', 27], ['adfly-defuser.js', 28], ['noeval-if.js', 29], ['prevent-eval-if.js', 30], ['no-fetch-if.js', 31], ['prevent-fetch.js', 32], ['no-xhr-if.js', 33], ['prevent-xhr.js', 34], ['prevent-refresh.js', 35], ['refresh-defuser.js', 36], ['no-requestAnimationFrame-if.js', 37], ['norafif.js', 38], ['prevent-requestAnimationFrame.js', 39], ['no-setInterval-if.js', 40], ['nosiif.js', 41], ['prevent-setInterval.js', 42], ['setInterval-defuser.js', 43], ['no-setTimeout-if.js', 44], ['nostif.js', 45], ['prevent-setTimeout.js', 46], ['setTimeout-defuser.js', 47], ['no-window-open-if.js', 48], ['nowoif.js', 49], ['prevent-window-open.js', 50], ['window.open-defuser.js', 51], ['remove-attr.js', 52], ['ra.js', 53], ['remove-class.js', 54], ['rc.js', 55], ['remove-cookie.js', 56], ['cookie-remover.js', 57], ['remove-node-text.js', 58], ['rmnt.js', 59], ['set-attr.js', 60], ['set-constant.js', 61], ['set.js', 62], ['set-cookie.js', 63], ['set-local-storage-item.js', 64], ['set-session-storage-item.js', 65], ['xml-prune.js', 66], ['webrtc-if.js', 67], ['overlay-buster.js', 68], ['alert-buster.js', 69], ['golem.de.js', 70], ['href-sanitizer.js', 71], ['call-nothrow.js', 72], ['window.name-defuser.js', 73], ['spoof-css.js', 74], ['trusted-set-constant.js', 75], ['trusted-set.js', 76], ['trusted-set-cookie.js', 77], ['trusted-set-local-storage-item.js', 78], ['trusted-replace-fetch-response.js', 79], ['json-prune-fetch-response.js', 80], ['json-prune-xhr-response.js', 81], ['trusted-replace-xhr-response.js', 82], ['multiup.js', 83], ['prevent-canvas.js', 84], ['set-cookie-reload.js', 85], ['trusted-set-cookie-reload.js', 86], ['trusted-click-element.js', 87], ['trusted-prune-inbound-object.js', 88], ['trusted-prune-outbound-object.js', 89], ['trusted-set-session-storage-item.js', 90], ['trusted-replace-node-text.js', 91], ['trusted-rpnt.js', 92], ['replace-node-text.js', 93], ['rpnt.js', 94]]);
6315
6329
  /**
6316
6330
  * Value map for binary deserialization. This helps to reduce the size of the serialized data,
@@ -6334,11 +6348,17 @@ class UboScriptletInjectionBodyParser extends ParserBase {
6334
6348
  let offset = 0;
6335
6349
  // Skip leading spaces
6336
6350
  offset = StringUtils.skipWS(raw, offset);
6351
+ let scriptletMaskLength = 0;
6352
+ if (raw.startsWith(UBO_SCRIPTLET_MASK, offset)) {
6353
+ scriptletMaskLength = UBO_SCRIPTLET_MASK.length;
6354
+ } else if (raw.startsWith(UBO_SCRIPTLET_MASK_LEGACY, offset)) {
6355
+ scriptletMaskLength = UBO_SCRIPTLET_MASK_LEGACY.length;
6356
+ }
6337
6357
  // Scriptlet call should start with "+js"
6338
- if (!raw.startsWith(UBO_SCRIPTLET_MASK, offset)) {
6358
+ if (!scriptletMaskLength) {
6339
6359
  throw new AdblockSyntaxError(this.ERROR_MESSAGES.NO_SCRIPTLET_MASK, baseOffset + offset, baseOffset + raw.length);
6340
6360
  }
6341
- offset += UBO_SCRIPTLET_MASK.length;
6361
+ offset += scriptletMaskLength;
6342
6362
  // Whitespace is not allowed after the mask
6343
6363
  if (raw[offset] === SPACE) {
6344
6364
  throw new AdblockSyntaxError(this.ERROR_MESSAGES.WHITESPACE_AFTER_MASK, baseOffset + offset, baseOffset + raw.length);
@@ -6383,6 +6403,7 @@ class UboScriptletInjectionBodyParser extends ParserBase {
6383
6403
  if (node.children.length > 1) {
6384
6404
  throw new Error(this.ERROR_MESSAGES.NO_MULTIPLE_SCRIPTLET_CALLS);
6385
6405
  }
6406
+ // During generation, we only support the modern scriptlet mask
6386
6407
  result.push(UBO_SCRIPTLET_MASK);
6387
6408
  result.push(OPEN_PARENTHESIS);
6388
6409
  if (node.children.length > 0) {
@@ -6448,6 +6469,7 @@ class AdgScriptletInjectionBodyParser extends ParserBase {
6448
6469
  *
6449
6470
  * @note Only 256 values can be represented this way.
6450
6471
  */
6472
+ // TODO: Update this map with the actual values
6451
6473
  static FREQUENT_ARGS_SERIALIZATION_MAP = new Map([['abort-current-inline-script', 0], ['abort-on-property-read', 1], ['abort-on-property-write', 2], ['abort-on-stack-trace', 3], ['adjust-setInterval', 4], ['adjust-setTimeout', 5], ['close-window', 6], ['debug-current-inline-script', 7], ['debug-on-property-read', 8], ['debug-on-property-write', 9], ['dir-string', 10], ['disable-newtab-links', 11], ['evaldata-prune', 12], ['json-prune', 13], ['log', 14], ['log-addEventListener', 15], ['log-eval', 16], ['log-on-stack-trace', 17], ['m3u-prune', 18], ['noeval', 19], ['nowebrtc', 20], ['no-topics', 21], ['prevent-addEventListener', 22], ['prevent-adfly', 23], ['prevent-bab', 24], ['prevent-eval-if', 25], ['prevent-fab-3.2.0', 26], ['prevent-fetch', 27], ['prevent-xhr', 28], ['prevent-popads-net', 29], ['prevent-refresh', 30], ['prevent-requestAnimationFrame', 31], ['prevent-setInterval', 32], ['prevent-setTimeout', 33], ['prevent-window-open', 34], ['remove-attr', 35], ['remove-class', 36], ['remove-cookie', 37], ['remove-node-text', 38], ['set-attr', 39], ['set-constant', 40], ['set-cookie', 41], ['set-cookie-reload', 42], ['set-local-storage-item', 43], ['set-popads-dummy', 44], ['set-session-storage-item', 45], ['xml-prune', 46]]);
6452
6474
  /**
6453
6475
  * Value map for binary deserialization. This helps to reduce the size of the serialized data,
@@ -6919,7 +6941,7 @@ class CosmeticRuleParser extends ParserBase {
6919
6941
  };
6920
6942
  };
6921
6943
  const parseUboScriptletInjection = () => {
6922
- if (!rawBody.startsWith(UBO_SCRIPTLET_MASK)) {
6944
+ if (!rawBody.startsWith(UBO_SCRIPTLET_MASK) && !rawBody.startsWith(UBO_SCRIPTLET_MASK_LEGACY)) {
6923
6945
  return null;
6924
6946
  }
6925
6947
  if (!options.parseUboSpecificRules) {
@@ -13874,7 +13896,8 @@ const redirectsCompatibilityTableData = {
13874
13896
  deprecationMessage: null,
13875
13897
  removed: false,
13876
13898
  removalMessage: null,
13877
- isBlocking: false
13899
+ isBlocking: false,
13900
+ resourceTypes: []
13878
13901
  }, {
13879
13902
  name: "1x1.gif",
13880
13903
  aliases: null,
@@ -13886,7 +13909,8 @@ const redirectsCompatibilityTableData = {
13886
13909
  deprecationMessage: null,
13887
13910
  removed: false,
13888
13911
  removalMessage: null,
13889
- isBlocking: false
13912
+ isBlocking: false,
13913
+ resourceTypes: ["image"]
13890
13914
  }, {
13891
13915
  name: "1x1-transparent-gif",
13892
13916
  aliases: null,
@@ -13898,7 +13922,8 @@ const redirectsCompatibilityTableData = {
13898
13922
  deprecationMessage: null,
13899
13923
  removed: false,
13900
13924
  removalMessage: null,
13901
- isBlocking: false
13925
+ isBlocking: false,
13926
+ resourceTypes: []
13902
13927
  }],
13903
13928
  map: {
13904
13929
  "1": 0,
@@ -13929,7 +13954,8 @@ const redirectsCompatibilityTableData = {
13929
13954
  deprecationMessage: null,
13930
13955
  removed: false,
13931
13956
  removalMessage: null,
13932
- isBlocking: false
13957
+ isBlocking: false,
13958
+ resourceTypes: []
13933
13959
  }, {
13934
13960
  name: "2x2.png",
13935
13961
  aliases: null,
@@ -13941,7 +13967,8 @@ const redirectsCompatibilityTableData = {
13941
13967
  deprecationMessage: null,
13942
13968
  removed: false,
13943
13969
  removalMessage: null,
13944
- isBlocking: false
13970
+ isBlocking: false,
13971
+ resourceTypes: ["image"]
13945
13972
  }, {
13946
13973
  name: "2x2-transparent-png",
13947
13974
  aliases: null,
@@ -13953,7 +13980,8 @@ const redirectsCompatibilityTableData = {
13953
13980
  deprecationMessage: null,
13954
13981
  removed: false,
13955
13982
  removalMessage: null,
13956
- isBlocking: false
13983
+ isBlocking: false,
13984
+ resourceTypes: []
13957
13985
  }],
13958
13986
  map: {
13959
13987
  "1": 0,
@@ -13984,7 +14012,8 @@ const redirectsCompatibilityTableData = {
13984
14012
  deprecationMessage: null,
13985
14013
  removed: false,
13986
14014
  removalMessage: null,
13987
- isBlocking: false
14015
+ isBlocking: false,
14016
+ resourceTypes: []
13988
14017
  }, {
13989
14018
  name: "32x32.png",
13990
14019
  aliases: null,
@@ -13996,7 +14025,8 @@ const redirectsCompatibilityTableData = {
13996
14025
  deprecationMessage: null,
13997
14026
  removed: false,
13998
14027
  removalMessage: null,
13999
- isBlocking: false
14028
+ isBlocking: false,
14029
+ resourceTypes: ["image"]
14000
14030
  }, {
14001
14031
  name: "32x32-transparent-png",
14002
14032
  aliases: null,
@@ -14008,7 +14038,8 @@ const redirectsCompatibilityTableData = {
14008
14038
  deprecationMessage: null,
14009
14039
  removed: false,
14010
14040
  removalMessage: null,
14011
- isBlocking: false
14041
+ isBlocking: false,
14042
+ resourceTypes: []
14012
14043
  }],
14013
14044
  map: {
14014
14045
  "1": 0,
@@ -14039,7 +14070,8 @@ const redirectsCompatibilityTableData = {
14039
14070
  deprecationMessage: null,
14040
14071
  removed: false,
14041
14072
  removalMessage: null,
14042
- isBlocking: false
14073
+ isBlocking: false,
14074
+ resourceTypes: []
14043
14075
  }, {
14044
14076
  name: "3x2.png",
14045
14077
  aliases: null,
@@ -14051,7 +14083,8 @@ const redirectsCompatibilityTableData = {
14051
14083
  deprecationMessage: null,
14052
14084
  removed: false,
14053
14085
  removalMessage: null,
14054
- isBlocking: false
14086
+ isBlocking: false,
14087
+ resourceTypes: ["image"]
14055
14088
  }, {
14056
14089
  name: "3x2-transparent-png",
14057
14090
  aliases: null,
@@ -14063,7 +14096,8 @@ const redirectsCompatibilityTableData = {
14063
14096
  deprecationMessage: null,
14064
14097
  removed: false,
14065
14098
  removalMessage: null,
14066
- isBlocking: false
14099
+ isBlocking: false,
14100
+ resourceTypes: []
14067
14101
  }],
14068
14102
  map: {
14069
14103
  "1": 0,
@@ -14094,7 +14128,8 @@ const redirectsCompatibilityTableData = {
14094
14128
  deprecationMessage: null,
14095
14129
  removed: false,
14096
14130
  removalMessage: null,
14097
- isBlocking: false
14131
+ isBlocking: false,
14132
+ resourceTypes: []
14098
14133
  }, {
14099
14134
  name: "amazon_apstag.js",
14100
14135
  aliases: null,
@@ -14106,7 +14141,8 @@ const redirectsCompatibilityTableData = {
14106
14141
  deprecationMessage: null,
14107
14142
  removed: false,
14108
14143
  removalMessage: null,
14109
- isBlocking: false
14144
+ isBlocking: false,
14145
+ resourceTypes: ["script"]
14110
14146
  }],
14111
14147
  map: {
14112
14148
  "1": 0,
@@ -14133,7 +14169,8 @@ const redirectsCompatibilityTableData = {
14133
14169
  deprecationMessage: null,
14134
14170
  removed: false,
14135
14171
  removalMessage: null,
14136
- isBlocking: false
14172
+ isBlocking: false,
14173
+ resourceTypes: ["script"]
14137
14174
  }],
14138
14175
  map: {
14139
14176
  "1024": 0,
@@ -14153,7 +14190,8 @@ const redirectsCompatibilityTableData = {
14153
14190
  deprecationMessage: null,
14154
14191
  removed: false,
14155
14192
  removalMessage: null,
14156
- isBlocking: false
14193
+ isBlocking: false,
14194
+ resourceTypes: ["script"]
14157
14195
  }],
14158
14196
  map: {
14159
14197
  "1024": 0,
@@ -14173,7 +14211,8 @@ const redirectsCompatibilityTableData = {
14173
14211
  deprecationMessage: null,
14174
14212
  removed: false,
14175
14213
  removalMessage: null,
14176
- isBlocking: false
14214
+ isBlocking: false,
14215
+ resourceTypes: []
14177
14216
  }],
14178
14217
  map: {
14179
14218
  "1": 0,
@@ -14196,7 +14235,8 @@ const redirectsCompatibilityTableData = {
14196
14235
  deprecationMessage: null,
14197
14236
  removed: false,
14198
14237
  removalMessage: null,
14199
- isBlocking: false
14238
+ isBlocking: false,
14239
+ resourceTypes: ["script"]
14200
14240
  }],
14201
14241
  map: {
14202
14242
  "1024": 0,
@@ -14216,7 +14256,8 @@ const redirectsCompatibilityTableData = {
14216
14256
  deprecationMessage: null,
14217
14257
  removed: false,
14218
14258
  removalMessage: null,
14219
- isBlocking: true
14259
+ isBlocking: true,
14260
+ resourceTypes: []
14220
14261
  }, {
14221
14262
  name: "click2load.html",
14222
14263
  aliases: null,
@@ -14228,7 +14269,8 @@ const redirectsCompatibilityTableData = {
14228
14269
  deprecationMessage: null,
14229
14270
  removed: false,
14230
14271
  removalMessage: null,
14231
- isBlocking: true
14272
+ isBlocking: true,
14273
+ resourceTypes: []
14232
14274
  }],
14233
14275
  map: {
14234
14276
  "1": 0,
@@ -14255,7 +14297,8 @@ const redirectsCompatibilityTableData = {
14255
14297
  deprecationMessage: null,
14256
14298
  removed: false,
14257
14299
  removalMessage: null,
14258
- isBlocking: false
14300
+ isBlocking: false,
14301
+ resourceTypes: []
14259
14302
  }],
14260
14303
  map: {
14261
14304
  "1": 0,
@@ -14278,7 +14321,8 @@ const redirectsCompatibilityTableData = {
14278
14321
  deprecationMessage: null,
14279
14322
  removed: false,
14280
14323
  removalMessage: null,
14281
- isBlocking: false
14324
+ isBlocking: false,
14325
+ resourceTypes: []
14282
14326
  }, {
14283
14327
  name: "empty",
14284
14328
  aliases: null,
@@ -14290,7 +14334,8 @@ const redirectsCompatibilityTableData = {
14290
14334
  deprecationMessage: null,
14291
14335
  removed: false,
14292
14336
  removalMessage: null,
14293
- isBlocking: false
14337
+ isBlocking: false,
14338
+ resourceTypes: []
14294
14339
  }],
14295
14340
  map: {
14296
14341
  "1": 0,
@@ -14317,7 +14362,8 @@ const redirectsCompatibilityTableData = {
14317
14362
  deprecationMessage: null,
14318
14363
  removed: false,
14319
14364
  removalMessage: null,
14320
- isBlocking: false
14365
+ isBlocking: false,
14366
+ resourceTypes: []
14321
14367
  }, {
14322
14368
  name: "fingerprint2.js",
14323
14369
  aliases: null,
@@ -14329,7 +14375,8 @@ const redirectsCompatibilityTableData = {
14329
14375
  deprecationMessage: null,
14330
14376
  removed: false,
14331
14377
  removalMessage: null,
14332
- isBlocking: false
14378
+ isBlocking: false,
14379
+ resourceTypes: ["script"]
14333
14380
  }],
14334
14381
  map: {
14335
14382
  "1": 0,
@@ -14356,7 +14403,8 @@ const redirectsCompatibilityTableData = {
14356
14403
  deprecationMessage: null,
14357
14404
  removed: false,
14358
14405
  removalMessage: null,
14359
- isBlocking: false
14406
+ isBlocking: false,
14407
+ resourceTypes: []
14360
14408
  }, {
14361
14409
  name: "fingerprint3.js",
14362
14410
  aliases: null,
@@ -14368,7 +14416,8 @@ const redirectsCompatibilityTableData = {
14368
14416
  deprecationMessage: null,
14369
14417
  removed: false,
14370
14418
  removalMessage: null,
14371
- isBlocking: false
14419
+ isBlocking: false,
14420
+ resourceTypes: ["script"]
14372
14421
  }],
14373
14422
  map: {
14374
14423
  "1": 0,
@@ -14395,7 +14444,8 @@ const redirectsCompatibilityTableData = {
14395
14444
  deprecationMessage: null,
14396
14445
  removed: false,
14397
14446
  removalMessage: null,
14398
- isBlocking: false
14447
+ isBlocking: false,
14448
+ resourceTypes: []
14399
14449
  }],
14400
14450
  map: {
14401
14451
  "1": 0,
@@ -14418,7 +14468,8 @@ const redirectsCompatibilityTableData = {
14418
14468
  deprecationMessage: null,
14419
14469
  removed: false,
14420
14470
  removalMessage: null,
14421
- isBlocking: false
14471
+ isBlocking: false,
14472
+ resourceTypes: []
14422
14473
  }, {
14423
14474
  name: "google-analytics_ga.js",
14424
14475
  aliases: null,
@@ -14430,7 +14481,8 @@ const redirectsCompatibilityTableData = {
14430
14481
  deprecationMessage: null,
14431
14482
  removed: false,
14432
14483
  removalMessage: null,
14433
- isBlocking: false
14484
+ isBlocking: false,
14485
+ resourceTypes: ["script"]
14434
14486
  }],
14435
14487
  map: {
14436
14488
  "1": 0,
@@ -14457,7 +14509,8 @@ const redirectsCompatibilityTableData = {
14457
14509
  deprecationMessage: null,
14458
14510
  removed: false,
14459
14511
  removalMessage: null,
14460
- isBlocking: false
14512
+ isBlocking: false,
14513
+ resourceTypes: []
14461
14514
  }, {
14462
14515
  name: "google-analytics_analytics.js",
14463
14516
  aliases: null,
@@ -14469,7 +14522,8 @@ const redirectsCompatibilityTableData = {
14469
14522
  deprecationMessage: null,
14470
14523
  removed: false,
14471
14524
  removalMessage: null,
14472
- isBlocking: false
14525
+ isBlocking: false,
14526
+ resourceTypes: ["script"]
14473
14527
  }],
14474
14528
  map: {
14475
14529
  "1": 0,
@@ -14496,7 +14550,8 @@ const redirectsCompatibilityTableData = {
14496
14550
  deprecationMessage: null,
14497
14551
  removed: false,
14498
14552
  removalMessage: null,
14499
- isBlocking: false
14553
+ isBlocking: false,
14554
+ resourceTypes: ["script"]
14500
14555
  }],
14501
14556
  map: {
14502
14557
  "1024": 0,
@@ -14516,7 +14571,8 @@ const redirectsCompatibilityTableData = {
14516
14571
  deprecationMessage: null,
14517
14572
  removed: false,
14518
14573
  removalMessage: null,
14519
- isBlocking: false
14574
+ isBlocking: false,
14575
+ resourceTypes: ["script"]
14520
14576
  }],
14521
14577
  map: {
14522
14578
  "1024": 0,
@@ -14536,7 +14592,8 @@ const redirectsCompatibilityTableData = {
14536
14592
  deprecationMessage: null,
14537
14593
  removed: false,
14538
14594
  removalMessage: null,
14539
- isBlocking: false
14595
+ isBlocking: false,
14596
+ resourceTypes: []
14540
14597
  }, {
14541
14598
  name: "google-ima.js",
14542
14599
  aliases: null,
@@ -14548,7 +14605,8 @@ const redirectsCompatibilityTableData = {
14548
14605
  deprecationMessage: null,
14549
14606
  removed: false,
14550
14607
  removalMessage: null,
14551
- isBlocking: false
14608
+ isBlocking: false,
14609
+ resourceTypes: ["script"]
14552
14610
  }],
14553
14611
  map: {
14554
14612
  "1": 0,
@@ -14566,7 +14624,7 @@ const redirectsCompatibilityTableData = {
14566
14624
  }, {
14567
14625
  shared: [{
14568
14626
  name: "googlesyndication-adsbygoogle",
14569
- aliases: ["ubo-googlesyndication_adsbygoogle.js", "googlesyndication_adsbygoogle.js"],
14627
+ aliases: ["ubo-googlesyndication_adsbygoogle.js", "ubo-googlesyndication.com/adsbygoogle.js", "googlesyndication_adsbygoogle.js"],
14570
14628
  description: "Mocks Google AdSense API.",
14571
14629
  docs: null,
14572
14630
  versionAdded: null,
@@ -14575,10 +14633,11 @@ const redirectsCompatibilityTableData = {
14575
14633
  deprecationMessage: null,
14576
14634
  removed: false,
14577
14635
  removalMessage: null,
14578
- isBlocking: false
14636
+ isBlocking: false,
14637
+ resourceTypes: []
14579
14638
  }, {
14580
14639
  name: "googlesyndication_adsbygoogle.js",
14581
- aliases: null,
14640
+ aliases: ["googlesyndication.com/adsbygoogle.js"],
14582
14641
  description: "Mocks Google AdSense API.",
14583
14642
  docs: null,
14584
14643
  versionAdded: null,
@@ -14587,7 +14646,8 @@ const redirectsCompatibilityTableData = {
14587
14646
  deprecationMessage: null,
14588
14647
  removed: false,
14589
14648
  removalMessage: null,
14590
- isBlocking: false
14649
+ isBlocking: false,
14650
+ resourceTypes: ["script"]
14591
14651
  }],
14592
14652
  map: {
14593
14653
  "1": 0,
@@ -14614,7 +14674,8 @@ const redirectsCompatibilityTableData = {
14614
14674
  deprecationMessage: null,
14615
14675
  removed: false,
14616
14676
  removalMessage: null,
14617
- isBlocking: false
14677
+ isBlocking: false,
14678
+ resourceTypes: []
14618
14679
  }, {
14619
14680
  name: "googletagservices_gpt.js",
14620
14681
  aliases: null,
@@ -14626,7 +14687,8 @@ const redirectsCompatibilityTableData = {
14626
14687
  deprecationMessage: null,
14627
14688
  removed: false,
14628
14689
  removalMessage: null,
14629
- isBlocking: false
14690
+ isBlocking: false,
14691
+ resourceTypes: ["script"]
14630
14692
  }],
14631
14693
  map: {
14632
14694
  "1": 0,
@@ -14653,7 +14715,8 @@ const redirectsCompatibilityTableData = {
14653
14715
  deprecationMessage: null,
14654
14716
  removed: false,
14655
14717
  removalMessage: null,
14656
- isBlocking: false
14718
+ isBlocking: false,
14719
+ resourceTypes: ["script"]
14657
14720
  }],
14658
14721
  map: {
14659
14722
  "1024": 0,
@@ -14673,7 +14736,8 @@ const redirectsCompatibilityTableData = {
14673
14736
  deprecationMessage: null,
14674
14737
  removed: false,
14675
14738
  removalMessage: null,
14676
- isBlocking: false
14739
+ isBlocking: false,
14740
+ resourceTypes: []
14677
14741
  }],
14678
14742
  map: {
14679
14743
  "1": 0,
@@ -14696,7 +14760,8 @@ const redirectsCompatibilityTableData = {
14696
14760
  deprecationMessage: null,
14697
14761
  removed: false,
14698
14762
  removalMessage: null,
14699
- isBlocking: false
14763
+ isBlocking: false,
14764
+ resourceTypes: []
14700
14765
  }],
14701
14766
  map: {
14702
14767
  "1": 0,
@@ -14719,7 +14784,8 @@ const redirectsCompatibilityTableData = {
14719
14784
  deprecationMessage: null,
14720
14785
  removed: false,
14721
14786
  removalMessage: null,
14722
- isBlocking: false
14787
+ isBlocking: false,
14788
+ resourceTypes: []
14723
14789
  }],
14724
14790
  map: {
14725
14791
  "1": 0,
@@ -14742,7 +14808,8 @@ const redirectsCompatibilityTableData = {
14742
14808
  deprecationMessage: null,
14743
14809
  removed: false,
14744
14810
  removalMessage: null,
14745
- isBlocking: false
14811
+ isBlocking: false,
14812
+ resourceTypes: []
14746
14813
  }],
14747
14814
  map: {
14748
14815
  "1": 0,
@@ -14765,7 +14832,8 @@ const redirectsCompatibilityTableData = {
14765
14832
  deprecationMessage: null,
14766
14833
  removed: false,
14767
14834
  removalMessage: null,
14768
- isBlocking: false
14835
+ isBlocking: false,
14836
+ resourceTypes: []
14769
14837
  }],
14770
14838
  map: {
14771
14839
  "1": 0,
@@ -14788,7 +14856,8 @@ const redirectsCompatibilityTableData = {
14788
14856
  deprecationMessage: null,
14789
14857
  removed: false,
14790
14858
  removalMessage: null,
14791
- isBlocking: false
14859
+ isBlocking: false,
14860
+ resourceTypes: []
14792
14861
  }],
14793
14862
  map: {
14794
14863
  "1": 0,
@@ -14811,7 +14880,8 @@ const redirectsCompatibilityTableData = {
14811
14880
  deprecationMessage: null,
14812
14881
  removed: false,
14813
14882
  removalMessage: null,
14814
- isBlocking: false
14883
+ isBlocking: false,
14884
+ resourceTypes: []
14815
14885
  }, {
14816
14886
  name: "noeval.js",
14817
14887
  aliases: null,
@@ -14823,7 +14893,8 @@ const redirectsCompatibilityTableData = {
14823
14893
  deprecationMessage: null,
14824
14894
  removed: false,
14825
14895
  removalMessage: null,
14826
- isBlocking: false
14896
+ isBlocking: false,
14897
+ resourceTypes: ["script"]
14827
14898
  }],
14828
14899
  map: {
14829
14900
  "1": 0,
@@ -14850,7 +14921,8 @@ const redirectsCompatibilityTableData = {
14850
14921
  deprecationMessage: null,
14851
14922
  removed: false,
14852
14923
  removalMessage: null,
14853
- isBlocking: false
14924
+ isBlocking: false,
14925
+ resourceTypes: ["media"]
14854
14926
  }],
14855
14927
  map: {
14856
14928
  "1024": 0,
@@ -14870,7 +14942,8 @@ const redirectsCompatibilityTableData = {
14870
14942
  deprecationMessage: null,
14871
14943
  removed: false,
14872
14944
  removalMessage: null,
14873
- isBlocking: false
14945
+ isBlocking: false,
14946
+ resourceTypes: []
14874
14947
  }, {
14875
14948
  name: "noop.css",
14876
14949
  aliases: null,
@@ -14882,7 +14955,8 @@ const redirectsCompatibilityTableData = {
14882
14955
  deprecationMessage: null,
14883
14956
  removed: false,
14884
14957
  removalMessage: null,
14885
- isBlocking: false
14958
+ isBlocking: false,
14959
+ resourceTypes: ["stylesheet"]
14886
14960
  }, {
14887
14961
  name: "blank-css",
14888
14962
  aliases: null,
@@ -14894,7 +14968,8 @@ const redirectsCompatibilityTableData = {
14894
14968
  deprecationMessage: null,
14895
14969
  removed: false,
14896
14970
  removalMessage: null,
14897
- isBlocking: false
14971
+ isBlocking: false,
14972
+ resourceTypes: []
14898
14973
  }],
14899
14974
  map: {
14900
14975
  "1": 0,
@@ -14925,7 +15000,8 @@ const redirectsCompatibilityTableData = {
14925
15000
  deprecationMessage: null,
14926
15001
  removed: false,
14927
15002
  removalMessage: null,
14928
- isBlocking: false
15003
+ isBlocking: false,
15004
+ resourceTypes: []
14929
15005
  }, {
14930
15006
  name: "noop.html",
14931
15007
  aliases: null,
@@ -14937,7 +15013,8 @@ const redirectsCompatibilityTableData = {
14937
15013
  deprecationMessage: null,
14938
15014
  removed: false,
14939
15015
  removalMessage: null,
14940
- isBlocking: false
15016
+ isBlocking: false,
15017
+ resourceTypes: ["sub_frame"]
14941
15018
  }, {
14942
15019
  name: "blank-html",
14943
15020
  aliases: null,
@@ -14949,7 +15026,8 @@ const redirectsCompatibilityTableData = {
14949
15026
  deprecationMessage: null,
14950
15027
  removed: false,
14951
15028
  removalMessage: null,
14952
- isBlocking: false
15029
+ isBlocking: false,
15030
+ resourceTypes: []
14953
15031
  }],
14954
15032
  map: {
14955
15033
  "1": 0,
@@ -14980,7 +15058,8 @@ const redirectsCompatibilityTableData = {
14980
15058
  deprecationMessage: null,
14981
15059
  removed: false,
14982
15060
  removalMessage: null,
14983
- isBlocking: false
15061
+ isBlocking: false,
15062
+ resourceTypes: []
14984
15063
  }, {
14985
15064
  name: "noop.js",
14986
15065
  aliases: null,
@@ -14992,7 +15071,8 @@ const redirectsCompatibilityTableData = {
14992
15071
  deprecationMessage: null,
14993
15072
  removed: false,
14994
15073
  removalMessage: null,
14995
- isBlocking: false
15074
+ isBlocking: false,
15075
+ resourceTypes: ["script"]
14996
15076
  }, {
14997
15077
  name: "blank-js",
14998
15078
  aliases: null,
@@ -15004,7 +15084,8 @@ const redirectsCompatibilityTableData = {
15004
15084
  deprecationMessage: null,
15005
15085
  removed: false,
15006
15086
  removalMessage: null,
15007
- isBlocking: false
15087
+ isBlocking: false,
15088
+ resourceTypes: []
15008
15089
  }],
15009
15090
  map: {
15010
15091
  "1": 0,
@@ -15035,7 +15116,8 @@ const redirectsCompatibilityTableData = {
15035
15116
  deprecationMessage: null,
15036
15117
  removed: false,
15037
15118
  removalMessage: null,
15038
- isBlocking: false
15119
+ isBlocking: false,
15120
+ resourceTypes: []
15039
15121
  }, {
15040
15122
  name: "noop.json",
15041
15123
  aliases: null,
@@ -15047,7 +15129,8 @@ const redirectsCompatibilityTableData = {
15047
15129
  deprecationMessage: null,
15048
15130
  removed: false,
15049
15131
  removalMessage: null,
15050
- isBlocking: false
15132
+ isBlocking: false,
15133
+ resourceTypes: []
15051
15134
  }],
15052
15135
  map: {
15053
15136
  "1": 0,
@@ -15074,7 +15157,8 @@ const redirectsCompatibilityTableData = {
15074
15157
  deprecationMessage: null,
15075
15158
  removed: false,
15076
15159
  removalMessage: null,
15077
- isBlocking: false
15160
+ isBlocking: false,
15161
+ resourceTypes: []
15078
15162
  }, {
15079
15163
  name: "noop-0.1s.mp3",
15080
15164
  aliases: null,
@@ -15086,7 +15170,8 @@ const redirectsCompatibilityTableData = {
15086
15170
  deprecationMessage: null,
15087
15171
  removed: false,
15088
15172
  removalMessage: null,
15089
- isBlocking: false
15173
+ isBlocking: false,
15174
+ resourceTypes: ["media"]
15090
15175
  }, {
15091
15176
  name: "blank-mp3",
15092
15177
  aliases: null,
@@ -15098,7 +15183,8 @@ const redirectsCompatibilityTableData = {
15098
15183
  deprecationMessage: null,
15099
15184
  removed: false,
15100
15185
  removalMessage: null,
15101
- isBlocking: false
15186
+ isBlocking: false,
15187
+ resourceTypes: []
15102
15188
  }],
15103
15189
  map: {
15104
15190
  "1": 0,
@@ -15129,7 +15215,8 @@ const redirectsCompatibilityTableData = {
15129
15215
  deprecationMessage: null,
15130
15216
  removed: false,
15131
15217
  removalMessage: null,
15132
- isBlocking: false
15218
+ isBlocking: false,
15219
+ resourceTypes: []
15133
15220
  }, {
15134
15221
  name: "noop-1s.mp4",
15135
15222
  aliases: null,
@@ -15141,7 +15228,8 @@ const redirectsCompatibilityTableData = {
15141
15228
  deprecationMessage: null,
15142
15229
  removed: false,
15143
15230
  removalMessage: null,
15144
- isBlocking: false
15231
+ isBlocking: false,
15232
+ resourceTypes: ["media"]
15145
15233
  }, {
15146
15234
  name: "blank-mp4",
15147
15235
  aliases: null,
@@ -15153,7 +15241,8 @@ const redirectsCompatibilityTableData = {
15153
15241
  deprecationMessage: null,
15154
15242
  removed: false,
15155
15243
  removalMessage: null,
15156
- isBlocking: false
15244
+ isBlocking: false,
15245
+ resourceTypes: []
15157
15246
  }],
15158
15247
  map: {
15159
15248
  "1": 0,
@@ -15184,7 +15273,8 @@ const redirectsCompatibilityTableData = {
15184
15273
  deprecationMessage: null,
15185
15274
  removed: false,
15186
15275
  removalMessage: null,
15187
- isBlocking: false
15276
+ isBlocking: false,
15277
+ resourceTypes: []
15188
15278
  }, {
15189
15279
  name: "noop.txt",
15190
15280
  aliases: null,
@@ -15196,7 +15286,8 @@ const redirectsCompatibilityTableData = {
15196
15286
  deprecationMessage: null,
15197
15287
  removed: false,
15198
15288
  removalMessage: null,
15199
- isBlocking: false
15289
+ isBlocking: false,
15290
+ resourceTypes: ["image", "media", "sub_frame", "stylesheet", "script", "xmlhttprequest", "other"]
15200
15291
  }, {
15201
15292
  name: "blank-text",
15202
15293
  aliases: null,
@@ -15208,7 +15299,8 @@ const redirectsCompatibilityTableData = {
15208
15299
  deprecationMessage: null,
15209
15300
  removed: false,
15210
15301
  removalMessage: null,
15211
- isBlocking: false
15302
+ isBlocking: false,
15303
+ resourceTypes: []
15212
15304
  }],
15213
15305
  map: {
15214
15306
  "1": 0,
@@ -15239,7 +15331,8 @@ const redirectsCompatibilityTableData = {
15239
15331
  deprecationMessage: null,
15240
15332
  removed: false,
15241
15333
  removalMessage: null,
15242
- isBlocking: false
15334
+ isBlocking: false,
15335
+ resourceTypes: []
15243
15336
  }],
15244
15337
  map: {
15245
15338
  "1": 0,
@@ -15262,7 +15355,8 @@ const redirectsCompatibilityTableData = {
15262
15355
  deprecationMessage: null,
15263
15356
  removed: false,
15264
15357
  removalMessage: null,
15265
- isBlocking: false
15358
+ isBlocking: false,
15359
+ resourceTypes: []
15266
15360
  }],
15267
15361
  map: {
15268
15362
  "1": 0,
@@ -15285,7 +15379,8 @@ const redirectsCompatibilityTableData = {
15285
15379
  deprecationMessage: null,
15286
15380
  removed: false,
15287
15381
  removalMessage: null,
15288
- isBlocking: false
15382
+ isBlocking: false,
15383
+ resourceTypes: []
15289
15384
  }],
15290
15385
  map: {
15291
15386
  "1": 0,
@@ -15308,7 +15403,8 @@ const redirectsCompatibilityTableData = {
15308
15403
  deprecationMessage: null,
15309
15404
  removed: false,
15310
15405
  removalMessage: null,
15311
- isBlocking: false
15406
+ isBlocking: false,
15407
+ resourceTypes: []
15312
15408
  }, {
15313
15409
  name: "noop-vmap1.0.xml",
15314
15410
  aliases: null,
@@ -15320,7 +15416,8 @@ const redirectsCompatibilityTableData = {
15320
15416
  deprecationMessage: null,
15321
15417
  removed: false,
15322
15418
  removalMessage: null,
15323
- isBlocking: false
15419
+ isBlocking: false,
15420
+ resourceTypes: ["media"]
15324
15421
  }],
15325
15422
  map: {
15326
15423
  "1": 0,
@@ -15347,7 +15444,8 @@ const redirectsCompatibilityTableData = {
15347
15444
  deprecationMessage: null,
15348
15445
  removed: false,
15349
15446
  removalMessage: null,
15350
- isBlocking: false
15447
+ isBlocking: false,
15448
+ resourceTypes: []
15351
15449
  }, {
15352
15450
  name: "nowebrtc.js",
15353
15451
  aliases: null,
@@ -15359,7 +15457,8 @@ const redirectsCompatibilityTableData = {
15359
15457
  deprecationMessage: null,
15360
15458
  removed: false,
15361
15459
  removalMessage: null,
15362
- isBlocking: false
15460
+ isBlocking: false,
15461
+ resourceTypes: ["other"]
15363
15462
  }],
15364
15463
  map: {
15365
15464
  "1": 0,
@@ -15386,7 +15485,8 @@ const redirectsCompatibilityTableData = {
15386
15485
  deprecationMessage: null,
15387
15486
  removed: false,
15388
15487
  removalMessage: null,
15389
- isBlocking: false
15488
+ isBlocking: false,
15489
+ resourceTypes: ["script"]
15390
15490
  }],
15391
15491
  map: {
15392
15492
  "1024": 0,
@@ -15406,7 +15506,8 @@ const redirectsCompatibilityTableData = {
15406
15506
  deprecationMessage: null,
15407
15507
  removed: false,
15408
15508
  removalMessage: null,
15409
- isBlocking: false
15509
+ isBlocking: false,
15510
+ resourceTypes: []
15410
15511
  }],
15411
15512
  map: {
15412
15513
  "1": 0,
@@ -15429,7 +15530,8 @@ const redirectsCompatibilityTableData = {
15429
15530
  deprecationMessage: null,
15430
15531
  removed: false,
15431
15532
  removalMessage: null,
15432
- isBlocking: false
15533
+ isBlocking: false,
15534
+ resourceTypes: []
15433
15535
  }, {
15434
15536
  name: "prebid-ads.js",
15435
15537
  aliases: null,
@@ -15441,7 +15543,8 @@ const redirectsCompatibilityTableData = {
15441
15543
  deprecationMessage: null,
15442
15544
  removed: false,
15443
15545
  removalMessage: null,
15444
- isBlocking: false
15546
+ isBlocking: false,
15547
+ resourceTypes: ["script"]
15445
15548
  }],
15446
15549
  map: {
15447
15550
  "1": 0,
@@ -15468,7 +15571,8 @@ const redirectsCompatibilityTableData = {
15468
15571
  deprecationMessage: null,
15469
15572
  removed: false,
15470
15573
  removalMessage: null,
15471
- isBlocking: false
15574
+ isBlocking: false,
15575
+ resourceTypes: []
15472
15576
  }],
15473
15577
  map: {
15474
15578
  "1": 0,
@@ -15491,7 +15595,8 @@ const redirectsCompatibilityTableData = {
15491
15595
  deprecationMessage: null,
15492
15596
  removed: false,
15493
15597
  removalMessage: null,
15494
- isBlocking: false
15598
+ isBlocking: false,
15599
+ resourceTypes: []
15495
15600
  }, {
15496
15601
  name: "nobab.js",
15497
15602
  aliases: null,
@@ -15503,7 +15608,8 @@ const redirectsCompatibilityTableData = {
15503
15608
  deprecationMessage: null,
15504
15609
  removed: false,
15505
15610
  removalMessage: null,
15506
- isBlocking: false
15611
+ isBlocking: false,
15612
+ resourceTypes: ["script"]
15507
15613
  }],
15508
15614
  map: {
15509
15615
  "1": 0,
@@ -15530,7 +15636,8 @@ const redirectsCompatibilityTableData = {
15530
15636
  deprecationMessage: null,
15531
15637
  removed: false,
15532
15638
  removalMessage: null,
15533
- isBlocking: false
15639
+ isBlocking: false,
15640
+ resourceTypes: []
15534
15641
  }, {
15535
15642
  name: "nobab2.js",
15536
15643
  aliases: null,
@@ -15542,7 +15649,8 @@ const redirectsCompatibilityTableData = {
15542
15649
  deprecationMessage: null,
15543
15650
  removed: false,
15544
15651
  removalMessage: null,
15545
- isBlocking: false
15652
+ isBlocking: false,
15653
+ resourceTypes: ["script"]
15546
15654
  }],
15547
15655
  map: {
15548
15656
  "1": 0,
@@ -15569,10 +15677,11 @@ const redirectsCompatibilityTableData = {
15569
15677
  deprecationMessage: null,
15570
15678
  removed: false,
15571
15679
  removalMessage: null,
15572
- isBlocking: false
15680
+ isBlocking: false,
15681
+ resourceTypes: []
15573
15682
  }, {
15574
15683
  name: "nofab.js",
15575
- aliases: null,
15684
+ aliases: ["fuckadblock.js-3.2.0", "fuckadblock.js-3.2.0.js"],
15576
15685
  description: "Mocks FAB script v3.2.0.",
15577
15686
  docs: null,
15578
15687
  versionAdded: null,
@@ -15581,7 +15690,8 @@ const redirectsCompatibilityTableData = {
15581
15690
  deprecationMessage: null,
15582
15691
  removed: false,
15583
15692
  removalMessage: null,
15584
- isBlocking: false
15693
+ isBlocking: false,
15694
+ resourceTypes: ["script"]
15585
15695
  }],
15586
15696
  map: {
15587
15697
  "1": 0,
@@ -15608,7 +15718,8 @@ const redirectsCompatibilityTableData = {
15608
15718
  deprecationMessage: null,
15609
15719
  removed: false,
15610
15720
  removalMessage: null,
15611
- isBlocking: false
15721
+ isBlocking: false,
15722
+ resourceTypes: []
15612
15723
  }, {
15613
15724
  name: "popads.js",
15614
15725
  aliases: null,
@@ -15620,7 +15731,8 @@ const redirectsCompatibilityTableData = {
15620
15731
  deprecationMessage: null,
15621
15732
  removed: false,
15622
15733
  removalMessage: null,
15623
- isBlocking: false
15734
+ isBlocking: false,
15735
+ resourceTypes: ["script"]
15624
15736
  }],
15625
15737
  map: {
15626
15738
  "1": 0,
@@ -15647,7 +15759,8 @@ const redirectsCompatibilityTableData = {
15647
15759
  deprecationMessage: null,
15648
15760
  removed: false,
15649
15761
  removalMessage: null,
15650
- isBlocking: false
15762
+ isBlocking: false,
15763
+ resourceTypes: []
15651
15764
  }, {
15652
15765
  name: "scorecardresearch_beacon.js",
15653
15766
  aliases: null,
@@ -15659,7 +15772,8 @@ const redirectsCompatibilityTableData = {
15659
15772
  deprecationMessage: null,
15660
15773
  removed: false,
15661
15774
  removalMessage: null,
15662
- isBlocking: false
15775
+ isBlocking: false,
15776
+ resourceTypes: ["script"]
15663
15777
  }],
15664
15778
  map: {
15665
15779
  "1": 0,
@@ -15686,7 +15800,8 @@ const redirectsCompatibilityTableData = {
15686
15800
  deprecationMessage: null,
15687
15801
  removed: false,
15688
15802
  removalMessage: null,
15689
- isBlocking: false
15803
+ isBlocking: false,
15804
+ resourceTypes: []
15690
15805
  }, {
15691
15806
  name: "popads-dummy.js",
15692
15807
  aliases: null,
@@ -15698,7 +15813,8 @@ const redirectsCompatibilityTableData = {
15698
15813
  deprecationMessage: null,
15699
15814
  removed: false,
15700
15815
  removalMessage: null,
15701
- isBlocking: false
15816
+ isBlocking: false,
15817
+ resourceTypes: ["script"]
15702
15818
  }],
15703
15819
  map: {
15704
15820
  "1": 0,
@@ -15759,7 +15875,9 @@ const redirectsCompatibilityTableData = {
15759
15875
  "google-ima.js": 19,
15760
15876
  "googlesyndication-adsbygoogle": 20,
15761
15877
  "ubo-googlesyndication_adsbygoogle.js": 20,
15878
+ "ubo-googlesyndication.com/adsbygoogle.js": 20,
15762
15879
  "googlesyndication_adsbygoogle.js": 20,
15880
+ "googlesyndication.com/adsbygoogle.js": 20,
15763
15881
  "googletagservices-gpt": 21,
15764
15882
  "ubo-googletagservices_gpt.js": 21,
15765
15883
  "googletagservices_gpt.js": 21,
@@ -15818,6 +15936,8 @@ const redirectsCompatibilityTableData = {
15818
15936
  "nobab2.js": 48,
15819
15937
  "prevent-fab-3.2.0": 49,
15820
15938
  "nofab.js": 49,
15939
+ "fuckadblock.js-3.2.0": 49,
15940
+ "fuckadblock.js-3.2.0.js": 49,
15821
15941
  "prevent-popads-net": 50,
15822
15942
  "popads.js": 50,
15823
15943
  "scorecardresearch-beacon": 51,
@@ -18525,7 +18645,7 @@ const scriptletsCompatibilityTableData = {
18525
18645
  }]
18526
18646
  }, {
18527
18647
  name: "remove-attr.js",
18528
- aliases: ["ra.js"],
18648
+ aliases: ["ra.js", "ra", "remove-attr"],
18529
18649
  description: null,
18530
18650
  docs: "https://github.com/gorhill/uBlock/wiki/Resources-Library#remove-attrjs-",
18531
18651
  versionAdded: null,
@@ -18606,7 +18726,7 @@ const scriptletsCompatibilityTableData = {
18606
18726
  }]
18607
18727
  }, {
18608
18728
  name: "remove-class.js",
18609
- aliases: ["rc.js"],
18729
+ aliases: ["rc.js", "rc", "remove-class"],
18610
18730
  description: null,
18611
18731
  docs: "https://github.com/gorhill/uBlock/wiki/Resources-Library#remove-classjs-",
18612
18732
  versionAdded: null,
@@ -20463,6 +20583,7 @@ const scriptletsCompatibilityTableData = {
20463
20583
  "ubo-ra.js": 59,
20464
20584
  "ubo-remove-attr": 59,
20465
20585
  "ubo-ra": 59,
20586
+ ra: 59,
20466
20587
  "remove-class": 60,
20467
20588
  "remove-class.js": 60,
20468
20589
  "ubo-remove-class.js": 60,
@@ -20470,6 +20591,7 @@ const scriptletsCompatibilityTableData = {
20470
20591
  "ubo-rc.js": 60,
20471
20592
  "ubo-remove-class": 60,
20472
20593
  "ubo-rc": 60,
20594
+ rc: 60,
20473
20595
  "remove-cookie": 61,
20474
20596
  "cookie-remover.js": 61,
20475
20597
  "ubo-cookie-remover.js": 61,
@@ -21527,6 +21649,44 @@ function getScriptletName(scriptletNode) {
21527
21649
  }
21528
21650
  return scriptletNode.children[0]?.value ?? EMPTY;
21529
21651
  }
21652
+ /**
21653
+ * Transform the nth argument of the scriptlet node
21654
+ *
21655
+ * @param scriptletNode Scriptlet node to transform argument of
21656
+ * @param index Index of the argument to transform (index 0 is the scriptlet name)
21657
+ * @param transform Function to transform the argument
21658
+ */
21659
+ function transformNthScriptletArgument(scriptletNode, index, transform) {
21660
+ const child = scriptletNode.children[index];
21661
+ if (!isUndefined(child)) {
21662
+ const transformed = transform(child?.value ?? null);
21663
+ if (isNull(transformed)) {
21664
+ // eslint-disable-next-line no-param-reassign
21665
+ scriptletNode.children[index] = null;
21666
+ return;
21667
+ }
21668
+ if (isNull(child)) {
21669
+ // eslint-disable-next-line no-param-reassign
21670
+ scriptletNode.children[index] = {
21671
+ type: 'Value',
21672
+ value: transformed
21673
+ };
21674
+ return;
21675
+ }
21676
+ child.value = transformed;
21677
+ }
21678
+ }
21679
+ /**
21680
+ * Transform all arguments of the scriptlet node
21681
+ *
21682
+ * @param scriptletNode Scriptlet node to transform arguments of
21683
+ * @param transform Function to transform the arguments
21684
+ */
21685
+ function transformAllScriptletArguments(scriptletNode, transform) {
21686
+ for (let i = 0; i < scriptletNode.children.length; i += 1) {
21687
+ transformNthScriptletArgument(scriptletNode, i, transform);
21688
+ }
21689
+ }
21530
21690
  /**
21531
21691
  * Set name of the scriptlet.
21532
21692
  * Modifies input `scriptletNode` if needed.
@@ -21535,10 +21695,7 @@ function getScriptletName(scriptletNode) {
21535
21695
  * @param name Name to set
21536
21696
  */
21537
21697
  function setScriptletName(scriptletNode, name) {
21538
- if (scriptletNode.children.length > 0 && !isNull(scriptletNode.children[0])) {
21539
- // eslint-disable-next-line no-param-reassign
21540
- scriptletNode.children[0].value = name;
21541
- }
21698
+ transformNthScriptletArgument(scriptletNode, 0, () => name);
21542
21699
  }
21543
21700
  /**
21544
21701
  * Set quote type of the scriptlet parameters
@@ -21547,1422 +21704,1761 @@ function setScriptletName(scriptletNode, name) {
21547
21704
  * @param quoteType Preferred quote type
21548
21705
  */
21549
21706
  function setScriptletQuoteType(scriptletNode, quoteType) {
21550
- if (scriptletNode.children.length > 0) {
21551
- for (let i = 0; i < scriptletNode.children.length; i += 1) {
21552
- const child = scriptletNode.children[i];
21553
- if (isNull(child)) {
21554
- continue;
21555
- }
21556
- // eslint-disable-next-line no-param-reassign
21557
- child.value = QuoteUtils.setStringQuoteType(child.value, quoteType);
21558
- }
21559
- }
21707
+ // null is a special value that means "no value", but we can't change its quote type,
21708
+ // so we need to convert it to empty string
21709
+ transformAllScriptletArguments(scriptletNode, value => QuoteUtils.setStringQuoteType(value ?? EMPTY, quoteType));
21560
21710
  }
21561
21711
 
21562
21712
  /**
21563
- * @file Scriptlet injection rule converter
21713
+ * @file Resource type schema.
21564
21714
  */
21565
- const ABP_SCRIPTLET_PREFIX = 'abp-';
21566
- const UBO_SCRIPTLET_PREFIX = 'ubo-';
21567
21715
  /**
21568
- * Scriptlet injection rule converter class
21716
+ * Resource type.
21569
21717
  *
21570
- * @todo Implement `convertToUbo` and `convertToAbp`
21718
+ * @see {@link https://developer.chrome.com/docs/extensions/reference/declarativeNetRequest/#type-ResourceType}
21571
21719
  */
21572
- class ScriptletRuleConverter extends RuleConverterBase {
21573
- /**
21574
- * Converts a scriptlet injection rule to AdGuard format, if possible.
21575
- *
21576
- * @param rule Rule node to convert
21577
- * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
21578
- * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
21579
- * If the rule was not converted, the result array will contain the original node with the same object reference
21580
- * @throws If the rule is invalid or cannot be converted
21581
- */
21582
- static convertToAdg(rule) {
21583
- // Ignore AdGuard rules
21584
- if (rule.syntax === exports.AdblockSyntax.Adg) {
21585
- return createNodeConversionResult([rule], false);
21586
- }
21587
- const separator = rule.separator.value;
21588
- let convertedSeparator = separator;
21589
- convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgJsInjectionException : exports.CosmeticRuleSeparator.AdgJsInjection;
21590
- const convertedScriptlets = [];
21591
- for (const scriptlet of rule.body.children) {
21592
- // Clone the node to avoid any side effects
21593
- const scriptletClone = cloneScriptletRuleNode(scriptlet);
21594
- // Remove possible quotes just to make it easier to work with the scriptlet name
21595
- const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), exports.QuoteType.None);
21596
- // Add prefix if it's not already there
21597
- let prefix;
21598
- switch (rule.syntax) {
21599
- case exports.AdblockSyntax.Abp:
21600
- prefix = ABP_SCRIPTLET_PREFIX;
21601
- break;
21602
- case exports.AdblockSyntax.Ubo:
21603
- prefix = UBO_SCRIPTLET_PREFIX;
21604
- break;
21605
- default:
21606
- prefix = EMPTY;
21607
- }
21608
- if (!scriptletName.startsWith(prefix)) {
21609
- setScriptletName(scriptletClone, `${prefix}${scriptletName}`);
21610
- }
21611
- // ADG scriptlet parameters should be quoted, and single quoted are preferred
21612
- setScriptletQuoteType(scriptletClone, exports.QuoteType.Single);
21613
- convertedScriptlets.push(scriptletClone);
21614
- }
21615
- return createNodeConversionResult(convertedScriptlets.map(scriptlet => {
21616
- const res = {
21617
- category: rule.category,
21618
- type: rule.type,
21619
- syntax: exports.AdblockSyntax.Adg,
21620
- exception: rule.exception,
21621
- domains: cloneDomainListNode(rule.domains),
21622
- separator: {
21623
- type: 'Value',
21624
- value: convertedSeparator
21625
- },
21626
- body: {
21627
- type: rule.body.type,
21628
- children: [scriptlet]
21629
- }
21630
- };
21631
- if (rule.modifiers) {
21632
- res.modifiers = cloneModifierListNode(rule.modifiers);
21633
- }
21634
- return res;
21635
- }), true);
21636
- }
21637
- }
21638
-
21720
+ exports.ResourceType = void 0;
21721
+ (function (ResourceType) {
21722
+ ResourceType["MainFrame"] = "main_frame";
21723
+ ResourceType["SubFrame"] = "sub_frame";
21724
+ ResourceType["Stylesheet"] = "stylesheet";
21725
+ ResourceType["Script"] = "script";
21726
+ ResourceType["Image"] = "image";
21727
+ ResourceType["Font"] = "font";
21728
+ ResourceType["Object"] = "object";
21729
+ ResourceType["XmlHttpRequest"] = "xmlhttprequest";
21730
+ ResourceType["Ping"] = "ping";
21731
+ ResourceType["Media"] = "media";
21732
+ ResourceType["WebSocket"] = "websocket";
21733
+ ResourceType["Other"] = "other";
21734
+ })(exports.ResourceType || (exports.ResourceType = {}));
21639
21735
  /**
21640
- * @file Utility functions for working with modifier nodes
21736
+ * Resource type schema.
21641
21737
  */
21738
+ const resourceTypeSchema = zod.nativeEnum(exports.ResourceType);
21739
+
21642
21740
  /**
21643
- * Creates a modifier node
21741
+ * Map of resource types to their corresponding adblock modifier names.
21742
+ *
21743
+ * @note Record type is used to ensure that all resource types are present in the map.
21744
+ */
21745
+ const RESOURCE_TYPE_MODIFIER_MAP = Object.freeze({
21746
+ [exports.ResourceType.MainFrame]: 'document',
21747
+ [exports.ResourceType.SubFrame]: 'subdocument',
21748
+ [exports.ResourceType.Stylesheet]: 'stylesheet',
21749
+ [exports.ResourceType.Script]: 'script',
21750
+ [exports.ResourceType.Image]: 'image',
21751
+ [exports.ResourceType.Font]: 'font',
21752
+ [exports.ResourceType.Object]: 'object',
21753
+ [exports.ResourceType.XmlHttpRequest]: 'xmlhttprequest',
21754
+ [exports.ResourceType.Ping]: 'ping',
21755
+ [exports.ResourceType.Media]: 'media',
21756
+ [exports.ResourceType.WebSocket]: 'websocket',
21757
+ [exports.ResourceType.Other]: 'other'
21758
+ });
21759
+ /**
21760
+ * Gets the adblock modifier name for the given resource type.
21644
21761
  *
21645
- * @param name Name of the modifier
21646
- * @param value Value of the modifier
21647
- * @param exception Whether the modifier is an exception
21648
- * @returns Modifier node
21762
+ * @param resourceType Resource type to get the modifier name for.
21763
+ * @param platform Platform to get the modifier for.
21764
+ *
21765
+ * @returns A string containing the adblock modifier name for the given resource type
21766
+ * or `null` if the modifier could not be found.
21649
21767
  */
21650
- function createModifierNode(name, value = undefined, exception = false) {
21651
- const result = {
21652
- type: 'Modifier',
21653
- exception,
21654
- name: {
21655
- type: 'Value',
21656
- value: name
21657
- }
21658
- };
21659
- if (!isUndefined(value)) {
21660
- result.value = {
21661
- type: 'Value',
21662
- value
21663
- };
21768
+ const getResourceTypeModifier = (resourceType, platform) => {
21769
+ const modifierName = RESOURCE_TYPE_MODIFIER_MAP[resourceType];
21770
+ if (!modifierName) {
21771
+ return null;
21664
21772
  }
21665
- return result;
21666
- }
21773
+ const modifierData = modifiersCompatibilityTable.getFirst(modifierName, platform);
21774
+ if (isNull(modifierData)) {
21775
+ return null;
21776
+ }
21777
+ return modifierData.name;
21778
+ };
21667
21779
  /**
21668
- * Creates a modifier list node
21780
+ * Checks if the given resource type is valid.
21669
21781
  *
21670
- * @param modifiers Modifiers to put in the list (optional, defaults to an empty list)
21671
- * @returns Modifier list node
21782
+ * @param resourceType Resource type to check.
21783
+ *
21784
+ * @returns `true` if the resource type is valid, `false` otherwise.
21672
21785
  */
21673
- function createModifierListNode(modifiers = []) {
21674
- const result = {
21675
- type: 'ModifierList',
21676
- // We need to clone the modifiers to avoid side effects
21677
- children: modifiers.length ? clone(modifiers) : []
21678
- };
21679
- return result;
21680
- }
21786
+ const isValidResourceType = resourceType => {
21787
+ return Object.values(exports.ResourceType).includes(resourceType);
21788
+ };
21681
21789
 
21682
21790
  /**
21683
- * A very simple map extension that allows to store multiple values for the same key
21684
- * by storing them in an array.
21791
+ * @file Compatibility tables for redirects.
21792
+ */
21793
+ /**
21794
+ * Prefix for resource redirection names.
21795
+ */
21796
+ const ABP_RESOURCE_PREFIX = 'abp-resource:';
21797
+ const ABP_RESOURCE_PREFIX_LENGTH = ABP_RESOURCE_PREFIX.length;
21798
+ /**
21799
+ * Normalizes the redirect name.
21685
21800
  *
21686
- * @todo Add more methods if needed
21801
+ * @param name Redirect name to normalize.
21802
+ *
21803
+ * @returns Normalized redirect name.
21804
+ *
21805
+ * @example
21806
+ * redirectNameNormalizer('abp-resource:my-resource') // => 'my-resource'
21807
+ * redirectNameNormalizer('noop.js:99') // => 'noop.js'
21687
21808
  */
21688
- class MultiValueMap extends Map {
21809
+ const redirectNameNormalizer = name => {
21810
+ // Remove ABP resource prefix, if present
21811
+ if (name.startsWith(ABP_RESOURCE_PREFIX)) {
21812
+ return name.slice(ABP_RESOURCE_PREFIX_LENGTH);
21813
+ }
21814
+ // Remove :[integer] priority suffix from the name, if present
21815
+ // See:
21816
+ // - https://github.com/AdguardTeam/tsurlfilter/issues/59
21817
+ // - https://github.com/gorhill/uBlock/wiki/Static-filter-syntax#redirect
21818
+ const colonIndex = name.lastIndexOf(COLON);
21819
+ if (colonIndex !== -1 && /^\d+$/.test(name.slice(colonIndex + 1))) {
21820
+ return name.slice(0, colonIndex);
21821
+ }
21822
+ return name;
21823
+ };
21824
+ /**
21825
+ * Compatibility table for redirects.
21826
+ */
21827
+ class RedirectsCompatibilityTable extends CompatibilityTableBase {
21689
21828
  /**
21690
- * Adds a value to the map. If the key already exists, the value will be appended to the existing array,
21691
- * otherwise a new array will be created for the key.
21829
+ * Creates a new instance of the compatibility table for redirects.
21692
21830
  *
21693
- * @param key Key to add
21694
- * @param values Value(s) to add
21831
+ * @param data Compatibility table data.
21695
21832
  */
21696
- add(key, ...values) {
21697
- let currentValues = super.get(key);
21698
- if (isUndefined(currentValues)) {
21699
- currentValues = [];
21700
- super.set(key, values);
21833
+ constructor(data) {
21834
+ super(data, redirectNameNormalizer);
21835
+ }
21836
+ /**
21837
+ * Gets the resource type adblock modifiers for the redirect for the given platform
21838
+ * based on the `resourceTypes` field.
21839
+ *
21840
+ * @param redirect Redirect name or redirect data.
21841
+ * @param platform Platform to get the modifiers for.
21842
+ *
21843
+ * @returns Set of resource type modifiers or an empty set if the redirect is not found or has no resource types.
21844
+ */
21845
+ getResourceTypeModifiers(redirect, platform) {
21846
+ let redirectData = null;
21847
+ if (isString(redirect)) {
21848
+ redirectData = this.getFirst(redirect, platform);
21849
+ } else {
21850
+ redirectData = redirect;
21701
21851
  }
21702
- currentValues.push(...values);
21852
+ const modifierNames = new Set();
21853
+ if (isNull(redirectData) || isUndefined(redirectData.resourceTypes)) {
21854
+ return modifierNames;
21855
+ }
21856
+ for (const resourceType of redirectData.resourceTypes) {
21857
+ const modifierName = getResourceTypeModifier(resourceType, platform);
21858
+ if (isNull(modifierName)) {
21859
+ continue;
21860
+ }
21861
+ modifierNames.add(modifierName);
21862
+ }
21863
+ return modifierNames;
21703
21864
  }
21704
21865
  }
21866
+ /**
21867
+ * Deep freeze the compatibility table data to avoid accidental modifications.
21868
+ */
21869
+ deepFreeze(redirectsCompatibilityTableData);
21870
+ /**
21871
+ * Compatibility table instance for redirects.
21872
+ */
21873
+ const redirectsCompatibilityTable = new RedirectsCompatibilityTable(redirectsCompatibilityTableData);
21705
21874
 
21706
21875
  /**
21707
- * @file Cosmetic rule modifier converter from uBO to ADG
21876
+ * @file Compatibility tables for scriptlets.
21708
21877
  */
21709
- const UBO_MATCHES_PATH_OPERATOR = 'matches-path';
21710
- const ADG_PATH_MODIFIER = 'path';
21711
21878
  /**
21712
- * Special characters in modifier regexps that should be escaped
21879
+ * Compatibility table for scriptlets.
21713
21880
  */
21714
- const SPECIAL_MODIFIER_REGEX_CHARS = new Set([OPEN_SQUARE_BRACKET, CLOSE_SQUARE_BRACKET, COMMA, ESCAPE_CHARACTER]);
21881
+ class ScriptletsCompatibilityTable extends CompatibilityTableBase {}
21715
21882
  /**
21716
- * Helper class for converting cosmetic rule modifiers from uBO to ADG
21883
+ * Deep freeze the compatibility table data to avoid accidental modifications.
21717
21884
  */
21718
- class AdgCosmeticRuleModifierConverter {
21719
- /**
21720
- * Converts a uBO cosmetic rule modifier list to ADG, if possible.
21721
- *
21722
- * @param modifierList Cosmetic rule modifier list node to convert
21723
- * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
21724
- * the converted node, and its `isConverted` flag indicates whether the original node was converted.
21725
- * If the node was not converted, the result will contain the original node with the same object reference
21726
- * @throws If the modifier list cannot be converted
21727
- * @see {@link https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#cosmetic-filter-operators}
21728
- */
21729
- static convertFromUbo(modifierList) {
21730
- const conversionMap = new MultiValueMap();
21731
- modifierList.children.forEach((modifier, index) => {
21732
- // :matches-path
21733
- if (modifier.name.value === UBO_MATCHES_PATH_OPERATOR) {
21734
- if (!modifier.value) {
21735
- throw new RuleConversionError(`'${UBO_MATCHES_PATH_OPERATOR}' operator requires a value`);
21736
- }
21737
- const value = RegExpUtils.isRegexPattern(modifier.value.value) ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS) : modifier.value.value;
21738
- // Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
21739
- conversionMap.add(index, createModifierNode(ADG_PATH_MODIFIER,
21740
- // We should negate the regexp if the modifier is an exception
21741
- modifier.exception
21742
- // eslint-disable-next-line max-len
21743
- ? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(value))}${REGEX_MARKER}` : value));
21744
- }
21745
- });
21746
- // Check if we have any converted modifiers
21747
- if (conversionMap.size) {
21748
- const modifierListClone = clone(modifierList);
21749
- // Replace the original modifiers with the converted ones
21750
- modifierListClone.children = modifierListClone.children.map((modifier, index) => {
21751
- const convertedModifier = conversionMap.get(index);
21752
- return convertedModifier ?? modifier;
21753
- }).flat();
21754
- return createConversionResult(modifierListClone, true);
21755
- }
21756
- // Otherwise, just return the original modifier list
21757
- return createConversionResult(modifierList, false);
21758
- }
21759
- }
21760
- const ERROR_MESSAGES$1 = {
21761
- // eslint-disable-next-line max-len
21762
- INVALID_ATTRIBUTE_VALUE: `Expected '${cssTokenizer.getFormattedTokenName(cssTokenizer.TokenType.Ident)}' or '${cssTokenizer.getFormattedTokenName(cssTokenizer.TokenType.String)}' as attribute value, but got '%s' with value '%s`
21763
- };
21764
- var PseudoClasses;
21765
- (function (PseudoClasses) {
21766
- PseudoClasses["AbpContains"] = "-abp-contains";
21767
- PseudoClasses["AbpHas"] = "-abp-has";
21768
- PseudoClasses["Contains"] = "contains";
21769
- PseudoClasses["Has"] = "has";
21770
- PseudoClasses["HasText"] = "has-text";
21771
- PseudoClasses["MatchesCss"] = "matches-css";
21772
- PseudoClasses["MatchesCssAfter"] = "matches-css-after";
21773
- PseudoClasses["MatchesCssBefore"] = "matches-css-before";
21774
- PseudoClasses["Not"] = "not";
21775
- })(PseudoClasses || (PseudoClasses = {}));
21776
- var PseudoElements;
21777
- (function (PseudoElements) {
21778
- PseudoElements["After"] = "after";
21779
- PseudoElements["Before"] = "before";
21780
- })(PseudoElements || (PseudoElements = {}));
21781
- const PSEUDO_ELEMENT_NAMES = new Set([PseudoElements.After, PseudoElements.Before]);
21885
+ deepFreeze(scriptletsCompatibilityTableData);
21782
21886
  /**
21783
- * CSS selector converter
21784
- *
21785
- * @todo Implement `convertToUbo` and `convertToAbp`
21887
+ * Compatibility table instance for scriptlets.
21786
21888
  */
21787
- class CssSelectorConverter extends ConverterBase {
21788
- /**
21789
- * Converts Extended CSS elements to AdGuard-compatible ones
21790
- *
21791
- * @param selectorList Selector list to convert
21792
- * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
21793
- * the converted node, and its `isConverted` flag indicates whether the original node was converted.
21794
- * If the node was not converted, the result will contain the original node with the same object reference
21795
- * @throws If the rule is invalid or incompatible
21796
- */
21797
- static convertToAdg(selectorList) {
21798
- const stream = selectorList instanceof CssTokenStream ? selectorList : new CssTokenStream(selectorList);
21799
- const converted = [];
21800
- const convertAndPushPseudo = pseudo => {
21801
- switch (pseudo) {
21802
- case PseudoClasses.AbpContains:
21803
- case PseudoClasses.HasText:
21804
- converted.push(PseudoClasses.Contains);
21805
- converted.push(OPEN_PARENTHESIS);
21806
- break;
21807
- case PseudoClasses.AbpHas:
21808
- converted.push(PseudoClasses.Has);
21809
- converted.push(OPEN_PARENTHESIS);
21810
- break;
21811
- // a bit special case:
21812
- // - `:matches-css-before(...)` → `:matches-css(before, ...)`
21813
- // - `:matches-css-after(...)` → `:matches-css(after, ...)`
21814
- case PseudoClasses.MatchesCssBefore:
21815
- case PseudoClasses.MatchesCssAfter:
21816
- converted.push(PseudoClasses.MatchesCss);
21817
- converted.push(OPEN_PARENTHESIS);
21818
- converted.push(pseudo.substring(PseudoClasses.MatchesCss.length + 1));
21819
- converted.push(COMMA);
21820
- break;
21821
- default:
21822
- converted.push(pseudo);
21823
- converted.push(OPEN_PARENTHESIS);
21824
- break;
21825
- }
21826
- };
21827
- while (!stream.isEof()) {
21828
- const token = stream.getOrFail();
21829
- if (token.type === cssTokenizer.TokenType.Colon) {
21830
- // Advance colon
21831
- stream.advance();
21832
- converted.push(COLON);
21833
- const tempToken = stream.getOrFail();
21834
- // Double colon is a pseudo-element
21835
- if (tempToken.type === cssTokenizer.TokenType.Colon) {
21836
- stream.advance();
21837
- converted.push(COLON);
21838
- continue;
21839
- }
21840
- if (tempToken.type === cssTokenizer.TokenType.Ident) {
21841
- const name = stream.source.slice(tempToken.start, tempToken.end);
21842
- if (PSEUDO_ELEMENT_NAMES.has(name)) {
21843
- // Add an extra colon to the name
21844
- converted.push(COLON);
21845
- converted.push(name);
21846
- } else {
21847
- // Add the name as is
21848
- converted.push(name);
21849
- }
21850
- // Advance the names
21851
- stream.advance();
21852
- } else if (tempToken.type === cssTokenizer.TokenType.Function) {
21853
- const name = stream.source.slice(tempToken.start, tempToken.end - 1); // omit the last parenthesis
21854
- // :-abp-contains(...) → :contains(...)
21855
- // :has-text(...) → :contains(...)
21856
- // :-abp-has(...) → :has(...)
21857
- convertAndPushPseudo(name);
21858
- // Advance the function name
21859
- stream.advance();
21860
- }
21861
- } else if (token.type === cssTokenizer.TokenType.OpenSquareBracket) {
21862
- let tempToken;
21863
- const {
21864
- start
21865
- } = token;
21866
- stream.advance();
21867
- // Converts legacy Extended CSS selectors to the modern Extended CSS syntax.
21868
- // For example:
21869
- // - `[-ext-has=...]` → `:has(...)`
21870
- // - `[-ext-contains=...]` → `:contains(...)`
21871
- // - `[-ext-matches-css-before=...]` → `:matches-css(before, ...)`
21872
- stream.skipWhitespace();
21873
- stream.expect(cssTokenizer.TokenType.Ident);
21874
- tempToken = stream.getOrFail();
21875
- let attr = stream.source.slice(tempToken.start, tempToken.end);
21876
- // Skip if the attribute name is not a legacy Extended CSS one
21877
- if (!(attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX) || attr.startsWith(ABP_EXT_CSS_PREFIX))) {
21878
- converted.push(stream.source.slice(start, tempToken.end));
21879
- stream.advance();
21880
- continue;
21881
- }
21882
- if (attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX)) {
21883
- attr = attr.slice(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX.length);
21884
- }
21885
- stream.advance();
21886
- stream.skipWhitespace();
21887
- // Next token should be an equality operator (=), because Extended CSS attribute selectors
21888
- // do not support other operators
21889
- stream.expect(cssTokenizer.TokenType.Delim, {
21890
- value: EQUALS
21891
- });
21892
- stream.advance();
21893
- // Skip optional whitespace after the operator
21894
- stream.skipWhitespace();
21895
- // Parse attribute value
21896
- tempToken = stream.getOrFail();
21897
- // According to the spec, attribute value should be an identifier or a string
21898
- if (tempToken.type !== cssTokenizer.TokenType.Ident && tempToken.type !== cssTokenizer.TokenType.String) {
21899
- throw new Error(sprintfJs.sprintf(ERROR_MESSAGES$1.INVALID_ATTRIBUTE_VALUE, cssTokenizer.getFormattedTokenName(tempToken.type), stream.source.slice(tempToken.start, tempToken.end)));
21900
- }
21901
- const value = stream.source.slice(tempToken.start, tempToken.end);
21902
- // Advance the attribute value
21903
- stream.advance();
21904
- // Skip optional whitespace after the attribute value
21905
- stream.skipWhitespace();
21906
- // Next character should be a closing square bracket
21907
- // We don't allow flags for Extended CSS attribute selectors
21908
- stream.expect(cssTokenizer.TokenType.CloseSquareBracket);
21909
- stream.advance();
21910
- converted.push(COLON);
21911
- convertAndPushPseudo(attr);
21912
- let processedValue = value.slice(1, -1); // omit the quotes
21913
- if (attr === PseudoClasses.Has) {
21914
- // TODO: Optimize this to avoid double tokenization
21915
- processedValue = CssSelectorConverter.convertToAdg(processedValue).result;
21916
- }
21917
- converted.push(processedValue);
21918
- converted.push(CLOSE_PARENTHESIS);
21919
- } else {
21920
- converted.push(stream.source.slice(token.start, token.end));
21921
- // Advance the token
21922
- stream.advance();
21923
- }
21924
- }
21925
- const convertedSelectorList = converted.join(EMPTY);
21926
- return createConversionResult(convertedSelectorList, stream.source !== convertedSelectorList);
21927
- }
21928
- }
21889
+ const scriptletsCompatibilityTable = new ScriptletsCompatibilityTable(scriptletsCompatibilityTableData);
21929
21890
 
21891
+ /* eslint-disable no-bitwise */
21930
21892
  /**
21931
- * @file CSS injection rule converter
21893
+ * @file Platform schema.
21932
21894
  */
21933
21895
  /**
21934
- * CSS injection rule converter class
21896
+ * Platform separator, e.g. 'adg_os_any|adg_safari_any' means any AdGuard OS platform and
21897
+ * any AdGuard Safari content blocker platform.
21898
+ */
21899
+ const PLATFORM_SEPARATOR = '|';
21900
+ /**
21901
+ * Platform negation character, e.g. 'adg_any|~adg_safari_any' means any AdGuard product except
21902
+ * Safari content blockers.
21903
+ */
21904
+ const PLATFORM_NEGATION = '~';
21905
+ /**
21906
+ * Parses a raw platform string into a platform bitmask.
21935
21907
  *
21936
- * @todo Implement `convertToUbo` and `convertToAbp`
21908
+ * @param rawPlatforms Raw platform string, e.g. 'adg_safari_any|adg_os_any'.
21909
+ *
21910
+ * @returns Platform bitmask.
21937
21911
  */
21938
- class CssInjectionRuleConverter extends RuleConverterBase {
21939
- /**
21940
- * Converts a CSS injection rule to AdGuard format, if possible.
21941
- *
21942
- * @param rule Rule node to convert
21943
- * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
21944
- * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
21945
- * If the rule was not converted, the result array will contain the original node with the same object reference
21946
- * @throws If the rule is invalid or cannot be converted
21947
- */
21948
- static convertToAdg(rule) {
21949
- const separator = rule.separator.value;
21950
- let convertedSeparator = separator;
21951
- const stream = new CssTokenStream(rule.body.selectorList.value);
21952
- const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
21953
- // Change the separator if the rule contains ExtendedCSS elements,
21954
- // but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
21955
- // because sometimes we use it to force executing ExtendedCSS library.
21956
- if (stream.hasAnySelectorExtendedCssNodeStrict() || rule.body.remove) {
21957
- convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgExtendedCssInjectionException : exports.CosmeticRuleSeparator.AdgExtendedCssInjection;
21958
- } else if (rule.syntax !== exports.AdblockSyntax.Adg) {
21959
- // If the original rule syntax is not AdGuard, use the default separator
21960
- // e.g. if the input rule is from uBO, we need to convert ## to #$#.
21961
- convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgCssInjectionException : exports.CosmeticRuleSeparator.AdgCssInjection;
21912
+ const parseRawPlatforms = rawPlatforms => {
21913
+ // e.g. 'adg_safari_any|adg_os_any'
21914
+ const rawPlatformList = rawPlatforms.split(PLATFORM_SEPARATOR).map(rawPlatform => rawPlatform.trim());
21915
+ let result = 0;
21916
+ for (let rawPlatform of rawPlatformList) {
21917
+ // negation, e.g. 'adg_any|~adg_safari_any' means any AdGuard product except Safari content blockers
21918
+ let negated = false;
21919
+ if (rawPlatform.startsWith(PLATFORM_NEGATION)) {
21920
+ negated = true;
21921
+ rawPlatform = rawPlatform.slice(1).trim();
21962
21922
  }
21963
- // Check if the rule needs to be converted
21964
- if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg) || separator !== convertedSeparator || convertedSelectorList.isConverted) {
21965
- // TODO: Replace with custom clone method
21966
- const ruleClone = clone(rule);
21967
- ruleClone.syntax = exports.AdblockSyntax.Adg;
21968
- ruleClone.separator.value = convertedSeparator;
21969
- ruleClone.body.selectorList.value = convertedSelectorList.result;
21970
- return createNodeConversionResult([ruleClone], true);
21923
+ const platform = SPECIFIC_PLATFORM_MAP.get(rawPlatform) ?? GENERIC_PLATFORM_MAP.get(rawPlatform);
21924
+ if (isUndefined(platform)) {
21925
+ throw new Error(`Unknown platform: ${rawPlatform}`);
21926
+ }
21927
+ if (negated) {
21928
+ result &= ~platform;
21929
+ } else {
21930
+ result |= platform;
21971
21931
  }
21972
- // Otherwise, return the original rule
21973
- return createNodeConversionResult([rule], false);
21974
21932
  }
21975
- }
21976
-
21933
+ if (result === 0) {
21934
+ throw new Error('No platforms specified');
21935
+ }
21936
+ return result;
21937
+ };
21977
21938
  /**
21978
- * @file Element hiding rule converter
21939
+ * Platform schema.
21979
21940
  */
21980
- /**
21981
- * Element hiding rule converter class
21982
- *
21983
- * @todo Implement `convertToUbo` and `convertToAbp`
21984
- */
21985
- class ElementHidingRuleConverter extends RuleConverterBase {
21986
- /**
21987
- * Converts an element hiding rule to AdGuard format, if possible.
21988
- *
21989
- * @param rule Rule node to convert
21990
- * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
21991
- * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
21992
- * If the rule was not converted, the result array will contain the original node with the same object reference
21993
- * @throws If the rule is invalid or cannot be converted
21994
- */
21995
- static convertToAdg(rule) {
21996
- const separator = rule.separator.value;
21997
- let convertedSeparator = separator;
21998
- const stream = new CssTokenStream(rule.body.selectorList.value);
21999
- const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
22000
- // Change the separator if the rule contains ExtendedCSS elements,
22001
- // but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
22002
- // because sometimes we use it to force executing ExtendedCSS library.
22003
- if (stream.hasAnySelectorExtendedCssNodeStrict()) {
22004
- convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.ExtendedElementHidingException : exports.CosmeticRuleSeparator.ExtendedElementHiding;
22005
- }
22006
- // Check if the rule needs to be converted
22007
- if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg) || separator !== convertedSeparator || convertedSelectorList.isConverted) {
22008
- // TODO: Replace with custom clone method
22009
- const ruleClone = clone(rule);
22010
- ruleClone.syntax = exports.AdblockSyntax.Adg;
22011
- ruleClone.separator.value = convertedSeparator;
22012
- ruleClone.body.selectorList.value = convertedSelectorList.result;
22013
- return createNodeConversionResult([ruleClone], true);
22014
- }
22015
- // Otherwise, return the original rule
22016
- return createNodeConversionResult([rule], false);
22017
- }
21941
+ zod.string().min(1).transform(value => parseRawPlatforms(value));
21942
+ function getDefaultExportFromCjs(x) {
21943
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
22018
21944
  }
21945
+ var mapObj$1 = {
21946
+ exports: {}
21947
+ };
21948
+ const isObject$1 = value => typeof value === 'object' && value !== null;
21949
+ const mapObjectSkip = Symbol('skip');
22019
21950
 
22020
- /**
22021
- * @file Utility functions for working with network rule nodes
22022
- */
22023
- /**
22024
- * Creates a network rule node
22025
- *
22026
- * @param pattern Rule pattern
22027
- * @param modifiers Rule modifiers (optional, default: undefined)
22028
- * @param exception Exception rule flag (optional, default: false)
22029
- * @param syntax Adblock syntax (optional, default: Common)
22030
- * @returns Network rule node
22031
- */
22032
- function createNetworkRuleNode(pattern, modifiers = undefined, exception = false, syntax = exports.AdblockSyntax.Common) {
22033
- const result = {
22034
- category: exports.RuleCategory.Network,
22035
- type: exports.NetworkRuleType.NetworkRule,
22036
- syntax,
22037
- exception,
22038
- pattern: {
22039
- type: 'Value',
22040
- value: pattern
22041
- }
21951
+ // Customized for this use-case
21952
+ const isObjectCustom = value => isObject$1(value) && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
21953
+ const mapObject = (object, mapper, options, isSeen = new WeakMap()) => {
21954
+ options = {
21955
+ deep: false,
21956
+ target: {},
21957
+ ...options
22042
21958
  };
22043
- if (!isUndefined(modifiers)) {
22044
- result.modifiers = clone(modifiers);
21959
+ if (isSeen.has(object)) {
21960
+ return isSeen.get(object);
22045
21961
  }
22046
- return result;
22047
- }
22048
-
22049
- /**
22050
- * @file Converter for request header removal rules
22051
- */
22052
- const UBO_RESPONSEHEADER_FN = 'responseheader';
22053
- const ADG_REMOVEHEADER_MODIFIER = 'removeheader';
22054
- const ERROR_MESSAGES = {
22055
- EMPTY_PARAMETER: `Empty parameter for '${UBO_RESPONSEHEADER_FN}' function`,
22056
- EXPECTED_END_OF_RULE: "Expected end of rule, but got '%s'",
22057
- MULTIPLE_DOMAINS_NOT_SUPPORTED: 'Multiple domains are not supported yet'
22058
- };
22059
- /**
22060
- * Converter for request header removal rules
22061
- *
22062
- * @todo Implement `convertToUbo` (ABP currently doesn't support header removal rules)
22063
- */
22064
- class HeaderRemovalRuleConverter extends RuleConverterBase {
22065
- /**
22066
- * Converts a header removal rule to AdGuard syntax, if possible.
22067
- *
22068
- * @param rule Rule node to convert
22069
- * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22070
- * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22071
- * If the rule was not converted, the result array will contain the original node with the same object reference
22072
- * @throws If the rule is invalid or cannot be converted
22073
- * @example
22074
- * If the input rule is:
22075
- * ```adblock
22076
- * example.com##^responseheader(header-name)
22077
- * ```
22078
- * The output will be:
22079
- * ```adblock
22080
- * ||example.com^$removeheader=header-name
22081
- * ```
22082
- */
22083
- static convertToAdg(rule) {
22084
- // TODO: Add support for ABP syntax once it starts supporting header removal rules
22085
- // Leave the rule as is if it's not a header removal rule
22086
- if (rule.category !== exports.RuleCategory.Cosmetic || rule.type !== exports.CosmeticRuleType.HtmlFilteringRule) {
22087
- return createNodeConversionResult([rule], false);
22088
- }
22089
- const stream = new CssTokenStream(rule.body.value);
22090
- let token;
22091
- // Skip leading whitespace
22092
- stream.skipWhitespace();
22093
- // Next token should be the `^` followed by a `responseheader` function
22094
- token = stream.get();
22095
- if (!token || token.type !== cssTokenizer.TokenType.Delim || rule.body.value[token.start] !== UBO_HTML_MASK) {
22096
- return createNodeConversionResult([rule], false);
22097
- }
22098
- stream.advance();
22099
- token = stream.get();
22100
- if (!token) {
22101
- return createNodeConversionResult([rule], false);
22102
- }
22103
- const functionName = rule.body.value.slice(token.start, token.end - 1);
22104
- if (functionName !== UBO_RESPONSEHEADER_FN) {
22105
- return createNodeConversionResult([rule], false);
21962
+ isSeen.set(object, options.target);
21963
+ const {
21964
+ target
21965
+ } = options;
21966
+ delete options.target;
21967
+ const mapArray = array => array.map(element => isObjectCustom(element) ? mapObject(element, mapper, options, isSeen) : element);
21968
+ if (Array.isArray(object)) {
21969
+ return mapArray(object);
21970
+ }
21971
+ for (const [key, value] of Object.entries(object)) {
21972
+ const mapResult = mapper(key, value, object);
21973
+ if (mapResult === mapObjectSkip) {
21974
+ continue;
22106
21975
  }
22107
- // Parse the parameter
22108
- const paramStart = token.end;
22109
- stream.skipUntilBalanced();
22110
- const paramEnd = stream.getOrFail().end;
22111
- const param = rule.body.value.slice(paramStart, paramEnd - 1).trim();
22112
- // Do not allow empty parameter
22113
- if (param.length === 0) {
22114
- throw new RuleConversionError(ERROR_MESSAGES.EMPTY_PARAMETER);
21976
+ let [newKey, newValue, {
21977
+ shouldRecurse = true
21978
+ } = {}] = mapResult;
21979
+
21980
+ // Drop `__proto__` keys.
21981
+ if (newKey === '__proto__') {
21982
+ continue;
22115
21983
  }
22116
- stream.expect(cssTokenizer.TokenType.CloseParenthesis);
22117
- stream.advance();
22118
- // Skip trailing whitespace after the function call
22119
- stream.skipWhitespace();
22120
- // Expect the end of the rule - so nothing should be left in the stream
22121
- if (!stream.isEof()) {
22122
- token = stream.getOrFail();
22123
- throw new RuleConversionError(sprintfJs.sprintf(ERROR_MESSAGES.EXPECTED_END_OF_RULE, cssTokenizer.getFormattedTokenName(token.type)));
21984
+ if (options.deep && shouldRecurse && isObjectCustom(newValue)) {
21985
+ newValue = Array.isArray(newValue) ? mapArray(newValue) : mapObject(newValue, mapper, options, isSeen);
22124
21986
  }
22125
- // Prepare network rule pattern
22126
- const pattern = [];
22127
- if (rule.domains.children.length === 1) {
22128
- // If the rule has only one domain, we can use a simple network rule pattern:
22129
- // ||single-domain-from-the-rule^
22130
- pattern.push(ADBLOCK_URL_START, rule.domains.children[0].value, ADBLOCK_URL_SEPARATOR);
22131
- } else if (rule.domains.children.length > 1) {
22132
- // TODO: Add support for multiple domains, for example:
22133
- // example.com,example.org,example.net##^responseheader(header-name)
22134
- // We should consider allowing $domain with $removeheader modifier,
22135
- // for example:
22136
- // $removeheader=header-name,domain=example.com|example.org|example.net
22137
- throw new RuleConversionError(ERROR_MESSAGES.MULTIPLE_DOMAINS_NOT_SUPPORTED);
21987
+ target[newKey] = newValue;
21988
+ }
21989
+ return target;
21990
+ };
21991
+ mapObj$1.exports = (object, mapper, options) => {
21992
+ if (!isObject$1(object)) {
21993
+ throw new TypeError(`Expected an object, got \`${object}\` (${typeof object})`);
21994
+ }
21995
+ return mapObject(object, mapper, options);
21996
+ };
21997
+ mapObj$1.exports.mapObjectSkip = mapObjectSkip;
21998
+ var mapObjExports = mapObj$1.exports;
21999
+ var camelcase = {
22000
+ exports: {}
22001
+ };
22002
+ const UPPERCASE = /[\p{Lu}]/u;
22003
+ const LOWERCASE = /[\p{Ll}]/u;
22004
+ const LEADING_CAPITAL = /^[\p{Lu}](?![\p{Lu}])/gu;
22005
+ const IDENTIFIER = /([\p{Alpha}\p{N}_]|$)/u;
22006
+ const SEPARATORS = /[_.\- ]+/;
22007
+ const LEADING_SEPARATORS = new RegExp('^' + SEPARATORS.source);
22008
+ const SEPARATORS_AND_IDENTIFIER = new RegExp(SEPARATORS.source + IDENTIFIER.source, 'gu');
22009
+ const NUMBERS_AND_IDENTIFIER = new RegExp('\\d+' + IDENTIFIER.source, 'gu');
22010
+ const preserveCamelCase = (string, toLowerCase, toUpperCase) => {
22011
+ let isLastCharLower = false;
22012
+ let isLastCharUpper = false;
22013
+ let isLastLastCharUpper = false;
22014
+ for (let i = 0; i < string.length; i++) {
22015
+ const character = string[i];
22016
+ if (isLastCharLower && UPPERCASE.test(character)) {
22017
+ string = string.slice(0, i) + '-' + string.slice(i);
22018
+ isLastCharLower = false;
22019
+ isLastLastCharUpper = isLastCharUpper;
22020
+ isLastCharUpper = true;
22021
+ i++;
22022
+ } else if (isLastCharUpper && isLastLastCharUpper && LOWERCASE.test(character)) {
22023
+ string = string.slice(0, i - 1) + '-' + string.slice(i - 1);
22024
+ isLastLastCharUpper = isLastCharUpper;
22025
+ isLastCharUpper = false;
22026
+ isLastCharLower = true;
22027
+ } else {
22028
+ isLastCharLower = toLowerCase(character) === character && toUpperCase(character) !== character;
22029
+ isLastLastCharUpper = isLastCharUpper;
22030
+ isLastCharUpper = toUpperCase(character) === character && toLowerCase(character) !== character;
22138
22031
  }
22139
- // Prepare network rule modifiers
22140
- const modifiers = createModifierListNode();
22141
- modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, param));
22142
- // Construct the network rule
22143
- return createNodeConversionResult([createNetworkRuleNode(pattern.join(EMPTY), modifiers,
22144
- // Copy the exception flag
22145
- rule.exception, exports.AdblockSyntax.Adg)], true);
22146
22032
  }
22147
- }
22148
-
22149
- /**
22150
- * @file Cosmetic rule converter
22151
- */
22152
- /**
22153
- * Cosmetic rule converter class (also known as "non-basic rule converter")
22154
- *
22155
- * @todo Implement `convertToUbo` and `convertToAbp`
22156
- */
22157
- class CosmeticRuleConverter extends RuleConverterBase {
22158
- /**
22159
- * Converts a cosmetic rule to AdGuard syntax, if possible.
22160
- *
22161
- * @param rule Rule node to convert
22162
- * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22163
- * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22164
- * If the rule was not converted, the result array will contain the original node with the same object reference
22165
- * @throws If the rule is invalid or cannot be converted
22166
- */
22167
- static convertToAdg(rule) {
22168
- let subconverterResult;
22169
- // Convert cosmetic rule based on its type
22170
- switch (rule.type) {
22171
- case exports.CosmeticRuleType.ElementHidingRule:
22172
- subconverterResult = ElementHidingRuleConverter.convertToAdg(rule);
22173
- break;
22174
- case exports.CosmeticRuleType.ScriptletInjectionRule:
22175
- subconverterResult = ScriptletRuleConverter.convertToAdg(rule);
22176
- break;
22177
- case exports.CosmeticRuleType.CssInjectionRule:
22178
- subconverterResult = CssInjectionRuleConverter.convertToAdg(rule);
22179
- break;
22180
- case exports.CosmeticRuleType.HtmlFilteringRule:
22181
- // Handle special case: uBO response header filtering rule
22182
- // TODO: Optimize double CSS tokenization here
22183
- subconverterResult = HeaderRemovalRuleConverter.convertToAdg(rule);
22184
- if (subconverterResult.isConverted) {
22185
- break;
22186
- }
22187
- subconverterResult = HtmlRuleConverter.convertToAdg(rule);
22188
- break;
22189
- // Note: Currently, only ADG supports JS injection rules, so we don't need to convert them
22190
- case exports.CosmeticRuleType.JsInjectionRule:
22191
- subconverterResult = createNodeConversionResult([rule], false);
22192
- break;
22193
- default:
22194
- throw new RuleConversionError('Unsupported cosmetic rule type');
22033
+ return string;
22034
+ };
22035
+ const preserveConsecutiveUppercase = (input, toLowerCase) => {
22036
+ LEADING_CAPITAL.lastIndex = 0;
22037
+ return input.replace(LEADING_CAPITAL, m1 => toLowerCase(m1));
22038
+ };
22039
+ const postProcess = (input, toUpperCase) => {
22040
+ SEPARATORS_AND_IDENTIFIER.lastIndex = 0;
22041
+ NUMBERS_AND_IDENTIFIER.lastIndex = 0;
22042
+ return input.replace(SEPARATORS_AND_IDENTIFIER, (_, identifier) => toUpperCase(identifier)).replace(NUMBERS_AND_IDENTIFIER, m => toUpperCase(m));
22043
+ };
22044
+ const camelCase$1 = (input, options) => {
22045
+ if (!(typeof input === 'string' || Array.isArray(input))) {
22046
+ throw new TypeError('Expected the input to be `string | string[]`');
22047
+ }
22048
+ options = {
22049
+ pascalCase: false,
22050
+ preserveConsecutiveUppercase: false,
22051
+ ...options
22052
+ };
22053
+ if (Array.isArray(input)) {
22054
+ input = input.map(x => x.trim()).filter(x => x.length).join('-');
22055
+ } else {
22056
+ input = input.trim();
22057
+ }
22058
+ if (input.length === 0) {
22059
+ return '';
22060
+ }
22061
+ const toLowerCase = options.locale === false ? string => string.toLowerCase() : string => string.toLocaleLowerCase(options.locale);
22062
+ const toUpperCase = options.locale === false ? string => string.toUpperCase() : string => string.toLocaleUpperCase(options.locale);
22063
+ if (input.length === 1) {
22064
+ return options.pascalCase ? toUpperCase(input) : toLowerCase(input);
22065
+ }
22066
+ const hasUpperCase = input !== toLowerCase(input);
22067
+ if (hasUpperCase) {
22068
+ input = preserveCamelCase(input, toLowerCase, toUpperCase);
22069
+ }
22070
+ input = input.replace(LEADING_SEPARATORS, '');
22071
+ if (options.preserveConsecutiveUppercase) {
22072
+ input = preserveConsecutiveUppercase(input, toLowerCase);
22073
+ } else {
22074
+ input = toLowerCase(input);
22075
+ }
22076
+ if (options.pascalCase) {
22077
+ input = toUpperCase(input.charAt(0)) + input.slice(1);
22078
+ }
22079
+ return postProcess(input, toUpperCase);
22080
+ };
22081
+ camelcase.exports = camelCase$1;
22082
+ // TODO: Remove this for the next major release
22083
+ camelcase.exports.default = camelCase$1;
22084
+ var camelcaseExports = camelcase.exports;
22085
+ class QuickLRU {
22086
+ constructor(options = {}) {
22087
+ if (!(options.maxSize && options.maxSize > 0)) {
22088
+ throw new TypeError('`maxSize` must be a number greater than 0');
22195
22089
  }
22196
- let convertedModifiers;
22197
- // Convert cosmetic rule modifiers, if any
22198
- if (rule.modifiers) {
22199
- if (rule.syntax === exports.AdblockSyntax.Ubo) {
22200
- // uBO doesn't support this rule:
22201
- // example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
22202
- if (rule.type === exports.CosmeticRuleType.ScriptletInjectionRule) {
22203
- throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
22090
+ this.maxSize = options.maxSize;
22091
+ this.onEviction = options.onEviction;
22092
+ this.cache = new Map();
22093
+ this.oldCache = new Map();
22094
+ this._size = 0;
22095
+ }
22096
+ _set(key, value) {
22097
+ this.cache.set(key, value);
22098
+ this._size++;
22099
+ if (this._size >= this.maxSize) {
22100
+ this._size = 0;
22101
+ if (typeof this.onEviction === 'function') {
22102
+ for (const [key, value] of this.oldCache.entries()) {
22103
+ this.onEviction(key, value);
22204
22104
  }
22205
- convertedModifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(rule.modifiers);
22206
- } else if (rule.syntax === exports.AdblockSyntax.Abp) {
22207
- // TODO: Implement once ABP starts supporting cosmetic rule modifiers
22208
- throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
22209
22105
  }
22106
+ this.oldCache = this.cache;
22107
+ this.cache = new Map();
22210
22108
  }
22211
- if (subconverterResult.result.length > 1 || subconverterResult.isConverted || convertedModifiers && convertedModifiers.isConverted) {
22212
- // Add modifier list to the subconverter result rules
22213
- subconverterResult.result.forEach(subconverterRule => {
22214
- if (convertedModifiers && subconverterRule.category === exports.RuleCategory.Cosmetic) {
22215
- // eslint-disable-next-line no-param-reassign
22216
- subconverterRule.modifiers = convertedModifiers.result;
22217
- }
22218
- });
22219
- return subconverterResult;
22109
+ }
22110
+ get(key) {
22111
+ if (this.cache.has(key)) {
22112
+ return this.cache.get(key);
22113
+ }
22114
+ if (this.oldCache.has(key)) {
22115
+ const value = this.oldCache.get(key);
22116
+ this.oldCache.delete(key);
22117
+ this._set(key, value);
22118
+ return value;
22220
22119
  }
22221
- return createNodeConversionResult([rule], false);
22222
22120
  }
22223
- }
22224
-
22225
- /**
22226
- * @file Compatibility tables for redirects.
22227
- */
22228
- /**
22229
- * Prefix for resource redirection names.
22230
- */
22231
- const ABP_RESOURCE_PREFIX = 'abp-resource:';
22232
- const ABP_RESOURCE_PREFIX_LENGTH = ABP_RESOURCE_PREFIX.length;
22233
- /**
22234
- * Transforms the name of an ABP redirect to a normalized form.
22235
- *
22236
- * @param name Redirect name to normalize.
22237
- *
22238
- * @returns Normalized redirect name.
22239
- *
22240
- * @example
22241
- * abpRedirectNameNormalizer('abp-resource:my-resource') // => 'my-resource'
22242
- */
22243
- const abpRedirectNameNormalizer = name => {
22244
- if (name.startsWith(ABP_RESOURCE_PREFIX)) {
22245
- return name.slice(ABP_RESOURCE_PREFIX_LENGTH);
22121
+ set(key, value) {
22122
+ if (this.cache.has(key)) {
22123
+ this.cache.set(key, value);
22124
+ } else {
22125
+ this._set(key, value);
22126
+ }
22127
+ return this;
22246
22128
  }
22247
- return name;
22248
- };
22249
- /**
22250
- * Compatibility table for redirects.
22251
- */
22252
- class RedirectsCompatibilityTable extends CompatibilityTableBase {
22253
- /**
22254
- * Creates a new instance of the compatibility table for redirects.
22255
- *
22256
- * @param data Compatibility table data.
22257
- */
22258
- constructor(data) {
22259
- super(data, abpRedirectNameNormalizer);
22129
+ has(key) {
22130
+ return this.cache.has(key) || this.oldCache.has(key);
22260
22131
  }
22261
- }
22262
- /**
22263
- * Deep freeze the compatibility table data to avoid accidental modifications.
22264
- */
22265
- deepFreeze(redirectsCompatibilityTableData);
22266
- /**
22267
- * Compatibility table instance for redirects.
22268
- */
22269
- const redirectsCompatibilityTable = new RedirectsCompatibilityTable(redirectsCompatibilityTableData);
22270
-
22271
- /**
22272
- * @file Compatibility tables for scriptlets.
22273
- */
22274
- /**
22275
- * Compatibility table for scriptlets.
22276
- */
22277
- class ScriptletsCompatibilityTable extends CompatibilityTableBase {}
22278
- /**
22279
- * Deep freeze the compatibility table data to avoid accidental modifications.
22280
- */
22281
- deepFreeze(scriptletsCompatibilityTableData);
22282
- /**
22283
- * Compatibility table instance for scriptlets.
22284
- */
22285
- const scriptletsCompatibilityTable = new ScriptletsCompatibilityTable(scriptletsCompatibilityTableData);
22286
-
22287
- /* eslint-disable no-bitwise */
22288
- /**
22289
- * @file Platform schema.
22290
- */
22291
- /**
22292
- * Platform separator, e.g. 'adg_os_any|adg_safari_any' means any AdGuard OS platform and
22293
- * any AdGuard Safari content blocker platform.
22294
- */
22295
- const PLATFORM_SEPARATOR = '|';
22296
- /**
22297
- * Platform negation character, e.g. 'adg_any|~adg_safari_any' means any AdGuard product except
22298
- * Safari content blockers.
22299
- */
22300
- const PLATFORM_NEGATION = '~';
22301
- /**
22302
- * Parses a raw platform string into a platform bitmask.
22303
- *
22304
- * @param rawPlatforms Raw platform string, e.g. 'adg_safari_any|adg_os_any'.
22305
- *
22306
- * @returns Platform bitmask.
22307
- */
22308
- const parseRawPlatforms = rawPlatforms => {
22309
- // e.g. 'adg_safari_any|adg_os_any'
22310
- const rawPlatformList = rawPlatforms.split(PLATFORM_SEPARATOR).map(rawPlatform => rawPlatform.trim());
22311
- let result = 0;
22312
- for (let rawPlatform of rawPlatformList) {
22313
- // negation, e.g. 'adg_any|~adg_safari_any' means any AdGuard product except Safari content blockers
22314
- let negated = false;
22315
- if (rawPlatform.startsWith(PLATFORM_NEGATION)) {
22316
- negated = true;
22317
- rawPlatform = rawPlatform.slice(1).trim();
22318
- }
22319
- const platform = SPECIFIC_PLATFORM_MAP.get(rawPlatform) ?? GENERIC_PLATFORM_MAP.get(rawPlatform);
22320
- if (isUndefined(platform)) {
22321
- throw new Error(`Unknown platform: ${rawPlatform}`);
22132
+ peek(key) {
22133
+ if (this.cache.has(key)) {
22134
+ return this.cache.get(key);
22322
22135
  }
22323
- if (negated) {
22324
- result &= ~platform;
22325
- } else {
22326
- result |= platform;
22136
+ if (this.oldCache.has(key)) {
22137
+ return this.oldCache.get(key);
22327
22138
  }
22328
22139
  }
22329
- if (result === 0) {
22330
- throw new Error('No platforms specified');
22140
+ delete(key) {
22141
+ const deleted = this.cache.delete(key);
22142
+ if (deleted) {
22143
+ this._size--;
22144
+ }
22145
+ return this.oldCache.delete(key) || deleted;
22331
22146
  }
22332
- return result;
22333
- };
22334
- /**
22335
- * Platform schema.
22336
- */
22337
- zod.string().min(1).transform(value => parseRawPlatforms(value));
22338
- function getDefaultExportFromCjs(x) {
22339
- return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
22340
- }
22341
- var mapObj$1 = {
22342
- exports: {}
22343
- };
22344
- const isObject$1 = value => typeof value === 'object' && value !== null;
22345
- const mapObjectSkip = Symbol('skip');
22346
-
22347
- // Customized for this use-case
22348
- const isObjectCustom = value => isObject$1(value) && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
22349
- const mapObject = (object, mapper, options, isSeen = new WeakMap()) => {
22350
- options = {
22351
- deep: false,
22352
- target: {},
22353
- ...options
22354
- };
22355
- if (isSeen.has(object)) {
22356
- return isSeen.get(object);
22147
+ clear() {
22148
+ this.cache.clear();
22149
+ this.oldCache.clear();
22150
+ this._size = 0;
22357
22151
  }
22358
- isSeen.set(object, options.target);
22359
- const {
22360
- target
22361
- } = options;
22362
- delete options.target;
22363
- const mapArray = array => array.map(element => isObjectCustom(element) ? mapObject(element, mapper, options, isSeen) : element);
22364
- if (Array.isArray(object)) {
22365
- return mapArray(object);
22152
+ *keys() {
22153
+ for (const [key] of this) {
22154
+ yield key;
22155
+ }
22366
22156
  }
22367
- for (const [key, value] of Object.entries(object)) {
22368
- const mapResult = mapper(key, value, object);
22369
- if (mapResult === mapObjectSkip) {
22370
- continue;
22157
+ *values() {
22158
+ for (const [, value] of this) {
22159
+ yield value;
22371
22160
  }
22372
- let [newKey, newValue, {
22373
- shouldRecurse = true
22374
- } = {}] = mapResult;
22375
-
22376
- // Drop `__proto__` keys.
22377
- if (newKey === '__proto__') {
22378
- continue;
22161
+ }
22162
+ *[Symbol.iterator]() {
22163
+ for (const item of this.cache) {
22164
+ yield item;
22379
22165
  }
22380
- if (options.deep && shouldRecurse && isObjectCustom(newValue)) {
22381
- newValue = Array.isArray(newValue) ? mapArray(newValue) : mapObject(newValue, mapper, options, isSeen);
22166
+ for (const item of this.oldCache) {
22167
+ const [key] = item;
22168
+ if (!this.cache.has(key)) {
22169
+ yield item;
22170
+ }
22382
22171
  }
22383
- target[newKey] = newValue;
22384
- }
22385
- return target;
22386
- };
22387
- mapObj$1.exports = (object, mapper, options) => {
22388
- if (!isObject$1(object)) {
22389
- throw new TypeError(`Expected an object, got \`${object}\` (${typeof object})`);
22390
22172
  }
22391
- return mapObject(object, mapper, options);
22392
- };
22393
- mapObj$1.exports.mapObjectSkip = mapObjectSkip;
22394
- var mapObjExports = mapObj$1.exports;
22395
- var camelcase = {
22396
- exports: {}
22397
- };
22398
- const UPPERCASE = /[\p{Lu}]/u;
22399
- const LOWERCASE = /[\p{Ll}]/u;
22400
- const LEADING_CAPITAL = /^[\p{Lu}](?![\p{Lu}])/gu;
22401
- const IDENTIFIER = /([\p{Alpha}\p{N}_]|$)/u;
22402
- const SEPARATORS = /[_.\- ]+/;
22403
- const LEADING_SEPARATORS = new RegExp('^' + SEPARATORS.source);
22404
- const SEPARATORS_AND_IDENTIFIER = new RegExp(SEPARATORS.source + IDENTIFIER.source, 'gu');
22405
- const NUMBERS_AND_IDENTIFIER = new RegExp('\\d+' + IDENTIFIER.source, 'gu');
22406
- const preserveCamelCase = (string, toLowerCase, toUpperCase) => {
22407
- let isLastCharLower = false;
22408
- let isLastCharUpper = false;
22409
- let isLastLastCharUpper = false;
22410
- for (let i = 0; i < string.length; i++) {
22411
- const character = string[i];
22412
- if (isLastCharLower && UPPERCASE.test(character)) {
22413
- string = string.slice(0, i) + '-' + string.slice(i);
22414
- isLastCharLower = false;
22415
- isLastLastCharUpper = isLastCharUpper;
22416
- isLastCharUpper = true;
22417
- i++;
22418
- } else if (isLastCharUpper && isLastLastCharUpper && LOWERCASE.test(character)) {
22419
- string = string.slice(0, i - 1) + '-' + string.slice(i - 1);
22420
- isLastLastCharUpper = isLastCharUpper;
22421
- isLastCharUpper = false;
22422
- isLastCharLower = true;
22423
- } else {
22424
- isLastCharLower = toLowerCase(character) === character && toUpperCase(character) !== character;
22425
- isLastLastCharUpper = isLastCharUpper;
22426
- isLastCharUpper = toUpperCase(character) === character && toLowerCase(character) !== character;
22173
+ get size() {
22174
+ let oldCacheSize = 0;
22175
+ for (const key of this.oldCache.keys()) {
22176
+ if (!this.cache.has(key)) {
22177
+ oldCacheSize++;
22178
+ }
22427
22179
  }
22180
+ return Math.min(this._size + oldCacheSize, this.maxSize);
22428
22181
  }
22429
- return string;
22430
- };
22431
- const preserveConsecutiveUppercase = (input, toLowerCase) => {
22432
- LEADING_CAPITAL.lastIndex = 0;
22433
- return input.replace(LEADING_CAPITAL, m1 => toLowerCase(m1));
22434
- };
22435
- const postProcess = (input, toUpperCase) => {
22436
- SEPARATORS_AND_IDENTIFIER.lastIndex = 0;
22437
- NUMBERS_AND_IDENTIFIER.lastIndex = 0;
22438
- return input.replace(SEPARATORS_AND_IDENTIFIER, (_, identifier) => toUpperCase(identifier)).replace(NUMBERS_AND_IDENTIFIER, m => toUpperCase(m));
22439
- };
22440
- const camelCase$1 = (input, options) => {
22441
- if (!(typeof input === 'string' || Array.isArray(input))) {
22442
- throw new TypeError('Expected the input to be `string | string[]`');
22182
+ }
22183
+ var quickLru = QuickLRU;
22184
+ const mapObj = mapObjExports;
22185
+ const camelCase = camelcaseExports;
22186
+ const QuickLru = quickLru;
22187
+ const has = (array, key) => array.some(x => {
22188
+ if (typeof x === 'string') {
22189
+ return x === key;
22190
+ }
22191
+ x.lastIndex = 0;
22192
+ return x.test(key);
22193
+ });
22194
+ const cache = new QuickLru({
22195
+ maxSize: 100000
22196
+ });
22197
+
22198
+ // Reproduces behavior from `map-obj`
22199
+ const isObject = value => typeof value === 'object' && value !== null && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
22200
+ const camelCaseConvert = (input, options) => {
22201
+ if (!isObject(input)) {
22202
+ return input;
22443
22203
  }
22444
22204
  options = {
22205
+ deep: false,
22445
22206
  pascalCase: false,
22446
- preserveConsecutiveUppercase: false,
22447
22207
  ...options
22448
22208
  };
22209
+ const {
22210
+ exclude,
22211
+ pascalCase,
22212
+ stopPaths,
22213
+ deep
22214
+ } = options;
22215
+ const stopPathsSet = new Set(stopPaths);
22216
+ const makeMapper = parentPath => (key, value) => {
22217
+ if (deep && isObject(value)) {
22218
+ const path = parentPath === undefined ? key : `${parentPath}.${key}`;
22219
+ if (!stopPathsSet.has(path)) {
22220
+ value = mapObj(value, makeMapper(path));
22221
+ }
22222
+ }
22223
+ if (!(exclude && has(exclude, key))) {
22224
+ const cacheKey = pascalCase ? `${key}_` : key;
22225
+ if (cache.has(cacheKey)) {
22226
+ key = cache.get(cacheKey);
22227
+ } else {
22228
+ const returnValue = camelCase(key, {
22229
+ pascalCase,
22230
+ locale: false
22231
+ });
22232
+ if (key.length < 100) {
22233
+ // Prevent abuse
22234
+ cache.set(cacheKey, returnValue);
22235
+ }
22236
+ key = returnValue;
22237
+ }
22238
+ }
22239
+ return [key, value];
22240
+ };
22241
+ return mapObj(input, makeMapper(undefined));
22242
+ };
22243
+ var camelcaseKeys = (input, options) => {
22449
22244
  if (Array.isArray(input)) {
22450
- input = input.map(x => x.trim()).filter(x => x.length).join('-');
22451
- } else {
22452
- input = input.trim();
22453
- }
22454
- if (input.length === 0) {
22455
- return '';
22456
- }
22457
- const toLowerCase = options.locale === false ? string => string.toLowerCase() : string => string.toLocaleLowerCase(options.locale);
22458
- const toUpperCase = options.locale === false ? string => string.toUpperCase() : string => string.toLocaleUpperCase(options.locale);
22459
- if (input.length === 1) {
22460
- return options.pascalCase ? toUpperCase(input) : toLowerCase(input);
22245
+ return Object.keys(input).map(key => camelCaseConvert(input[key], options));
22461
22246
  }
22462
- const hasUpperCase = input !== toLowerCase(input);
22463
- if (hasUpperCase) {
22464
- input = preserveCamelCase(input, toLowerCase, toUpperCase);
22247
+ return camelCaseConvert(input, options);
22248
+ };
22249
+ var camelCaseKeys = /*@__PURE__*/getDefaultExportFromCjs(camelcaseKeys);
22250
+
22251
+ /**
22252
+ * @file Zod camelCase utility.
22253
+ */
22254
+ // eslint-disable-next-line import/no-extraneous-dependencies
22255
+ /**
22256
+ * Transforms Zod schema to camelCase.
22257
+ *
22258
+ * @param zod Zod schema.
22259
+ *
22260
+ * @returns Zod schema with camelCase properties.
22261
+ *
22262
+ * @see {@link https://github.com/colinhacks/zod/issues/486#issuecomment-1501097361}
22263
+ */
22264
+ const zodToCamelCase = zod => {
22265
+ return zod.transform(val => camelCaseKeys(val));
22266
+ };
22267
+
22268
+ /**
22269
+ * @file Base compatibility data schema, which is commonly used in compatibility tables.
22270
+ */
22271
+ /**
22272
+ * Zod schema for boolean values. Accepts both boolean and string values.
22273
+ */
22274
+ const booleanSchema = zod.union([zod.string().transform(val => val.trim().toLowerCase() === 'true'), zod.boolean()]);
22275
+ /**
22276
+ * Zod schema for non-empty string values.
22277
+ */
22278
+ const nonEmptyStringSchema = zod.string().transform(val => val.trim()).pipe(zod.string().min(1));
22279
+ /**
22280
+ * Zod schema for base compatibility data.
22281
+ * Here we use snake_case properties because the compatibility data is stored in YAML files.
22282
+ */
22283
+ const baseCompatibilityDataSchema = zod.object({
22284
+ /**
22285
+ * Name of the actual entity.
22286
+ */
22287
+ name: nonEmptyStringSchema,
22288
+ /**
22289
+ * List of aliases for the entity (if any).
22290
+ */
22291
+ aliases: zod.array(nonEmptyStringSchema).nullable().default(null),
22292
+ /**
22293
+ * Short description of the actual entity.
22294
+ * If not specified or it's value is `null`, then the description is not available.
22295
+ */
22296
+ description: nonEmptyStringSchema.nullable().default(null),
22297
+ /**
22298
+ * Link to the documentation. If not specified or it's value is `null`, then the documentation is not available.
22299
+ */
22300
+ docs: nonEmptyStringSchema.nullable().default(null),
22301
+ /**
22302
+ * The version of the adblocker in which the entity was added.
22303
+ * For AdGuard resources, the version of the library is specified.
22304
+ */
22305
+ version_added: nonEmptyStringSchema.nullable().default(null),
22306
+ /**
22307
+ * The version of the adblocker when the entity was removed.
22308
+ */
22309
+ version_removed: nonEmptyStringSchema.nullable().default(null),
22310
+ /**
22311
+ * Describes whether the entity is deprecated.
22312
+ */
22313
+ deprecated: booleanSchema.default(false),
22314
+ /**
22315
+ * Message that describes why the entity is deprecated.
22316
+ * If not specified or it's value is `null`, then the message is not available.
22317
+ * It's value is omitted if the entity is not marked as deprecated.
22318
+ */
22319
+ deprecation_message: nonEmptyStringSchema.nullable().default(null),
22320
+ /**
22321
+ * Describes whether the entity is removed; for *already removed* features.
22322
+ */
22323
+ removed: booleanSchema.default(false),
22324
+ /**
22325
+ * Message that describes why the entity is removed.
22326
+ * If not specified or it's value is `null`, then the message is not available.
22327
+ * It's value is omitted if the entity is not marked as deprecated.
22328
+ */
22329
+ removal_message: nonEmptyStringSchema.nullable().default(null)
22330
+ });
22331
+ /**
22332
+ * Zod schema for base compatibility data with camelCase properties.
22333
+ */
22334
+ zodToCamelCase(baseCompatibilityDataSchema);
22335
+ /**
22336
+ * Refinement logic for base compatibility data.
22337
+ *
22338
+ * @param data Base compatibility data.
22339
+ * @param ctx Refinement context.
22340
+ */
22341
+ const baseRefineLogic = (data, ctx) => {
22342
+ if (data.deprecated && !data.deprecation_message) {
22343
+ ctx.addIssue({
22344
+ code: zod.ZodIssueCode.custom,
22345
+ message: 'deprecation_message is required for deprecated modifiers'
22346
+ });
22465
22347
  }
22466
- input = input.replace(LEADING_SEPARATORS, '');
22467
- if (options.preserveConsecutiveUppercase) {
22468
- input = preserveConsecutiveUppercase(input, toLowerCase);
22469
- } else {
22470
- input = toLowerCase(input);
22348
+ if (!data.deprecated && data.deprecation_message) {
22349
+ ctx.addIssue({
22350
+ code: zod.ZodIssueCode.custom,
22351
+ message: 'deprecation_message is only allowed for deprecated modifiers'
22352
+ });
22471
22353
  }
22472
- if (options.pascalCase) {
22473
- input = toUpperCase(input.charAt(0)) + input.slice(1);
22354
+ if (data.aliases && data.aliases.length !== new Set(data.aliases).size) {
22355
+ ctx.addIssue({
22356
+ code: zod.ZodIssueCode.custom,
22357
+ message: 'Aliases must be unique'
22358
+ });
22474
22359
  }
22475
- return postProcess(input, toUpperCase);
22476
22360
  };
22477
- camelcase.exports = camelCase$1;
22478
- // TODO: Remove this for the next major release
22479
- camelcase.exports.default = camelCase$1;
22480
- var camelcaseExports = camelcase.exports;
22481
- class QuickLRU {
22482
- constructor(options = {}) {
22483
- if (!(options.maxSize && options.maxSize > 0)) {
22484
- throw new TypeError('`maxSize` must be a number greater than 0');
22485
- }
22486
- this.maxSize = options.maxSize;
22487
- this.onEviction = options.onEviction;
22488
- this.cache = new Map();
22489
- this.oldCache = new Map();
22490
- this._size = 0;
22361
+
22362
+ /**
22363
+ * Checks if error has message.
22364
+ *
22365
+ * @param error Error object.
22366
+ * @returns If param is error.
22367
+ */
22368
+ function isErrorWithMessage(error) {
22369
+ return typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string';
22370
+ }
22371
+ /**
22372
+ * Converts error to the error with message.
22373
+ *
22374
+ * @param maybeError Possible error.
22375
+ * @returns Error with message.
22376
+ */
22377
+ function toErrorWithMessage(maybeError) {
22378
+ if (isErrorWithMessage(maybeError)) {
22379
+ return maybeError;
22491
22380
  }
22492
- _set(key, value) {
22493
- this.cache.set(key, value);
22494
- this._size++;
22495
- if (this._size >= this.maxSize) {
22496
- this._size = 0;
22497
- if (typeof this.onEviction === 'function') {
22498
- for (const [key, value] of this.oldCache.entries()) {
22499
- this.onEviction(key, value);
22500
- }
22501
- }
22502
- this.oldCache = this.cache;
22503
- this.cache = new Map();
22504
- }
22381
+ try {
22382
+ return new Error(JSON.stringify(maybeError));
22383
+ } catch {
22384
+ // fallback in case there's an error stringifying the maybeError
22385
+ // like with circular references for example.
22386
+ return new Error(String(maybeError));
22387
+ }
22388
+ }
22389
+ /**
22390
+ * Converts error object to error with message. This method might be helpful to handle thrown errors.
22391
+ *
22392
+ * @param error Error object.
22393
+ *
22394
+ * @returns Message of the error.
22395
+ */
22396
+ function getErrorMessage(error) {
22397
+ return toErrorWithMessage(error).message;
22398
+ }
22399
+
22400
+ /**
22401
+ * @file Schema for modifier data.
22402
+ */
22403
+ /**
22404
+ * Known validators that don't need to be validated as regex.
22405
+ */
22406
+ const KNOWN_VALIDATORS = new Set(['domain', 'pipe_separated_domains', 'regexp', 'url']);
22407
+ /**
22408
+ * Zod schema for modifier data.
22409
+ */
22410
+ zodToCamelCase(baseCompatibilityDataSchema.extend({
22411
+ /**
22412
+ * List of modifiers that are incompatible with the actual one.
22413
+ */
22414
+ conflicts: zod.array(nonEmptyStringSchema).nullable().default(null),
22415
+ /**
22416
+ * The actual modifier is incompatible with all other modifiers, except the ones listed in `conflicts`.
22417
+ */
22418
+ inverse_conflicts: booleanSchema.default(false),
22419
+ /**
22420
+ * Describes whether the actual modifier supports value assignment. For example, `$domain` is assignable,
22421
+ * so it can be used like this: `$domain=domain.com\|~subdomain.domain.com`, where `=` is the assignment operator
22422
+ * and `domain.com\|~subdomain.domain.com` is the value.
22423
+ */
22424
+ assignable: booleanSchema.default(false),
22425
+ /**
22426
+ * Describes whether the actual modifier can be negated. For example, `$third-party` is negatable,
22427
+ * so it can be used like this: `$~third-party`.
22428
+ */
22429
+ negatable: booleanSchema.default(true),
22430
+ /**
22431
+ * The actual modifier can only be used in blocking rules, it cannot be used in exceptions.
22432
+ * If it's value is `true`, then the modifier can be used only in blocking rules.
22433
+ * `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
22434
+ */
22435
+ block_only: booleanSchema.default(false),
22436
+ /**
22437
+ * The actual modifier can only be used in exceptions, it cannot be used in blocking rules.
22438
+ * If it's value is `true`, then the modifier can be used only in exceptions.
22439
+ * `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
22440
+ */
22441
+ exception_only: booleanSchema.default(false),
22442
+ /**
22443
+ * Describes whether the *assignable* modifier value is required.
22444
+ * For example, `$cookie` is assignable but it can be used without a value in exception rules:
22445
+ * `@@\|\|example.com^$cookie`.
22446
+ * If `false`, the `value_format` is required, e.g. the value of `$app` should always be specified
22447
+ */
22448
+ value_optional: booleanSchema.default(false),
22449
+ /**
22450
+ * Describes the format of the value for the *assignable* modifier.
22451
+ * Its value can be a regex pattern or a known validator name (e.g. `domain`, `pipe_separated_domains`, etc.).
22452
+ */
22453
+ value_format: nonEmptyStringSchema.nullable().default(null)
22454
+ }).superRefine((data, ctx) => {
22455
+ // TODO: find something better, for now we can't add refine logic to the base schema:
22456
+ // https://github.com/colinhacks/zod/issues/454#issuecomment-848370721
22457
+ baseRefineLogic(data, ctx);
22458
+ if (data.block_only && data.exception_only) {
22459
+ ctx.addIssue({
22460
+ code: zod.ZodIssueCode.custom,
22461
+ message: 'block_only and exception_only are mutually exclusive'
22462
+ });
22505
22463
  }
22506
- get(key) {
22507
- if (this.cache.has(key)) {
22508
- return this.cache.get(key);
22509
- }
22510
- if (this.oldCache.has(key)) {
22511
- const value = this.oldCache.get(key);
22512
- this.oldCache.delete(key);
22513
- this._set(key, value);
22514
- return value;
22515
- }
22464
+ if (data.assignable && !data.value_format) {
22465
+ ctx.addIssue({
22466
+ code: zod.ZodIssueCode.custom,
22467
+ message: 'value_format is required for assignable modifiers'
22468
+ });
22516
22469
  }
22517
- set(key, value) {
22518
- if (this.cache.has(key)) {
22519
- this.cache.set(key, value);
22520
- } else {
22521
- this._set(key, value);
22470
+ if (data.value_format) {
22471
+ const valueFormat = data.value_format.trim();
22472
+ // if it is a known validator, we don't need to validate it further
22473
+ if (KNOWN_VALIDATORS.has(valueFormat)) {
22474
+ return;
22475
+ }
22476
+ // otherwise, we need to validate it as a regex
22477
+ try {
22478
+ XRegExp(valueFormat);
22479
+ } catch (error) {
22480
+ ctx.addIssue({
22481
+ code: zod.ZodIssueCode.custom,
22482
+ message: getErrorMessage(error)
22483
+ });
22522
22484
  }
22523
- return this;
22524
22485
  }
22525
- has(key) {
22526
- return this.cache.has(key) || this.oldCache.has(key);
22486
+ }));
22487
+
22488
+ /**
22489
+ * @file Schema for redirect data.
22490
+ */
22491
+ /**
22492
+ * Zod schema for redirect data.
22493
+ */
22494
+ zodToCamelCase(baseCompatibilityDataSchema.extend({
22495
+ /**
22496
+ * Whether the redirect is blocking.
22497
+ */
22498
+ is_blocking: booleanSchema.default(false),
22499
+ /**
22500
+ * Resource type(s) belonging to the redirect.
22501
+ *
22502
+ * @see {@link https://developer.chrome.com/docs/extensions/reference/declarativeNetRequest/#type-ResourceType}
22503
+ */
22504
+ resource_types: zod.array(resourceTypeSchema).default([])
22505
+ }).superRefine(baseRefineLogic));
22506
+
22507
+ /**
22508
+ * @file Schema for scriptlet data.
22509
+ */
22510
+ /**
22511
+ * Zod schema for scriptlet parameter data.
22512
+ */
22513
+ const scriptletParameterSchema = zod.object({
22514
+ /**
22515
+ * Name of the actual parameter.
22516
+ */
22517
+ name: nonEmptyStringSchema,
22518
+ /**
22519
+ * Describes whether the parameter is required. Empty parameters are not allowed.
22520
+ */
22521
+ required: booleanSchema,
22522
+ /**
22523
+ * Short description of the parameter.
22524
+ * If not specified or it's value is `null`,then the description is not available.
22525
+ */
22526
+ description: nonEmptyStringSchema.nullable().default(null),
22527
+ /**
22528
+ * Regular expression that matches the value of the parameter.
22529
+ * If it's value is `null`, then the parameter value is not checked.
22530
+ */
22531
+ pattern: nonEmptyStringSchema.nullable().default(null),
22532
+ /**
22533
+ * Default value of the parameter (if any).
22534
+ */
22535
+ default: nonEmptyStringSchema.nullable().default(null),
22536
+ /**
22537
+ * Describes whether the parameter is used only for debugging purposes.
22538
+ */
22539
+ debug: booleanSchema.default(false)
22540
+ });
22541
+ /**
22542
+ * Zod schema for scriptlet parameters.
22543
+ */
22544
+ const scriptletParametersSchema = zod.array(scriptletParameterSchema);
22545
+ /**
22546
+ * Zod schema for scriptlet data.
22547
+ */
22548
+ zodToCamelCase(baseCompatibilityDataSchema.extend({
22549
+ /**
22550
+ * List of parameters that the scriptlet accepts.
22551
+ * **Every** parameter should be listed here, because we check that the scriptlet is used correctly
22552
+ * (e.g. that the number of parameters is correct).
22553
+ */
22554
+ parameters: scriptletParametersSchema.optional()
22555
+ }).superRefine((data, ctx) => {
22556
+ // TODO: find something better, for now we can't add refine logic to the base schema:
22557
+ // https://github.com/colinhacks/zod/issues/454#issuecomment-848370721
22558
+ baseRefineLogic(data, ctx);
22559
+ // we don't allow required parameters after optional ones
22560
+ if (!data.parameters) {
22561
+ return;
22527
22562
  }
22528
- peek(key) {
22529
- if (this.cache.has(key)) {
22530
- return this.cache.get(key);
22531
- }
22532
- if (this.oldCache.has(key)) {
22533
- return this.oldCache.get(key);
22563
+ let optionalFound = false;
22564
+ for (const parameter of data.parameters) {
22565
+ if (optionalFound && parameter.required) {
22566
+ ctx.addIssue({
22567
+ code: zod.ZodIssueCode.custom,
22568
+ message: 'Required parameters must be before optional ones'
22569
+ });
22534
22570
  }
22535
- }
22536
- delete(key) {
22537
- const deleted = this.cache.delete(key);
22538
- if (deleted) {
22539
- this._size--;
22571
+ if (!parameter.required) {
22572
+ optionalFound = true;
22540
22573
  }
22541
- return this.oldCache.delete(key) || deleted;
22542
- }
22543
- clear() {
22544
- this.cache.clear();
22545
- this.oldCache.clear();
22546
- this._size = 0;
22547
22574
  }
22548
- *keys() {
22549
- for (const [key] of this) {
22550
- yield key;
22575
+ }));
22576
+
22577
+ /**
22578
+ * @file Scriptlet injection rule converter
22579
+ */
22580
+ const ABP_SCRIPTLET_PREFIX = 'abp-';
22581
+ const UBO_SCRIPTLET_PREFIX = 'ubo-';
22582
+ const UBO_SCRIPTLET_PREFIX_LENGTH = UBO_SCRIPTLET_PREFIX.length;
22583
+ const UBO_SCRIPTLET_JS_SUFFIX = '.js';
22584
+ const UBO_SCRIPTLET_JS_SUFFIX_LENGTH = UBO_SCRIPTLET_JS_SUFFIX.length;
22585
+ const COMMA_SEPARATOR = ',';
22586
+ const ADG_SET_CONSTANT_NAME = 'set-constant';
22587
+ const ADG_SET_CONSTANT_EMPTY_STRING = '';
22588
+ const ADG_SET_CONSTANT_EMPTY_ARRAY = 'emptyArr';
22589
+ const ADG_SET_CONSTANT_EMPTY_OBJECT = 'emptyObj';
22590
+ const UBO_SET_CONSTANT_EMPTY_STRING = '\'\'';
22591
+ const UBO_SET_CONSTANT_EMPTY_ARRAY = '[]';
22592
+ const UBO_SET_CONSTANT_EMPTY_OBJECT = '{}';
22593
+ const ADG_PREVENT_FETCH_NAME = 'prevent-fetch';
22594
+ const ADG_PREVENT_FETCH_EMPTY_STRING = '';
22595
+ const ADG_PREVENT_FETCH_WILDCARD = '*';
22596
+ const UBO_NO_FETCH_IF_WILDCARD = '/^/';
22597
+ const UBO_REMOVE_CLASS_NAME = 'remove-class.js';
22598
+ const UBO_REMOVE_ATTR_NAME = 'remove-attr.js';
22599
+ const setConstantAdgToUboMap = {
22600
+ [ADG_SET_CONSTANT_EMPTY_STRING]: UBO_SET_CONSTANT_EMPTY_STRING,
22601
+ [ADG_SET_CONSTANT_EMPTY_ARRAY]: UBO_SET_CONSTANT_EMPTY_ARRAY,
22602
+ [ADG_SET_CONSTANT_EMPTY_OBJECT]: UBO_SET_CONSTANT_EMPTY_OBJECT
22603
+ };
22604
+ const REMOVE_ATTR_CLASS_APPLYING = new Set(['asap', 'stay', 'complete']);
22605
+ /**
22606
+ * Scriptlet injection rule converter class
22607
+ *
22608
+ * @todo Implement `convertToUbo` and `convertToAbp`
22609
+ */
22610
+ class ScriptletRuleConverter extends RuleConverterBase {
22611
+ /**
22612
+ * Converts a scriptlet injection rule to AdGuard format, if possible.
22613
+ *
22614
+ * @param rule Rule node to convert
22615
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22616
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22617
+ * If the rule was not converted, the result array will contain the original node with the same object reference
22618
+ * @throws If the rule is invalid or cannot be converted
22619
+ */
22620
+ static convertToAdg(rule) {
22621
+ // Ignore AdGuard rules
22622
+ if (rule.syntax === exports.AdblockSyntax.Adg) {
22623
+ return createNodeConversionResult([rule], false);
22551
22624
  }
22552
- }
22553
- *values() {
22554
- for (const [, value] of this) {
22555
- yield value;
22625
+ const separator = rule.separator.value;
22626
+ let convertedSeparator = separator;
22627
+ convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgJsInjectionException : exports.CosmeticRuleSeparator.AdgJsInjection;
22628
+ const convertedScriptlets = [];
22629
+ // Special case: empty uBO exception scriptlet, e.g. `example.com#@#+js()`
22630
+ if (rule.syntax === exports.AdblockSyntax.Ubo && rule.body.children.length === 1 && rule.body.children[0].children.length === 0) {
22631
+ convertedScriptlets.push(rule.body.children[0]);
22632
+ } else {
22633
+ for (const scriptlet of rule.body.children) {
22634
+ // Clone the node to avoid any side effects
22635
+ const scriptletClone = cloneScriptletRuleNode(scriptlet);
22636
+ // Remove possible quotes just to make it easier to work with the scriptlet name
22637
+ const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), exports.QuoteType.None);
22638
+ // Add prefix if it's not already there
22639
+ let prefix;
22640
+ // In uBO / ABP syntax, if a parameter contains the separator character, it should be escaped,
22641
+ // but during the conversion, we need to unescape them, because AdGuard syntax uses quotes to
22642
+ // distinguish between parameters.
22643
+ let charToUnescape;
22644
+ switch (rule.syntax) {
22645
+ case exports.AdblockSyntax.Abp:
22646
+ prefix = ABP_SCRIPTLET_PREFIX;
22647
+ charToUnescape = SPACE;
22648
+ break;
22649
+ case exports.AdblockSyntax.Ubo:
22650
+ prefix = UBO_SCRIPTLET_PREFIX;
22651
+ charToUnescape = COMMA_SEPARATOR;
22652
+ break;
22653
+ default:
22654
+ prefix = EMPTY;
22655
+ }
22656
+ if (!scriptletName.startsWith(prefix)) {
22657
+ setScriptletName(scriptletClone, `${prefix}${scriptletName}`);
22658
+ }
22659
+ if (!isUndefined(charToUnescape)) {
22660
+ transformAllScriptletArguments(scriptletClone, value => {
22661
+ if (!isNull(value)) {
22662
+ return QuoteUtils.unescapeSingleEscapedOccurrences(value, charToUnescape);
22663
+ }
22664
+ return value;
22665
+ });
22666
+ }
22667
+ if (rule.syntax === exports.AdblockSyntax.Ubo) {
22668
+ const scriptletData = scriptletsCompatibilityTable.getFirst(scriptletName, exports.GenericPlatform.UboAny);
22669
+ // Some scriptlets have special values that need to be converted
22670
+ if (scriptletData && (scriptletData.name === UBO_REMOVE_CLASS_NAME || scriptletData.name === UBO_REMOVE_ATTR_NAME) && scriptletClone.children.length > 2) {
22671
+ const selectors = [];
22672
+ let applying = null;
22673
+ let lastArg = scriptletClone.children.pop();
22674
+ // The very last argument might be the 'applying' parameter
22675
+ if (lastArg) {
22676
+ if (REMOVE_ATTR_CLASS_APPLYING.has(lastArg.value)) {
22677
+ applying = lastArg.value;
22678
+ } else {
22679
+ selectors.push(lastArg.value);
22680
+ }
22681
+ }
22682
+ while (scriptletClone.children.length > 2) {
22683
+ lastArg = scriptletClone.children.pop();
22684
+ if (lastArg) {
22685
+ selectors.push(lastArg.value.trim());
22686
+ }
22687
+ }
22688
+ // Set last arg to be the combined selectors (in reverse order, because we popped them)
22689
+ if (selectors.length > 0) {
22690
+ scriptletClone.children.push({
22691
+ type: 'Value',
22692
+ value: selectors.reverse().join(', ')
22693
+ });
22694
+ }
22695
+ // Push back the 'applying' parameter if it was found previously
22696
+ if (!isNull(applying)) {
22697
+ // If we don't have any selectors,
22698
+ // we need to add an empty parameter before the 'applying' one
22699
+ if (selectors.length === 0) {
22700
+ scriptletClone.children.push({
22701
+ type: 'Value',
22702
+ value: EMPTY
22703
+ });
22704
+ }
22705
+ scriptletClone.children.push({
22706
+ type: 'Value',
22707
+ value: applying
22708
+ });
22709
+ }
22710
+ }
22711
+ }
22712
+ // ADG scriptlet parameters should be quoted, and single quoted are preferred
22713
+ setScriptletQuoteType(scriptletClone, exports.QuoteType.Single);
22714
+ convertedScriptlets.push(scriptletClone);
22715
+ }
22556
22716
  }
22717
+ return createNodeConversionResult(convertedScriptlets.map(scriptlet => {
22718
+ const res = {
22719
+ category: rule.category,
22720
+ type: rule.type,
22721
+ syntax: exports.AdblockSyntax.Adg,
22722
+ exception: rule.exception,
22723
+ domains: cloneDomainListNode(rule.domains),
22724
+ separator: {
22725
+ type: 'Value',
22726
+ value: convertedSeparator
22727
+ },
22728
+ body: {
22729
+ type: rule.body.type,
22730
+ children: [scriptlet]
22731
+ }
22732
+ };
22733
+ if (rule.modifiers) {
22734
+ res.modifiers = cloneModifierListNode(rule.modifiers);
22735
+ }
22736
+ return res;
22737
+ }), true);
22557
22738
  }
22558
- *[Symbol.iterator]() {
22559
- for (const item of this.cache) {
22560
- yield item;
22739
+ /**
22740
+ * Converts a scriptlet injection rule to uBlock format, if possible.
22741
+ *
22742
+ * @param rule Rule node to convert
22743
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
22744
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
22745
+ * If the rule was not converted, the result array will contain the original node with the same object reference
22746
+ * @throws If the rule is invalid or cannot be converted
22747
+ */
22748
+ static convertToUbo(rule) {
22749
+ // Ignore uBlock rules
22750
+ if (rule.syntax === exports.AdblockSyntax.Ubo) {
22751
+ return createNodeConversionResult([rule], false);
22561
22752
  }
22562
- for (const item of this.oldCache) {
22563
- const [key] = item;
22564
- if (!this.cache.has(key)) {
22565
- yield item;
22753
+ const separator = rule.separator.value;
22754
+ let convertedSeparator = separator;
22755
+ convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.ElementHidingException : exports.CosmeticRuleSeparator.ElementHiding;
22756
+ const convertedScriptlets = [];
22757
+ // Special case: empty AdGuard exception scriptlet, e.g. `example.com#%#//scriptlet()`
22758
+ if (rule.syntax === exports.AdblockSyntax.Adg && rule.body.children.length === 1 && rule.body.children[0].children.length === 0) {
22759
+ convertedScriptlets.push(rule.body.children[0]);
22760
+ } else {
22761
+ for (const scriptlet of rule.body.children) {
22762
+ // Clone the node to avoid any side effects
22763
+ const scriptletClone = cloneScriptletRuleNode(scriptlet);
22764
+ // Remove possible quotes just to make it easier to work with the scriptlet name
22765
+ const scriptletName = QuoteUtils.setStringQuoteType(getScriptletName(scriptletClone), exports.QuoteType.None);
22766
+ let uboScriptletName;
22767
+ if (rule.syntax === exports.AdblockSyntax.Adg && scriptletName.startsWith(UBO_SCRIPTLET_PREFIX)) {
22768
+ // Special case: AdGuard syntax 'preserves' the original scriptlet name,
22769
+ // so we need to convert it back by removing the uBO prefix
22770
+ uboScriptletName = scriptletName.slice(UBO_SCRIPTLET_PREFIX_LENGTH);
22771
+ } else {
22772
+ // Otherwise, try to find the corresponding uBO scriptlet name, or use the original one if not found
22773
+ const uboScriptlet = scriptletsCompatibilityTable.getFirst(scriptletName, exports.GenericPlatform.UboAny);
22774
+ uboScriptletName = uboScriptlet?.name ?? scriptletName;
22775
+ }
22776
+ // Remove the '.js' suffix if it's there - its presence is not mandatory
22777
+ if (uboScriptletName.endsWith(UBO_SCRIPTLET_JS_SUFFIX)) {
22778
+ uboScriptletName = uboScriptletName.slice(0, -UBO_SCRIPTLET_JS_SUFFIX_LENGTH);
22779
+ }
22780
+ setScriptletName(scriptletClone, uboScriptletName);
22781
+ setScriptletQuoteType(scriptletClone, exports.QuoteType.None);
22782
+ // Escape unescaped commas in parameters, because uBlock Origin uses them as separators.
22783
+ // For example, the following AdGuard rule:
22784
+ //
22785
+ // example.com#%#//scriptlet('spoof-css', '.adsbygoogle, #ads', 'visibility', 'visible')
22786
+ //
22787
+ // ↓↓ should be converted to ↓↓
22788
+ //
22789
+ // example.com##+js(spoof-css.js, .adsbygoogle\, #ads, visibility, visible)
22790
+ // ------------ ------------------- ---------- -------
22791
+ // arg 0 arg 1 arg 2 arg 3
22792
+ //
22793
+ // and we need to escape the comma in the second argument to prevent it from being treated
22794
+ // as two separate arguments.
22795
+ transformAllScriptletArguments(scriptletClone, value => {
22796
+ if (!isNull(value)) {
22797
+ return QuoteUtils.escapeUnescapedOccurrences(value, COMMA_SEPARATOR);
22798
+ }
22799
+ return value;
22800
+ });
22801
+ // Unescape spaces in parameters, because uBlock Origin doesn't treat them as separators.
22802
+ if (rule.syntax === exports.AdblockSyntax.Abp) {
22803
+ transformAllScriptletArguments(scriptletClone, value => {
22804
+ if (!isNull(value)) {
22805
+ return QuoteUtils.unescapeSingleEscapedOccurrences(value, SPACE);
22806
+ }
22807
+ return value;
22808
+ });
22809
+ }
22810
+ // Some scriptlets have special values that need to be converted
22811
+ switch (scriptletName) {
22812
+ case ADG_SET_CONSTANT_NAME:
22813
+ transformNthScriptletArgument(scriptletClone, 2, value => {
22814
+ if (!isNull(value)) {
22815
+ return setConstantAdgToUboMap[value] ?? value;
22816
+ }
22817
+ return value;
22818
+ });
22819
+ break;
22820
+ case ADG_PREVENT_FETCH_NAME:
22821
+ transformNthScriptletArgument(scriptletClone, 1, value => {
22822
+ if (value === ADG_PREVENT_FETCH_EMPTY_STRING || value === ADG_PREVENT_FETCH_WILDCARD) {
22823
+ return UBO_NO_FETCH_IF_WILDCARD;
22824
+ }
22825
+ return value;
22826
+ });
22827
+ break;
22828
+ }
22829
+ convertedScriptlets.push(scriptletClone);
22566
22830
  }
22567
22831
  }
22568
- }
22569
- get size() {
22570
- let oldCacheSize = 0;
22571
- for (const key of this.oldCache.keys()) {
22572
- if (!this.cache.has(key)) {
22573
- oldCacheSize++;
22832
+ return createNodeConversionResult(convertedScriptlets.map(scriptlet => {
22833
+ const res = {
22834
+ category: rule.category,
22835
+ type: rule.type,
22836
+ syntax: exports.AdblockSyntax.Ubo,
22837
+ exception: rule.exception,
22838
+ domains: cloneDomainListNode(rule.domains),
22839
+ separator: {
22840
+ type: 'Value',
22841
+ value: convertedSeparator
22842
+ },
22843
+ body: {
22844
+ type: rule.body.type,
22845
+ children: [scriptlet]
22846
+ }
22847
+ };
22848
+ if (rule.modifiers) {
22849
+ res.modifiers = cloneModifierListNode(rule.modifiers);
22574
22850
  }
22575
- }
22576
- return Math.min(this._size + oldCacheSize, this.maxSize);
22851
+ return res;
22852
+ }), true);
22577
22853
  }
22578
22854
  }
22579
- var quickLru = QuickLRU;
22580
- const mapObj = mapObjExports;
22581
- const camelCase = camelcaseExports;
22582
- const QuickLru = quickLru;
22583
- const has = (array, key) => array.some(x => {
22584
- if (typeof x === 'string') {
22585
- return x === key;
22586
- }
22587
- x.lastIndex = 0;
22588
- return x.test(key);
22589
- });
22590
- const cache = new QuickLru({
22591
- maxSize: 100000
22592
- });
22593
22855
 
22594
- // Reproduces behavior from `map-obj`
22595
- const isObject = value => typeof value === 'object' && value !== null && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date);
22596
- const camelCaseConvert = (input, options) => {
22597
- if (!isObject(input)) {
22598
- return input;
22856
+ /**
22857
+ * @file Utility functions for working with modifier nodes
22858
+ */
22859
+ /**
22860
+ * Creates a modifier node
22861
+ *
22862
+ * @param name Name of the modifier
22863
+ * @param value Value of the modifier
22864
+ * @param exception Whether the modifier is an exception
22865
+ * @returns Modifier node
22866
+ */
22867
+ function createModifierNode(name, value = undefined, exception = false) {
22868
+ const result = {
22869
+ type: 'Modifier',
22870
+ exception,
22871
+ name: {
22872
+ type: 'Value',
22873
+ value: name
22874
+ }
22875
+ };
22876
+ if (!isUndefined(value)) {
22877
+ result.value = {
22878
+ type: 'Value',
22879
+ value
22880
+ };
22599
22881
  }
22600
- options = {
22601
- deep: false,
22602
- pascalCase: false,
22603
- ...options
22882
+ return result;
22883
+ }
22884
+ /**
22885
+ * Creates a modifier list node
22886
+ *
22887
+ * @param modifiers Modifiers to put in the list (optional, defaults to an empty list)
22888
+ * @returns Modifier list node
22889
+ */
22890
+ function createModifierListNode(modifiers = []) {
22891
+ const result = {
22892
+ type: 'ModifierList',
22893
+ // We need to clone the modifiers to avoid side effects
22894
+ children: modifiers.length ? clone(modifiers) : []
22604
22895
  };
22605
- const {
22606
- exclude,
22607
- pascalCase,
22608
- stopPaths,
22609
- deep
22610
- } = options;
22611
- const stopPathsSet = new Set(stopPaths);
22612
- const makeMapper = parentPath => (key, value) => {
22613
- if (deep && isObject(value)) {
22614
- const path = parentPath === undefined ? key : `${parentPath}.${key}`;
22615
- if (!stopPathsSet.has(path)) {
22616
- value = mapObj(value, makeMapper(path));
22896
+ return result;
22897
+ }
22898
+
22899
+ /**
22900
+ * A very simple map extension that allows to store multiple values for the same key
22901
+ * by storing them in an array.
22902
+ *
22903
+ * @todo Add more methods if needed
22904
+ */
22905
+ class MultiValueMap extends Map {
22906
+ /**
22907
+ * Adds a value to the map. If the key already exists, the value will be appended to the existing array,
22908
+ * otherwise a new array will be created for the key.
22909
+ *
22910
+ * @param key Key to add
22911
+ * @param values Value(s) to add
22912
+ */
22913
+ add(key, ...values) {
22914
+ let currentValues = super.get(key);
22915
+ if (isUndefined(currentValues)) {
22916
+ currentValues = [];
22917
+ super.set(key, values);
22918
+ }
22919
+ currentValues.push(...values);
22920
+ }
22921
+ }
22922
+
22923
+ /**
22924
+ * @file Cosmetic rule modifier converter from uBO to ADG
22925
+ */
22926
+ const UBO_MATCHES_PATH_OPERATOR = 'matches-path';
22927
+ const ADG_PATH_MODIFIER = 'path';
22928
+ /**
22929
+ * Special characters in modifier regexps that should be escaped
22930
+ */
22931
+ const SPECIAL_MODIFIER_REGEX_CHARS = new Set([OPEN_SQUARE_BRACKET, CLOSE_SQUARE_BRACKET, COMMA, ESCAPE_CHARACTER]);
22932
+ /**
22933
+ * Helper class for converting cosmetic rule modifiers from uBO to ADG
22934
+ */
22935
+ class AdgCosmeticRuleModifierConverter {
22936
+ /**
22937
+ * Converts a uBO cosmetic rule modifier list to ADG, if possible.
22938
+ *
22939
+ * @param modifierList Cosmetic rule modifier list node to convert
22940
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
22941
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
22942
+ * If the node was not converted, the result will contain the original node with the same object reference
22943
+ * @throws If the modifier list cannot be converted
22944
+ * @see {@link https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters#cosmetic-filter-operators}
22945
+ */
22946
+ static convertFromUbo(modifierList) {
22947
+ const conversionMap = new MultiValueMap();
22948
+ modifierList.children.forEach((modifier, index) => {
22949
+ // :matches-path
22950
+ if (modifier.name.value === UBO_MATCHES_PATH_OPERATOR) {
22951
+ if (!modifier.value) {
22952
+ throw new RuleConversionError(`'${UBO_MATCHES_PATH_OPERATOR}' operator requires a value`);
22953
+ }
22954
+ const value = RegExpUtils.isRegexPattern(modifier.value.value) ? StringUtils.escapeCharacters(modifier.value.value, SPECIAL_MODIFIER_REGEX_CHARS) : modifier.value.value;
22955
+ // Convert uBO's `:matches-path(...)` operator to ADG's `$path=...` modifier
22956
+ conversionMap.add(index, createModifierNode(ADG_PATH_MODIFIER,
22957
+ // We should negate the regexp if the modifier is an exception
22958
+ modifier.exception
22959
+ // eslint-disable-next-line max-len
22960
+ ? `${REGEX_MARKER}${RegExpUtils.negateRegexPattern(RegExpUtils.patternToRegexp(value))}${REGEX_MARKER}` : value));
22617
22961
  }
22962
+ });
22963
+ // Check if we have any converted modifiers
22964
+ if (conversionMap.size) {
22965
+ const modifierListClone = clone(modifierList);
22966
+ // Replace the original modifiers with the converted ones
22967
+ modifierListClone.children = modifierListClone.children.map((modifier, index) => {
22968
+ const convertedModifier = conversionMap.get(index);
22969
+ return convertedModifier ?? modifier;
22970
+ }).flat();
22971
+ return createConversionResult(modifierListClone, true);
22618
22972
  }
22619
- if (!(exclude && has(exclude, key))) {
22620
- const cacheKey = pascalCase ? `${key}_` : key;
22621
- if (cache.has(cacheKey)) {
22622
- key = cache.get(cacheKey);
22623
- } else {
22624
- const returnValue = camelCase(key, {
22625
- pascalCase,
22626
- locale: false
22973
+ // Otherwise, just return the original modifier list
22974
+ return createConversionResult(modifierList, false);
22975
+ }
22976
+ }
22977
+ const ERROR_MESSAGES$1 = {
22978
+ // eslint-disable-next-line max-len
22979
+ INVALID_ATTRIBUTE_VALUE: `Expected '${cssTokenizer.getFormattedTokenName(cssTokenizer.TokenType.Ident)}' or '${cssTokenizer.getFormattedTokenName(cssTokenizer.TokenType.String)}' as attribute value, but got '%s' with value '%s`
22980
+ };
22981
+ var PseudoClasses;
22982
+ (function (PseudoClasses) {
22983
+ PseudoClasses["AbpContains"] = "-abp-contains";
22984
+ PseudoClasses["AbpHas"] = "-abp-has";
22985
+ PseudoClasses["Contains"] = "contains";
22986
+ PseudoClasses["Has"] = "has";
22987
+ PseudoClasses["HasText"] = "has-text";
22988
+ PseudoClasses["MatchesCss"] = "matches-css";
22989
+ PseudoClasses["MatchesCssAfter"] = "matches-css-after";
22990
+ PseudoClasses["MatchesCssBefore"] = "matches-css-before";
22991
+ PseudoClasses["Not"] = "not";
22992
+ })(PseudoClasses || (PseudoClasses = {}));
22993
+ var PseudoElements;
22994
+ (function (PseudoElements) {
22995
+ PseudoElements["After"] = "after";
22996
+ PseudoElements["Before"] = "before";
22997
+ })(PseudoElements || (PseudoElements = {}));
22998
+ const PSEUDO_ELEMENT_NAMES = new Set([PseudoElements.After, PseudoElements.Before]);
22999
+ /**
23000
+ * CSS selector converter
23001
+ *
23002
+ * @todo Implement `convertToUbo` and `convertToAbp`
23003
+ */
23004
+ class CssSelectorConverter extends ConverterBase {
23005
+ /**
23006
+ * Converts Extended CSS elements to AdGuard-compatible ones
23007
+ *
23008
+ * @param selectorList Selector list to convert
23009
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
23010
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
23011
+ * If the node was not converted, the result will contain the original node with the same object reference
23012
+ * @throws If the rule is invalid or incompatible
23013
+ */
23014
+ static convertToAdg(selectorList) {
23015
+ const stream = selectorList instanceof CssTokenStream ? selectorList : new CssTokenStream(selectorList);
23016
+ const converted = [];
23017
+ const convertAndPushPseudo = pseudo => {
23018
+ switch (pseudo) {
23019
+ case PseudoClasses.AbpContains:
23020
+ case PseudoClasses.HasText:
23021
+ converted.push(PseudoClasses.Contains);
23022
+ converted.push(OPEN_PARENTHESIS);
23023
+ break;
23024
+ case PseudoClasses.AbpHas:
23025
+ converted.push(PseudoClasses.Has);
23026
+ converted.push(OPEN_PARENTHESIS);
23027
+ break;
23028
+ // a bit special case:
23029
+ // - `:matches-css-before(...)` → `:matches-css(before, ...)`
23030
+ // - `:matches-css-after(...)` → `:matches-css(after, ...)`
23031
+ case PseudoClasses.MatchesCssBefore:
23032
+ case PseudoClasses.MatchesCssAfter:
23033
+ converted.push(PseudoClasses.MatchesCss);
23034
+ converted.push(OPEN_PARENTHESIS);
23035
+ converted.push(pseudo.substring(PseudoClasses.MatchesCss.length + 1));
23036
+ converted.push(COMMA);
23037
+ break;
23038
+ default:
23039
+ converted.push(pseudo);
23040
+ converted.push(OPEN_PARENTHESIS);
23041
+ break;
23042
+ }
23043
+ };
23044
+ while (!stream.isEof()) {
23045
+ const token = stream.getOrFail();
23046
+ if (token.type === cssTokenizer.TokenType.Colon) {
23047
+ // Advance colon
23048
+ stream.advance();
23049
+ converted.push(COLON);
23050
+ const tempToken = stream.getOrFail();
23051
+ // Double colon is a pseudo-element
23052
+ if (tempToken.type === cssTokenizer.TokenType.Colon) {
23053
+ stream.advance();
23054
+ converted.push(COLON);
23055
+ continue;
23056
+ }
23057
+ if (tempToken.type === cssTokenizer.TokenType.Ident) {
23058
+ const name = stream.source.slice(tempToken.start, tempToken.end);
23059
+ if (PSEUDO_ELEMENT_NAMES.has(name)) {
23060
+ // Add an extra colon to the name
23061
+ converted.push(COLON);
23062
+ converted.push(name);
23063
+ } else {
23064
+ // Add the name as is
23065
+ converted.push(name);
23066
+ }
23067
+ // Advance the names
23068
+ stream.advance();
23069
+ } else if (tempToken.type === cssTokenizer.TokenType.Function) {
23070
+ const name = stream.source.slice(tempToken.start, tempToken.end - 1); // omit the last parenthesis
23071
+ // :-abp-contains(...) → :contains(...)
23072
+ // :has-text(...) → :contains(...)
23073
+ // :-abp-has(...) → :has(...)
23074
+ convertAndPushPseudo(name);
23075
+ // Advance the function name
23076
+ stream.advance();
23077
+ }
23078
+ } else if (token.type === cssTokenizer.TokenType.OpenSquareBracket) {
23079
+ let tempToken;
23080
+ const {
23081
+ start
23082
+ } = token;
23083
+ stream.advance();
23084
+ // Converts legacy Extended CSS selectors to the modern Extended CSS syntax.
23085
+ // For example:
23086
+ // - `[-ext-has=...]` → `:has(...)`
23087
+ // - `[-ext-contains=...]` → `:contains(...)`
23088
+ // - `[-ext-matches-css-before=...]` → `:matches-css(before, ...)`
23089
+ stream.skipWhitespace();
23090
+ stream.expect(cssTokenizer.TokenType.Ident);
23091
+ tempToken = stream.getOrFail();
23092
+ let attr = stream.source.slice(tempToken.start, tempToken.end);
23093
+ // Skip if the attribute name is not a legacy Extended CSS one
23094
+ if (!(attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX) || attr.startsWith(ABP_EXT_CSS_PREFIX))) {
23095
+ converted.push(stream.source.slice(start, tempToken.end));
23096
+ stream.advance();
23097
+ continue;
23098
+ }
23099
+ if (attr.startsWith(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX)) {
23100
+ attr = attr.slice(LEGACY_EXT_CSS_ATTRIBUTE_PREFIX.length);
23101
+ }
23102
+ stream.advance();
23103
+ stream.skipWhitespace();
23104
+ // Next token should be an equality operator (=), because Extended CSS attribute selectors
23105
+ // do not support other operators
23106
+ stream.expect(cssTokenizer.TokenType.Delim, {
23107
+ value: EQUALS
22627
23108
  });
22628
- if (key.length < 100) {
22629
- // Prevent abuse
22630
- cache.set(cacheKey, returnValue);
23109
+ stream.advance();
23110
+ // Skip optional whitespace after the operator
23111
+ stream.skipWhitespace();
23112
+ // Parse attribute value
23113
+ tempToken = stream.getOrFail();
23114
+ // According to the spec, attribute value should be an identifier or a string
23115
+ if (tempToken.type !== cssTokenizer.TokenType.Ident && tempToken.type !== cssTokenizer.TokenType.String) {
23116
+ throw new Error(sprintfJs.sprintf(ERROR_MESSAGES$1.INVALID_ATTRIBUTE_VALUE, cssTokenizer.getFormattedTokenName(tempToken.type), stream.source.slice(tempToken.start, tempToken.end)));
22631
23117
  }
22632
- key = returnValue;
23118
+ const value = stream.source.slice(tempToken.start, tempToken.end);
23119
+ // Advance the attribute value
23120
+ stream.advance();
23121
+ // Skip optional whitespace after the attribute value
23122
+ stream.skipWhitespace();
23123
+ // Next character should be a closing square bracket
23124
+ // We don't allow flags for Extended CSS attribute selectors
23125
+ stream.expect(cssTokenizer.TokenType.CloseSquareBracket);
23126
+ stream.advance();
23127
+ converted.push(COLON);
23128
+ convertAndPushPseudo(attr);
23129
+ let processedValue = value.slice(1, -1); // omit the quotes
23130
+ if (attr === PseudoClasses.Has) {
23131
+ // TODO: Optimize this to avoid double tokenization
23132
+ processedValue = CssSelectorConverter.convertToAdg(processedValue).result;
23133
+ }
23134
+ converted.push(processedValue);
23135
+ converted.push(CLOSE_PARENTHESIS);
23136
+ } else {
23137
+ converted.push(stream.source.slice(token.start, token.end));
23138
+ // Advance the token
23139
+ stream.advance();
22633
23140
  }
22634
23141
  }
22635
- return [key, value];
22636
- };
22637
- return mapObj(input, makeMapper(undefined));
22638
- };
22639
- var camelcaseKeys = (input, options) => {
22640
- if (Array.isArray(input)) {
22641
- return Object.keys(input).map(key => camelCaseConvert(input[key], options));
23142
+ const convertedSelectorList = converted.join(EMPTY);
23143
+ return createConversionResult(convertedSelectorList, stream.source !== convertedSelectorList);
22642
23144
  }
22643
- return camelCaseConvert(input, options);
22644
- };
22645
- var camelCaseKeys = /*@__PURE__*/getDefaultExportFromCjs(camelcaseKeys);
23145
+ }
22646
23146
 
22647
23147
  /**
22648
- * @file Zod camelCase utility.
23148
+ * @file CSS injection rule converter
22649
23149
  */
22650
- // eslint-disable-next-line import/no-extraneous-dependencies
22651
23150
  /**
22652
- * Transforms Zod schema to camelCase.
22653
- *
22654
- * @param zod Zod schema.
22655
- *
22656
- * @returns Zod schema with camelCase properties.
23151
+ * CSS injection rule converter class
22657
23152
  *
22658
- * @see {@link https://github.com/colinhacks/zod/issues/486#issuecomment-1501097361}
22659
- */
22660
- const zodToCamelCase = zod => {
22661
- return zod.transform(val => camelCaseKeys(val));
22662
- };
22663
-
22664
- /**
22665
- * @file Base compatibility data schema, which is commonly used in compatibility tables.
22666
- */
22667
- /**
22668
- * Zod schema for boolean values. Accepts both boolean and string values.
22669
- */
22670
- const booleanSchema = zod.union([zod.string().transform(val => val.trim().toLowerCase() === 'true'), zod.boolean()]);
22671
- /**
22672
- * Zod schema for non-empty string values.
22673
- */
22674
- const nonEmptyStringSchema = zod.string().transform(val => val.trim()).pipe(zod.string().min(1));
22675
- /**
22676
- * Zod schema for base compatibility data.
22677
- * Here we use snake_case properties because the compatibility data is stored in YAML files.
23153
+ * @todo Implement `convertToUbo` and `convertToAbp`
22678
23154
  */
22679
- const baseCompatibilityDataSchema = zod.object({
22680
- /**
22681
- * Name of the actual entity.
22682
- */
22683
- name: nonEmptyStringSchema,
22684
- /**
22685
- * List of aliases for the entity (if any).
22686
- */
22687
- aliases: zod.array(nonEmptyStringSchema).nullable().default(null),
22688
- /**
22689
- * Short description of the actual entity.
22690
- * If not specified or it's value is `null`, then the description is not available.
22691
- */
22692
- description: nonEmptyStringSchema.nullable().default(null),
22693
- /**
22694
- * Link to the documentation. If not specified or it's value is `null`, then the documentation is not available.
22695
- */
22696
- docs: nonEmptyStringSchema.nullable().default(null),
22697
- /**
22698
- * The version of the adblocker in which the entity was added.
22699
- * For AdGuard resources, the version of the library is specified.
22700
- */
22701
- version_added: nonEmptyStringSchema.nullable().default(null),
22702
- /**
22703
- * The version of the adblocker when the entity was removed.
22704
- */
22705
- version_removed: nonEmptyStringSchema.nullable().default(null),
22706
- /**
22707
- * Describes whether the entity is deprecated.
22708
- */
22709
- deprecated: booleanSchema.default(false),
22710
- /**
22711
- * Message that describes why the entity is deprecated.
22712
- * If not specified or it's value is `null`, then the message is not available.
22713
- * It's value is omitted if the entity is not marked as deprecated.
22714
- */
22715
- deprecation_message: nonEmptyStringSchema.nullable().default(null),
22716
- /**
22717
- * Describes whether the entity is removed; for *already removed* features.
22718
- */
22719
- removed: booleanSchema.default(false),
23155
+ class CssInjectionRuleConverter extends RuleConverterBase {
22720
23156
  /**
22721
- * Message that describes why the entity is removed.
22722
- * If not specified or it's value is `null`, then the message is not available.
22723
- * It's value is omitted if the entity is not marked as deprecated.
23157
+ * Converts a CSS injection rule to AdGuard format, if possible.
23158
+ *
23159
+ * @param rule Rule node to convert
23160
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
23161
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
23162
+ * If the rule was not converted, the result array will contain the original node with the same object reference
23163
+ * @throws If the rule is invalid or cannot be converted
22724
23164
  */
22725
- removal_message: nonEmptyStringSchema.nullable().default(null)
22726
- });
22727
- /**
22728
- * Zod schema for base compatibility data with camelCase properties.
22729
- */
22730
- zodToCamelCase(baseCompatibilityDataSchema);
22731
- /**
22732
- * Refinement logic for base compatibility data.
22733
- *
22734
- * @param data Base compatibility data.
22735
- * @param ctx Refinement context.
22736
- */
22737
- const baseRefineLogic = (data, ctx) => {
22738
- if (data.deprecated && !data.deprecation_message) {
22739
- ctx.addIssue({
22740
- code: zod.ZodIssueCode.custom,
22741
- message: 'deprecation_message is required for deprecated modifiers'
22742
- });
22743
- }
22744
- if (!data.deprecated && data.deprecation_message) {
22745
- ctx.addIssue({
22746
- code: zod.ZodIssueCode.custom,
22747
- message: 'deprecation_message is only allowed for deprecated modifiers'
22748
- });
22749
- }
22750
- if (data.aliases && data.aliases.length !== new Set(data.aliases).size) {
22751
- ctx.addIssue({
22752
- code: zod.ZodIssueCode.custom,
22753
- message: 'Aliases must be unique'
22754
- });
23165
+ static convertToAdg(rule) {
23166
+ const separator = rule.separator.value;
23167
+ let convertedSeparator = separator;
23168
+ const stream = new CssTokenStream(rule.body.selectorList.value);
23169
+ const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
23170
+ // Change the separator if the rule contains ExtendedCSS elements,
23171
+ // but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
23172
+ // because sometimes we use it to force executing ExtendedCSS library.
23173
+ if (stream.hasAnySelectorExtendedCssNodeStrict() || rule.body.remove) {
23174
+ convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgExtendedCssInjectionException : exports.CosmeticRuleSeparator.AdgExtendedCssInjection;
23175
+ } else if (rule.syntax !== exports.AdblockSyntax.Adg) {
23176
+ // If the original rule syntax is not AdGuard, use the default separator
23177
+ // e.g. if the input rule is from uBO, we need to convert ## to #$#.
23178
+ convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.AdgCssInjectionException : exports.CosmeticRuleSeparator.AdgCssInjection;
23179
+ }
23180
+ // Check if the rule needs to be converted
23181
+ if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg) || separator !== convertedSeparator || convertedSelectorList.isConverted) {
23182
+ // TODO: Replace with custom clone method
23183
+ const ruleClone = clone(rule);
23184
+ ruleClone.syntax = exports.AdblockSyntax.Adg;
23185
+ ruleClone.separator.value = convertedSeparator;
23186
+ ruleClone.body.selectorList.value = convertedSelectorList.result;
23187
+ return createNodeConversionResult([ruleClone], true);
23188
+ }
23189
+ // Otherwise, return the original rule
23190
+ return createNodeConversionResult([rule], false);
22755
23191
  }
22756
- };
23192
+ }
22757
23193
 
22758
23194
  /**
22759
- * Checks if error has message.
22760
- *
22761
- * @param error Error object.
22762
- * @returns If param is error.
23195
+ * @file Element hiding rule converter
22763
23196
  */
22764
- function isErrorWithMessage(error) {
22765
- return typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string';
22766
- }
22767
23197
  /**
22768
- * Converts error to the error with message.
23198
+ * Element hiding rule converter class
22769
23199
  *
22770
- * @param maybeError Possible error.
22771
- * @returns Error with message.
23200
+ * @todo Implement `convertToUbo` and `convertToAbp`
22772
23201
  */
22773
- function toErrorWithMessage(maybeError) {
22774
- if (isErrorWithMessage(maybeError)) {
22775
- return maybeError;
22776
- }
22777
- try {
22778
- return new Error(JSON.stringify(maybeError));
22779
- } catch {
22780
- // fallback in case there's an error stringifying the maybeError
22781
- // like with circular references for example.
22782
- return new Error(String(maybeError));
23202
+ class ElementHidingRuleConverter extends RuleConverterBase {
23203
+ /**
23204
+ * Converts an element hiding rule to AdGuard format, if possible.
23205
+ *
23206
+ * @param rule Rule node to convert
23207
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
23208
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
23209
+ * If the rule was not converted, the result array will contain the original node with the same object reference
23210
+ * @throws If the rule is invalid or cannot be converted
23211
+ */
23212
+ static convertToAdg(rule) {
23213
+ const separator = rule.separator.value;
23214
+ let convertedSeparator = separator;
23215
+ const stream = new CssTokenStream(rule.body.selectorList.value);
23216
+ const convertedSelectorList = CssSelectorConverter.convertToAdg(stream);
23217
+ // Change the separator if the rule contains ExtendedCSS elements,
23218
+ // but do not force non-extended CSS separator if the rule does not contain any ExtendedCSS selectors,
23219
+ // because sometimes we use it to force executing ExtendedCSS library.
23220
+ if (stream.hasAnySelectorExtendedCssNodeStrict()) {
23221
+ convertedSeparator = rule.exception ? exports.CosmeticRuleSeparator.ExtendedElementHidingException : exports.CosmeticRuleSeparator.ExtendedElementHiding;
23222
+ }
23223
+ // Check if the rule needs to be converted
23224
+ if (!(rule.syntax === exports.AdblockSyntax.Common || rule.syntax === exports.AdblockSyntax.Adg) || separator !== convertedSeparator || convertedSelectorList.isConverted) {
23225
+ // TODO: Replace with custom clone method
23226
+ const ruleClone = clone(rule);
23227
+ ruleClone.syntax = exports.AdblockSyntax.Adg;
23228
+ ruleClone.separator.value = convertedSeparator;
23229
+ ruleClone.body.selectorList.value = convertedSelectorList.result;
23230
+ return createNodeConversionResult([ruleClone], true);
23231
+ }
23232
+ // Otherwise, return the original rule
23233
+ return createNodeConversionResult([rule], false);
22783
23234
  }
22784
23235
  }
22785
- /**
22786
- * Converts error object to error with message. This method might be helpful to handle thrown errors.
22787
- *
22788
- * @param error Error object.
22789
- *
22790
- * @returns Message of the error.
22791
- */
22792
- function getErrorMessage(error) {
22793
- return toErrorWithMessage(error).message;
22794
- }
22795
23236
 
22796
23237
  /**
22797
- * @file Schema for modifier data.
22798
- */
22799
- /**
22800
- * Known validators that don't need to be validated as regex.
23238
+ * @file Utility functions for working with network rule nodes
22801
23239
  */
22802
- const KNOWN_VALIDATORS = new Set(['domain', 'pipe_separated_domains', 'regexp', 'url']);
22803
23240
  /**
22804
- * Zod schema for modifier data.
23241
+ * Creates a network rule node
23242
+ *
23243
+ * @param pattern Rule pattern
23244
+ * @param modifiers Rule modifiers (optional, default: undefined)
23245
+ * @param exception Exception rule flag (optional, default: false)
23246
+ * @param syntax Adblock syntax (optional, default: Common)
23247
+ * @returns Network rule node
22805
23248
  */
22806
- zodToCamelCase(baseCompatibilityDataSchema.extend({
22807
- /**
22808
- * List of modifiers that are incompatible with the actual one.
22809
- */
22810
- conflicts: zod.array(nonEmptyStringSchema).nullable().default(null),
22811
- /**
22812
- * The actual modifier is incompatible with all other modifiers, except the ones listed in `conflicts`.
22813
- */
22814
- inverse_conflicts: booleanSchema.default(false),
22815
- /**
22816
- * Describes whether the actual modifier supports value assignment. For example, `$domain` is assignable,
22817
- * so it can be used like this: `$domain=domain.com\|~subdomain.domain.com`, where `=` is the assignment operator
22818
- * and `domain.com\|~subdomain.domain.com` is the value.
22819
- */
22820
- assignable: booleanSchema.default(false),
22821
- /**
22822
- * Describes whether the actual modifier can be negated. For example, `$third-party` is negatable,
22823
- * so it can be used like this: `$~third-party`.
22824
- */
22825
- negatable: booleanSchema.default(true),
22826
- /**
22827
- * The actual modifier can only be used in blocking rules, it cannot be used in exceptions.
22828
- * If it's value is `true`, then the modifier can be used only in blocking rules.
22829
- * `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
22830
- */
22831
- block_only: booleanSchema.default(false),
22832
- /**
22833
- * The actual modifier can only be used in exceptions, it cannot be used in blocking rules.
22834
- * If it's value is `true`, then the modifier can be used only in exceptions.
22835
- * `exception_only` and `block_only` cannot be used together (they are mutually exclusive).
22836
- */
22837
- exception_only: booleanSchema.default(false),
22838
- /**
22839
- * Describes whether the *assignable* modifier value is required.
22840
- * For example, `$cookie` is assignable but it can be used without a value in exception rules:
22841
- * `@@\|\|example.com^$cookie`.
22842
- * If `false`, the `value_format` is required, e.g. the value of `$app` should always be specified
22843
- */
22844
- value_optional: booleanSchema.default(false),
22845
- /**
22846
- * Describes the format of the value for the *assignable* modifier.
22847
- * Its value can be a regex pattern or a known validator name (e.g. `domain`, `pipe_separated_domains`, etc.).
22848
- */
22849
- value_format: nonEmptyStringSchema.nullable().default(null)
22850
- }).superRefine((data, ctx) => {
22851
- // TODO: find something better, for now we can't add refine logic to the base schema:
22852
- // https://github.com/colinhacks/zod/issues/454#issuecomment-848370721
22853
- baseRefineLogic(data, ctx);
22854
- if (data.block_only && data.exception_only) {
22855
- ctx.addIssue({
22856
- code: zod.ZodIssueCode.custom,
22857
- message: 'block_only and exception_only are mutually exclusive'
22858
- });
22859
- }
22860
- if (data.assignable && !data.value_format) {
22861
- ctx.addIssue({
22862
- code: zod.ZodIssueCode.custom,
22863
- message: 'value_format is required for assignable modifiers'
22864
- });
22865
- }
22866
- if (data.value_format) {
22867
- const valueFormat = data.value_format.trim();
22868
- // if it is a known validator, we don't need to validate it further
22869
- if (KNOWN_VALIDATORS.has(valueFormat)) {
22870
- return;
22871
- }
22872
- // otherwise, we need to validate it as a regex
22873
- try {
22874
- XRegExp(valueFormat);
22875
- } catch (error) {
22876
- ctx.addIssue({
22877
- code: zod.ZodIssueCode.custom,
22878
- message: getErrorMessage(error)
22879
- });
23249
+ function createNetworkRuleNode(pattern, modifiers = undefined, exception = false, syntax = exports.AdblockSyntax.Common) {
23250
+ const result = {
23251
+ category: exports.RuleCategory.Network,
23252
+ type: exports.NetworkRuleType.NetworkRule,
23253
+ syntax,
23254
+ exception,
23255
+ pattern: {
23256
+ type: 'Value',
23257
+ value: pattern
22880
23258
  }
23259
+ };
23260
+ if (!isUndefined(modifiers)) {
23261
+ result.modifiers = clone(modifiers);
22881
23262
  }
22882
- }));
23263
+ return result;
23264
+ }
22883
23265
 
22884
23266
  /**
22885
- * @file Schema for redirect data.
23267
+ * @file Converter for request header removal rules
22886
23268
  */
23269
+ const UBO_RESPONSEHEADER_FN = 'responseheader';
23270
+ const ADG_REMOVEHEADER_MODIFIER = 'removeheader';
23271
+ const ERROR_MESSAGES = {
23272
+ EMPTY_PARAMETER: `Empty parameter for '${UBO_RESPONSEHEADER_FN}' function`,
23273
+ EXPECTED_END_OF_RULE: "Expected end of rule, but got '%s'",
23274
+ MULTIPLE_DOMAINS_NOT_SUPPORTED: 'Multiple domains are not supported yet'
23275
+ };
22887
23276
  /**
22888
- * Zod schema for redirect data.
23277
+ * Converter for request header removal rules
23278
+ *
23279
+ * @todo Implement `convertToUbo` (ABP currently doesn't support header removal rules)
22889
23280
  */
22890
- zodToCamelCase(baseCompatibilityDataSchema.extend({
23281
+ class HeaderRemovalRuleConverter extends RuleConverterBase {
22891
23282
  /**
22892
- * Whether the redirect is blocking.
23283
+ * Converts a header removal rule to AdGuard syntax, if possible.
23284
+ *
23285
+ * @param rule Rule node to convert
23286
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
23287
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
23288
+ * If the rule was not converted, the result array will contain the original node with the same object reference
23289
+ * @throws If the rule is invalid or cannot be converted
23290
+ * @example
23291
+ * If the input rule is:
23292
+ * ```adblock
23293
+ * example.com##^responseheader(header-name)
23294
+ * ```
23295
+ * The output will be:
23296
+ * ```adblock
23297
+ * ||example.com^$removeheader=header-name
23298
+ * ```
22893
23299
  */
22894
- is_blocking: booleanSchema.default(false)
22895
- }).superRefine(baseRefineLogic));
23300
+ static convertToAdg(rule) {
23301
+ // TODO: Add support for ABP syntax once it starts supporting header removal rules
23302
+ // Leave the rule as is if it's not a header removal rule
23303
+ if (rule.category !== exports.RuleCategory.Cosmetic || rule.type !== exports.CosmeticRuleType.HtmlFilteringRule) {
23304
+ return createNodeConversionResult([rule], false);
23305
+ }
23306
+ const stream = new CssTokenStream(rule.body.value);
23307
+ let token;
23308
+ // Skip leading whitespace
23309
+ stream.skipWhitespace();
23310
+ // Next token should be the `^` followed by a `responseheader` function
23311
+ token = stream.get();
23312
+ if (!token || token.type !== cssTokenizer.TokenType.Delim || rule.body.value[token.start] !== UBO_HTML_MASK) {
23313
+ return createNodeConversionResult([rule], false);
23314
+ }
23315
+ stream.advance();
23316
+ token = stream.get();
23317
+ if (!token) {
23318
+ return createNodeConversionResult([rule], false);
23319
+ }
23320
+ const functionName = rule.body.value.slice(token.start, token.end - 1);
23321
+ if (functionName !== UBO_RESPONSEHEADER_FN) {
23322
+ return createNodeConversionResult([rule], false);
23323
+ }
23324
+ // Parse the parameter
23325
+ const paramStart = token.end;
23326
+ stream.skipUntilBalanced();
23327
+ const paramEnd = stream.getOrFail().end;
23328
+ const param = rule.body.value.slice(paramStart, paramEnd - 1).trim();
23329
+ // Do not allow empty parameter
23330
+ if (param.length === 0) {
23331
+ throw new RuleConversionError(ERROR_MESSAGES.EMPTY_PARAMETER);
23332
+ }
23333
+ stream.expect(cssTokenizer.TokenType.CloseParenthesis);
23334
+ stream.advance();
23335
+ // Skip trailing whitespace after the function call
23336
+ stream.skipWhitespace();
23337
+ // Expect the end of the rule - so nothing should be left in the stream
23338
+ if (!stream.isEof()) {
23339
+ token = stream.getOrFail();
23340
+ throw new RuleConversionError(sprintfJs.sprintf(ERROR_MESSAGES.EXPECTED_END_OF_RULE, cssTokenizer.getFormattedTokenName(token.type)));
23341
+ }
23342
+ // Prepare network rule pattern
23343
+ const pattern = [];
23344
+ if (rule.domains.children.length === 1) {
23345
+ // If the rule has only one domain, we can use a simple network rule pattern:
23346
+ // ||single-domain-from-the-rule^
23347
+ pattern.push(ADBLOCK_URL_START, rule.domains.children[0].value, ADBLOCK_URL_SEPARATOR);
23348
+ } else if (rule.domains.children.length > 1) {
23349
+ // TODO: Add support for multiple domains, for example:
23350
+ // example.com,example.org,example.net##^responseheader(header-name)
23351
+ // We should consider allowing $domain with $removeheader modifier,
23352
+ // for example:
23353
+ // $removeheader=header-name,domain=example.com|example.org|example.net
23354
+ throw new RuleConversionError(ERROR_MESSAGES.MULTIPLE_DOMAINS_NOT_SUPPORTED);
23355
+ }
23356
+ // Prepare network rule modifiers
23357
+ const modifiers = createModifierListNode();
23358
+ modifiers.children.push(createModifierNode(ADG_REMOVEHEADER_MODIFIER, param));
23359
+ // Construct the network rule
23360
+ return createNodeConversionResult([createNetworkRuleNode(pattern.join(EMPTY), modifiers,
23361
+ // Copy the exception flag
23362
+ rule.exception, exports.AdblockSyntax.Adg)], true);
23363
+ }
23364
+ }
22896
23365
 
22897
23366
  /**
22898
- * @file Schema for scriptlet data.
22899
- */
22900
- /**
22901
- * Zod schema for scriptlet parameter data.
22902
- */
22903
- const scriptletParameterSchema = zod.object({
22904
- /**
22905
- * Name of the actual parameter.
22906
- */
22907
- name: nonEmptyStringSchema,
22908
- /**
22909
- * Describes whether the parameter is required. Empty parameters are not allowed.
22910
- */
22911
- required: booleanSchema,
22912
- /**
22913
- * Short description of the parameter.
22914
- * If not specified or it's value is `null`,then the description is not available.
22915
- */
22916
- description: nonEmptyStringSchema.nullable().default(null),
22917
- /**
22918
- * Regular expression that matches the value of the parameter.
22919
- * If it's value is `null`, then the parameter value is not checked.
22920
- */
22921
- pattern: nonEmptyStringSchema.nullable().default(null),
22922
- /**
22923
- * Default value of the parameter (if any).
22924
- */
22925
- default: nonEmptyStringSchema.nullable().default(null),
22926
- /**
22927
- * Describes whether the parameter is used only for debugging purposes.
22928
- */
22929
- debug: booleanSchema.default(false)
22930
- });
22931
- /**
22932
- * Zod schema for scriptlet parameters.
23367
+ * @file Cosmetic rule converter
22933
23368
  */
22934
- const scriptletParametersSchema = zod.array(scriptletParameterSchema);
22935
23369
  /**
22936
- * Zod schema for scriptlet data.
23370
+ * Cosmetic rule converter class (also known as "non-basic rule converter")
23371
+ *
23372
+ * @todo Implement `convertToUbo` and `convertToAbp`
22937
23373
  */
22938
- zodToCamelCase(baseCompatibilityDataSchema.extend({
23374
+ class CosmeticRuleConverter extends RuleConverterBase {
22939
23375
  /**
22940
- * List of parameters that the scriptlet accepts.
22941
- * **Every** parameter should be listed here, because we check that the scriptlet is used correctly
22942
- * (e.g. that the number of parameters is correct).
23376
+ * Converts a cosmetic rule to AdGuard syntax, if possible.
23377
+ *
23378
+ * @param rule Rule node to convert
23379
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
23380
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
23381
+ * If the rule was not converted, the result array will contain the original node with the same object reference
23382
+ * @throws If the rule is invalid or cannot be converted
22943
23383
  */
22944
- parameters: scriptletParametersSchema.optional()
22945
- }).superRefine((data, ctx) => {
22946
- // TODO: find something better, for now we can't add refine logic to the base schema:
22947
- // https://github.com/colinhacks/zod/issues/454#issuecomment-848370721
22948
- baseRefineLogic(data, ctx);
22949
- // we don't allow required parameters after optional ones
22950
- if (!data.parameters) {
22951
- return;
22952
- }
22953
- let optionalFound = false;
22954
- for (const parameter of data.parameters) {
22955
- if (optionalFound && parameter.required) {
22956
- ctx.addIssue({
22957
- code: zod.ZodIssueCode.custom,
22958
- message: 'Required parameters must be before optional ones'
23384
+ static convertToAdg(rule) {
23385
+ let subconverterResult;
23386
+ // Convert cosmetic rule based on its type
23387
+ switch (rule.type) {
23388
+ case exports.CosmeticRuleType.ElementHidingRule:
23389
+ subconverterResult = ElementHidingRuleConverter.convertToAdg(rule);
23390
+ break;
23391
+ case exports.CosmeticRuleType.ScriptletInjectionRule:
23392
+ subconverterResult = ScriptletRuleConverter.convertToAdg(rule);
23393
+ break;
23394
+ case exports.CosmeticRuleType.CssInjectionRule:
23395
+ subconverterResult = CssInjectionRuleConverter.convertToAdg(rule);
23396
+ break;
23397
+ case exports.CosmeticRuleType.HtmlFilteringRule:
23398
+ // Handle special case: uBO response header filtering rule
23399
+ // TODO: Optimize double CSS tokenization here
23400
+ subconverterResult = HeaderRemovalRuleConverter.convertToAdg(rule);
23401
+ if (subconverterResult.isConverted) {
23402
+ break;
23403
+ }
23404
+ subconverterResult = HtmlRuleConverter.convertToAdg(rule);
23405
+ break;
23406
+ // Note: Currently, only ADG supports JS injection rules, so we don't need to convert them
23407
+ case exports.CosmeticRuleType.JsInjectionRule:
23408
+ subconverterResult = createNodeConversionResult([rule], false);
23409
+ break;
23410
+ default:
23411
+ throw new RuleConversionError('Unsupported cosmetic rule type');
23412
+ }
23413
+ let convertedModifiers;
23414
+ // Convert cosmetic rule modifiers, if any
23415
+ if (rule.modifiers) {
23416
+ if (rule.syntax === exports.AdblockSyntax.Ubo) {
23417
+ // uBO doesn't support this rule:
23418
+ // example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
23419
+ if (rule.type === exports.CosmeticRuleType.ScriptletInjectionRule) {
23420
+ throw new RuleConversionError('uBO scriptlet injection rules don\'t support cosmetic rule modifiers');
23421
+ }
23422
+ convertedModifiers = AdgCosmeticRuleModifierConverter.convertFromUbo(rule.modifiers);
23423
+ } else if (rule.syntax === exports.AdblockSyntax.Abp) {
23424
+ // TODO: Implement once ABP starts supporting cosmetic rule modifiers
23425
+ throw new RuleConversionError('ABP don\'t support cosmetic rule modifiers');
23426
+ }
23427
+ }
23428
+ if (subconverterResult.result.length > 1 || subconverterResult.isConverted || convertedModifiers && convertedModifiers.isConverted) {
23429
+ // Add modifier list to the subconverter result rules
23430
+ subconverterResult.result.forEach(subconverterRule => {
23431
+ if (convertedModifiers && subconverterRule.category === exports.RuleCategory.Cosmetic) {
23432
+ // eslint-disable-next-line no-param-reassign
23433
+ subconverterRule.modifiers = convertedModifiers.result;
23434
+ }
22959
23435
  });
23436
+ return subconverterResult;
22960
23437
  }
22961
- if (!parameter.required) {
22962
- optionalFound = true;
23438
+ return createNodeConversionResult([rule], false);
23439
+ }
23440
+ /**
23441
+ * Converts a cosmetic rule to uBlock Origin syntax, if possible.
23442
+ *
23443
+ * @param rule Rule node to convert
23444
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
23445
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
23446
+ * If the rule was not converted, the result array will contain the original node with the same object reference
23447
+ * @throws If the rule is invalid or cannot be converted
23448
+ */
23449
+ // TODO: Add support for other cosmetic rule types
23450
+ static convertToUbo(rule) {
23451
+ // Convert cosmetic rule based on its type
23452
+ if (rule.type === exports.CosmeticRuleType.ScriptletInjectionRule) {
23453
+ if (rule.syntax === exports.AdblockSyntax.Adg && rule.modifiers?.children.length) {
23454
+ // e.g. example.com##+js(set-constant.js, foo, bar):matches-path(/baz)
23455
+ throw new RuleConversionError('uBO scriptlet injection rules do not support cosmetic rule modifiers');
23456
+ }
23457
+ return ScriptletRuleConverter.convertToUbo(rule);
22963
23458
  }
23459
+ return createNodeConversionResult([rule], false);
22964
23460
  }
22965
- }));
23461
+ }
22966
23462
 
22967
23463
  /**
22968
23464
  * @file Network rule modifier list converter.
@@ -22989,6 +23485,10 @@ const REDIRECT_MODIFIER = 'redirect';
22989
23485
  * @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#redirect-rule-modifier}
22990
23486
  */
22991
23487
  const REDIRECT_RULE_MODIFIER = 'redirect-rule';
23488
+ /**
23489
+ * @see {@link https://github.com/gorhill/uBlock/wiki/Resources-Library#empty-redirect-resources}
23490
+ */
23491
+ const UBO_NOOP_TEXT_RESOURCE = 'noop.txt';
22992
23492
  /**
22993
23493
  * Redirect-related modifiers.
22994
23494
  */
@@ -23138,6 +23638,112 @@ class NetworkRuleModifierListConverter extends ConverterBase {
23138
23638
  }
23139
23639
  return createConversionResult(modifierList, false);
23140
23640
  }
23641
+ /**
23642
+ * Converts a network rule modifier list to uBlock format, if possible.
23643
+ *
23644
+ * @param modifierList Network rule modifier list node to convert
23645
+ * @param isException If `true`, the rule is an exception rule
23646
+ * @returns An object which follows the {@link ConversionResult} interface. Its `result` property contains
23647
+ * the converted node, and its `isConverted` flag indicates whether the original node was converted.
23648
+ * If the node was not converted, the result will contain the original node with the same object reference
23649
+ * @throws If the conversion is not possible
23650
+ */
23651
+ // TODO: Optimize
23652
+ static convertToUbo(modifierList, isException = false) {
23653
+ const conversionMap = new MultiValueMap();
23654
+ const resourceTypeModifiersToAdd = new Set();
23655
+ modifierList.children.forEach((modifierNode, index) => {
23656
+ const originalModifierName = modifierNode.name.value;
23657
+ const modifierData = modifiersCompatibilityTable.getFirst(originalModifierName, exports.GenericPlatform.UboAny);
23658
+ // Handle special case: resource redirection modifiers
23659
+ if (REDIRECT_MODIFIERS.has(originalModifierName)) {
23660
+ // Redirect modifiers cannot be negated
23661
+ if (modifierNode.exception === true) {
23662
+ throw new RuleConversionError(`Modifier '${modifierNode.name.value}' cannot be negated`);
23663
+ }
23664
+ // Convert the redirect resource name to uBO format
23665
+ const redirectResourceName = modifierNode.value?.value;
23666
+ // Special case: for exception rules, $redirect without value is allowed,
23667
+ // and in this case it means an exception for all redirects
23668
+ if (!redirectResourceName && !isException) {
23669
+ throw new RuleConversionError(`No redirect resource specified for '${modifierNode.name.value}' modifier`);
23670
+ }
23671
+ if (!redirectResourceName) {
23672
+ // Jump to the next modifier if the redirect resource is not specified
23673
+ return;
23674
+ }
23675
+ // Leave $redirect and $redirect-rule modifiers as is, but convert $rewrite to $redirect
23676
+ const modifierName = modifierNode.name.value === ABP_REWRITE_MODIFIER ? REDIRECT_MODIFIER : modifierNode.name.value;
23677
+ const convertedRedirectResourceData = redirectsCompatibilityTable.getFirst(redirectResourceName, exports.GenericPlatform.UboAny);
23678
+ const convertedRedirectResourceName = convertedRedirectResourceData?.name ?? redirectResourceName;
23679
+ // uBlock requires the $redirect modifier to have a resource type
23680
+ // https://github.com/AdguardTeam/Scriptlets/issues/101
23681
+ if (convertedRedirectResourceData?.resourceTypes?.length) {
23682
+ // Convert the resource types to uBO modifiers
23683
+ const uboResourceTypeModifiers = redirectsCompatibilityTable.getResourceTypeModifiers(convertedRedirectResourceData, exports.GenericPlatform.UboAny);
23684
+ // Special case: noop text resource
23685
+ // If any of resource type is already present, we don't need to add other resource types,
23686
+ // otherwise, add all resource types
23687
+ // TODO: Optimize this logic
23688
+ // Check if the current resource is the noop text resource
23689
+ const isNoopTextResource = convertedRedirectResourceName === UBO_NOOP_TEXT_RESOURCE;
23690
+ // Determine if there are any valid resource types already present
23691
+ const hasValidResourceType = modifierList.children.some(modifier => {
23692
+ const name = modifier.name.value;
23693
+ if (!isValidResourceType(name)) {
23694
+ return false;
23695
+ }
23696
+ const convertedModifierData = modifiersCompatibilityTable.getFirst(name, exports.GenericPlatform.UboAny);
23697
+ return uboResourceTypeModifiers.has(convertedModifierData?.name ?? name);
23698
+ });
23699
+ // If it's not the noop text resource or if no valid resource types are present
23700
+ if (!isNoopTextResource || !hasValidResourceType) {
23701
+ uboResourceTypeModifiers.forEach(resourceType => {
23702
+ resourceTypeModifiersToAdd.add(resourceType);
23703
+ });
23704
+ }
23705
+ }
23706
+ // Check if the modifier name or the redirect resource name is different from the original modifier.
23707
+ // If so, add the converted modifier to the list
23708
+ if (modifierName !== originalModifierName || !isUndefined(convertedRedirectResourceName) && convertedRedirectResourceName !== redirectResourceName) {
23709
+ conversionMap.add(index, createModifierNode(modifierName,
23710
+ // If the redirect resource name is unknown, fall back to the original one
23711
+ // Later, the validator will throw an error if the resource name is invalid
23712
+ convertedRedirectResourceName || redirectResourceName, modifierNode.exception));
23713
+ }
23714
+ return;
23715
+ }
23716
+ // Generic modifier conversion
23717
+ if (modifierData && modifierData.name !== originalModifierName) {
23718
+ conversionMap.add(index, createModifierNode(modifierData.name, modifierNode.value?.value, modifierNode.exception));
23719
+ }
23720
+ });
23721
+ // Prepare the result if there are any converted modifiers or $csp modifiers
23722
+ if (conversionMap.size || resourceTypeModifiersToAdd.size) {
23723
+ const modifierListClone = cloneModifierListNode(modifierList);
23724
+ // Replace the original modifiers with the converted ones
23725
+ // One modifier may be replaced with multiple modifiers, so we need to flatten the array
23726
+ modifierListClone.children = modifierListClone.children.map((modifierNode, index) => {
23727
+ const conversionRecord = conversionMap.get(index);
23728
+ if (conversionRecord) {
23729
+ return conversionRecord;
23730
+ }
23731
+ return modifierNode;
23732
+ }).flat();
23733
+ // Before returning the result, remove duplicated modifiers
23734
+ modifierListClone.children = modifierListClone.children.filter((modifierNode, index, self) => self.findIndex(m => m.name.value === modifierNode.name.value && m.exception === modifierNode.exception && m.value?.value === modifierNode.value?.value) === index);
23735
+ if (resourceTypeModifiersToAdd.size) {
23736
+ const modifierNameSet = new Set(modifierList.children.map(m => m.name.value));
23737
+ resourceTypeModifiersToAdd.forEach(resourceType => {
23738
+ if (!modifierNameSet.has(resourceType)) {
23739
+ modifierListClone.children.push(createModifierNode(resourceType));
23740
+ }
23741
+ });
23742
+ }
23743
+ return createConversionResult(modifierListClone, true);
23744
+ }
23745
+ return createConversionResult(modifierList, false);
23746
+ }
23141
23747
  }
23142
23748
 
23143
23749
  /**
@@ -23187,6 +23793,44 @@ class NetworkRuleConverter extends RuleConverterBase {
23187
23793
  // If the modifiers were not converted, return the original rule
23188
23794
  return createNodeConversionResult([rule], false);
23189
23795
  }
23796
+ /**
23797
+ * Converts a network rule to uBlock format, if possible.
23798
+ *
23799
+ * @param rule Rule node to convert
23800
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
23801
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
23802
+ * If the rule was not converted, the result array will contain the original node with the same object reference
23803
+ * @throws If the rule is invalid or cannot be converted
23804
+ */
23805
+ static convertToUbo(rule) {
23806
+ // TODO: add support for host rules
23807
+ if (rule.type !== exports.NetworkRuleType.NetworkRule) {
23808
+ throw new Error(`Invalid rule type: ${rule.type}`);
23809
+ }
23810
+ if (rule.modifiers) {
23811
+ const modifiers = NetworkRuleModifierListConverter.convertToUbo(rule.modifiers, rule.exception);
23812
+ // If the object reference is different, it means that the modifiers were converted
23813
+ // In this case, we should clone the entire rule and replace the modifiers with the converted ones
23814
+ if (modifiers.isConverted) {
23815
+ return {
23816
+ result: [{
23817
+ category: exports.RuleCategory.Network,
23818
+ type: exports.NetworkRuleType.NetworkRule,
23819
+ syntax: rule.syntax,
23820
+ exception: rule.exception,
23821
+ pattern: {
23822
+ type: 'Value',
23823
+ value: rule.pattern.value
23824
+ },
23825
+ modifiers: modifiers.result
23826
+ }],
23827
+ isConverted: true
23828
+ };
23829
+ }
23830
+ }
23831
+ // If the modifiers were not converted, return the original rule
23832
+ return createNodeConversionResult([rule], false);
23833
+ }
23190
23834
  }
23191
23835
 
23192
23836
  /**
@@ -23234,6 +23878,25 @@ class RuleConverter extends RuleConverterBase {
23234
23878
  throw new RuleConversionError('Unknown rule category');
23235
23879
  }
23236
23880
  }
23881
+ /**
23882
+ * Converts an adblock filtering rule to uBlock Origin format, if possible.
23883
+ *
23884
+ * @param rule Rule node to convert
23885
+ * @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
23886
+ * the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
23887
+ * If the rule was not converted, the result array will contain the original node with the same object reference
23888
+ * @throws If the rule is invalid or cannot be converted
23889
+ */
23890
+ // TODO: Add support for other rule types
23891
+ static convertToUbo(rule) {
23892
+ if (rule.category === exports.RuleCategory.Cosmetic) {
23893
+ return CosmeticRuleConverter.convertToUbo(rule);
23894
+ }
23895
+ if (rule.category === exports.RuleCategory.Network) {
23896
+ return NetworkRuleConverter.convertToUbo(rule);
23897
+ }
23898
+ return createConversionResult([rule], false);
23899
+ }
23237
23900
  }
23238
23901
 
23239
23902
  /**
@@ -23627,7 +24290,7 @@ class ByteBuffer {
23627
24290
  * @see {@link https://stackoverflow.com/a/62797156}
23628
24291
  */
23629
24292
  const isChromium = () => {
23630
- return typeof window !== 'undefined' && (Object.prototype.hasOwnProperty.call(window, 'chrome') || typeof window.navigator !== 'undefined' && /chrome/i.test(window.navigator.userAgent || ''));
24293
+ return typeof window !== 'undefined' && (Object.prototype.hasOwnProperty.call(window, 'chrome') || typeof window.navigator !== 'undefined' && /chrome/i.test(window.navigator.userAgent));
23631
24294
  };
23632
24295
 
23633
24296
  /* eslint-disable no-param-reassign */
@@ -23718,16 +24381,23 @@ class OutputByteBuffer extends ByteBuffer {
23718
24381
  */
23719
24382
  offset;
23720
24383
  /**
23721
- * Size of the shared buffer for encoding strings.
24384
+ * Size of the shared buffer for encoding strings in bytes.
24385
+ * This is a divisor of ByteBuffer.CHUNK_SIZE and experience shows that this value works optimally.
24386
+ * This is sufficient for most strings that occur in filter lists (we checked average string length in popular
24387
+ * filter lists).
23722
24388
  */
23723
24389
  static ENCODER_BUFFER_SIZE = 8192;
23724
24390
  /**
23725
- * Threshold for using a shared buffer for encoding strings.
24391
+ * Length threshold for using a shared buffer for encoding strings.
24392
+ * This temp buffer is needed because we write the short strings in it
24393
+ * (so there is no need to constantly allocate a new buffer).
24394
+ * The reason for dividing ENCODER_BUFFER_SIZE by 4 is to ensure that the encoded string fits in the buffer,
24395
+ * if we also take into account the worst possible case (each character is encoded with 4 bytes).
23726
24396
  */
23727
24397
  static SHORT_STRING_THRESHOLD = 2048; // 8192 / 4
23728
24398
  /**
23729
24399
  * Represents the maximum value that can be written as a 'storage optimized' unsigned integer.
23730
- * 0x1FFFFFFF means 32 bits minus 3 bits, because the last bit in each byte is a flag indicating
24400
+ * 0x1FFFFFFF means 29 bits — 32 bits minus 3 bits because the last bit in each byte is a flag indicating
23731
24401
  * if there are more bytes (except for the last byte).
23732
24402
  */
23733
24403
  static MAX_OPTIMIZED_UINT = 0x1FFFFFFF;
@@ -24274,7 +24944,7 @@ class RuleCategorizer {
24274
24944
  }
24275
24945
  }
24276
24946
  }
24277
- const version = "2.0.0-alpha.0";
24947
+ const version = "2.0.1";
24278
24948
 
24279
24949
  /**
24280
24950
  * @file AGTree version
@@ -24355,8 +25025,10 @@ exports.decodeTextPolyfill = decodeTextPolyfill;
24355
25025
  exports.defaultParserOptions = defaultParserOptions;
24356
25026
  exports.encodeIntoPolyfill = encodeIntoPolyfill;
24357
25027
  exports.getPlatformId = getPlatformId;
25028
+ exports.getResourceTypeModifier = getResourceTypeModifier;
24358
25029
  exports.getSpecificPlatformName = getSpecificPlatformName;
24359
25030
  exports.isGenericPlatform = isGenericPlatform;
25031
+ exports.isValidResourceType = isValidResourceType;
24360
25032
  exports.modifierValidator = modifierValidator;
24361
25033
  exports.modifiersCompatibilityTable = modifiersCompatibilityTable;
24362
25034
  exports.parseRawPlatforms = parseRawPlatforms;