html-minifier-next 8.0.0 → 8.1.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/README.md +15 -2
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/lib/constants.d.ts +3 -1
- package/dist/types/lib/constants.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/unused-css.d.ts +1 -1
- package/dist/types/lib/utils.d.ts +19 -3
- package/dist/types/lib/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/htmlminifier.js +81 -13
- package/src/htmlparser.js +11 -1
- package/src/lib/constants.js +4 -0
- package/src/lib/fragments.js +1 -1
- package/src/lib/options.js +21 -2
- package/src/lib/unused-css.js +1 -1
- package/src/lib/utils.js +434 -71
package/README.md
CHANGED
|
@@ -547,6 +547,14 @@ For partial HTML fragments (such as template includes, SSI fragments, or closing
|
|
|
547
547
|
|
|
548
548
|
To validate complete HTML markup, use [the W3C validator](https://validator.w3.org/) or one of [several validator packages](https://meiert.com/blog/html-validator-packages/).
|
|
549
549
|
|
|
550
|
+
### Regex options and flags
|
|
551
|
+
|
|
552
|
+
`customAttrAssign` and `customAttrSurround` patterns are merged into one attribute pattern which carries no flags of its own. `i` and `s` are written into each pattern’s source instead, so they survive the merge. `u`, `v`, and `m` cannot be, and none of them fails loudly when dropped: `u` and `v` only narrow what syntax is legal, so a source valid under either stays valid without it and quietly matches something else—a dropped `u` leaves `\p{L}` matching the literal text `p{L}`—while a dropped `m` leaves `^` and `$` matching at the ends of the input rather than of each line.
|
|
553
|
+
|
|
554
|
+
A pattern is therefore refused with an error where the flag changes what its source matches—a property or code point escape, a character past the BMP, a character `i` folds by Unicode rules only while `u` is there (`/s/iu` matches `\u017F`, `/k/iu` matches `\u212A`), a `v` class that nests, subtracts, intersects, or holds strings, or—under `m`—an anchor whose meaning moves. A flag the source does not depend on, as in `/x=/u`, is left alone.
|
|
555
|
+
|
|
556
|
+
Patterns given as strings, in a configuration file or on the command line, may be written either bare (`ng-class`) or delimited with flags (`/ng-class/i`).
|
|
557
|
+
|
|
550
558
|
## Security
|
|
551
559
|
|
|
552
560
|
### ReDoS protection
|
|
@@ -555,7 +563,11 @@ You can use `ignoreCustomFragments` to hand HTML Minifier Next a regular express
|
|
|
555
563
|
|
|
556
564
|
* Matching without backtracking: A pattern that wraps an any-character or negated-class body in literal delimiters—`<%[\s\S]*?%>` or `\{\{[^}]*?\}\}`, and every other shape below—is matched by scanning for those delimiters in linear time, with no regular expression involved. Patterns of other shapes run as regular expressions, one per pattern, so each keeps its own flags.
|
|
557
565
|
|
|
558
|
-
* Pattern detection: HMN warns about the shapes that backtrack catastrophically—an unlimited quantifier over a group that itself contains a quantifier that can vary (`(a+)+`, `(a?)+`) or alternation (`(a|b)*`), and
|
|
566
|
+
* Pattern detection: HMN warns about the shapes that backtrack catastrophically—an unlimited quantifier over a group that itself contains a quantifier that can vary (`(a+)+`, `(a?)+`) or alternation (`(a|b)*`), and two unbounded repeats that can consume the same character with only atoms matching empty between them (`.*.*`, `[a]*a*`, `\w*\d*`, `a*b*a*`).
|
|
567
|
+
|
|
568
|
+
A group is no wall: It counts by what its body can match, and repeats meet across its boundary, so `\s*(\w*)\s*` and `(a*)a*` are flagged like `\s*\w*\s*` and `a*a*`. A lookaround backtracks nothing, so `(?=a*)a*` passes. A fixed count does not vary, so `(?:a{4})+` passes; repeats that share no character leave nothing ambiguous to split, so `\s*\S*` passes. A pattern is read the way its own flags make it match, so `/.*\n*/s` and `/[a]*A*/i` are flagged where those same sources without the flags are not. Under `v`, a class that nests reads as the union it is, while one that subtracts (`--`) or intersects (`&&`) is left unread and passes.
|
|
569
|
+
|
|
570
|
+
These are also shapes a linear scan cannot stand in for. `strictCustomFragments` refuses them with an error instead, which is worth enabling where the patterns or the input are not entirely under your control. A pattern longer than 10,000 characters or nested more than 50 groups deep is judged risky without being analyzed further, so that reading the pattern cannot itself become the expensive step. The check reads shapes, not languages: It warns about the common ones rather than proving a pattern linear, and misses repeats that overlap only across whole subexpressions (`(ab)*(abab)*`). **Treat a pattern that passes as unflagged, not as vetted.**
|
|
559
571
|
|
|
560
572
|
* Input length limits: The `maxInputLength` option allows you to set a maximum input size to prevent processing of excessively large inputs that could cause performance issues.
|
|
561
573
|
|
|
@@ -579,7 +591,8 @@ ignoreCustomFragments: [
|
|
|
579
591
|
ignoreCustomFragments: [
|
|
580
592
|
/<%(\s|\S)*?%>/, // Unlimited quantifier over an alternating group
|
|
581
593
|
/\{\{([^}]+)+\}\}/, // Nested unlimited quantifiers
|
|
582
|
-
/<!--[\s\S]*[\s\S]
|
|
594
|
+
/<!--[\s\S]*[\s\S]*-->/, // Two unbounded repeats in a row
|
|
595
|
+
/<%\w*\d*%>/ // Two unbounded repeats over overlapping sets
|
|
583
596
|
]
|
|
584
597
|
```
|
|
585
598
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"htmlparser.d.ts","sourceRoot":"","sources":["../../src/htmlparser.js"],"names":[],"mappings":"AAcG,YAWG,iBAAiB,GAXV;IACR,KAAK,CAAC,WAAW;IACjB,GAAG,CAAC,WAAW;IACf,KAAK,CAAC,WAAW;IACjB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,WAAW;IACnB,oBAAoB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,SAAS,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;CACxC,CAAmB;AAyCvB,eAAO,MAAM,MAAM,QAA8C,CAAC;AA2HlE,qBAAa,UAAU;IAMd,IAAI;IACJ,OAAO;IANd;;;OAGG;IACH,YAAY,IAAI,EAHL,MAGK,EAAE,OAAO,EAFd,iBAEc,EAGxB;IAEK,KAAK,
|
|
1
|
+
{"version":3,"file":"htmlparser.d.ts","sourceRoot":"","sources":["../../src/htmlparser.js"],"names":[],"mappings":"AAcG,YAWG,iBAAiB,GAXV;IACR,KAAK,CAAC,WAAW;IACjB,GAAG,CAAC,WAAW;IACf,KAAK,CAAC,WAAW;IACjB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,WAAW;IACnB,oBAAoB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,SAAS,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;CACxC,CAAmB;AAyCvB,eAAO,MAAM,MAAM,QAA8C,CAAC;AA2HlE,qBAAa,UAAU;IAMd,IAAI;IACJ,OAAO;IANd;;;OAGG;IACH,YAAY,IAAI,EAHL,MAGK,EAAE,OAAO,EAFd,iBAEc,EAGxB;IAEK,KAAK,kBAmqBV;CACF"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
declare const RE_WS_START: RegExp;
|
|
2
2
|
declare const RE_WS_END: RegExp;
|
|
3
|
+
declare const RE_WS_ONLY: RegExp;
|
|
4
|
+
declare const RE_WS_NBSP_END: RegExp;
|
|
3
5
|
declare const RE_ALL_WS_NBSP: RegExp;
|
|
4
6
|
declare const RE_NBSP_LEADING_GROUP: RegExp;
|
|
5
7
|
declare const RE_NBSP_LEAD_GROUP: RegExp;
|
|
@@ -95,5 +97,5 @@ declare const looseElements: Set<string>;
|
|
|
95
97
|
declare const trailingElements: Set<string>;
|
|
96
98
|
declare const htmlElements: Set<string>;
|
|
97
99
|
declare const specialContentElements: Set<string>;
|
|
98
|
-
export { RE_WS_START, RE_WS_END, RE_ALL_WS_NBSP, RE_NBSP_LEADING_GROUP, RE_NBSP_LEAD_GROUP, RE_NBSP_TRAILING_GROUP, RE_NBSP_TRAILING_STRIP, RE_EVENT_ATTR_DEFAULT, RE_CAN_REMOVE_ATTR_QUOTES, RE_TRAILING_SEMICOLON, RE_AMP_ENTITY, RE_LEGACY_ENTITIES, RE_ESCAPE_LT, RE_ATTR_WS_CHECK, RE_ATTR_WS_COLLAPSE, RE_ATTR_WS_TRIM, RE_STYLE_ELEMENT, RE_EMPTY_ATTRIBUTE, inlineElementsToKeepWhitespaceAround, inlineElementsToKeepWhitespaceWithin, inlineElementsToKeepWhitespace, formControlElements, generalDefaults, tagDefaults, executableScriptsMimetypes, keepScriptsMimetypes, jsonScriptTypes, isSimpleBoolean, isBooleanValue, collapsibleValues, srcsetElements, optionalStartTags, optionalEndTags, headerElements, descriptionElements, pBlockElements, pInlineElements, rubyEndTagOmission, rubyRtcEndTagOmission, optionElements, tableContentElements, tableSectionElements, cellElements, topLevelElements, compactElements, looseElements, trailingElements, htmlElements, specialContentElements };
|
|
100
|
+
export { RE_WS_START, RE_WS_END, RE_WS_ONLY, RE_WS_NBSP_END, RE_ALL_WS_NBSP, RE_NBSP_LEADING_GROUP, RE_NBSP_LEAD_GROUP, RE_NBSP_TRAILING_GROUP, RE_NBSP_TRAILING_STRIP, RE_EVENT_ATTR_DEFAULT, RE_CAN_REMOVE_ATTR_QUOTES, RE_TRAILING_SEMICOLON, RE_AMP_ENTITY, RE_LEGACY_ENTITIES, RE_ESCAPE_LT, RE_ATTR_WS_CHECK, RE_ATTR_WS_COLLAPSE, RE_ATTR_WS_TRIM, RE_STYLE_ELEMENT, RE_EMPTY_ATTRIBUTE, inlineElementsToKeepWhitespaceAround, inlineElementsToKeepWhitespaceWithin, inlineElementsToKeepWhitespace, formControlElements, generalDefaults, tagDefaults, executableScriptsMimetypes, keepScriptsMimetypes, jsonScriptTypes, isSimpleBoolean, isBooleanValue, collapsibleValues, srcsetElements, optionalStartTags, optionalEndTags, headerElements, descriptionElements, pBlockElements, pInlineElements, rubyEndTagOmission, rubyRtcEndTagOmission, optionElements, tableContentElements, tableSectionElements, cellElements, topLevelElements, compactElements, looseElements, trailingElements, htmlElements, specialContentElements };
|
|
99
101
|
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/constants.js"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/constants.js"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"AACA,OAAO,EAAE,GAAG,
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Collect the class names and IDs a document references.
|
|
3
3
|
*
|
|
4
4
|
* Style sheet contents are excluded, so that a style sheet never counts as
|
|
5
|
-
* evidence for its own selectors. An `iframe srcdoc` value is not scanned
|
|
5
|
+
* evidence for its own selectors. An `iframe srcdoc` value is not scanned,
|
|
6
6
|
* either: It holds a document of its own, minified against its own symbol set.
|
|
7
7
|
* Over-collecting is safe here (a symbol wrongly considered used is merely
|
|
8
8
|
* kept), under-collecting is not.
|
|
@@ -51,6 +51,17 @@ declare function lowercase(value: string): string;
|
|
|
51
51
|
declare function replaceAsync(str: string, regex: RegExp, asyncFn: Function): Promise<string>;
|
|
52
52
|
/** @param {string | RegExp} value */
|
|
53
53
|
declare function parseRegExp(value: string | RegExp): RegExp | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Which flag changes what a source matches, so that embedding the source where the
|
|
56
|
+
* flag cannot follow would silently match something else. `u` and `v` only narrow
|
|
57
|
+
* what syntax is legal, so a source valid under either stays valid without it—the
|
|
58
|
+
* difference never surfaces as a syntax error, and this stands in for the one that
|
|
59
|
+
* would otherwise be raised.
|
|
60
|
+
* @param {RegExp} pattern
|
|
61
|
+
* @returns {'u' | 'v' | 'm' | null} The flag the source depends on, or `null` where
|
|
62
|
+
* every flag it carries would leave the source matching the same
|
|
63
|
+
*/
|
|
64
|
+
declare function lostFlag(pattern: RegExp): 'u' | 'v' | 'm' | null;
|
|
54
65
|
/**
|
|
55
66
|
* A pattern’s source, rewritten to match the way its own flags make it match, for
|
|
56
67
|
* embedding in a larger regex that cannot carry them. `i` and `s` fit into a
|
|
@@ -62,12 +73,17 @@ declare function parseRegExp(value: string | RegExp): RegExp | undefined;
|
|
|
62
73
|
* @returns {string}
|
|
63
74
|
*/
|
|
64
75
|
declare function embedSource(pattern: RegExp): string;
|
|
76
|
+
export type Repeat = {
|
|
77
|
+
text: string;
|
|
78
|
+
ranges: [number, number][] | null;
|
|
79
|
+
};
|
|
65
80
|
/**
|
|
66
|
-
* @param {string}
|
|
81
|
+
* @param {RegExp | string} pattern - Pattern to judge, or a bare source to read
|
|
82
|
+
* as it stands
|
|
67
83
|
* @returns {string | null} What makes the pattern a backtracking risk, phrased
|
|
68
84
|
* to follow the pattern itself, or `null` where it is none
|
|
69
85
|
*/
|
|
70
|
-
declare function describeQuantifierRisk(
|
|
86
|
+
declare function describeQuantifierRisk(pattern: RegExp | string): string | null;
|
|
71
87
|
/**
|
|
72
88
|
* Find the index of the `>` that closes an opening tag, correctly skipping
|
|
73
89
|
* over quoted attribute values (which may contain `>`).
|
|
@@ -76,5 +92,5 @@ declare function describeQuantifierRisk(source: string): string | null;
|
|
|
76
92
|
* @returns {number} Index of the closing `>`, or -1 if not found
|
|
77
93
|
*/
|
|
78
94
|
declare function findTagEnd(html: string, pos: number): number;
|
|
79
|
-
export { stableStringify, findTagEnd, LRU, MAX_CACHE_ENTRY_SIZE, hashContent, uniqueId, identity, isThenable, lowercase, replaceAsync, parseRegExp, embedSource, describeQuantifierRisk };
|
|
95
|
+
export { stableStringify, findTagEnd, LRU, MAX_CACHE_ENTRY_SIZE, hashContent, uniqueId, identity, isThenable, lowercase, replaceAsync, parseRegExp, embedSource, lostFlag, describeQuantifierRisk };
|
|
80
96
|
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/lib/utils.js"],"names":[],"mappings":"AAEA;;;GAGG;AACH,iBAAS,eAAe,CAAC,GAAG,EAHjB,OAGiB,GAFf,MAAM,CAYlB;AAID,cAAM,GAAG;IAEA,KAAK;IACL,IAAI;IACJ,IAAI;IACT,mCAAmC;IAC9B,GAAG,EADG,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC;IAJjC,YAAY,KAAK,SAAM,EAMtB;IACD,0BAA0B;IAC1B,GAAG,CAAC,GAAG,EADK,MACL,WAUN;IACD;;;OAGG;IACH,GAAG,CAAC,GAAG,EAHI,MAGJ,EAAE,KAAK,EAFH,OAEG,QAOb;IACD,0BAA0B;IAC1B,MAAM,CAAC,GAAG,EADE,MACF,QAA2B;IACrC,6EAA6E;IAC7E,KAAK,IADS;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAGxE;CACF;AAKD,QAAA,MAAM,oBAAoB,QAAc,CAAC;AAIzC,0BAA0B;AAC1B,iBAAS,WAAW,CAAC,GAAG,EADZ,MACY,UAOvB;AAID,4BAA4B;AAC5B,iBAAS,QAAQ,CAAC,KAAK,EADX,MACW,UAMtB;AAID,4BAA4B;AAC5B,iBAAS,QAAQ,CAAC,KAAK,EADX,MACW,UAEtB;AAED;;;GAGG;AACH,iBAAS,UAAU,CAAC,KAAK,EAHd,OAGc,GAFZ,KAAK,IAAI,WAAW,CAAC,GAAG,CAAC,CAIrC;AAED,4BAA4B;AAC5B,iBAAS,SAAS,CAAC,KAAK,EADZ,MACY,UAEvB;AAID;;;;;;GAMG;AACH,iBAAe,YAAY,CAAC,GAAG,EALpB,MAKoB,EAAE,KAAK,EAJ3B,MAI2B,EAAE,OAAO,UAAA,GAFlC,OAAO,CAAC,MAAM,CAAC,CAe3B;AAID,qCAAqC;AACrC,iBAAS,WAAW,CAAC,KAAK,EADd,MAAM,GAAG,MACK,sBAUzB;
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/lib/utils.js"],"names":[],"mappings":"AAEA;;;GAGG;AACH,iBAAS,eAAe,CAAC,GAAG,EAHjB,OAGiB,GAFf,MAAM,CAYlB;AAID,cAAM,GAAG;IAEA,KAAK;IACL,IAAI;IACJ,IAAI;IACT,mCAAmC;IAC9B,GAAG,EADG,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC;IAJjC,YAAY,KAAK,SAAM,EAMtB;IACD,0BAA0B;IAC1B,GAAG,CAAC,GAAG,EADK,MACL,WAUN;IACD;;;OAGG;IACH,GAAG,CAAC,GAAG,EAHI,MAGJ,EAAE,KAAK,EAFH,OAEG,QAOb;IACD,0BAA0B;IAC1B,MAAM,CAAC,GAAG,EADE,MACF,QAA2B;IACrC,6EAA6E;IAC7E,KAAK,IADS;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAGxE;CACF;AAKD,QAAA,MAAM,oBAAoB,QAAc,CAAC;AAIzC,0BAA0B;AAC1B,iBAAS,WAAW,CAAC,GAAG,EADZ,MACY,UAOvB;AAID,4BAA4B;AAC5B,iBAAS,QAAQ,CAAC,KAAK,EADX,MACW,UAMtB;AAID,4BAA4B;AAC5B,iBAAS,QAAQ,CAAC,KAAK,EADX,MACW,UAEtB;AAED;;;GAGG;AACH,iBAAS,UAAU,CAAC,KAAK,EAHd,OAGc,GAFZ,KAAK,IAAI,WAAW,CAAC,GAAG,CAAC,CAIrC;AAED,4BAA4B;AAC5B,iBAAS,SAAS,CAAC,KAAK,EADZ,MACY,UAEvB;AAID;;;;;;GAMG;AACH,iBAAe,YAAY,CAAC,GAAG,EALpB,MAKoB,EAAE,KAAK,EAJ3B,MAI2B,EAAE,OAAO,UAAA,GAFlC,OAAO,CAAC,MAAM,CAAC,CAe3B;AAID,qCAAqC;AACrC,iBAAS,WAAW,CAAC,KAAK,EADd,MAAM,GAAG,MACK,sBAUzB;AAyYD;;;;;;;;;GASG;AACH,iBAAS,QAAQ,CAAC,OAAO,EAJd,MAIc,GAHZ,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,CAqDlC;AAED;;;;;;;;;GASG;AACH,iBAAS,WAAW,CAAC,OAAO,EAHjB,MAGiB,GAFf,MAAM,CAOlB;AAGE,YAA6D,MAAM,GAAzD;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,IAAI,CAAA;CAAC,CAE7D;AAqKD;;;;;GAKG;AACH,iBAAS,sBAAsB,CAAC,OAAO,EAL5B,MAAM,GAAG,MAKmB,GAH1B,MAAM,GAAG,IAAI,CAqBzB;AAED;;;;;;GAMG;AACH,iBAAS,UAAU,CAAC,IAAI,EAJb,MAIa,EAAE,GAAG,EAHlB,MAGkB,GAFhB,MAAM,CAelB;AAID,OAAO,EACL,eAAe,EACf,UAAU,EACV,GAAG,EACH,oBAAoB,EACpB,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,SAAS,EACT,YAAY,EACZ,WAAW,EACX,WAAW,EACX,QAAQ,EACR,sBAAsB,EACvB,CAAC"}
|
package/package.json
CHANGED
package/src/htmlminifier.js
CHANGED
|
@@ -8,6 +8,10 @@ import { collectUsedSymbols } from './lib/unused-css.js';
|
|
|
8
8
|
import {
|
|
9
9
|
RE_LEGACY_ENTITIES,
|
|
10
10
|
RE_ESCAPE_LT,
|
|
11
|
+
RE_WS_START,
|
|
12
|
+
RE_WS_END,
|
|
13
|
+
RE_WS_ONLY,
|
|
14
|
+
RE_WS_NBSP_END,
|
|
11
15
|
RE_STYLE_ELEMENT,
|
|
12
16
|
inlineElementsToKeepWhitespaceAround,
|
|
13
17
|
inlineElementsToKeepWhitespaceWithin,
|
|
@@ -574,6 +578,8 @@ const DEFAULT_JS_TYPES = new Set(['', 'text/javascript', 'application/javascript
|
|
|
574
578
|
// Pre-compiled patterns for buffer scanning
|
|
575
579
|
const RE_START_TAG = /^<[^/!]/;
|
|
576
580
|
const RE_END_TAG = /^<\//;
|
|
581
|
+
const RE_WHITESPACE_ONLY = /^\s+$/;
|
|
582
|
+
const RE_TRAILING_WHITESPACE = /\s$/;
|
|
577
583
|
|
|
578
584
|
// Pre-compiled patterns for `htmlmin:ignore` block content analysis
|
|
579
585
|
const RE_HTML_COMMENT_START = /^\s*<!--/;
|
|
@@ -1117,11 +1123,57 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1117
1123
|
buffer.length = Math.max(0, index);
|
|
1118
1124
|
}
|
|
1119
1125
|
|
|
1126
|
+
// Whether whitespace-only text between tags survives minification
|
|
1127
|
+
function keepsWhitespace() {
|
|
1128
|
+
return !options.collapseWhitespace || Boolean(options.conservativeCollapse) || Boolean(options.preserveLineBreaks);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// The two whitespace runs an omitted tag separated become one run, which collapses to the
|
|
1132
|
+
// single character it is due: a line break under `preserveLineBreaks`, a no-break space
|
|
1133
|
+
// where the run holds one, a space under `conservativeCollapse`
|
|
1134
|
+
function mergeWhitespaceRuns(/** @type {number} */ index) {
|
|
1135
|
+
let before = index - 1;
|
|
1136
|
+
while (before >= 0 && buffer[before] === '') {
|
|
1137
|
+
before--;
|
|
1138
|
+
}
|
|
1139
|
+
let after = index;
|
|
1140
|
+
while (after < buffer.length && buffer[after] === '') {
|
|
1141
|
+
after++;
|
|
1142
|
+
}
|
|
1143
|
+
if (before < 0 || after >= buffer.length) {
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
const prev = buffer[before] ?? '';
|
|
1147
|
+
const next = buffer[after] ?? '';
|
|
1148
|
+
if (!RE_WS_NBSP_END.test(prev) || !RE_WS_START.test(next)) {
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
// A line break the run already carries outranks the whitespace ahead of it; trimming
|
|
1152
|
+
// the other side takes ASCII whitespace only, so a no-break space stays as is
|
|
1153
|
+
if (options.preserveLineBreaks && /[\n\r]/.test((next.match(RE_WS_START) ?? [''])[0])) {
|
|
1154
|
+
buffer[before] = prev.replace(RE_WS_END, '');
|
|
1155
|
+
} else {
|
|
1156
|
+
buffer[after] = next.replace(RE_WS_START, '');
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1120
1160
|
function removeEndTag() {
|
|
1121
1161
|
let index = buffer.length - 1;
|
|
1122
1162
|
while (index > 0 && !RE_END_TAG.test(buffer[index] ?? '')) {
|
|
1123
1163
|
index--;
|
|
1124
1164
|
}
|
|
1165
|
+
// Text that follows the end tag is kept as is, so drop the tag alone. Whitespace kept
|
|
1166
|
+
// verbatim—in `pre`, in `textarea`, or wherever `canTrimWhitespace` says so—counts as
|
|
1167
|
+
// kept text too, no matter how whitespace is collapsed elsewhere.
|
|
1168
|
+
const noTrim = stackNoTrimWhitespace.length > 0;
|
|
1169
|
+
if (index < buffer.length - 1 && (keepsWhitespace() || noTrim) && RE_END_TAG.test(buffer[index] ?? '')) {
|
|
1170
|
+
buffer.splice(index, 1);
|
|
1171
|
+
// Only collapsed whitespace can merge
|
|
1172
|
+
if (options.collapseWhitespace && !noTrim) {
|
|
1173
|
+
mergeWhitespaceRuns(index);
|
|
1174
|
+
}
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1125
1177
|
buffer.length = Math.max(0, index);
|
|
1126
1178
|
}
|
|
1127
1179
|
|
|
@@ -1143,11 +1195,15 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1143
1195
|
// element which has now been removed
|
|
1144
1196
|
function squashTrailingWhitespace(/** @type {string} */ nextTag) {
|
|
1145
1197
|
let charsIndex = buffer.length - 1;
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1198
|
+
// Step back over every trailing comment and empty entry, not just one—a single step
|
|
1199
|
+
// stops the search at the comment itself, leaving whitespace behind that a second
|
|
1200
|
+
// minification run then removes
|
|
1201
|
+
while (charsIndex > 0) {
|
|
1202
|
+
const item = buffer[charsIndex] ?? '';
|
|
1203
|
+
if (!/^(?:<!|$)/.test(item) || (uidIgnore && item.indexOf(uidIgnore) !== -1)) {
|
|
1204
|
+
break;
|
|
1150
1205
|
}
|
|
1206
|
+
charsIndex--;
|
|
1151
1207
|
}
|
|
1152
1208
|
trimTrailingWhitespace(charsIndex, nextTag);
|
|
1153
1209
|
}
|
|
@@ -1240,7 +1296,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1240
1296
|
} else {
|
|
1241
1297
|
text = collapseWhitespace(text, options, true, true);
|
|
1242
1298
|
}
|
|
1243
|
-
if (!text &&
|
|
1299
|
+
if (!text && RE_TRAILING_WHITESPACE.test(currentChars) && textPrevTag && textPrevTag.charAt(0) === '/') {
|
|
1244
1300
|
trimTrailingWhitespace(buffer.length - 1, textNextTag);
|
|
1245
1301
|
}
|
|
1246
1302
|
}
|
|
@@ -1277,9 +1333,16 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1277
1333
|
// `</head>`, `</colgroup>`, or `</caption>` may be omitted if not followed by space or comment
|
|
1278
1334
|
if (optionalEndTagEmitted && (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(effectiveText)))) {
|
|
1279
1335
|
removeEndTag();
|
|
1336
|
+
// The tag is gone from the buffer, so nothing is left to omit later
|
|
1337
|
+
optionalEndTag = '';
|
|
1338
|
+
optionalEndTagEmitted = false;
|
|
1280
1339
|
}
|
|
1281
|
-
//
|
|
1282
|
-
|
|
1340
|
+
// Whitespace-only text doesn’t prevent omission of the pending end tag: It’s either
|
|
1341
|
+
// collapsed away, or kept in place while the tag alone is dropped—except for
|
|
1342
|
+
// `</head>`, `</colgroup>`, and `</caption>`, which may not be followed by space.
|
|
1343
|
+
// Only ASCII whitespace counts, as no-break space and friends are content
|
|
1344
|
+
// https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
|
|
1345
|
+
if (!RE_WS_ONLY.test(text) || (keepsWhitespace() && looseElements.has(optionalEndTag))) {
|
|
1283
1346
|
optionalEndTag = '';
|
|
1284
1347
|
optionalEndTagEmitted = false;
|
|
1285
1348
|
}
|
|
@@ -1347,7 +1410,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1347
1410
|
const prevComment = buffer[buffer.length - 2];
|
|
1348
1411
|
|
|
1349
1412
|
// Check if previous item is whitespace-only and item before that is ignore-placeholder
|
|
1350
|
-
if (prevText &&
|
|
1413
|
+
if (prevText && RE_WHITESPACE_ONLY.test(prevText) && prevComment && uidIgnorePlaceholderPattern.test(prevComment)) {
|
|
1351
1414
|
// Extract the index from both placeholders to check their content
|
|
1352
1415
|
const currentMatch = comment.match(uidIgnorePlaceholderPattern);
|
|
1353
1416
|
const prevMatch = prevComment.match(uidIgnorePlaceholderPattern);
|
|
@@ -1390,7 +1453,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1390
1453
|
// Apply `collapseWhitespace` with appropriate context
|
|
1391
1454
|
if (!stackNoTrimWhitespace.length && !stackNoCollapseWhitespace.length) {
|
|
1392
1455
|
// Not in pre or other no-collapse context
|
|
1393
|
-
if (
|
|
1456
|
+
if (prevText.includes('\xA0')) {
|
|
1457
|
+
// No-break spaces are content, so every one of them survives in place;
|
|
1458
|
+
// the ASCII whitespace around them collapses as it would anywhere else
|
|
1459
|
+
collapsedText = collapseWhitespace(prevText, options, true, true, true);
|
|
1460
|
+
} else if (options.preserveLineBreaks && /[\n\r]/.test(prevText)) {
|
|
1394
1461
|
// Preserve line break as single newline
|
|
1395
1462
|
collapsedText = '\n';
|
|
1396
1463
|
} else if (options.conservativeCollapse) {
|
|
@@ -1639,10 +1706,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1639
1706
|
removeStartTag();
|
|
1640
1707
|
}
|
|
1641
1708
|
optionalStartTag = '';
|
|
1642
|
-
// `</html>` or `</body>` may be omitted if not followed by comment
|
|
1643
|
-
// `</head>` may be omitted if not followed by space or comment
|
|
1644
|
-
// `</p>` may be omitted if no more content in parent,
|
|
1645
|
-
//
|
|
1709
|
+
// `</html>` or `</body>` may be omitted if not followed by comment,
|
|
1710
|
+
// `</head>` may be omitted if not followed by space or comment,
|
|
1711
|
+
// `</p>` may be omitted if no more content in parent,
|
|
1712
|
+
// unless parent is in `pInlineElements` or is a custom element
|
|
1713
|
+
// (https://html.spec.whatwg.org/multipage/syntax.html#optional-tags);
|
|
1646
1714
|
// except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
|
|
1647
1715
|
if (tag && optionalEndTag && optionalEndTagEmitted && !trailingElements.has(optionalEndTag) && (optionalEndTag !== 'p' || (!pInlineElements.has(tag) && !tag.includes('-')))) {
|
|
1648
1716
|
removeEndTag();
|
package/src/htmlparser.js
CHANGED
|
@@ -703,7 +703,17 @@ export class HTMLParser {
|
|
|
703
703
|
|
|
704
704
|
if (lastTagLower === 'p' && nonPhrasing.has(lowerTagName)) {
|
|
705
705
|
await parseEndTag('', lastTag);
|
|
706
|
-
}
|
|
706
|
+
}
|
|
707
|
+
// A row or cell start tag closes the row or cell still open in the same table,
|
|
708
|
+
// which may sit deeper down the stack than the element that was opened last
|
|
709
|
+
if (lowerTagName === 'tr') {
|
|
710
|
+
closeIfFoundInCurrentTable('tr');
|
|
711
|
+
} else if (lowerTagName === 'td' || lowerTagName === 'th') {
|
|
712
|
+
if (!closeIfFoundInCurrentTable('td')) {
|
|
713
|
+
closeIfFoundInCurrentTable('th');
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (lowerTagName === 'tbody') {
|
|
707
717
|
if (!closeIfFoundInCurrentTable('tfoot')) {
|
|
708
718
|
closeIfFoundInCurrentTable('thead');
|
|
709
719
|
}
|
package/src/lib/constants.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const RE_WS_START = /^[ \n\r\t\f]+/;
|
|
4
4
|
const RE_WS_END = /[ \n\r\t\f]+$/;
|
|
5
|
+
const RE_WS_ONLY = /^[ \n\r\t\f]+$/;
|
|
6
|
+
const RE_WS_NBSP_END = /[ \n\r\t\f\xA0]$/;
|
|
5
7
|
const RE_ALL_WS_NBSP = /[ \n\r\t\f\xA0]+/g;
|
|
6
8
|
const RE_NBSP_LEADING_GROUP = /(^|\xA0+)[^\xA0]+/g;
|
|
7
9
|
const RE_NBSP_LEAD_GROUP = /(\xA0+)[^\xA0]+/g;
|
|
@@ -176,6 +178,8 @@ export {
|
|
|
176
178
|
// Regex patterns
|
|
177
179
|
RE_WS_START,
|
|
178
180
|
RE_WS_END,
|
|
181
|
+
RE_WS_ONLY,
|
|
182
|
+
RE_WS_NBSP_END,
|
|
179
183
|
RE_ALL_WS_NBSP,
|
|
180
184
|
RE_NBSP_LEADING_GROUP,
|
|
181
185
|
RE_NBSP_LEAD_GROUP,
|
package/src/lib/fragments.js
CHANGED
|
@@ -225,7 +225,7 @@ function replaceCustomFragments(value, fragments, replacer) {
|
|
|
225
225
|
? previous
|
|
226
226
|
: ('open' in fragment ? scan(fragment, i, from, false) : run(fragment, from, false));
|
|
227
227
|
found[i] = match;
|
|
228
|
-
// Nothing ahead now means nothing ahead later either
|
|
228
|
+
// Nothing ahead now means nothing ahead later, either
|
|
229
229
|
if (!match) exhausted[i] = true;
|
|
230
230
|
}
|
|
231
231
|
|
package/src/lib/options.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createUrlMinifier } from './urls.js';
|
|
2
|
-
import { LRU, MAX_CACHE_ENTRY_SIZE, stableStringify, hashContent, identity, lowercase, replaceAsync, parseRegExp, describeQuantifierRisk } from './utils.js';
|
|
2
|
+
import { LRU, MAX_CACHE_ENTRY_SIZE, stableStringify, hashContent, identity, lowercase, replaceAsync, parseRegExp, describeQuantifierRisk, lostFlag } from './utils.js';
|
|
3
3
|
import { RE_TRAILING_SEMICOLON } from './constants.js';
|
|
4
4
|
import { canCollapseWhitespace, canTrimWhitespace } from './whitespace.js';
|
|
5
5
|
import { wrapCSS, unwrapCSS } from './content.js';
|
|
@@ -602,6 +602,25 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
602
602
|
}
|
|
603
603
|
});
|
|
604
604
|
|
|
605
|
+
// The parser merges these into one attribute pattern, and a merged pattern
|
|
606
|
+
// carries no flags of its own. `i` and `s` are written into the source instead,
|
|
607
|
+
// but `u` and `v` change how a source reads and cannot be: dropped, `\p{L}`
|
|
608
|
+
// stops being a property escape and matches the literal text `p{L}`, silently
|
|
609
|
+
// and without failing to compile. Refusing them beats matching the wrong thing.
|
|
610
|
+
/** @type {[string, RegExp[]][]} */
|
|
611
|
+
const merged = [
|
|
612
|
+
['customAttrAssign', options.customAttrAssign || []],
|
|
613
|
+
['customAttrSurround', (options.customAttrSurround || []).flat()]
|
|
614
|
+
];
|
|
615
|
+
for (const [key, patterns] of merged) {
|
|
616
|
+
for (const re of patterns) {
|
|
617
|
+
if (!(re instanceof RegExp)) continue;
|
|
618
|
+
const flag = lostFlag(re);
|
|
619
|
+
if (!flag) continue;
|
|
620
|
+
throw new Error(`HTML Minifier Next: \`${key}\` pattern \`/${re.source}/${re.flags}\` carries \`${flag}\`, which the merged attribute pattern cannot carry—rewrite it without \`${flag}\``);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
605
624
|
// Fragments that compound quantifiers or alternation under unbounded repetition
|
|
606
625
|
// are the shapes that backtrack catastrophically, and they are also the ones a
|
|
607
626
|
// linear scan cannot stand in for; so are patterns too long or too deeply
|
|
@@ -610,7 +629,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
610
629
|
// fragments have exactly that shape, so the check flagging it would mean
|
|
611
630
|
// warning about the defaults themselves.
|
|
612
631
|
for (const re of options.ignoreCustomFragments || []) {
|
|
613
|
-
const risk = describeQuantifierRisk(re
|
|
632
|
+
const risk = describeQuantifierRisk(re);
|
|
614
633
|
if (!risk) continue;
|
|
615
634
|
const problem = `Custom fragment \`/${re.source}/\` ${risk}`;
|
|
616
635
|
if (options.strictCustomFragments) {
|
package/src/lib/unused-css.js
CHANGED
|
@@ -170,7 +170,7 @@ function findRawTextElements(haystack, tagName) {
|
|
|
170
170
|
* Collect the class names and IDs a document references.
|
|
171
171
|
*
|
|
172
172
|
* Style sheet contents are excluded, so that a style sheet never counts as
|
|
173
|
-
* evidence for its own selectors. An `iframe srcdoc` value is not scanned
|
|
173
|
+
* evidence for its own selectors. An `iframe srcdoc` value is not scanned,
|
|
174
174
|
* either: It holds a document of its own, minified against its own symbol set.
|
|
175
175
|
* Over-collecting is safe here (a symbol wrongly considered used is merely
|
|
176
176
|
* kept), under-collecting is not.
|
package/src/lib/utils.js
CHANGED
|
@@ -145,6 +145,67 @@ function parseRegExp(value) {
|
|
|
145
145
|
return value;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// Regex source reading, shared by the analysis and the rewriting below
|
|
149
|
+
|
|
150
|
+
const RE_ASCII_LETTER = /^[a-zA-Z]$/;
|
|
151
|
+
const RE_HEX_PAIR = /^[0-9a-fA-F]{2}$/;
|
|
152
|
+
const RE_HEX_QUAD = /^[0-9a-fA-F]{4}$/;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {string} source @param {number} index - Index of the opening `[`
|
|
156
|
+
* @param {boolean} [nested] - Whether the pattern carries `v`, where a class nests
|
|
157
|
+
* inside a class and the first `]` need not be the one that closes it
|
|
158
|
+
* @returns {number} Index just past the closing `]`
|
|
159
|
+
*/
|
|
160
|
+
function skipCharacterClass(source, index, nested = false) {
|
|
161
|
+
let i = index + 1;
|
|
162
|
+
let open = 1;
|
|
163
|
+
while (i < source.length && open > 0) {
|
|
164
|
+
const char = source[i];
|
|
165
|
+
if (char === '\\') i += tokenLength(source, i);
|
|
166
|
+
else {
|
|
167
|
+
if (nested && char === '[') open++;
|
|
168
|
+
else if (char === ']') open--;
|
|
169
|
+
i++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return i;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Length of the token at `index`, so that a multi-character escape is read whole
|
|
177
|
+
* rather than leaving its tail to be mistaken for literals.
|
|
178
|
+
* @param {string} source @param {number} index
|
|
179
|
+
*/
|
|
180
|
+
function tokenLength(source, index) {
|
|
181
|
+
if (source[index] !== '\\') return 1;
|
|
182
|
+
const next = source[index + 1];
|
|
183
|
+
if (next === 'x' && RE_HEX_PAIR.test(source.slice(index + 2, index + 4))) return 4;
|
|
184
|
+
if (next === 'u') {
|
|
185
|
+
if (source[index + 2] === '{') {
|
|
186
|
+
const close = source.indexOf('}', index + 3);
|
|
187
|
+
if (close !== -1) return close - index + 1;
|
|
188
|
+
}
|
|
189
|
+
if (RE_HEX_QUAD.test(source.slice(index + 2, index + 6))) return 6;
|
|
190
|
+
}
|
|
191
|
+
if (next === 'c' && RE_ASCII_LETTER.test(source[index + 2] ?? '')) return 3;
|
|
192
|
+
// A `v` class holds string literals, which fold as strings or not at all
|
|
193
|
+
if (next === 'q' && source[index + 2] === '{') {
|
|
194
|
+
const close = source.indexOf('}', index + 3);
|
|
195
|
+
if (close !== -1) return close - index + 1;
|
|
196
|
+
}
|
|
197
|
+
// A property name or a group name is syntax, not text to fold
|
|
198
|
+
if ((next === 'p' || next === 'P') && source[index + 2] === '{') {
|
|
199
|
+
const close = source.indexOf('}', index + 3);
|
|
200
|
+
if (close !== -1) return close - index + 1;
|
|
201
|
+
}
|
|
202
|
+
if (next === 'k' && source[index + 2] === '<') {
|
|
203
|
+
const close = source.indexOf('>', index + 3);
|
|
204
|
+
if (close !== -1) return close - index + 1;
|
|
205
|
+
}
|
|
206
|
+
return 2;
|
|
207
|
+
}
|
|
208
|
+
|
|
148
209
|
// ReDoS risk detection for user-supplied patterns
|
|
149
210
|
|
|
150
211
|
// Quantifier following an atom, with its optional lazy `?`; the captures hold
|
|
@@ -164,14 +225,15 @@ const RE_TRANSPARENT_GROUP = /^\((?:\?:|\?<[^>=!][^>]*>)?([\s\S]*)\)$/;
|
|
|
164
225
|
// A quantifier of exactly one repetition, which is notation rather than shape
|
|
165
226
|
const RE_EXACT_ONE = /\{1(?:,1)?\}\??$/;
|
|
166
227
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
228
|
+
// An atom that matches without consuming, so what precedes it stays adjacent to
|
|
229
|
+
// what follows; a group that can match empty is left out, being rare enough that
|
|
230
|
+
// missing it costs less than reading every body for it
|
|
231
|
+
const RE_ZERO_WIDTH = /^(?:[$^]|\\[bB]|\((?:\?[=!]|\?<[=!]))/;
|
|
232
|
+
|
|
233
|
+
// How many repeats an atom may still be adjacent to across atoms matching empty;
|
|
234
|
+
// comparing against every one of them is what the bound keeps from turning
|
|
235
|
+
// quadratic on a pattern that is nothing but repeats
|
|
236
|
+
const MAX_REACHABLE = 50;
|
|
175
237
|
|
|
176
238
|
/** @param {string} atom - Atom as it reads in the source */
|
|
177
239
|
function unwrapAtom(atom) {
|
|
@@ -184,15 +246,156 @@ function unwrapAtom(atom) {
|
|
|
184
246
|
return inner;
|
|
185
247
|
}
|
|
186
248
|
|
|
249
|
+
// Two atoms that repeat unboundedly side by side split a run between them in as
|
|
250
|
+
// many ways as the run is long, whenever both can consume the same character.
|
|
251
|
+
// Comparing what they match, rather than how they are spelled, is what catches
|
|
252
|
+
// `[a]*a*` and `\w*\d*` alongside `a*a*`.
|
|
253
|
+
|
|
254
|
+
const MAX_CODE_POINT = 0x10FFFF;
|
|
255
|
+
|
|
256
|
+
/** @type {Record<string, [number, number][]>} */
|
|
257
|
+
const CLASS_ESCAPE_RANGES = {
|
|
258
|
+
d: [[0x30, 0x39]],
|
|
259
|
+
w: [[0x30, 0x39], [0x41, 0x5A], [0x5F, 0x5F], [0x61, 0x7A]],
|
|
260
|
+
s: [[0x09, 0x0D], [0x20, 0x20], [0xA0, 0xA0], [0x1680, 0x1680], [0x2000, 0x200A],
|
|
261
|
+
[0x2028, 0x2029], [0x202F, 0x202F], [0x205F, 0x205F], [0x3000, 0x3000], [0xFEFF, 0xFEFF]]
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/** @type {Record<string, number>} */
|
|
265
|
+
const CONTROL_ESCAPE_CODES = { 0: 0x00, f: 0x0C, n: 0x0A, r: 0x0D, t: 0x09, v: 0x0B };
|
|
266
|
+
|
|
267
|
+
// An escape that stands for something other than one character of text
|
|
268
|
+
const RE_NON_CHARACTER_ESCAPE = /[bBdDkpPsSwW1-9]/;
|
|
269
|
+
|
|
270
|
+
/** @param {[number, number][]} ranges */
|
|
271
|
+
function complement(ranges) {
|
|
272
|
+
const sorted = [...ranges].sort((one, other) => one[0] - other[0]);
|
|
273
|
+
/** @type {[number, number][]} */
|
|
274
|
+
const out = [];
|
|
275
|
+
let next = 0;
|
|
276
|
+
for (const [low, high] of sorted) {
|
|
277
|
+
if (low > next) out.push([next, low - 1]);
|
|
278
|
+
next = Math.max(next, high + 1);
|
|
279
|
+
}
|
|
280
|
+
if (next <= MAX_CODE_POINT) out.push([next, MAX_CODE_POINT]);
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// `.` as a bare source reads it, without the `s` flag the source cannot carry
|
|
285
|
+
const DOT_RANGES = complement([[0x0A, 0x0A], [0x0D, 0x0D], [0x2028, 0x2029]]);
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* @param {string} token - One token, as `tokenLength` measures it
|
|
289
|
+
* @param {boolean} [inClass] - Whether the token sits inside a character class
|
|
290
|
+
* @returns {[number, number][] | null} What it matches, or `null` where it is not
|
|
291
|
+
* one character of text
|
|
292
|
+
*/
|
|
293
|
+
function tokenRanges(token, inClass = false) {
|
|
294
|
+
if (token[0] !== '\\') {
|
|
295
|
+
const code = token.codePointAt(0) ?? 0;
|
|
296
|
+
return String.fromCodePoint(code) === token ? [[code, code]] : null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const kind = token[1] ?? '';
|
|
300
|
+
const named = CLASS_ESCAPE_RANGES[kind.toLowerCase()];
|
|
301
|
+
if (named) return kind === kind.toLowerCase() ? named : complement(named);
|
|
302
|
+
|
|
303
|
+
const control = CONTROL_ESCAPE_CODES[kind];
|
|
304
|
+
if (control !== undefined) return [[control, control]];
|
|
305
|
+
|
|
306
|
+
if (kind === 'x' || kind === 'u') {
|
|
307
|
+
const code = Number.parseInt(token[2] === '{' ? token.slice(3, -1) : token.slice(2), 16);
|
|
308
|
+
return Number.isNaN(code) || code > MAX_CODE_POINT ? null : [[code, code]];
|
|
309
|
+
}
|
|
310
|
+
if (kind === 'c') {
|
|
311
|
+
const code = token.charCodeAt(2) % 32;
|
|
312
|
+
return [[code, code]];
|
|
313
|
+
}
|
|
314
|
+
// `\b` asserts a word boundary on its own, and is a backspace inside a class
|
|
315
|
+
if (kind === 'b' && inClass) return [[0x08, 0x08]];
|
|
316
|
+
// `\q{…}` stands for whole strings, not for a character
|
|
317
|
+
if (token.startsWith('\\q{')) return null;
|
|
318
|
+
if (RE_NON_CHARACTER_ESCAPE.test(kind)) return null;
|
|
319
|
+
|
|
320
|
+
// The rest is punctuation escaped to be read as itself
|
|
321
|
+
const code = token.codePointAt(1) ?? 0;
|
|
322
|
+
return String.fromCodePoint(code) === token.slice(1) ? [[code, code]] : null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** @param {[number, number][] | null} ranges @returns {number | null} The one code point it holds */
|
|
326
|
+
function singleCode(ranges) {
|
|
327
|
+
const [range] = ranges ?? [];
|
|
328
|
+
return ranges?.length === 1 && range && range[0] === range[1] ? range[0] : null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @param {string} atom - Atom as it reads in the source, already unwrapped
|
|
333
|
+
* @param {boolean} [nested] - Whether the pattern carries `v`
|
|
334
|
+
* @param {number} [depth] - Class nesting level of this call
|
|
335
|
+
* @returns {[number, number][] | null} What it matches, or `null` where it is not
|
|
336
|
+
* one character of text, or one this does not read
|
|
337
|
+
*/
|
|
338
|
+
function atomRanges(atom, nested = false, depth = 0) {
|
|
339
|
+
if (depth > MAX_PATTERN_DEPTH) return null;
|
|
340
|
+
if (atom === '.') return DOT_RANGES;
|
|
341
|
+
if (atom[0] !== '[') return tokenRanges(atom);
|
|
342
|
+
if (!atom.endsWith(']')) return null;
|
|
343
|
+
|
|
344
|
+
let i = atom[1] === '^' ? 2 : 1;
|
|
345
|
+
const negated = i === 2;
|
|
346
|
+
const end = atom.length - 1;
|
|
347
|
+
/** @type {[number, number][]} */
|
|
348
|
+
const members = [];
|
|
349
|
+
|
|
350
|
+
while (i < end) {
|
|
351
|
+
// A `v` class nests, and nesting alone is a union to read through
|
|
352
|
+
if (nested && atom[i] === '[') {
|
|
353
|
+
const close = skipCharacterClass(atom, i, true);
|
|
354
|
+
const inner = atomRanges(atom.slice(i, close), true, depth + 1);
|
|
355
|
+
if (!inner) return null;
|
|
356
|
+
members.push(...inner);
|
|
357
|
+
i = close;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
// Subtraction and intersection are not unions, and are left unread
|
|
361
|
+
//
|
|
362
|
+
// @@ Read `--` and `&&` as set difference and intersection
|
|
363
|
+
// (so that a `v` class built is compared rather than passed unjudged)
|
|
364
|
+
if (nested && (atom.startsWith('--', i) || atom.startsWith('&&', i))) return null;
|
|
365
|
+
|
|
366
|
+
const fromLength = tokenLength(atom, i);
|
|
367
|
+
const from = tokenRanges(atom.slice(i, i + fromLength), true);
|
|
368
|
+
if (!from) return null;
|
|
369
|
+
i += fromLength;
|
|
370
|
+
|
|
371
|
+
// A dash right before the closing `]` is a member, not the start of a range
|
|
372
|
+
if (atom[i] !== '-' || i + 1 >= end) {
|
|
373
|
+
members.push(...from);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const toLength = tokenLength(atom, i + 1);
|
|
377
|
+
// Only single characters bound a range; `\d-z` is not a range at all
|
|
378
|
+
const low = singleCode(from);
|
|
379
|
+
const high = singleCode(tokenRanges(atom.slice(i + 1, i + 1 + toLength), true));
|
|
380
|
+
if (low === null || high === null) return null;
|
|
381
|
+
members.push([low, high]);
|
|
382
|
+
i += 1 + toLength;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return negated ? complement(members) : members;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** @param {[number, number][]} ranges @param {[number, number][]} other */
|
|
389
|
+
function rangesIntersect(ranges, other) {
|
|
390
|
+
return ranges.some(([low, high]) => other.some(([otherLow, otherHigh]) =>
|
|
391
|
+
low <= otherHigh && otherLow <= high));
|
|
392
|
+
}
|
|
393
|
+
|
|
187
394
|
// Embedding a pattern in a larger regex drops the flags it carried, so the two
|
|
188
395
|
// a source can carry on its own are rewritten into it
|
|
189
396
|
//
|
|
190
397
|
// @@ Replace with inline `(?i:…)` and `(?s:…)` modifiers once Node floor reaches 24
|
|
191
398
|
|
|
192
|
-
const RE_ASCII_LETTER = /^[a-zA-Z]$/;
|
|
193
|
-
const RE_HEX_PAIR = /^[0-9a-fA-F]{2}$/;
|
|
194
|
-
const RE_HEX_QUAD = /^[0-9a-fA-F]{4}$/;
|
|
195
|
-
|
|
196
399
|
/** @param {string} char @returns {boolean} Whether the character has a single-character counterpart */
|
|
197
400
|
function foldsCase(char) {
|
|
198
401
|
const lower = char.toLowerCase();
|
|
@@ -206,36 +409,10 @@ function isLower(char) {
|
|
|
206
409
|
}
|
|
207
410
|
|
|
208
411
|
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* @param {string} source @param {number} index
|
|
412
|
+
* @param {string} source - Regex source, assumed syntactically valid
|
|
413
|
+
* @param {boolean} [nested] - Whether the pattern carries `v`
|
|
212
414
|
*/
|
|
213
|
-
function
|
|
214
|
-
if (source[index] !== '\\') return 1;
|
|
215
|
-
const next = source[index + 1];
|
|
216
|
-
if (next === 'x' && RE_HEX_PAIR.test(source.slice(index + 2, index + 4))) return 4;
|
|
217
|
-
if (next === 'u') {
|
|
218
|
-
if (source[index + 2] === '{') {
|
|
219
|
-
const close = source.indexOf('}', index + 3);
|
|
220
|
-
if (close !== -1) return close - index + 1;
|
|
221
|
-
}
|
|
222
|
-
if (RE_HEX_QUAD.test(source.slice(index + 2, index + 6))) return 6;
|
|
223
|
-
}
|
|
224
|
-
if (next === 'c' && RE_ASCII_LETTER.test(source[index + 2] ?? '')) return 3;
|
|
225
|
-
// A property name or a group name is syntax, not text to fold
|
|
226
|
-
if ((next === 'p' || next === 'P') && source[index + 2] === '{') {
|
|
227
|
-
const close = source.indexOf('}', index + 3);
|
|
228
|
-
if (close !== -1) return close - index + 1;
|
|
229
|
-
}
|
|
230
|
-
if (next === 'k' && source[index + 2] === '<') {
|
|
231
|
-
const close = source.indexOf('>', index + 3);
|
|
232
|
-
if (close !== -1) return close - index + 1;
|
|
233
|
-
}
|
|
234
|
-
return 2;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** @param {string} source - Regex source, assumed syntactically valid */
|
|
238
|
-
function expandDotAll(source) {
|
|
415
|
+
function expandDotAll(source, nested = false) {
|
|
239
416
|
let out = '';
|
|
240
417
|
let i = 0;
|
|
241
418
|
while (i < source.length) {
|
|
@@ -245,7 +422,7 @@ function expandDotAll(source) {
|
|
|
245
422
|
out += source.slice(i, i + length);
|
|
246
423
|
i += length;
|
|
247
424
|
} else if (char === '[') {
|
|
248
|
-
const end = skipCharacterClass(source, i);
|
|
425
|
+
const end = skipCharacterClass(source, i, nested);
|
|
249
426
|
out += source.slice(i, end);
|
|
250
427
|
i = end;
|
|
251
428
|
} else if (char === '.') {
|
|
@@ -259,22 +436,25 @@ function expandDotAll(source) {
|
|
|
259
436
|
return out;
|
|
260
437
|
}
|
|
261
438
|
|
|
262
|
-
/**
|
|
263
|
-
|
|
439
|
+
/**
|
|
440
|
+
* @param {string} source - Regex source, assumed syntactically valid
|
|
441
|
+
* @param {boolean} [nested] - Whether the pattern carries `v`
|
|
442
|
+
*/
|
|
443
|
+
function foldCase(source, nested = false) {
|
|
264
444
|
let out = '';
|
|
265
445
|
let i = 0;
|
|
266
|
-
let
|
|
446
|
+
let open = 0;
|
|
267
447
|
|
|
268
448
|
while (i < source.length) {
|
|
269
449
|
const char = source[i] ?? '';
|
|
270
450
|
|
|
271
|
-
if (
|
|
451
|
+
if (open === 0) {
|
|
272
452
|
if (char === '\\') {
|
|
273
453
|
const length = tokenLength(source, i);
|
|
274
454
|
out += source.slice(i, i + length);
|
|
275
455
|
i += length;
|
|
276
456
|
} else if (char === '[') {
|
|
277
|
-
|
|
457
|
+
open = 1;
|
|
278
458
|
out += char;
|
|
279
459
|
i++;
|
|
280
460
|
} else if (char === '(' && source.startsWith('(?<', i) &&
|
|
@@ -291,8 +471,15 @@ function foldCase(source) {
|
|
|
291
471
|
continue;
|
|
292
472
|
}
|
|
293
473
|
|
|
474
|
+
if (nested && char === '[') {
|
|
475
|
+
open++;
|
|
476
|
+
out += char;
|
|
477
|
+
i++;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
|
|
294
481
|
if (char === ']') {
|
|
295
|
-
|
|
482
|
+
open--;
|
|
296
483
|
out += char;
|
|
297
484
|
i++;
|
|
298
485
|
continue;
|
|
@@ -324,6 +511,93 @@ function foldCase(source) {
|
|
|
324
511
|
return out;
|
|
325
512
|
}
|
|
326
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Whether a source anchors anywhere, so that `m` would move where it matches
|
|
516
|
+
* @param {string} source @param {boolean} [nested] - Whether the pattern carries `v`
|
|
517
|
+
*/
|
|
518
|
+
function hasAnchor(source, nested = false) {
|
|
519
|
+
let i = 0;
|
|
520
|
+
while (i < source.length) {
|
|
521
|
+
if (source[i] === '\\') i += tokenLength(source, i);
|
|
522
|
+
// Inside a class `^` negates and `$` is a member, so neither anchors there
|
|
523
|
+
else if (source[i] === '[') i = skipCharacterClass(source, i, nested);
|
|
524
|
+
else if (source[i] === '^' || source[i] === '$') return true;
|
|
525
|
+
else i++;
|
|
526
|
+
}
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// A property escape or a code point escape, both of which read as literal text
|
|
531
|
+
// where the flag that gives them meaning is gone
|
|
532
|
+
const RE_UNICODE_ESCAPE = /\\[pP]\{|\\u\{/;
|
|
533
|
+
|
|
534
|
+
// The characters `u` folds by Unicode rules and a source without it does not:
|
|
535
|
+
// `/s/iu` matches `\u017F` and `/k/iu` matches `\u212A`, where neither does on its
|
|
536
|
+
// own. Characters past the BMP fold this way, too, and are caught as astral first.
|
|
537
|
+
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]/;
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Which flag changes what a source matches, so that embedding the source where the
|
|
541
|
+
* flag cannot follow would silently match something else. `u` and `v` only narrow
|
|
542
|
+
* what syntax is legal, so a source valid under either stays valid without it—the
|
|
543
|
+
* difference never surfaces as a syntax error, and this stands in for the one that
|
|
544
|
+
* would otherwise be raised.
|
|
545
|
+
* @param {RegExp} pattern
|
|
546
|
+
* @returns {'u' | 'v' | 'm' | null} The flag the source depends on, or `null` where
|
|
547
|
+
* every flag it carries would leave the source matching the same
|
|
548
|
+
*/
|
|
549
|
+
function lostFlag(pattern) {
|
|
550
|
+
const { source, unicode, unicodeSets } = pattern;
|
|
551
|
+
// `m` moves where `^` and `$` match, and only matters where the source has one
|
|
552
|
+
if (pattern.multiline && hasAnchor(source, unicodeSets)) return 'm';
|
|
553
|
+
if (!unicode && !unicodeSets) return null;
|
|
554
|
+
const flag = unicodeSets ? 'v' : 'u';
|
|
555
|
+
if (RE_UNICODE_ESCAPE.test(source)) return flag;
|
|
556
|
+
// Case folding under `i` follows Unicode rules only while the flag is there. A
|
|
557
|
+
// character can be written literally or as an escape, so the source is read
|
|
558
|
+
// token by token rather than scanned for the characters themselves; `singleCode`
|
|
559
|
+
// leaves out `\d` and friends, which fold alike with the flag and without it.
|
|
560
|
+
if (pattern.ignoreCase) {
|
|
561
|
+
for (let i = 0; i < source.length;) {
|
|
562
|
+
const length = tokenLength(source, i);
|
|
563
|
+
const code = singleCode(tokenRanges(source.slice(i, i + length), true));
|
|
564
|
+
if (code !== null && RE_UNICODE_FOLDING.test(String.fromCodePoint(code))) return flag;
|
|
565
|
+
i += length;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// Past the BMP a character is one code point with the flag, two units without,
|
|
569
|
+
// which is what a quantifier or a class beside it would go on to read wrongly
|
|
570
|
+
for (const char of source) {
|
|
571
|
+
if ((char.codePointAt(0) ?? 0) > 0xFFFF) return flag;
|
|
572
|
+
}
|
|
573
|
+
if (!unicodeSets) return null;
|
|
574
|
+
|
|
575
|
+
// `v` alone lets a class nest, subtract, intersect, and hold whole strings
|
|
576
|
+
let i = 0;
|
|
577
|
+
while (i < source.length) {
|
|
578
|
+
if (source[i] === '\\') {
|
|
579
|
+
i += tokenLength(source, i);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (source[i] !== '[') {
|
|
583
|
+
i++;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const end = skipCharacterClass(source, i, true);
|
|
587
|
+
for (let j = i + 1; j < end - 1;) {
|
|
588
|
+
if (source[j] === '\\') {
|
|
589
|
+
if (source.startsWith('\\q{', j)) return 'v';
|
|
590
|
+
j += tokenLength(source, j);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (source[j] === '[' || source.startsWith('--', j) || source.startsWith('&&', j)) return 'v';
|
|
594
|
+
j++;
|
|
595
|
+
}
|
|
596
|
+
i = end;
|
|
597
|
+
}
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
327
601
|
/**
|
|
328
602
|
* A pattern’s source, rewritten to match the way its own flags make it match, for
|
|
329
603
|
* embedding in a larger regex that cannot carry them. `i` and `s` fit into a
|
|
@@ -336,30 +610,77 @@ function foldCase(source) {
|
|
|
336
610
|
*/
|
|
337
611
|
function embedSource(pattern) {
|
|
338
612
|
let source = pattern.source;
|
|
339
|
-
if (pattern.dotAll) source = expandDotAll(source);
|
|
340
|
-
if (pattern.ignoreCase) source = foldCase(source);
|
|
613
|
+
if (pattern.dotAll) source = expandDotAll(source, pattern.unicodeSets);
|
|
614
|
+
if (pattern.ignoreCase) source = foldCase(source, pattern.unicodeSets);
|
|
341
615
|
return source;
|
|
342
616
|
}
|
|
343
617
|
|
|
618
|
+
/**
|
|
619
|
+
* @typedef {{text: string, ranges: [number, number][] | null}} Repeat - An unbounded
|
|
620
|
+
* repeat, as it reads and as the set of characters it matches
|
|
621
|
+
*/
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* @param {Repeat[]} reachable - Repeats that could still sit adjacent to what comes next
|
|
625
|
+
* @param {Repeat} repeat
|
|
626
|
+
* @returns {boolean} Whether the two can consume the same character, and so split
|
|
627
|
+
* the same run of input between them
|
|
628
|
+
*/
|
|
629
|
+
function splitsWith(reachable, repeat) {
|
|
630
|
+
return reachable.some(earlier => earlier.text === repeat.text ||
|
|
631
|
+
(!!repeat.ranges && !!earlier.ranges && rangesIntersect(repeat.ranges, earlier.ranges)));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* @param {Repeat[]} into
|
|
636
|
+
* @param {Repeat[]} repeats
|
|
637
|
+
* @returns {void} Adds the repeats, keeping only the most recent ones the walk
|
|
638
|
+
* still compares against
|
|
639
|
+
*/
|
|
640
|
+
function collectReachable(into, repeats) {
|
|
641
|
+
for (const repeat of repeats) {
|
|
642
|
+
into.push(repeat);
|
|
643
|
+
if (into.length > MAX_REACHABLE) into.shift();
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
344
647
|
/**
|
|
345
648
|
* Walk a regex source for the shapes whose backtracking blows up: an unlimited
|
|
346
649
|
* quantifier over a group that itself contains a variable quantifier (`(a+)+`,
|
|
347
|
-
* `(a?)+`) or alternates, and
|
|
348
|
-
*
|
|
349
|
-
*
|
|
650
|
+
* `(a?)+`) or alternates, and two unbounded repeats that can consume the same
|
|
651
|
+
* character with only atoms matching empty between them (`.*.*`, `[a]*a*`,
|
|
652
|
+
* `\w*\d*`, `a*b*a*`, `a*(b*)a*`, `(a*)a*`). A lone unlimited quantifier stays
|
|
653
|
+
* linear, so `[\s\S]*?` up to a literal terminator passes.
|
|
350
654
|
* @param {string} source - Regex source, assumed syntactically valid
|
|
351
655
|
* @param {number} [depth] - Group nesting level of this call
|
|
352
|
-
* @
|
|
656
|
+
* @param {boolean} [nested] - Whether the pattern carries `v`
|
|
657
|
+
* @returns {{risky: boolean, varies: boolean, alternates: boolean, deep: boolean,
|
|
658
|
+
* empty: boolean, leading: Repeat[], trailing: Repeat[]}} What the walk found,
|
|
659
|
+
* with the repeats that reach the source’s start and end for a caller to compare
|
|
660
|
+
* against what stands beside it
|
|
353
661
|
*/
|
|
354
|
-
function analyzeQuantifiers(source, depth = 0) {
|
|
355
|
-
if (depth > MAX_PATTERN_DEPTH) return { risky: true, varies: true, alternates: true, deep: true };
|
|
662
|
+
function analyzeQuantifiers(source, depth = 0, nested = false) {
|
|
663
|
+
if (depth > MAX_PATTERN_DEPTH) return { risky: true, varies: true, alternates: true, deep: true, empty: true, leading: [], trailing: [] };
|
|
356
664
|
|
|
357
665
|
let risky = false;
|
|
358
666
|
let varies = false;
|
|
359
667
|
let alternates = false;
|
|
360
668
|
let deep = false;
|
|
361
|
-
|
|
362
|
-
|
|
669
|
+
// Whether every atom of the branch being read matches empty, and whether any
|
|
670
|
+
// branch before it did
|
|
671
|
+
let branchEmpty = true;
|
|
672
|
+
let anyBranchEmpty = false;
|
|
673
|
+
// The unbounded repeats that could still sit adjacent to what comes next, most
|
|
674
|
+
// recent last; an atom matching empty leaves the ones before it reachable
|
|
675
|
+
/** @type {Repeat[]} */
|
|
676
|
+
const reachable = [];
|
|
677
|
+
// The repeats a caller can reach from either end, gathered across branches
|
|
678
|
+
/** @type {Repeat[]} */
|
|
679
|
+
const leading = [];
|
|
680
|
+
/** @type {Repeat[]} */
|
|
681
|
+
const trailing = [];
|
|
682
|
+
/** @type {Repeat[]} */
|
|
683
|
+
let branchLeading = [];
|
|
363
684
|
let i = 0;
|
|
364
685
|
|
|
365
686
|
while (i < source.length) {
|
|
@@ -371,22 +692,27 @@ function analyzeQuantifiers(source, depth = 0) {
|
|
|
371
692
|
if (char === '|') {
|
|
372
693
|
// Alternatives are separate expressions, so nothing carries across
|
|
373
694
|
alternates = true;
|
|
374
|
-
|
|
695
|
+
anyBranchEmpty ||= branchEmpty;
|
|
696
|
+
branchEmpty = true;
|
|
697
|
+
collectReachable(leading, branchLeading);
|
|
698
|
+
collectReachable(trailing, reachable);
|
|
699
|
+
branchLeading = [];
|
|
700
|
+
reachable.length = 0;
|
|
375
701
|
i++;
|
|
376
702
|
continue;
|
|
377
703
|
} else if (char === '\\') {
|
|
378
|
-
i +=
|
|
704
|
+
i += tokenLength(source, i);
|
|
379
705
|
} else if (char === '[') {
|
|
380
|
-
i = skipCharacterClass(source, i);
|
|
706
|
+
i = skipCharacterClass(source, i, nested);
|
|
381
707
|
} else if (char === '(') {
|
|
382
708
|
let open = 1;
|
|
383
709
|
i++;
|
|
384
710
|
while (i < source.length && open > 0) {
|
|
385
711
|
const inner = source[i];
|
|
386
712
|
if (inner === '\\') {
|
|
387
|
-
i +=
|
|
713
|
+
i += tokenLength(source, i);
|
|
388
714
|
} else if (inner === '[') {
|
|
389
|
-
i = skipCharacterClass(source, i);
|
|
715
|
+
i = skipCharacterClass(source, i, nested);
|
|
390
716
|
} else {
|
|
391
717
|
if (inner === '(') open++;
|
|
392
718
|
else if (inner === ')') open--;
|
|
@@ -394,7 +720,7 @@ function analyzeQuantifiers(source, depth = 0) {
|
|
|
394
720
|
}
|
|
395
721
|
}
|
|
396
722
|
// Drop the group prefix (`?:`, `?=`, `?<name>`, …) before reading the body
|
|
397
|
-
group = analyzeQuantifiers(source.slice(start + 1, i - 1).replace(/^\?(?:[:=!]|<[=!]|<[^>]*>)/, ''), depth + 1);
|
|
723
|
+
group = analyzeQuantifiers(source.slice(start + 1, i - 1).replace(/^\?(?:[:=!]|<[=!]|<[^>]*>)/, ''), depth + 1, nested);
|
|
398
724
|
} else {
|
|
399
725
|
i++;
|
|
400
726
|
}
|
|
@@ -410,35 +736,71 @@ function analyzeQuantifiers(source, depth = 0) {
|
|
|
410
736
|
const exact = !!quantifier && quantifier[0][0] === '{' &&
|
|
411
737
|
(upper === undefined || (upper !== '' && Number(upper) === Number(quantifier[1])));
|
|
412
738
|
const variable = !!quantifier && !exact;
|
|
739
|
+
// An atom matches empty where it is zero-width, where its quantifier may
|
|
740
|
+
// repeat none, or where a group body matches empty however often it repeats—
|
|
741
|
+
// `(b*)`, carrying the quantifier inside the group rather than on it
|
|
742
|
+
const least = quantifier && (quantifier[0][0] === '+' ? 1
|
|
743
|
+
: quantifier[0][0] === '{' ? Number(quantifier[1]) : 0);
|
|
744
|
+
const body = RE_ZERO_WIDTH.test(atom) || (!!group && group.empty);
|
|
745
|
+
const nullable = body || (!!quantifier && least === 0);
|
|
413
746
|
if (quantifier) i = RE_QUANTIFIER.lastIndex;
|
|
414
747
|
|
|
748
|
+
// A lookaround is atomic: It backtracks nothing, so what it holds neither
|
|
749
|
+
// reaches out of it nor is reached into
|
|
750
|
+
const opaque = RE_ZERO_WIDTH.test(atom);
|
|
415
751
|
if (group) {
|
|
416
752
|
if (group.risky || (repeats && (group.varies || group.alternates))) risky = true;
|
|
417
753
|
if (group.varies) varies = true;
|
|
418
754
|
// A group alternates whether the `|` sits at its top level or deeper
|
|
419
755
|
if (group.alternates) alternates = true;
|
|
420
756
|
if (group.deep) deep = true;
|
|
757
|
+
// A group is no wall: What it repeats at its start splits the same run as
|
|
758
|
+
// what stands unbounded before it
|
|
759
|
+
if (!opaque && group.leading.some(repeat => splitsWith(reachable, repeat))) risky = true;
|
|
421
760
|
}
|
|
422
761
|
if (variable) varies = true;
|
|
423
762
|
const text = unwrapAtom(atom);
|
|
424
|
-
|
|
425
|
-
|
|
763
|
+
const ranges = atomRanges(text, nested);
|
|
764
|
+
if (repeats && splitsWith(reachable, { text, ranges })) risky = true;
|
|
765
|
+
|
|
766
|
+
// Whatever a caller could reach before this atom, it reaches this one, too
|
|
767
|
+
if (branchEmpty) {
|
|
768
|
+
if (repeats) collectReachable(branchLeading, [{ text, ranges }]);
|
|
769
|
+
if (group && !opaque) collectReachable(branchLeading, group.leading);
|
|
770
|
+
}
|
|
771
|
+
// Nothing reaches past an atom that has to consume something
|
|
772
|
+
if (!nullable) {
|
|
773
|
+
reachable.length = 0;
|
|
774
|
+
branchEmpty = false;
|
|
775
|
+
}
|
|
776
|
+
if (group && !opaque) collectReachable(reachable, group.trailing);
|
|
777
|
+
if (repeats) collectReachable(reachable, [{ text, ranges }]);
|
|
426
778
|
}
|
|
427
779
|
|
|
428
|
-
|
|
780
|
+
collectReachable(leading, branchLeading);
|
|
781
|
+
collectReachable(trailing, reachable);
|
|
782
|
+
|
|
783
|
+
return { risky, varies, alternates, deep, empty: anyBranchEmpty || branchEmpty, leading, trailing };
|
|
429
784
|
}
|
|
430
785
|
|
|
431
786
|
/**
|
|
432
|
-
* @param {string}
|
|
787
|
+
* @param {RegExp | string} pattern - Pattern to judge, or a bare source to read
|
|
788
|
+
* as it stands
|
|
433
789
|
* @returns {string | null} What makes the pattern a backtracking risk, phrased
|
|
434
790
|
* to follow the pattern itself, or `null` where it is none
|
|
435
791
|
*/
|
|
436
|
-
function describeQuantifierRisk(
|
|
437
|
-
|
|
792
|
+
function describeQuantifierRisk(pattern) {
|
|
793
|
+
const source = typeof pattern === 'string' ? pattern : pattern.source;
|
|
794
|
+
// A pattern too big to read is refused for that, not for a shape nobody saw;
|
|
795
|
+
// its own length is what counts, not the length folding case inflates it to
|
|
438
796
|
if (source.length > MAX_PATTERN_LENGTH) {
|
|
439
797
|
return `runs past ${MAX_PATTERN_LENGTH.toLocaleString()} characters, too long to analyze for catastrophic backtracking—shorten it, or split it into several patterns`;
|
|
440
798
|
}
|
|
441
|
-
|
|
799
|
+
// `i` and `s` change what the source matches, so they are written into it
|
|
800
|
+
// before the shapes are read
|
|
801
|
+
const analysis = typeof pattern === 'string'
|
|
802
|
+
? analyzeQuantifiers(source)
|
|
803
|
+
: analyzeQuantifiers(embedSource(pattern), 0, pattern.unicodeSets);
|
|
442
804
|
if (analysis.deep) {
|
|
443
805
|
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`;
|
|
444
806
|
}
|
|
@@ -484,5 +846,6 @@ export {
|
|
|
484
846
|
replaceAsync,
|
|
485
847
|
parseRegExp,
|
|
486
848
|
embedSource,
|
|
849
|
+
lostFlag,
|
|
487
850
|
describeQuantifierRisk
|
|
488
851
|
};
|