html-minifier 4.0.0 → 5.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.
Files changed (55) hide show
  1. package/LICENSE.txt +23 -0
  2. package/README.md +851 -110
  3. package/cli.js +991 -207
  4. package/dist/types/htmlminifier.d.ts +629 -0
  5. package/dist/types/htmlminifier.d.ts.map +1 -0
  6. package/dist/types/htmlparser.d.ts +28 -0
  7. package/dist/types/htmlparser.d.ts.map +1 -0
  8. package/dist/types/lib/attributes.d.ts +87 -0
  9. package/dist/types/lib/attributes.d.ts.map +1 -0
  10. package/dist/types/lib/constants.d.ts +421 -0
  11. package/dist/types/lib/constants.d.ts.map +1 -0
  12. package/dist/types/lib/content.d.ts +44 -0
  13. package/dist/types/lib/content.d.ts.map +1 -0
  14. package/dist/types/lib/elements.d.ts +48 -0
  15. package/dist/types/lib/elements.d.ts.map +1 -0
  16. package/dist/types/lib/fragments.d.ts +47 -0
  17. package/dist/types/lib/fragments.d.ts.map +1 -0
  18. package/dist/types/lib/option-definitions.d.ts +33 -0
  19. package/dist/types/lib/option-definitions.d.ts.map +1 -0
  20. package/dist/types/lib/options.d.ts +93 -0
  21. package/dist/types/lib/options.d.ts.map +1 -0
  22. package/dist/types/lib/unused-css.d.ts +76 -0
  23. package/dist/types/lib/unused-css.d.ts.map +1 -0
  24. package/dist/types/lib/urls.d.ts +15 -0
  25. package/dist/types/lib/urls.d.ts.map +1 -0
  26. package/dist/types/lib/utils.d.ts +132 -0
  27. package/dist/types/lib/utils.d.ts.map +1 -0
  28. package/dist/types/lib/whitespace.d.ts +97 -0
  29. package/dist/types/lib/whitespace.d.ts.map +1 -0
  30. package/dist/types/presets.d.ts +50 -0
  31. package/dist/types/presets.d.ts.map +1 -0
  32. package/dist/types/tokenchain.d.ts +36 -0
  33. package/dist/types/tokenchain.d.ts.map +1 -0
  34. package/html-minifier.schema.json +322 -0
  35. package/package.json +71 -60
  36. package/src/htmlminifier.js +2862 -1106
  37. package/src/htmlparser.js +1163 -452
  38. package/src/lib/attributes.js +1196 -0
  39. package/src/lib/constants.js +521 -0
  40. package/src/lib/content.js +237 -0
  41. package/src/lib/elements.js +268 -0
  42. package/src/lib/file-pool.js +193 -0
  43. package/src/lib/file-write.js +87 -0
  44. package/src/lib/fragments.js +340 -0
  45. package/src/lib/option-definitions.js +246 -0
  46. package/src/lib/options.js +841 -0
  47. package/src/lib/unused-css.js +499 -0
  48. package/src/lib/urls.js +146 -0
  49. package/src/lib/utils.js +1026 -0
  50. package/src/lib/whitespace.js +431 -0
  51. package/src/presets.js +58 -0
  52. package/src/tokenchain.js +142 -56
  53. package/LICENSE +0 -22
  54. package/sample-cli-config-file.conf +0 -40
  55. package/src/utils.js +0 -18
@@ -0,0 +1,1026 @@
1
+ /**
2
+ * General utility functions
3
+ */
4
+
5
+ import { quirksPublicIdPrefixes, quirksPublicIdPrefixesNoSystemId, quirksPublicIds, quirksSystemIds, RE_DOCTYPE, RE_XML_ESCAPE_ATTR, RE_XML_ESCAPE_TEXT } from './constants.js';
6
+
7
+ // Functions and other non-plain objects have no structure to compare (closures with the
8
+ // same source can behave differently), so they are told apart by identity
9
+ /** @type {WeakMap<object, number>} */
10
+ const objectIds = new WeakMap();
11
+ let objectIdLast = 0;
12
+
13
+ /** @param {object} obj */
14
+ function objectId(obj) {
15
+ let id = objectIds.get(obj);
16
+ if (id === undefined) {
17
+ id = ++objectIdLast;
18
+ objectIds.set(obj, id);
19
+ }
20
+ return id;
21
+ }
22
+
23
+ /**
24
+ * Stringify with sorted keys, for cache keys—values that can behave differently must not
25
+ * stringify alike, so unquoted markers stand in for what JSON cannot express
26
+ * @param {unknown} obj
27
+ * @returns {string}
28
+ */
29
+ function stableStringify(obj) {
30
+ if (typeof obj === 'function') return 'function#' + objectId(obj);
31
+ if (obj == null || typeof obj !== 'object') return JSON.stringify(obj);
32
+ if (obj instanceof RegExp) return String(obj);
33
+ if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
34
+ const proto = Object.getPrototypeOf(obj);
35
+ if (proto !== Object.prototype && proto !== null) return 'object#' + objectId(obj);
36
+ const keys = Object.keys(obj).sort();
37
+ let out = '{';
38
+ for (let i = 0; i < keys.length; i++) {
39
+ const k = keys[i] ?? '';
40
+ out += JSON.stringify(k) + ':' + stableStringify(/** @type {Record<string, unknown>} */ (obj)[k]) + (i < keys.length - 1 ? ',' : '');
41
+ }
42
+ return out + '}';
43
+ }
44
+
45
+ // LRU cache for strings and promises
46
+
47
+ class LRU {
48
+ constructor(limit = 200) {
49
+ this.limit = limit;
50
+ this.gets = 0;
51
+ this.hits = 0;
52
+ /** @type {Map<string, unknown>} */
53
+ this.map = new Map();
54
+ }
55
+ /** @param {string} key */
56
+ get(key) {
57
+ this.gets++;
58
+ if (this.map.has(key)) {
59
+ this.hits++;
60
+ const v = this.map.get(key);
61
+ this.map.delete(key);
62
+ this.map.set(key, v);
63
+ return v;
64
+ }
65
+ return undefined;
66
+ }
67
+ /**
68
+ * @param {string} key
69
+ * @param {unknown} value
70
+ */
71
+ set(key, value) {
72
+ if (this.map.has(key)) this.map.delete(key);
73
+ this.map.set(key, value);
74
+ if (this.map.size > this.limit) {
75
+ const first = this.map.keys().next().value;
76
+ if (first !== undefined) this.map.delete(first);
77
+ }
78
+ }
79
+ /** @param {string} key */
80
+ delete(key) { this.map.delete(key); }
81
+ /** @returns {{ gets: number, hits: number, size: number, limit: number }} */
82
+ stats() {
83
+ return { gets: this.gets, hits: this.hits, size: this.map.size, limit: this.limit };
84
+ }
85
+ }
86
+
87
+ // Content longer than this (in UTF-16 code units, roughly bytes for typical CSS/JS/SVG) is
88
+ // minified normally but never stored in a minification cache—caps worst-case cache memory
89
+ // (entry count × this size) without affecting realistically sized inline content
90
+ const MAX_CACHE_ENTRY_SIZE = 1024 * 1024; // 1 MB
91
+
92
+ // 53-bit hash (cyrb53) for large-input cache keys, where a collision would hand one
93
+ // input another’s result; as fast as a 32-bit hash, whose collisions are within reach
94
+
95
+ /** @param {string} str */
96
+ function hashContent(str) {
97
+ let h1 = 0xdeadbeef;
98
+ let h2 = 0x41c6ce57;
99
+ for (let i = 0; i < str.length; i++) {
100
+ const code = str.charCodeAt(i);
101
+ h1 = Math.imul(h1 ^ code, 2654435761);
102
+ h2 = Math.imul(h2 ^ code, 1597334677);
103
+ }
104
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
105
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
106
+ return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36);
107
+ }
108
+
109
+ /** @param {string} value */
110
+ function uniqueId(value) {
111
+ let id;
112
+ do {
113
+ id = 'u' + crypto.randomUUID().replace(/-/g, '');
114
+ } while (~value.indexOf(id));
115
+ return id;
116
+ }
117
+
118
+ /** @type {Record<string, string>} */
119
+ const xmlEscapes = { '&': '&amp;', '<': '&lt;', ']]>': ']]&gt;', '\t': '&#9;', '\n': '&#10;', '\r': '&#13;' };
120
+
121
+ /** @param {string} match */
122
+ function escapeXMLMatch(match) {
123
+ return xmlEscapes[match] ?? match;
124
+ }
125
+
126
+ /**
127
+ * Write decoded text or an attribute value for an XML parser to read it the same
128
+ * @param {string} value - Text or attribute value, decoded
129
+ * @param {boolean} [attribute] - Whether the value is an attribute value
130
+ * @returns {string}
131
+ */
132
+ function escapeXML(value, attribute = false) {
133
+ return value.replace(attribute ? RE_XML_ESCAPE_ATTR : RE_XML_ESCAPE_TEXT, escapeXMLMatch);
134
+ }
135
+
136
+ /**
137
+ * Read text in foreign content as HTML does: CDATA sections hold text as written,
138
+ * while what surrounds them reads character references
139
+ * @param {string} text
140
+ * @param {(text: string) => string} decode - `decodeHTML`
141
+ * @returns {string}
142
+ */
143
+ function decodeForeignText(text, decode) {
144
+ let out = '';
145
+ let pos = 0;
146
+ while (pos < text.length) {
147
+ const start = text.indexOf('<![CDATA[', pos);
148
+ if (start === -1) return out + decode(text.slice(pos));
149
+ out += decode(text.slice(pos, start));
150
+ // An unclosed section runs to the end
151
+ const end = text.indexOf(']]>', start + 9);
152
+ if (end === -1) return out + text.slice(start + 9);
153
+ out += text.slice(start + 9, end);
154
+ pos = end + 3;
155
+ }
156
+ return out;
157
+ }
158
+
159
+ /** @param {string} value */
160
+ function identity(value) {
161
+ return value;
162
+ }
163
+
164
+ /**
165
+ * @param {unknown} value
166
+ * @returns {value is PromiseLike<any>}
167
+ */
168
+ function isThenable(value) {
169
+ return value != null && typeof value === 'object' && typeof /** @type {any} */ (value).then === 'function';
170
+ }
171
+
172
+ /** @param {string} value */
173
+ function lowercase(value) {
174
+ return value.toLowerCase();
175
+ }
176
+
177
+ // Option name to CLI flag name (names are ASCII, so no Unicode handling is needed)
178
+ /** @param {string} value */
179
+ function paramCase(value) {
180
+ return value.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
181
+ }
182
+
183
+ /**
184
+ * Asynchronously replace matches in a string.
185
+ * @param {string} str - Input string
186
+ * @param {RegExp} regex - Regular expression with global flag
187
+ * @param {Function} asyncFn - Async function to process each match
188
+ * @returns {Promise<string>} Processed string
189
+ */
190
+ async function replaceAsync(str, regex, asyncFn) {
191
+ /** @type {Promise<string>[]} */
192
+ const promises = [];
193
+
194
+ str.replace(regex, /** @returns {string} */ (match, ...args) => {
195
+ const promise = asyncFn(match, ...args);
196
+ promises.push(promise);
197
+ return match;
198
+ });
199
+
200
+ const data = await Promise.all(promises);
201
+ let next = 0;
202
+ return str.replace(regex, () => data[next++] ?? '');
203
+ }
204
+
205
+ // String patterns to RegExp conversion (for JSON config support)
206
+
207
+ /** @param {string | RegExp} value */
208
+ function parseRegExp(value) {
209
+ if (typeof value === 'string') {
210
+ if (!value) return undefined; // Empty string = not configured
211
+ const match = value.match(/^\/(.+)\/([dgimsuvy]*)$/);
212
+ if (match) {
213
+ return new RegExp(match[1] ?? '', match[2] ?? '');
214
+ }
215
+ return new RegExp(value);
216
+ }
217
+ return value;
218
+ }
219
+
220
+ // Regex source reading, shared by the analysis and the rewriting below
221
+
222
+ const RE_ASCII_LETTER = /^[a-zA-Z]$/;
223
+ const RE_HEX_PAIR = /^[0-9a-fA-F]{2}$/;
224
+ const RE_HEX_QUAD = /^[0-9a-fA-F]{4}$/;
225
+
226
+ /**
227
+ * @param {string} source @param {number} index - Index of the opening `[`
228
+ * @param {boolean} [nested] - Whether the pattern carries `v`, where a class nests
229
+ * inside a class and the first `]` need not be the one that closes it
230
+ * @returns {number} Index just past the closing `]`
231
+ */
232
+ function skipCharacterClass(source, index, nested = false) {
233
+ let i = index + 1;
234
+ let open = 1;
235
+ while (i < source.length && open > 0) {
236
+ const char = source[i];
237
+ if (char === '\\') i += tokenLength(source, i);
238
+ else {
239
+ if (nested && char === '[') open++;
240
+ else if (char === ']') open--;
241
+ i++;
242
+ }
243
+ }
244
+ return i;
245
+ }
246
+
247
+ /**
248
+ * Length of the token at `index`, so that a multi-character escape is read whole
249
+ * rather than leaving its tail to be mistaken for literals.
250
+ * @param {string} source @param {number} index
251
+ */
252
+ function tokenLength(source, index) {
253
+ if (source[index] !== '\\') return 1;
254
+ const next = source[index + 1];
255
+ if (next === 'x' && RE_HEX_PAIR.test(source.slice(index + 2, index + 4))) return 4;
256
+ if (next === 'u') {
257
+ if (source[index + 2] === '{') {
258
+ const close = source.indexOf('}', index + 3);
259
+ if (close !== -1) return close - index + 1;
260
+ }
261
+ if (RE_HEX_QUAD.test(source.slice(index + 2, index + 6))) return 6;
262
+ }
263
+ if (next === 'c' && RE_ASCII_LETTER.test(source[index + 2] ?? '')) return 3;
264
+ // A `v` class holds string literals, which fold as strings or not at all
265
+ if (next === 'q' && source[index + 2] === '{') {
266
+ const close = source.indexOf('}', index + 3);
267
+ if (close !== -1) return close - index + 1;
268
+ }
269
+ // A property name or a group name is syntax, not text to fold
270
+ if ((next === 'p' || next === 'P') && source[index + 2] === '{') {
271
+ const close = source.indexOf('}', index + 3);
272
+ if (close !== -1) return close - index + 1;
273
+ }
274
+ if (next === 'k' && source[index + 2] === '<') {
275
+ const close = source.indexOf('>', index + 3);
276
+ if (close !== -1) return close - index + 1;
277
+ }
278
+ return 2;
279
+ }
280
+
281
+ // ReDoS risk detection for user-supplied patterns
282
+
283
+ // Quantifier following an atom, with its optional lazy `?`; the captures hold
284
+ // the bounds of a `{n,m}` form, the upper one empty when the form is `{n,}`
285
+ const RE_QUANTIFIER = /(?:[*+?]|\{(\d+)(?:,(\d*))?\})\??/y;
286
+
287
+ // Bounds for the walk below: Patterns beyond either are judged risky unread,
288
+ // which keeps a pathological source from nesting the analysis into a stack
289
+ // overflow or making it rescan its groups once per level
290
+ const MAX_PATTERN_LENGTH = 10000;
291
+ const MAX_PATTERN_DEPTH = 50;
292
+
293
+ // A group that only wraps its body backtracks the way that body does, so
294
+ // `(?:a)` and `a` count as the same atom; lookarounds are left alone
295
+ const RE_TRANSPARENT_GROUP = /^\((?:\?:|\?<[^>=!][^>]*>)?([\s\S]*)\)$/;
296
+
297
+ // A quantifier of exactly one repetition, which is notation rather than shape
298
+ const RE_EXACT_ONE = /\{1(?:,1)?\}\??$/;
299
+
300
+ // An atom that matches without consuming, so what precedes it stays adjacent to
301
+ // what follows; a group that can match empty is left out, being rare enough that
302
+ // missing it costs less than reading every body for it
303
+ const RE_ZERO_WIDTH = /^(?:[$^]|\\[bB]|\((?:\?[=!]|\?<[=!]))/;
304
+
305
+ // How many repeats an atom may still be adjacent to across atoms matching empty;
306
+ // comparing against every one of them is what the bound keeps from turning
307
+ // quadratic on a pattern that is nothing but repeats
308
+ const MAX_REACHABLE = 50;
309
+
310
+ /** @param {string} atom - Atom as it reads in the source */
311
+ function unwrapAtom(atom) {
312
+ let inner = atom;
313
+ for (let level = 0; level < MAX_PATTERN_DEPTH; level++) {
314
+ const next = inner.replace(RE_EXACT_ONE, '').replace(RE_TRANSPARENT_GROUP, '$1');
315
+ if (next === inner) break;
316
+ inner = next;
317
+ }
318
+ return inner;
319
+ }
320
+
321
+ // Two atoms that repeat unboundedly side by side split a run between them in as
322
+ // many ways as the run is long, whenever both can consume the same character.
323
+ // Comparing what they match, rather than how they are spelled, is what catches
324
+ // `[a]*a*` and `\w*\d*` alongside `a*a*`.
325
+
326
+ const MAX_CODE_POINT = 0x10FFFF;
327
+
328
+ /** @type {Record<string, [number, number][]>} */
329
+ const CLASS_ESCAPE_RANGES = {
330
+ d: [[0x30, 0x39]],
331
+ w: [[0x30, 0x39], [0x41, 0x5A], [0x5F, 0x5F], [0x61, 0x7A]],
332
+ s: [[0x09, 0x0D], [0x20, 0x20], [0xA0, 0xA0], [0x1680, 0x1680], [0x2000, 0x200A],
333
+ [0x2028, 0x2029], [0x202F, 0x202F], [0x205F, 0x205F], [0x3000, 0x3000], [0xFEFF, 0xFEFF]]
334
+ };
335
+
336
+ /** @type {Record<string, number>} */
337
+ const CONTROL_ESCAPE_CODES = { 0: 0x00, f: 0x0C, n: 0x0A, r: 0x0D, t: 0x09, v: 0x0B };
338
+
339
+ // An escape that stands for something other than one character of text
340
+ const RE_NON_CHARACTER_ESCAPE = /[bBdDkpPsSwW1-9]/;
341
+
342
+ /** @param {[number, number][]} ranges */
343
+ function complement(ranges) {
344
+ const sorted = [...ranges].sort((one, other) => one[0] - other[0]);
345
+ /** @type {[number, number][]} */
346
+ const out = [];
347
+ let next = 0;
348
+ for (const [low, high] of sorted) {
349
+ if (low > next) out.push([next, low - 1]);
350
+ next = Math.max(next, high + 1);
351
+ }
352
+ if (next <= MAX_CODE_POINT) out.push([next, MAX_CODE_POINT]);
353
+ return out;
354
+ }
355
+
356
+ // `.` as a bare source reads it, without the `s` flag the source cannot carry
357
+ const DOT_RANGES = complement([[0x0A, 0x0A], [0x0D, 0x0D], [0x2028, 0x2029]]);
358
+
359
+ /**
360
+ * @param {string} token - One token, as `tokenLength` measures it
361
+ * @param {boolean} [inClass] - Whether the token sits inside a character class
362
+ * @returns {[number, number][] | null} What it matches, or `null` where it is not
363
+ * one character of text
364
+ */
365
+ function tokenRanges(token, inClass = false) {
366
+ if (token[0] !== '\\') {
367
+ const code = token.codePointAt(0) ?? 0;
368
+ return String.fromCodePoint(code) === token ? [[code, code]] : null;
369
+ }
370
+
371
+ const kind = token[1] ?? '';
372
+ const named = CLASS_ESCAPE_RANGES[kind.toLowerCase()];
373
+ if (named) return kind === kind.toLowerCase() ? named : complement(named);
374
+
375
+ const control = CONTROL_ESCAPE_CODES[kind];
376
+ if (control !== undefined) return [[control, control]];
377
+
378
+ if (kind === 'x' || kind === 'u') {
379
+ const code = Number.parseInt(token[2] === '{' ? token.slice(3, -1) : token.slice(2), 16);
380
+ return Number.isNaN(code) || code > MAX_CODE_POINT ? null : [[code, code]];
381
+ }
382
+ if (kind === 'c') {
383
+ const code = token.charCodeAt(2) % 32;
384
+ return [[code, code]];
385
+ }
386
+ // `\b` asserts a word boundary on its own, and is a backspace inside a class
387
+ if (kind === 'b' && inClass) return [[0x08, 0x08]];
388
+ // `\q{…}` stands for whole strings, not for a character
389
+ if (token.startsWith('\\q{')) return null;
390
+ if (RE_NON_CHARACTER_ESCAPE.test(kind)) return null;
391
+
392
+ // The rest is punctuation escaped to be read as itself
393
+ const code = token.codePointAt(1) ?? 0;
394
+ return String.fromCodePoint(code) === token.slice(1) ? [[code, code]] : null;
395
+ }
396
+
397
+ /** @param {[number, number][] | null} ranges @returns {number | null} The one code point it holds */
398
+ function singleCode(ranges) {
399
+ const [range] = ranges ?? [];
400
+ return ranges?.length === 1 && range && range[0] === range[1] ? range[0] : null;
401
+ }
402
+
403
+ /**
404
+ * @param {string} source @param {number} i @param {boolean} nested - Whether the pattern carries `v`
405
+ * @returns {boolean} Whether a `v` class subtracts or intersects at the position
406
+ */
407
+ function isSetOperator(source, i, nested) {
408
+ return nested && i >= 0 && (source.startsWith('--', i) || source.startsWith('&&', i));
409
+ }
410
+
411
+ /**
412
+ * @param {string} atom - Atom as it reads in the source, already unwrapped
413
+ * @param {boolean} [nested] - Whether the pattern carries `v`
414
+ * @param {number} [depth] - Class nesting level of this call
415
+ * @returns {[number, number][] | null} What it matches, or `null` where it is not
416
+ * one character of text, or one this does not read
417
+ */
418
+ function atomRanges(atom, nested = false, depth = 0) {
419
+ if (depth > MAX_PATTERN_DEPTH) return null;
420
+ if (atom === '.') return DOT_RANGES;
421
+ if (atom[0] !== '[') return tokenRanges(atom);
422
+ if (!atom.endsWith(']')) return null;
423
+
424
+ let i = atom[1] === '^' ? 2 : 1;
425
+ const negated = i === 2;
426
+ const end = atom.length - 1;
427
+ /** @type {[number, number][]} */
428
+ let members = [];
429
+ // A `v` class subtracts or intersects where it does not unite, one operand at a time
430
+ let operator = '';
431
+
432
+ while (i < end) {
433
+ if (isSetOperator(atom, i, nested)) {
434
+ operator = atom.slice(i, i + 2);
435
+ i += 2;
436
+ continue;
437
+ }
438
+
439
+ /** @type {[number, number][] | null} */
440
+ let operand;
441
+ if (nested && atom[i] === '[') {
442
+ const close = skipCharacterClass(atom, i, true);
443
+ operand = atomRanges(atom.slice(i, close), true, depth + 1);
444
+ i = close;
445
+ } else {
446
+ const fromLength = tokenLength(atom, i);
447
+ operand = tokenRanges(atom.slice(i, i + fromLength), true);
448
+ i += fromLength;
449
+ // A dash right before the closing `]` is a member, not the start of a range,
450
+ // and so is the `--` that subtracts
451
+ if (operand && atom[i] === '-' && i + 1 < end && !isSetOperator(atom, i, nested)) {
452
+ const toLength = tokenLength(atom, i + 1);
453
+ // Only single characters bound a range; `\d-z` is not a range at all
454
+ const low = singleCode(operand);
455
+ const high = singleCode(tokenRanges(atom.slice(i + 1, i + 1 + toLength), true));
456
+ operand = low === null || high === null ? null : [[low, high]];
457
+ i += 1 + toLength;
458
+ }
459
+ }
460
+ if (!operand) return null;
461
+
462
+ members = operator === '--' ? intersectRanges(members, complement(operand))
463
+ : operator === '&&' ? intersectRanges(members, operand)
464
+ : members.concat(operand);
465
+ }
466
+
467
+ return negated ? complement(members) : members;
468
+ }
469
+
470
+ /** @param {[number, number][]} ranges @returns {[number, number][]} The same set, sorted and merged */
471
+ function mergeRanges(ranges) {
472
+ const sorted = [...ranges].sort((one, other) => one[0] - other[0]);
473
+ /** @type {[number, number][]} */
474
+ const out = [];
475
+ for (const [low, high] of sorted) {
476
+ const last = out[out.length - 1];
477
+ if (last && low <= last[1] + 1) last[1] = Math.max(last[1], high);
478
+ else out.push([low, high]);
479
+ }
480
+ return out;
481
+ }
482
+
483
+ /**
484
+ * @param {[number, number][]} ranges @param {[number, number][]} other
485
+ * @returns {[number, number][]} What both match, merged so that repeated
486
+ * operations cannot multiply the pieces
487
+ */
488
+ function intersectRanges(ranges, other) {
489
+ const one = mergeRanges(ranges);
490
+ const two = mergeRanges(other);
491
+ /** @type {[number, number][]} */
492
+ const out = [];
493
+ let i = 0;
494
+ let j = 0;
495
+ while (i < one.length && j < two.length) {
496
+ const [low, high] = /** @type {[number, number]} */ (one[i]);
497
+ const [otherLow, otherHigh] = /** @type {[number, number]} */ (two[j]);
498
+ const from = Math.max(low, otherLow);
499
+ const to = Math.min(high, otherHigh);
500
+ if (from <= to) out.push([from, to]);
501
+ if (high < otherHigh) i++;
502
+ else j++;
503
+ }
504
+ return out;
505
+ }
506
+
507
+ /** @param {[number, number][]} ranges @param {[number, number][]} other */
508
+ function rangesIntersect(ranges, other) {
509
+ return ranges.some(([low, high]) => other.some(([otherLow, otherHigh]) =>
510
+ low <= otherHigh && otherLow <= high));
511
+ }
512
+
513
+ // Embedding a pattern in a larger regex drops the flags it carried, so the two
514
+ // a source can carry on its own are rewritten into it
515
+ //
516
+ // @@ Replace with inline `(?i:…)` and `(?s:…)` modifiers once Node floor reaches 24
517
+
518
+ /** @param {string} char @returns {boolean} Whether the character has a single-character counterpart */
519
+ function foldsCase(char) {
520
+ const lower = char.toLowerCase();
521
+ const upper = char.toUpperCase();
522
+ return lower !== upper && lower.length === 1 && upper.length === 1;
523
+ }
524
+
525
+ /** @param {string} char */
526
+ function isLower(char) {
527
+ return char === char.toLowerCase();
528
+ }
529
+
530
+ /**
531
+ * @param {string} source - Regex source, assumed syntactically valid
532
+ * @param {boolean} [nested] - Whether the pattern carries `v`
533
+ */
534
+ function expandDotAll(source, nested = false) {
535
+ let out = '';
536
+ let i = 0;
537
+ while (i < source.length) {
538
+ const char = source[i];
539
+ if (char === '\\') {
540
+ const length = tokenLength(source, i);
541
+ out += source.slice(i, i + length);
542
+ i += length;
543
+ } else if (char === '[') {
544
+ const end = skipCharacterClass(source, i, nested);
545
+ out += source.slice(i, end);
546
+ i = end;
547
+ } else if (char === '.') {
548
+ out += '[\\s\\S]';
549
+ i++;
550
+ } else {
551
+ out += char;
552
+ i++;
553
+ }
554
+ }
555
+ return out;
556
+ }
557
+
558
+ /**
559
+ * @param {string} source - Regex source, assumed syntactically valid
560
+ * @param {boolean} [nested] - Whether the pattern carries `v`
561
+ */
562
+ function foldCase(source, nested = false) {
563
+ let out = '';
564
+ let i = 0;
565
+ let open = 0;
566
+
567
+ while (i < source.length) {
568
+ const char = source[i] ?? '';
569
+
570
+ if (open === 0) {
571
+ if (char === '\\') {
572
+ const length = tokenLength(source, i);
573
+ out += source.slice(i, i + length);
574
+ i += length;
575
+ } else if (char === '[') {
576
+ open = 1;
577
+ out += char;
578
+ i++;
579
+ } else if (char === '(' && source.startsWith('(?<', i) &&
580
+ source[i + 3] !== '=' && source[i + 3] !== '!') {
581
+ // A capture group’s name is syntax, and folding it is a syntax error
582
+ const close = source.indexOf('>', i + 3);
583
+ const end = close === -1 ? i + 3 : close + 1;
584
+ out += source.slice(i, end);
585
+ i = end;
586
+ } else {
587
+ out += foldsCase(char) ? '[' + char.toLowerCase() + char.toUpperCase() + ']' : char;
588
+ i++;
589
+ }
590
+ continue;
591
+ }
592
+
593
+ if (nested && char === '[') {
594
+ open++;
595
+ out += char;
596
+ i++;
597
+ continue;
598
+ }
599
+
600
+ if (char === ']') {
601
+ open--;
602
+ out += char;
603
+ i++;
604
+ continue;
605
+ }
606
+
607
+ if (isSetOperator(source, i, nested)) {
608
+ out += source.slice(i, i + 2);
609
+ i += 2;
610
+ continue;
611
+ }
612
+
613
+ const fromLength = tokenLength(source, i);
614
+ const from = source.slice(i, i + fromLength);
615
+ const afterFrom = i + fromLength;
616
+
617
+ // A range needs the other case’s range beside it—only where both ends are
618
+ // ASCII letters, the one span whose two cases are contiguous and parallel
619
+ // (`ÿ` uppercases to `Ÿ`, 150 code points past where its range ends)
620
+ if (source[afterFrom] === '-' && afterFrom + 1 < source.length && source[afterFrom + 1] !== ']' && !isSetOperator(source, afterFrom, nested)) {
621
+ const toLength = tokenLength(source, afterFrom + 1);
622
+ const to = source.slice(afterFrom + 1, afterFrom + 1 + toLength);
623
+ out += RE_ASCII_LETTER.test(from) && RE_ASCII_LETTER.test(to) && isLower(from) === isLower(to)
624
+ ? from + '-' + to + (isLower(from)
625
+ ? from.toUpperCase() + '-' + to.toUpperCase()
626
+ : from.toLowerCase() + '-' + to.toLowerCase())
627
+ : from + '-' + to;
628
+ i = afterFrom + 1 + toLength;
629
+ continue;
630
+ }
631
+
632
+ // A set operand is one character, so both cases go into a class of their own
633
+ const folded = from.toLowerCase() + from.toUpperCase();
634
+ out += fromLength !== 1 || !foldsCase(from) ? from
635
+ : isSetOperator(source, afterFrom, nested) || isSetOperator(source, i - 2, nested) ? '[' + folded + ']'
636
+ : folded;
637
+ i = afterFrom;
638
+ }
639
+
640
+ return out;
641
+ }
642
+
643
+ /**
644
+ * Whether a source anchors anywhere, so that `m` would move where it matches
645
+ * @param {string} source @param {boolean} [nested] - Whether the pattern carries `v`
646
+ */
647
+ function hasAnchor(source, nested = false) {
648
+ let i = 0;
649
+ while (i < source.length) {
650
+ if (source[i] === '\\') i += tokenLength(source, i);
651
+ // Inside a class `^` negates and `$` is a member, so neither anchors there
652
+ else if (source[i] === '[') i = skipCharacterClass(source, i, nested);
653
+ else if (source[i] === '^' || source[i] === '$') return true;
654
+ else i++;
655
+ }
656
+ return false;
657
+ }
658
+
659
+ // A property escape or a code point escape, both of which read as literal text
660
+ // where the flag that gives them meaning is gone
661
+ const RE_UNICODE_ESCAPE = /\\[pP]\{|\\u\{/;
662
+
663
+ // The characters `u` folds by Unicode rules and a source without it does not:
664
+ // `/s/iu` matches `\u017F` and `/k/iu` matches `\u212A`, where neither does on its
665
+ // own. Characters past the BMP fold this way, too, and are caught as astral first.
666
+ const RE_UNICODE_FOLDING = /[\u004B\u0053\u006B\u0073\u00C5\u00DF\u00E5\u017F\u0398\u03A9\u03B8\u03C9\u03D1\u03F4\u1E9E\u1F80-\u1FAF\u1FB3\u1FBC\u1FC3\u1FCC\u1FF3\u1FFC\u2126\u212A\u212B]/;
667
+
668
+ /**
669
+ * Which flag changes what a source matches, so that embedding the source where the
670
+ * flag cannot follow would silently match something else. `u` and `v` only narrow
671
+ * what syntax is legal, so a source valid under either stays valid without it—the
672
+ * difference never surfaces as a syntax error, and this stands in for the one that
673
+ * would otherwise be raised.
674
+ * @param {RegExp} pattern
675
+ * @returns {'u' | 'v' | 'm' | null} The flag the source depends on, or `null` where
676
+ * every flag it carries would leave the source matching the same
677
+ */
678
+ function lostFlag(pattern) {
679
+ const { source, unicode, unicodeSets } = pattern;
680
+ // `m` moves where `^` and `$` match, and only matters where the source has one
681
+ if (pattern.multiline && hasAnchor(source, unicodeSets)) return 'm';
682
+ if (!unicode && !unicodeSets) return null;
683
+ const flag = unicodeSets ? 'v' : 'u';
684
+ if (RE_UNICODE_ESCAPE.test(source)) return flag;
685
+ // Case folding under `i` follows Unicode rules only while the flag is there. A
686
+ // character can be written literally or as an escape, so the source is read
687
+ // token by token rather than scanned for the characters themselves; `singleCode`
688
+ // leaves out `\d` and friends, which fold alike with the flag and without it.
689
+ if (pattern.ignoreCase) {
690
+ for (let i = 0; i < source.length;) {
691
+ const length = tokenLength(source, i);
692
+ const code = singleCode(tokenRanges(source.slice(i, i + length), true));
693
+ if (code !== null && RE_UNICODE_FOLDING.test(String.fromCodePoint(code))) return flag;
694
+ i += length;
695
+ }
696
+ }
697
+ // Past the BMP a character is one code point with the flag, two units without,
698
+ // which is what a quantifier or a class beside it would go on to read wrongly
699
+ for (const char of source) {
700
+ if ((char.codePointAt(0) ?? 0) > 0xFFFF) return flag;
701
+ }
702
+ if (!unicodeSets) return null;
703
+
704
+ // `v` alone lets a class nest, subtract, intersect, and hold whole strings
705
+ let i = 0;
706
+ while (i < source.length) {
707
+ if (source[i] === '\\') {
708
+ i += tokenLength(source, i);
709
+ continue;
710
+ }
711
+ if (source[i] !== '[') {
712
+ i++;
713
+ continue;
714
+ }
715
+ const end = skipCharacterClass(source, i, true);
716
+ for (let j = i + 1; j < end - 1;) {
717
+ if (source[j] === '\\') {
718
+ if (source.startsWith('\\q{', j)) return 'v';
719
+ j += tokenLength(source, j);
720
+ continue;
721
+ }
722
+ if (source[j] === '[' || isSetOperator(source, j, true)) return 'v';
723
+ j++;
724
+ }
725
+ i = end;
726
+ }
727
+ return null;
728
+ }
729
+
730
+ /**
731
+ * A pattern’s source, rewritten to match the way its own flags make it match, for
732
+ * embedding in a larger regex that cannot carry them. `i` and `s` fit into a
733
+ * source; `m`, `u`, and `v` do not, and are left to the pattern it joins. Where
734
+ * `i` cannot be written in—a backreference, a range outside ASCII, a range that
735
+ * spans letters without being one—the source stands as it is, matching less than
736
+ * the pattern would rather than more.
737
+ * @param {RegExp} pattern
738
+ * @returns {string}
739
+ */
740
+ function embedSource(pattern) {
741
+ let source = pattern.source;
742
+ if (pattern.dotAll) source = expandDotAll(source, pattern.unicodeSets);
743
+ if (pattern.ignoreCase) source = foldCase(source, pattern.unicodeSets);
744
+ return source;
745
+ }
746
+
747
+ /**
748
+ * @typedef {{text: string, ranges: [number, number][] | null}} Repeat - An unbounded
749
+ * repeat, as it reads and as the set of characters it matches
750
+ */
751
+
752
+ /**
753
+ * @param {Repeat[]} reachable - Repeats that could still sit adjacent to what comes next
754
+ * @param {Repeat} repeat
755
+ * @returns {boolean} Whether the two can consume the same character, and so split
756
+ * the same run of input between them
757
+ */
758
+ function splitsWith(reachable, repeat) {
759
+ return reachable.some(earlier => earlier.text === repeat.text ||
760
+ (!!repeat.ranges && !!earlier.ranges && rangesIntersect(repeat.ranges, earlier.ranges)));
761
+ }
762
+
763
+ /**
764
+ * @param {Repeat[]} into
765
+ * @param {Repeat[]} repeats
766
+ * @returns {void} Adds the repeats, keeping only the most recent ones the walk
767
+ * still compares against
768
+ */
769
+ function collectReachable(into, repeats) {
770
+ for (const repeat of repeats) {
771
+ into.push(repeat);
772
+ if (into.length > MAX_REACHABLE) into.shift();
773
+ }
774
+ }
775
+
776
+ /**
777
+ * Walk a regex source for the shapes whose backtracking blows up: an unlimited
778
+ * quantifier over a group that itself contains a variable quantifier (`(a+)+`,
779
+ * `(a?)+`) or alternates, and two unbounded repeats that can consume the same
780
+ * character with only atoms matching empty between them (`.*.*`, `[a]*a*`,
781
+ * `\w*\d*`, `a*b*a*`, `a*(b*)a*`, `(a*)a*`). A lone unlimited quantifier stays
782
+ * linear, so `[\s\S]*?` up to a literal terminator passes.
783
+ * @param {string} source - Regex source, assumed syntactically valid
784
+ * @param {number} [depth] - Group nesting level of this call
785
+ * @param {boolean} [nested] - Whether the pattern carries `v`
786
+ * @returns {{risky: boolean, varies: boolean, alternates: boolean, deep: boolean,
787
+ * empty: boolean, leading: Repeat[], trailing: Repeat[]}} What the walk found,
788
+ * with the repeats that reach the source’s start and end for a caller to compare
789
+ * against what stands beside it
790
+ */
791
+ function analyzeQuantifiers(source, depth = 0, nested = false) {
792
+ if (depth > MAX_PATTERN_DEPTH) return { risky: true, varies: true, alternates: true, deep: true, empty: true, leading: [], trailing: [] };
793
+
794
+ let risky = false;
795
+ let varies = false;
796
+ let alternates = false;
797
+ let deep = false;
798
+ // Whether every atom of the branch being read matches empty, and whether any
799
+ // branch before it did
800
+ let branchEmpty = true;
801
+ let anyBranchEmpty = false;
802
+ // The unbounded repeats that could still sit adjacent to what comes next, most
803
+ // recent last; an atom matching empty leaves the ones before it reachable
804
+ /** @type {Repeat[]} */
805
+ const reachable = [];
806
+ // The repeats a caller can reach from either end, gathered across branches
807
+ /** @type {Repeat[]} */
808
+ const leading = [];
809
+ /** @type {Repeat[]} */
810
+ const trailing = [];
811
+ /** @type {Repeat[]} */
812
+ let branchLeading = [];
813
+ let i = 0;
814
+
815
+ while (i < source.length) {
816
+ const start = i;
817
+ const char = source[i];
818
+ /** @type {ReturnType<typeof analyzeQuantifiers> | null} */
819
+ let group = null;
820
+
821
+ if (char === '|') {
822
+ // Alternatives are separate expressions, so nothing carries across
823
+ alternates = true;
824
+ anyBranchEmpty ||= branchEmpty;
825
+ branchEmpty = true;
826
+ collectReachable(leading, branchLeading);
827
+ collectReachable(trailing, reachable);
828
+ branchLeading = [];
829
+ reachable.length = 0;
830
+ i++;
831
+ continue;
832
+ } else if (char === '\\') {
833
+ i += tokenLength(source, i);
834
+ } else if (char === '[') {
835
+ i = skipCharacterClass(source, i, nested);
836
+ } else if (char === '(') {
837
+ let open = 1;
838
+ i++;
839
+ while (i < source.length && open > 0) {
840
+ const inner = source[i];
841
+ if (inner === '\\') {
842
+ i += tokenLength(source, i);
843
+ } else if (inner === '[') {
844
+ i = skipCharacterClass(source, i, nested);
845
+ } else {
846
+ if (inner === '(') open++;
847
+ else if (inner === ')') open--;
848
+ i++;
849
+ }
850
+ }
851
+ // Drop the group prefix (`?:`, `?=`, `?<name>`, …) before reading the body
852
+ group = analyzeQuantifiers(source.slice(start + 1, i - 1).replace(/^\?(?:[:=!]|<[=!]|<[^>]*>)/, ''), depth + 1, nested);
853
+ } else {
854
+ i++;
855
+ }
856
+
857
+ const atom = source.slice(start, i);
858
+ RE_QUANTIFIER.lastIndex = i;
859
+ const quantifier = RE_QUANTIFIER.exec(source);
860
+ const upper = quantifier?.[2];
861
+ const repeats = !!quantifier && (quantifier[0][0] === '*' || quantifier[0][0] === '+' || upper === '');
862
+ // A count that can vary—anything but `{n}` and its `{n,n}` spelling—makes
863
+ // the group ambiguous about how much it consumes, which multiplies under an
864
+ // unlimited repeat
865
+ const exact = !!quantifier && quantifier[0][0] === '{' &&
866
+ (upper === undefined || (upper !== '' && Number(upper) === Number(quantifier[1])));
867
+ const variable = !!quantifier && !exact;
868
+ // An atom matches empty where it is zero-width, where its quantifier may
869
+ // repeat none, or where a group body matches empty however often it repeats—
870
+ // `(b*)`, carrying the quantifier inside the group rather than on it
871
+ const least = quantifier && (quantifier[0][0] === '+' ? 1
872
+ : quantifier[0][0] === '{' ? Number(quantifier[1]) : 0);
873
+ const body = RE_ZERO_WIDTH.test(atom) || (!!group && group.empty);
874
+ const nullable = body || (!!quantifier && least === 0);
875
+ if (quantifier) i = RE_QUANTIFIER.lastIndex;
876
+
877
+ // A lookaround is atomic: It backtracks nothing, so what it holds neither
878
+ // reaches out of it nor is reached into
879
+ const opaque = RE_ZERO_WIDTH.test(atom);
880
+ if (group) {
881
+ if (group.risky || (repeats && (group.varies || group.alternates))) risky = true;
882
+ if (group.varies) varies = true;
883
+ // A group alternates whether the `|` sits at its top level or deeper
884
+ if (group.alternates) alternates = true;
885
+ if (group.deep) deep = true;
886
+ // A group is no wall: What it repeats at its start splits the same run as
887
+ // what stands unbounded before it
888
+ if (!opaque && group.leading.some(repeat => splitsWith(reachable, repeat))) risky = true;
889
+ }
890
+ if (variable) varies = true;
891
+ const text = unwrapAtom(atom);
892
+ const ranges = atomRanges(text, nested);
893
+ if (repeats && splitsWith(reachable, { text, ranges })) risky = true;
894
+
895
+ // Whatever a caller could reach before this atom, it reaches this one, too
896
+ if (branchEmpty) {
897
+ if (repeats) collectReachable(branchLeading, [{ text, ranges }]);
898
+ if (group && !opaque) collectReachable(branchLeading, group.leading);
899
+ }
900
+ // Nothing reaches past an atom that has to consume something
901
+ if (!nullable) {
902
+ reachable.length = 0;
903
+ branchEmpty = false;
904
+ }
905
+ if (group && !opaque) collectReachable(reachable, group.trailing);
906
+ if (repeats) collectReachable(reachable, [{ text, ranges }]);
907
+ }
908
+
909
+ collectReachable(leading, branchLeading);
910
+ collectReachable(trailing, reachable);
911
+
912
+ return { risky, varies, alternates, deep, empty: anyBranchEmpty || branchEmpty, leading, trailing };
913
+ }
914
+
915
+ /**
916
+ * @param {RegExp | string} pattern - Pattern to judge, or a bare source to read
917
+ * as it stands
918
+ * @returns {string | null} What makes the pattern a backtracking risk, phrased
919
+ * to follow the pattern itself, or `null` where it is none
920
+ */
921
+ function describeQuantifierRisk(pattern) {
922
+ const source = typeof pattern === 'string' ? pattern : pattern.source;
923
+ // A pattern too big to read is refused for that, not for a shape nobody saw;
924
+ // its own length is what counts, not the length folding case inflates it to
925
+ if (source.length > MAX_PATTERN_LENGTH) {
926
+ return `runs past ${MAX_PATTERN_LENGTH.toLocaleString()} characters, too long to analyze for catastrophic backtracking—shorten it, or split it into several patterns`;
927
+ }
928
+ // `i` and `s` change what the source matches, so they are written into it
929
+ // before the shapes are read
930
+ const analysis = typeof pattern === 'string'
931
+ ? analyzeQuantifiers(source)
932
+ : analyzeQuantifiers(embedSource(pattern), 0, pattern.unicodeSets);
933
+ if (analysis.deep) {
934
+ return `nests groups more than ${MAX_PATTERN_DEPTH.toLocaleString()} deep, too deep to analyze for catastrophic backtracking—flatten it, or split it into several patterns`;
935
+ }
936
+ return analysis.risky
937
+ ? 'compounds quantifiers or alternation in a way that may cause ReDoS—bound the repetition (e.g., `{0,1000}`) instead'
938
+ : null;
939
+ }
940
+
941
+ /**
942
+ * Find the index of the `>` that closes an opening tag, correctly skipping
943
+ * over quoted attribute values (which may contain `>`).
944
+ * @param {string} html
945
+ * @param {number} pos - Start position (just after the tag name)
946
+ * @param {number} [limit] - Position to stop the search at (exclusive)
947
+ * @returns {number} Index of the closing `>`, or -1 if not found
948
+ */
949
+ function findTagEnd(html, pos, limit = html.length) {
950
+ let i = pos;
951
+ while (i < limit) {
952
+ const ch = html[i];
953
+ if (ch === '>') return i;
954
+ if (ch === '"' || ch === "'") {
955
+ const q = ch;
956
+ i++;
957
+ while (i < limit && html[i] !== q) i++;
958
+ }
959
+ i++;
960
+ }
961
+ return -1;
962
+ }
963
+
964
+ /**
965
+ * A package that resolves but fails to load—e.g., a native binding built for
966
+ * another platform—is not one another `npm install` fixes, so name what happened
967
+ * rather than the install command
968
+ *
969
+ * @param {string} label - Minifier name as it should read in the message
970
+ * @param {string} specifier - Package that failed to load
971
+ * @param {unknown} cause - Error the import rejected with
972
+ * @returns {string} Message describing the failure
973
+ */
974
+ function describeDependencyFailure(label, specifier, cause) {
975
+ const err = /** @type {any} */ (cause);
976
+ const notInstalled =
977
+ err?.code === 'ERR_MODULE_NOT_FOUND' && String(err?.message).includes(`'${specifier}'`);
978
+ if (notInstalled) {
979
+ return `The ${label} minifier requires ${specifier} to be installed.\n` +
980
+ `Install it with: npm install ${specifier}`;
981
+ }
982
+ return `The ${label} minifier could not load ${specifier}: ` +
983
+ (cause instanceof Error ? cause.message : String(cause));
984
+ }
985
+
986
+ // Whether a doctype sets quirks mode, per the HTML parser (limited-quirks mode counts as no-quirks,
987
+ // as it only differs in line height calculation)
988
+ // https://html.spec.whatwg.org/multipage/parsing.html#the-initial-insertion-mode
989
+ /** @param {string} doctype */
990
+ function isQuirksDoctype(doctype) {
991
+ const match = RE_DOCTYPE.exec(doctype);
992
+ if (!match) return true;
993
+ const [, name = '', publicQuoted, systemQuoted, systemOnlyQuoted, rest] = match;
994
+ const systemId = (systemQuoted ?? systemOnlyQuoted)?.slice(1, -1).toLowerCase();
995
+ // The force-quirks flag
996
+ if (systemId === undefined && rest !== '>') return true;
997
+ if (name.toLowerCase() !== 'html') return true;
998
+ if (systemId !== undefined && quirksSystemIds.has(systemId)) return true;
999
+ if (publicQuoted === undefined) return false;
1000
+ const publicId = publicQuoted.slice(1, -1).toLowerCase();
1001
+ return quirksPublicIds.has(publicId) ||
1002
+ quirksPublicIdPrefixes.some(prefix => publicId.startsWith(prefix)) ||
1003
+ (!systemId && quirksPublicIdPrefixesNoSystemId.some(prefix => publicId.startsWith(prefix)));
1004
+ }
1005
+
1006
+ export {
1007
+ stableStringify,
1008
+ findTagEnd,
1009
+ LRU,
1010
+ MAX_CACHE_ENTRY_SIZE,
1011
+ hashContent,
1012
+ uniqueId,
1013
+ escapeXML,
1014
+ decodeForeignText,
1015
+ identity,
1016
+ isQuirksDoctype,
1017
+ isThenable,
1018
+ lowercase,
1019
+ paramCase,
1020
+ replaceAsync,
1021
+ parseRegExp,
1022
+ embedSource,
1023
+ lostFlag,
1024
+ describeQuantifierRisk,
1025
+ describeDependencyFailure
1026
+ };