@aglyn/shared-util-tools 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +39 -0
- package/src/index.d.ts +72 -0
- package/src/index.js +87 -0
- package/src/index.js.map +1 -0
- package/src/lib/apply-mixins.d.ts +24 -0
- package/src/lib/apply-mixins.js +31 -0
- package/src/lib/apply-mixins.js.map +1 -0
- package/src/lib/array/array-copy-deep.d.ts +6 -0
- package/src/lib/array/array-copy-deep.js +25 -0
- package/src/lib/array/array-copy-deep.js.map +1 -0
- package/src/lib/array/array-copy-shallow.d.ts +19 -0
- package/src/lib/array/array-copy-shallow.js +20 -0
- package/src/lib/array/array-copy-shallow.js.map +1 -0
- package/src/lib/array/array-from-length.d.ts +18 -0
- package/src/lib/array/array-from-length.js +23 -0
- package/src/lib/array/array-from-length.js.map +1 -0
- package/src/lib/array/array-from.d.ts +24 -0
- package/src/lib/array/array-from.js +26 -0
- package/src/lib/array/array-from.js.map +1 -0
- package/src/lib/array/array-move-at-index.d.ts +24 -0
- package/src/lib/array/array-move-at-index.js +32 -0
- package/src/lib/array/array-move-at-index.js.map +1 -0
- package/src/lib/array/array-of-entries-to-object.d.ts +23 -0
- package/src/lib/array/array-of-entries-to-object.js +26 -0
- package/src/lib/array/array-of-entries-to-object.js.map +1 -0
- package/src/lib/array/array-overrides.d.ts +29 -0
- package/src/lib/array/array-overrides.js +71 -0
- package/src/lib/array/array-overrides.js.map +1 -0
- package/src/lib/array/array-push-at-index.d.ts +28 -0
- package/src/lib/array/array-push-at-index.js +32 -0
- package/src/lib/array/array-push-at-index.js.map +1 -0
- package/src/lib/array/array-remove-at-index.d.ts +23 -0
- package/src/lib/array/array-remove-at-index.js +26 -0
- package/src/lib/array/array-remove-at-index.js.map +1 -0
- package/src/lib/array/array-remove-item.d.ts +23 -0
- package/src/lib/array/array-remove-item.js +27 -0
- package/src/lib/array/array-remove-item.js.map +1 -0
- package/src/lib/array/array-safe.d.ts +21 -0
- package/src/lib/array/array-safe.js +29 -0
- package/src/lib/array/array-safe.js.map +1 -0
- package/src/lib/array/array-sort-by-deep-property.d.ts +26 -0
- package/src/lib/array/array-sort-by-deep-property.js +39 -0
- package/src/lib/array/array-sort-by-deep-property.js.map +1 -0
- package/src/lib/array/array-sort-by.d.ts +26 -0
- package/src/lib/array/array-sort-by.js +36 -0
- package/src/lib/array/array-sort-by.js.map +1 -0
- package/src/lib/array/array-update-at-index.d.ts +18 -0
- package/src/lib/array/array-update-at-index.js +22 -0
- package/src/lib/array/array-update-at-index.js.map +1 -0
- package/src/lib/array/array-update.d.ts +23 -0
- package/src/lib/array/array-update.js +25 -0
- package/src/lib/array/array-update.js.map +1 -0
- package/src/lib/bitwise/bitwise-has-all-attributes.d.ts +18 -0
- package/src/lib/bitwise/bitwise-has-all-attributes.js +21 -0
- package/src/lib/bitwise/bitwise-has-all-attributes.js.map +1 -0
- package/src/lib/bitwise/bitwise-has-attribute.d.ts +18 -0
- package/src/lib/bitwise/bitwise-has-attribute.js +21 -0
- package/src/lib/bitwise/bitwise-has-attribute.js.map +1 -0
- package/src/lib/bitwise/bitwise-has-only-attributes.d.ts +18 -0
- package/src/lib/bitwise/bitwise-has-only-attributes.js +21 -0
- package/src/lib/bitwise/bitwise-has-only-attributes.js.map +1 -0
- package/src/lib/contrast.d.ts +65 -0
- package/src/lib/contrast.js +78 -0
- package/src/lib/contrast.js.map +1 -0
- package/src/lib/copy-shallow.d.ts +19 -0
- package/src/lib/copy-shallow.js +20 -0
- package/src/lib/copy-shallow.js.map +1 -0
- package/src/lib/copy.d.ts +19 -0
- package/src/lib/copy.js +20 -0
- package/src/lib/copy.js.map +1 -0
- package/src/lib/create-chained-function.d.ts +38 -0
- package/src/lib/create-chained-function.js +46 -0
- package/src/lib/create-chained-function.js.map +1 -0
- package/src/lib/crud.d.ts +46 -0
- package/src/lib/crud.js +55 -0
- package/src/lib/crud.js.map +1 -0
- package/src/lib/css.d.ts +41 -0
- package/src/lib/css.js +54 -0
- package/src/lib/css.js.map +1 -0
- package/src/lib/escape-html.d.ts +38 -0
- package/src/lib/escape-html.js +47 -0
- package/src/lib/escape-html.js.map +1 -0
- package/src/lib/falsy.d.ts +23 -0
- package/src/lib/falsy.js +25 -0
- package/src/lib/falsy.js.map +1 -0
- package/src/lib/get-display-name.d.ts +46 -0
- package/src/lib/get-display-name.js +64 -0
- package/src/lib/get-display-name.js.map +1 -0
- package/src/lib/get-property.d.ts +30 -0
- package/src/lib/get-property.js +36 -0
- package/src/lib/get-property.js.map +1 -0
- package/src/lib/get-static-field.d.ts +23 -0
- package/src/lib/get-static-field.js +25 -0
- package/src/lib/get-static-field.js.map +1 -0
- package/src/lib/guards/index.d.ts +46 -0
- package/src/lib/guards/index.js +47 -0
- package/src/lib/guards/index.js.map +1 -0
- package/src/lib/guards/lib/_has-own-property.d.ts +24 -0
- package/src/lib/guards/lib/_has-own-property.js +27 -0
- package/src/lib/guards/lib/_has-own-property.js.map +1 -0
- package/src/lib/guards/lib/_is-arr-empty.d.ts +23 -0
- package/src/lib/guards/lib/_is-arr-empty.js +25 -0
- package/src/lib/guards/lib/_is-arr-empty.js.map +1 -0
- package/src/lib/guards/lib/_is-arr-of-arr.d.ts +25 -0
- package/src/lib/guards/lib/_is-arr-of-arr.js +31 -0
- package/src/lib/guards/lib/_is-arr-of-arr.js.map +1 -0
- package/src/lib/guards/lib/_is-arr.d.ts +21 -0
- package/src/lib/guards/lib/_is-arr.js +23 -0
- package/src/lib/guards/lib/_is-arr.js.map +1 -0
- package/src/lib/guards/lib/_is-big.d.ts +25 -0
- package/src/lib/guards/lib/_is-big.js +27 -0
- package/src/lib/guards/lib/_is-big.js.map +1 -0
- package/src/lib/guards/lib/_is-bool.d.ts +25 -0
- package/src/lib/guards/lib/_is-bool.js +27 -0
- package/src/lib/guards/lib/_is-bool.js.map +1 -0
- package/src/lib/guards/lib/_is-buff.d.ts +25 -0
- package/src/lib/guards/lib/_is-buff.js +29 -0
- package/src/lib/guards/lib/_is-buff.js.map +1 -0
- package/src/lib/guards/lib/_is-ctor.d.ts +25 -0
- package/src/lib/guards/lib/_is-ctor.js +30 -0
- package/src/lib/guards/lib/_is-ctor.js.map +1 -0
- package/src/lib/guards/lib/_is-empty-str-or-arr.d.ts +25 -0
- package/src/lib/guards/lib/_is-empty-str-or-arr.js +29 -0
- package/src/lib/guards/lib/_is-empty-str-or-arr.js.map +1 -0
- package/src/lib/guards/lib/_is-equality-same-type.d.ts +25 -0
- package/src/lib/guards/lib/_is-equality-same-type.js +33 -0
- package/src/lib/guards/lib/_is-equality-same-type.js.map +1 -0
- package/src/lib/guards/lib/_is-fn-t.d.ts +25 -0
- package/src/lib/guards/lib/_is-fn-t.js +27 -0
- package/src/lib/guards/lib/_is-fn-t.js.map +1 -0
- package/src/lib/guards/lib/_is-length-of.d.ts +47 -0
- package/src/lib/guards/lib/_is-length-of.js +49 -0
- package/src/lib/guards/lib/_is-length-of.js.map +1 -0
- package/src/lib/guards/lib/_is-length.d.ts +25 -0
- package/src/lib/guards/lib/_is-length.js +34 -0
- package/src/lib/guards/lib/_is-length.js.map +1 -0
- package/src/lib/guards/lib/_is-null.d.ts +25 -0
- package/src/lib/guards/lib/_is-null.js +27 -0
- package/src/lib/guards/lib/_is-null.js.map +1 -0
- package/src/lib/guards/lib/_is-num-neg.d.ts +25 -0
- package/src/lib/guards/lib/_is-num-neg.js +28 -0
- package/src/lib/guards/lib/_is-num-neg.js.map +1 -0
- package/src/lib/guards/lib/_is-num-pos.d.ts +25 -0
- package/src/lib/guards/lib/_is-num-pos.js +28 -0
- package/src/lib/guards/lib/_is-num-pos.js.map +1 -0
- package/src/lib/guards/lib/_is-num-t.d.ts +25 -0
- package/src/lib/guards/lib/_is-num-t.js +27 -0
- package/src/lib/guards/lib/_is-num-t.js.map +1 -0
- package/src/lib/guards/lib/_is-num-zero.d.ts +25 -0
- package/src/lib/guards/lib/_is-num-zero.js +28 -0
- package/src/lib/guards/lib/_is-num-zero.js.map +1 -0
- package/src/lib/guards/lib/_is-num.d.ts +26 -0
- package/src/lib/guards/lib/_is-num.js +29 -0
- package/src/lib/guards/lib/_is-num.js.map +1 -0
- package/src/lib/guards/lib/_is-obj-t.d.ts +26 -0
- package/src/lib/guards/lib/_is-obj-t.js +28 -0
- package/src/lib/guards/lib/_is-obj-t.js.map +1 -0
- package/src/lib/guards/lib/_is-obj.d.ts +27 -0
- package/src/lib/guards/lib/_is-obj.js +32 -0
- package/src/lib/guards/lib/_is-obj.js.map +1 -0
- package/src/lib/guards/lib/_is-prim.d.ts +27 -0
- package/src/lib/guards/lib/_is-prim.js +35 -0
- package/src/lib/guards/lib/_is-prim.js.map +1 -0
- package/src/lib/guards/lib/_is-promise-like.d.ts +24 -0
- package/src/lib/guards/lib/_is-promise-like.js +28 -0
- package/src/lib/guards/lib/_is-promise-like.js.map +1 -0
- package/src/lib/guards/lib/_is-str-empty.d.ts +25 -0
- package/src/lib/guards/lib/_is-str-empty.js +28 -0
- package/src/lib/guards/lib/_is-str-empty.js.map +1 -0
- package/src/lib/guards/lib/_is-str-t.d.ts +26 -0
- package/src/lib/guards/lib/_is-str-t.js +28 -0
- package/src/lib/guards/lib/_is-str-t.js.map +1 -0
- package/src/lib/guards/lib/_is-sym-t.d.ts +25 -0
- package/src/lib/guards/lib/_is-sym-t.js +27 -0
- package/src/lib/guards/lib/_is-sym-t.js.map +1 -0
- package/src/lib/guards/lib/_is-und-or-null.d.ts +25 -0
- package/src/lib/guards/lib/_is-und-or-null.js +29 -0
- package/src/lib/guards/lib/_is-und-or-null.js.map +1 -0
- package/src/lib/guards/lib/_is-und-t.d.ts +25 -0
- package/src/lib/guards/lib/_is-und-t.js +27 -0
- package/src/lib/guards/lib/_is-und-t.js.map +1 -0
- package/src/lib/interop-default.d.ts +18 -0
- package/src/lib/interop-default.js +21 -0
- package/src/lib/interop-default.js.map +1 -0
- package/src/lib/length.d.ts +22 -0
- package/src/lib/length.js +28 -0
- package/src/lib/length.js.map +1 -0
- package/src/lib/linear-regex.d.ts +136 -0
- package/src/lib/linear-regex.js +1140 -0
- package/src/lib/linear-regex.js.map +1 -0
- package/src/lib/no-side-effects.d.ts +29 -0
- package/src/lib/no-side-effects.js +33 -0
- package/src/lib/no-side-effects.js.map +1 -0
- package/src/lib/noop.d.ts +21 -0
- package/src/lib/noop.js +22 -0
- package/src/lib/noop.js.map +1 -0
- package/src/lib/normalized.d.ts +164 -0
- package/src/lib/normalized.js +232 -0
- package/src/lib/normalized.js.map +1 -0
- package/src/lib/number/number-from-hexadecimal.d.ts +24 -0
- package/src/lib/number/number-from-hexadecimal.js +29 -0
- package/src/lib/number/number-from-hexadecimal.js.map +1 -0
- package/src/lib/number/number-to-hexadecimal.d.ts +22 -0
- package/src/lib/number/number-to-hexadecimal.js +24 -0
- package/src/lib/number/number-to-hexadecimal.js.map +1 -0
- package/src/lib/number/to-num.d.ts +41 -0
- package/src/lib/number/to-num.js +41 -0
- package/src/lib/number/to-num.js.map +1 -0
- package/src/lib/numeronym.d.ts +41 -0
- package/src/lib/numeronym.js +54 -0
- package/src/lib/numeronym.js.map +1 -0
- package/src/lib/object/object-clone-deep.d.ts +21 -0
- package/src/lib/object/object-clone-deep.js +24 -0
- package/src/lib/object/object-clone-deep.js.map +1 -0
- package/src/lib/object/object-clone.d.ts +21 -0
- package/src/lib/object/object-clone.js +24 -0
- package/src/lib/object/object-clone.js.map +1 -0
- package/src/lib/object/object-delete-property.d.ts +28 -0
- package/src/lib/object/object-delete-property.js +32 -0
- package/src/lib/object/object-delete-property.js.map +1 -0
- package/src/lib/object/object-get-deep-property.d.ts +35 -0
- package/src/lib/object/object-get-deep-property.js +40 -0
- package/src/lib/object/object-get-deep-property.js.map +1 -0
- package/src/lib/object/object-get-keys-and-symbol-properties.d.ts +23 -0
- package/src/lib/object/object-get-keys-and-symbol-properties.js +30 -0
- package/src/lib/object/object-get-keys-and-symbol-properties.js.map +1 -0
- package/src/lib/object/object-remap.d.ts +33 -0
- package/src/lib/object/object-remap.js +36 -0
- package/src/lib/object/object-remap.js.map +1 -0
- package/src/lib/object/object-safe.d.ts +27 -0
- package/src/lib/object/object-safe.js +30 -0
- package/src/lib/object/object-safe.js.map +1 -0
- package/src/lib/object/object-set-deep-property.d.ts +35 -0
- package/src/lib/object/object-set-deep-property.js +44 -0
- package/src/lib/object/object-set-deep-property.js.map +1 -0
- package/src/lib/object/object-update.d.ts +28 -0
- package/src/lib/object/object-update.js +32 -0
- package/src/lib/object/object-update.js.map +1 -0
- package/src/lib/operator/compare.d.ts +26 -0
- package/src/lib/operator/compare.js +42 -0
- package/src/lib/operator/compare.js.map +1 -0
- package/src/lib/operator/definitions.d.ts +184 -0
- package/src/lib/operator/definitions.js +110 -0
- package/src/lib/operator/definitions.js.map +1 -0
- package/src/lib/operator/index.d.ts +19 -0
- package/src/lib/operator/index.js +20 -0
- package/src/lib/operator/index.js.map +1 -0
- package/src/lib/operator/inference.d.ts +66 -0
- package/src/lib/operator/inference.js +60 -0
- package/src/lib/operator/inference.js.map +1 -0
- package/src/lib/secret-box.d.ts +152 -0
- package/src/lib/secret-box.js +263 -0
- package/src/lib/secret-box.js.map +1 -0
- package/src/lib/serialize/base64-isomorphic-decode.d.ts +23 -0
- package/src/lib/serialize/base64-isomorphic-decode.js +28 -0
- package/src/lib/serialize/base64-isomorphic-decode.js.map +1 -0
- package/src/lib/serialize/base64-isomorphic-encode.d.ts +23 -0
- package/src/lib/serialize/base64-isomorphic-encode.js +28 -0
- package/src/lib/serialize/base64-isomorphic-encode.js.map +1 -0
- package/src/lib/serialize/json-deserialize.d.ts +25 -0
- package/src/lib/serialize/json-deserialize.js +27 -0
- package/src/lib/serialize/json-deserialize.js.map +1 -0
- package/src/lib/serialize/json-serialize.d.ts +25 -0
- package/src/lib/serialize/json-serialize.js +27 -0
- package/src/lib/serialize/json-serialize.js.map +1 -0
- package/src/lib/serialize/php-serial-deserialize-from-byte-stream.d.ts +77 -0
- package/src/lib/serialize/php-serial-deserialize-from-byte-stream.js +77 -0
- package/src/lib/serialize/php-serial-deserialize-from-byte-stream.js.map +1 -0
- package/src/lib/serialize/php-serial-is-serialized-byte-stream.d.ts +77 -0
- package/src/lib/serialize/php-serial-is-serialized-byte-stream.js +77 -0
- package/src/lib/serialize/php-serial-is-serialized-byte-stream.js.map +1 -0
- package/src/lib/serialize/php-serial-serialize-to-byte-stream.d.ts +77 -0
- package/src/lib/serialize/php-serial-serialize-to-byte-stream.js +77 -0
- package/src/lib/serialize/php-serial-serialize-to-byte-stream.js.map +1 -0
- package/src/lib/split-display-name.d.ts +36 -0
- package/src/lib/split-display-name.js +47 -0
- package/src/lib/split-display-name.js.map +1 -0
- package/src/lib/str.d.ts +21 -0
- package/src/lib/str.js +23 -0
- package/src/lib/str.js.map +1 -0
- package/src/lib/trim.d.ts +25 -0
- package/src/lib/trim.js +28 -0
- package/src/lib/trim.js.map +1 -0
- package/src/lib/truthy.d.ts +23 -0
- package/src/lib/truthy.js +30 -0
- package/src/lib/truthy.js.map +1 -0
|
@@ -0,0 +1,1140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* A linear-time regular-expression engine (SEC-M8 / AGL-1881), for patterns
|
|
18
|
+
* someone other than the platform wrote that run where every tenant renders:
|
|
19
|
+
* host redirect rules, and the conditions a component or layout property
|
|
20
|
+
* declares (AGL-2893).
|
|
21
|
+
*
|
|
22
|
+
* WHY THIS EXISTS
|
|
23
|
+
*
|
|
24
|
+
* Redirect regexes are attacker-authored (any `author` on a paid site can
|
|
25
|
+
* write `source` straight to Firestore — the console's validation is not a
|
|
26
|
+
* control) and they execute on the tenant render path, in the Node process
|
|
27
|
+
* shared by every tenant. `RegExp.prototype.exec` is a backtracking matcher,
|
|
28
|
+
* so a pattern like `(a|a|aa)+` against `"aaaa…a!"` costs exponential time:
|
|
29
|
+
* measured at 59 s for a 9-character pattern and a 27-character path, and it
|
|
30
|
+
* keeps doubling from there. That is a multi-tenant denial of service. A
|
|
31
|
+
* property condition's pattern is the same hazard with a wider door: it can
|
|
32
|
+
* arrive from a marketplace listing, and it runs on every page that places
|
|
33
|
+
* the component and in the console of everyone who installed it.
|
|
34
|
+
*
|
|
35
|
+
* Three heuristics have been tried and bypassed (AGL-505 star height, a
|
|
36
|
+
* length cap, nesting limits). A shape heuristic can never be sound: deciding
|
|
37
|
+
* whether an arbitrary backtracking regex is safe is the same problem as
|
|
38
|
+
* bounding its search tree. So this module does not inspect patterns for
|
|
39
|
+
* dangerous shapes. It removes the backtracking engine.
|
|
40
|
+
*
|
|
41
|
+
* HOW IT IS LINEAR BY CONSTRUCTION
|
|
42
|
+
*
|
|
43
|
+
* The pattern is parsed into an AST, compiled to a small NFA bytecode, and
|
|
44
|
+
* executed by a Thompson/Pike simulation: instead of trying one path at a
|
|
45
|
+
* time and backtracking, it advances *all* live NFA states one input
|
|
46
|
+
* character at a time, deduplicated by program counter. Each of the
|
|
47
|
+
* `programLength` states is visited at most once per input character, so the
|
|
48
|
+
* worst case is exactly `O(inputLength × programLength)` — there is no input
|
|
49
|
+
* that makes it exponential, because there is no search tree to explode.
|
|
50
|
+
*
|
|
51
|
+
* Thread priority ordering, and cutting lower-priority threads when a match
|
|
52
|
+
* is found, reproduce JavaScript's leftmost-first (greedy/lazy) semantics and
|
|
53
|
+
* its capture-group results, so ordinary patterns behave exactly as they did.
|
|
54
|
+
*
|
|
55
|
+
* `new RegExp` is never called on the pattern. Constructs that cannot be
|
|
56
|
+
* simulated in linear time — backreferences and lookaround — are not merely
|
|
57
|
+
* rejected by a checker that might have a hole; there is no instruction that
|
|
58
|
+
* could execute them, and the parser has no production that accepts them.
|
|
59
|
+
*
|
|
60
|
+
* WHAT THIS DOES NOT DO
|
|
61
|
+
*
|
|
62
|
+
* It is not a general-purpose RegExp replacement. It implements the subset
|
|
63
|
+
* documented in `UNSUPPORTED_SYNTAX` below and nothing else, and it makes no
|
|
64
|
+
* attempt at unicode mode or `lastIndex`. Of the flags, `i`, `m`, `s` and `y`
|
|
65
|
+
* mean what they mean to a fresh `RegExp`'s `test` (see {@link FLAG_CHARS});
|
|
66
|
+
* `g` and `d` change nothing a match answers, so they are accepted and have
|
|
67
|
+
* no effect; `u` and `v` are refused.
|
|
68
|
+
*/ /**
|
|
69
|
+
* Syntax deliberately outside the supported subset. Each entry is a
|
|
70
|
+
* construct that either cannot be simulated in linear time at all
|
|
71
|
+
* (backreferences, lookaround) or that is simply not implemented. Word
|
|
72
|
+
* boundaries are the exception `compileLinearTest` makes: it reads them, and
|
|
73
|
+
* `compileLinearPattern`, which redirect rules are held to, does not.
|
|
74
|
+
*/ export const UNSUPPORTED_SYNTAX = [
|
|
75
|
+
'lookahead `(?=…)` / `(?!…)`',
|
|
76
|
+
'lookbehind `(?<=…)` / `(?<!…)`',
|
|
77
|
+
'backreferences `\\1`…`\\9`, `\\k<name>`',
|
|
78
|
+
'named groups `(?<name>…)`',
|
|
79
|
+
'word boundaries `\\b` / `\\B`',
|
|
80
|
+
'unicode property escapes `\\p{…}` / `\\P{…}`',
|
|
81
|
+
'inline flags and other `(?…)` groups',
|
|
82
|
+
'negated shorthands (`\\D`, `\\W`, `\\S`) inside a `[…]` class'
|
|
83
|
+
];
|
|
84
|
+
/**
|
|
85
|
+
* Ceiling on compiled program size. Bounded repeats are expanded (`a{3}`
|
|
86
|
+
* becomes three copies), so this is what stops `(\d{99}){99}` from turning a
|
|
87
|
+
* short pattern into a huge program.
|
|
88
|
+
*
|
|
89
|
+
* This is a resource bound, not a shape heuristic: execution is already
|
|
90
|
+
* linear in program length, so bounding the program bounds total work at
|
|
91
|
+
* `MAX_PROGRAM_LENGTH × inputLength` ≈ 1000 × 500 = 5×10^5 state visits in
|
|
92
|
+
* the absolute worst case — a few milliseconds, and flat in the input rather
|
|
93
|
+
* than exponential. Nothing about *which* patterns are dangerous is being
|
|
94
|
+
* guessed here; every accepted pattern is already safe.
|
|
95
|
+
*/ const MAX_PROGRAM_LENGTH = 1000;
|
|
96
|
+
/** Largest bounded-repeat count accepted, e.g. the `99` in `a{2,99}`. */ const MAX_REPEAT_COUNT = 100;
|
|
97
|
+
/** Largest capture-group count. Destinations only substitute `$1`…`$9`. */ const MAX_CAPTURE_GROUPS = 20;
|
|
98
|
+
/** Thrown for any pattern outside the supported subset. Never escapes. */ let PatternError = class PatternError extends Error {
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* The flags a pattern may carry, as the characters `RegExp` spells them.
|
|
102
|
+
* `g` and `d` are here because a fresh `RegExp`'s `test` answers the same
|
|
103
|
+
* with them as without; `u` and `v` are not, because unicode mode is not
|
|
104
|
+
* implemented.
|
|
105
|
+
*/ export const FLAG_CHARS = 'dgimsy';
|
|
106
|
+
/** A pattern with no flags — what every redirect rule is compiled with. */ const NO_FLAGS = {
|
|
107
|
+
ignoreCase: false,
|
|
108
|
+
multiline: false,
|
|
109
|
+
dotAll: false,
|
|
110
|
+
sticky: false
|
|
111
|
+
};
|
|
112
|
+
/** Reads a flags string, refusing what `RegExp` would and what is unsupported. */ function parseFlags(flags) {
|
|
113
|
+
const seen = new Set();
|
|
114
|
+
for (const flag of flags){
|
|
115
|
+
if (flag === 'u' || flag === 'v') {
|
|
116
|
+
throw new PatternError(`the ${flag} flag is not supported`);
|
|
117
|
+
}
|
|
118
|
+
if (!FLAG_CHARS.includes(flag)) {
|
|
119
|
+
throw new PatternError(`"${flag}" is not a pattern flag`);
|
|
120
|
+
}
|
|
121
|
+
if (seen.has(flag)) throw new PatternError(`the ${flag} flag is repeated`);
|
|
122
|
+
seen.add(flag);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
ignoreCase: seen.has('i'),
|
|
126
|
+
multiline: seen.has('m'),
|
|
127
|
+
dotAll: seen.has('s'),
|
|
128
|
+
sticky: seen.has('y')
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* One UTF-16 code unit case-folded the way `RegExp` folds it without the `u`
|
|
133
|
+
* flag (ECMAScript `Canonicalize`): to its upper case, unless that is more
|
|
134
|
+
* than one unit, or would carry a non-ASCII character into ASCII.
|
|
135
|
+
*/ function canonicalize(code) {
|
|
136
|
+
const upper = String.fromCharCode(code).toUpperCase();
|
|
137
|
+
if (upper.length !== 1) return code;
|
|
138
|
+
const folded = upper.charCodeAt(0);
|
|
139
|
+
return code >= 128 && folded < 128 ? code : folded;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Every code unit whose case-folded form is another, with that form, as flat
|
|
143
|
+
* `[unit, folded, …]` pairs in unit order. Built once, on the first pattern
|
|
144
|
+
* that asks for `i`.
|
|
145
|
+
*/ let foldedUnits;
|
|
146
|
+
function unitsThatFold() {
|
|
147
|
+
if (!foldedUnits) {
|
|
148
|
+
foldedUnits = [];
|
|
149
|
+
for(let unit = 0; unit <= 0xffff; unit++){
|
|
150
|
+
const folded = canonicalize(unit);
|
|
151
|
+
if (folded !== unit) foldedUnits.push(unit, folded);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return foldedUnits;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* A class's ranges with the case-folded form of every member added, so that
|
|
158
|
+
* testing an input's folded form against them asks what `RegExp` asks: is
|
|
159
|
+
* there a member whose folded form this is. Every folded form folds to
|
|
160
|
+
* itself, so the members left as written are never an input's folded form
|
|
161
|
+
* unless they already fold to themselves.
|
|
162
|
+
*/ function foldRanges(ranges) {
|
|
163
|
+
const units = unitsThatFold();
|
|
164
|
+
const added = new Set();
|
|
165
|
+
for(let i = 0; i < ranges.length; i += 2){
|
|
166
|
+
const lo = ranges[i];
|
|
167
|
+
const hi = Math.min(ranges[i + 1], 0xffff);
|
|
168
|
+
for(let j = 0; j < units.length; j += 2){
|
|
169
|
+
if (units[j] > hi) break;
|
|
170
|
+
const folded = units[j + 1];
|
|
171
|
+
if (units[j] >= lo && !rangesContain(ranges, folded)) added.add(folded);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!added.size) return ranges;
|
|
175
|
+
// Adjacent folded forms merge into one range, so `[a-z]` gains `A-Z` as
|
|
176
|
+
// one pair rather than twenty-six.
|
|
177
|
+
const merged = [];
|
|
178
|
+
for (const code of [
|
|
179
|
+
...added
|
|
180
|
+
].sort((a, b)=>a - b)){
|
|
181
|
+
if (merged.length && merged[merged.length - 1] === code - 1) {
|
|
182
|
+
merged[merged.length - 1] = code;
|
|
183
|
+
} else {
|
|
184
|
+
merged.push(code, code);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return [
|
|
188
|
+
...ranges,
|
|
189
|
+
...merged
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Character-class helpers
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
const DIGIT_RANGES = [
|
|
196
|
+
0x30,
|
|
197
|
+
0x39
|
|
198
|
+
];
|
|
199
|
+
const WORD_RANGES = [
|
|
200
|
+
0x30,
|
|
201
|
+
0x39,
|
|
202
|
+
0x41,
|
|
203
|
+
0x5a,
|
|
204
|
+
0x5f,
|
|
205
|
+
0x5f,
|
|
206
|
+
0x61,
|
|
207
|
+
0x7a
|
|
208
|
+
];
|
|
209
|
+
const SPACE_RANGES = [
|
|
210
|
+
0x09,
|
|
211
|
+
0x0d,
|
|
212
|
+
0x20,
|
|
213
|
+
0x20,
|
|
214
|
+
0xa0,
|
|
215
|
+
0xa0,
|
|
216
|
+
0x1680,
|
|
217
|
+
0x1680,
|
|
218
|
+
0x2000,
|
|
219
|
+
0x200a,
|
|
220
|
+
0x2028,
|
|
221
|
+
0x2029,
|
|
222
|
+
0x202f,
|
|
223
|
+
0x202f,
|
|
224
|
+
0x205f,
|
|
225
|
+
0x205f,
|
|
226
|
+
0x3000,
|
|
227
|
+
0x3000,
|
|
228
|
+
0xfeff,
|
|
229
|
+
0xfeff
|
|
230
|
+
];
|
|
231
|
+
/** `.` matches anything but a line terminator (no `s` flag), as in JS. */ const isLineTerminator = (code)=>code === 0x0a || code === 0x0d || code === 0x2028 || code === 0x2029;
|
|
232
|
+
/** Ranges are stored as flat `[lo, hi, lo, hi, …]` pairs. */ function rangesContain(ranges, code) {
|
|
233
|
+
for(let i = 0; i < ranges.length; i += 2){
|
|
234
|
+
if (code >= ranges[i] && code <= ranges[i + 1]) return true;
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
/** The single-character escapes JS gives a non-literal meaning. */ function controlEscape(ch) {
|
|
239
|
+
switch(ch){
|
|
240
|
+
case 'n':
|
|
241
|
+
return 0x0a;
|
|
242
|
+
case 'r':
|
|
243
|
+
return 0x0d;
|
|
244
|
+
case 't':
|
|
245
|
+
return 0x09;
|
|
246
|
+
case 'f':
|
|
247
|
+
return 0x0c;
|
|
248
|
+
case 'v':
|
|
249
|
+
return 0x0b;
|
|
250
|
+
case '0':
|
|
251
|
+
return 0x00;
|
|
252
|
+
default:
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// Parser
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
/**
|
|
260
|
+
* Recursive-descent parser for the supported subset.
|
|
261
|
+
*
|
|
262
|
+
* The grammar has no production for lookaround or backreferences, so those
|
|
263
|
+
* are rejected structurally rather than by pattern inspection.
|
|
264
|
+
*/ let Parser = class Parser {
|
|
265
|
+
parse() {
|
|
266
|
+
const node = this.parseAlternation();
|
|
267
|
+
if (this.pos < this.src.length) {
|
|
268
|
+
// Only an unbalanced `)` can get here.
|
|
269
|
+
throw new PatternError(`unexpected "${this.src[this.pos]}"`);
|
|
270
|
+
}
|
|
271
|
+
return node;
|
|
272
|
+
}
|
|
273
|
+
peek() {
|
|
274
|
+
return this.src[this.pos];
|
|
275
|
+
}
|
|
276
|
+
parseAlternation() {
|
|
277
|
+
const options = [
|
|
278
|
+
this.parseConcat()
|
|
279
|
+
];
|
|
280
|
+
while(this.peek() === '|'){
|
|
281
|
+
this.pos++;
|
|
282
|
+
options.push(this.parseConcat());
|
|
283
|
+
}
|
|
284
|
+
return options.length === 1 ? options[0] : {
|
|
285
|
+
t: 'alt',
|
|
286
|
+
options
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
parseConcat() {
|
|
290
|
+
const items = [];
|
|
291
|
+
while(this.pos < this.src.length){
|
|
292
|
+
const ch = this.peek();
|
|
293
|
+
if (ch === '|' || ch === ')') break;
|
|
294
|
+
items.push(this.parseRepeat());
|
|
295
|
+
}
|
|
296
|
+
if (items.length === 0) return {
|
|
297
|
+
t: 'empty'
|
|
298
|
+
};
|
|
299
|
+
return items.length === 1 ? items[0] : {
|
|
300
|
+
t: 'cat',
|
|
301
|
+
items
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
parseRepeat() {
|
|
305
|
+
const atom = this.parseAtom();
|
|
306
|
+
let min;
|
|
307
|
+
let max;
|
|
308
|
+
const ch = this.peek();
|
|
309
|
+
if (ch === '*') {
|
|
310
|
+
this.pos++;
|
|
311
|
+
min = 0;
|
|
312
|
+
max = Infinity;
|
|
313
|
+
} else if (ch === '+') {
|
|
314
|
+
this.pos++;
|
|
315
|
+
min = 1;
|
|
316
|
+
max = Infinity;
|
|
317
|
+
} else if (ch === '?') {
|
|
318
|
+
this.pos++;
|
|
319
|
+
min = 0;
|
|
320
|
+
max = 1;
|
|
321
|
+
} else if (ch === '{') {
|
|
322
|
+
const bounds = this.tryParseBounds();
|
|
323
|
+
if (bounds === null) return atom;
|
|
324
|
+
min = bounds[0];
|
|
325
|
+
max = bounds[1];
|
|
326
|
+
} else {
|
|
327
|
+
return atom;
|
|
328
|
+
}
|
|
329
|
+
let greedy = true;
|
|
330
|
+
if (this.peek() === '?') {
|
|
331
|
+
this.pos++;
|
|
332
|
+
greedy = false;
|
|
333
|
+
}
|
|
334
|
+
if (this.peek() === '*' || this.peek() === '+') {
|
|
335
|
+
// `a**` is a syntax error in JS too; refuse rather than guess.
|
|
336
|
+
throw new PatternError('a quantifier cannot follow a quantifier');
|
|
337
|
+
}
|
|
338
|
+
if (atom.t === 'assertStart' || atom.t === 'assertEnd' || atom.t === 'wordBoundary' || atom.t === 'empty') {
|
|
339
|
+
throw new PatternError('nothing to repeat');
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
t: 'rep',
|
|
343
|
+
node: atom,
|
|
344
|
+
min,
|
|
345
|
+
max,
|
|
346
|
+
greedy
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* `{2}`, `{2,}`, `{2,5}`. A `{` that is not a valid bound is a literal
|
|
351
|
+
* brace in JS (`/a{x/` is fine), so return null and let the caller keep
|
|
352
|
+
* the atom; `parseAtom` will have already consumed the `{` as a literal.
|
|
353
|
+
*/ tryParseBounds() {
|
|
354
|
+
const start = this.pos;
|
|
355
|
+
this.pos++; // consume `{`
|
|
356
|
+
const digits = ()=>{
|
|
357
|
+
const from = this.pos;
|
|
358
|
+
while(this.pos < this.src.length && /[0-9]/.test(this.src[this.pos])){
|
|
359
|
+
this.pos++;
|
|
360
|
+
}
|
|
361
|
+
if (this.pos === from) return null;
|
|
362
|
+
return Number(this.src.slice(from, this.pos));
|
|
363
|
+
};
|
|
364
|
+
const min = digits();
|
|
365
|
+
if (min === null) {
|
|
366
|
+
this.pos = start;
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
let max = min;
|
|
370
|
+
if (this.peek() === ',') {
|
|
371
|
+
this.pos++;
|
|
372
|
+
if (this.peek() === '}') {
|
|
373
|
+
max = Infinity;
|
|
374
|
+
} else {
|
|
375
|
+
const parsed = digits();
|
|
376
|
+
if (parsed === null) {
|
|
377
|
+
this.pos = start;
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
max = parsed;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (this.peek() !== '}') {
|
|
384
|
+
this.pos = start;
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
this.pos++;
|
|
388
|
+
if (min > MAX_REPEAT_COUNT || max !== Infinity && max > MAX_REPEAT_COUNT) {
|
|
389
|
+
throw new PatternError(`repeat counts above ${MAX_REPEAT_COUNT}`);
|
|
390
|
+
}
|
|
391
|
+
if (max < min) throw new PatternError('repeat range is backwards');
|
|
392
|
+
return [
|
|
393
|
+
min,
|
|
394
|
+
max
|
|
395
|
+
];
|
|
396
|
+
}
|
|
397
|
+
parseAtom() {
|
|
398
|
+
const ch = this.src[this.pos];
|
|
399
|
+
if (ch === '(') return this.parseGroup();
|
|
400
|
+
if (ch === '[') return this.parseClass();
|
|
401
|
+
if (ch === '\\') return this.parseEscape();
|
|
402
|
+
if (ch === '.') {
|
|
403
|
+
this.pos++;
|
|
404
|
+
return {
|
|
405
|
+
t: 'any'
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (ch === '^') {
|
|
409
|
+
this.pos++;
|
|
410
|
+
return {
|
|
411
|
+
t: 'assertStart'
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (ch === '$') {
|
|
415
|
+
this.pos++;
|
|
416
|
+
return {
|
|
417
|
+
t: 'assertEnd'
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (ch === '*' || ch === '+' || ch === '?') {
|
|
421
|
+
throw new PatternError('nothing to repeat');
|
|
422
|
+
}
|
|
423
|
+
this.pos++;
|
|
424
|
+
return {
|
|
425
|
+
t: 'char',
|
|
426
|
+
c: ch.charCodeAt(0)
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
parseGroup() {
|
|
430
|
+
this.pos++; // consume `(`
|
|
431
|
+
let index = 0;
|
|
432
|
+
if (this.peek() === '?') {
|
|
433
|
+
const kind = this.src[this.pos + 1];
|
|
434
|
+
if (kind === ':') {
|
|
435
|
+
this.pos += 2;
|
|
436
|
+
} else if (kind === '=' || kind === '!') {
|
|
437
|
+
throw new PatternError('lookahead is not supported');
|
|
438
|
+
} else if (kind === '<') {
|
|
439
|
+
const after = this.src[this.pos + 2];
|
|
440
|
+
throw new PatternError(after === '=' || after === '!' ? 'lookbehind is not supported' : 'named groups are not supported');
|
|
441
|
+
} else {
|
|
442
|
+
throw new PatternError('this group type is not supported');
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
this.groupCount++;
|
|
446
|
+
if (this.groupCount > MAX_CAPTURE_GROUPS) {
|
|
447
|
+
throw new PatternError(`more than ${MAX_CAPTURE_GROUPS} capture groups`);
|
|
448
|
+
}
|
|
449
|
+
index = this.groupCount;
|
|
450
|
+
}
|
|
451
|
+
const node = this.parseAlternation();
|
|
452
|
+
if (this.peek() !== ')') throw new PatternError('unbalanced "("');
|
|
453
|
+
this.pos++;
|
|
454
|
+
return index === 0 ? node : {
|
|
455
|
+
t: 'group',
|
|
456
|
+
index,
|
|
457
|
+
node
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
parseEscape() {
|
|
461
|
+
this.pos++; // consume `\`
|
|
462
|
+
const ch = this.src[this.pos];
|
|
463
|
+
if (ch === undefined) throw new PatternError('trailing backslash');
|
|
464
|
+
this.pos++;
|
|
465
|
+
if (ch >= '1' && ch <= '9') {
|
|
466
|
+
throw new PatternError('backreferences are not supported');
|
|
467
|
+
}
|
|
468
|
+
if (ch === 'k') throw new PatternError('backreferences are not supported');
|
|
469
|
+
if (ch === 'b' || ch === 'B') {
|
|
470
|
+
if (!this.wordBoundaries) {
|
|
471
|
+
throw new PatternError('word boundaries are not supported');
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
t: 'wordBoundary',
|
|
475
|
+
negated: ch === 'B'
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
if (ch === 'p' || ch === 'P') {
|
|
479
|
+
throw new PatternError('unicode property escapes are not supported');
|
|
480
|
+
}
|
|
481
|
+
if (ch === 'd') return {
|
|
482
|
+
t: 'class',
|
|
483
|
+
ranges: DIGIT_RANGES,
|
|
484
|
+
negated: false
|
|
485
|
+
};
|
|
486
|
+
if (ch === 'D') return {
|
|
487
|
+
t: 'class',
|
|
488
|
+
ranges: DIGIT_RANGES,
|
|
489
|
+
negated: true
|
|
490
|
+
};
|
|
491
|
+
if (ch === 'w') return {
|
|
492
|
+
t: 'class',
|
|
493
|
+
ranges: WORD_RANGES,
|
|
494
|
+
negated: false
|
|
495
|
+
};
|
|
496
|
+
if (ch === 'W') return {
|
|
497
|
+
t: 'class',
|
|
498
|
+
ranges: WORD_RANGES,
|
|
499
|
+
negated: true
|
|
500
|
+
};
|
|
501
|
+
if (ch === 's') return {
|
|
502
|
+
t: 'class',
|
|
503
|
+
ranges: SPACE_RANGES,
|
|
504
|
+
negated: false
|
|
505
|
+
};
|
|
506
|
+
if (ch === 'S') return {
|
|
507
|
+
t: 'class',
|
|
508
|
+
ranges: SPACE_RANGES,
|
|
509
|
+
negated: true
|
|
510
|
+
};
|
|
511
|
+
if (ch === 'u' || ch === 'x') {
|
|
512
|
+
const width = ch === 'u' ? 4 : 2;
|
|
513
|
+
const hex = this.src.slice(this.pos, this.pos + width);
|
|
514
|
+
if (!new RegExp(`^[0-9a-fA-F]{${width}}$`).test(hex)) {
|
|
515
|
+
throw new PatternError(`malformed \\${ch} escape`);
|
|
516
|
+
}
|
|
517
|
+
this.pos += width;
|
|
518
|
+
return {
|
|
519
|
+
t: 'char',
|
|
520
|
+
c: parseInt(hex, 16)
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
const control = controlEscape(ch);
|
|
524
|
+
if (control !== null) return {
|
|
525
|
+
t: 'char',
|
|
526
|
+
c: control
|
|
527
|
+
};
|
|
528
|
+
// `\.` `\/` `\(` … — an escaped literal.
|
|
529
|
+
return {
|
|
530
|
+
t: 'char',
|
|
531
|
+
c: ch.charCodeAt(0)
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
parseClass() {
|
|
535
|
+
this.pos++; // consume `[`
|
|
536
|
+
let negated = false;
|
|
537
|
+
if (this.peek() === '^') {
|
|
538
|
+
this.pos++;
|
|
539
|
+
negated = true;
|
|
540
|
+
}
|
|
541
|
+
const ranges = [];
|
|
542
|
+
let first = true;
|
|
543
|
+
for(;;){
|
|
544
|
+
const ch = this.src[this.pos];
|
|
545
|
+
if (ch === undefined) throw new PatternError('unterminated "["');
|
|
546
|
+
if (ch === ']' && !first) {
|
|
547
|
+
this.pos++;
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
first = false;
|
|
551
|
+
const lo = this.parseClassMember(ranges);
|
|
552
|
+
if (lo === null) continue; // a shorthand already pushed its own ranges
|
|
553
|
+
// A `-` before `]` is a literal dash.
|
|
554
|
+
if (this.peek() === '-' && this.src[this.pos + 1] !== ']') {
|
|
555
|
+
this.pos++;
|
|
556
|
+
const hi = this.parseClassMember(ranges);
|
|
557
|
+
if (hi === null) {
|
|
558
|
+
throw new PatternError('a shorthand cannot be a range endpoint');
|
|
559
|
+
}
|
|
560
|
+
if (hi < lo) throw new PatternError('character range is backwards');
|
|
561
|
+
ranges.push(lo, hi);
|
|
562
|
+
} else {
|
|
563
|
+
ranges.push(lo, lo);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (ranges.length === 0 && !negated) {
|
|
567
|
+
// `[]` never matches in JS. Represent it as a negated match-everything.
|
|
568
|
+
return {
|
|
569
|
+
t: 'class',
|
|
570
|
+
ranges: [
|
|
571
|
+
0,
|
|
572
|
+
0x10ffff
|
|
573
|
+
],
|
|
574
|
+
negated: true
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
t: 'class',
|
|
579
|
+
ranges,
|
|
580
|
+
negated
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* One member of a class. Returns its code point, or null when it was a
|
|
585
|
+
* shorthand (`\d`, `\w`, `\s`) whose ranges were appended directly.
|
|
586
|
+
*/ parseClassMember(ranges) {
|
|
587
|
+
const ch = this.src[this.pos];
|
|
588
|
+
if (ch !== '\\') {
|
|
589
|
+
this.pos++;
|
|
590
|
+
return ch.charCodeAt(0);
|
|
591
|
+
}
|
|
592
|
+
this.pos++;
|
|
593
|
+
const esc = this.src[this.pos];
|
|
594
|
+
if (esc === undefined) throw new PatternError('trailing backslash');
|
|
595
|
+
this.pos++;
|
|
596
|
+
if (esc >= '1' && esc <= '9') {
|
|
597
|
+
throw new PatternError('backreferences are not supported');
|
|
598
|
+
}
|
|
599
|
+
if (esc === 'D' || esc === 'W' || esc === 'S') {
|
|
600
|
+
// The union of a negated shorthand with other members is not a simple
|
|
601
|
+
// range list. Refuse rather than get it subtly wrong.
|
|
602
|
+
throw new PatternError(`\\${esc} inside a character class is not supported`);
|
|
603
|
+
}
|
|
604
|
+
if (esc === 'p' || esc === 'P') {
|
|
605
|
+
throw new PatternError('unicode property escapes are not supported');
|
|
606
|
+
}
|
|
607
|
+
if (esc === 'd') {
|
|
608
|
+
ranges.push(...DIGIT_RANGES);
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
if (esc === 'w') {
|
|
612
|
+
ranges.push(...WORD_RANGES);
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
if (esc === 's') {
|
|
616
|
+
ranges.push(...SPACE_RANGES);
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
if (esc === 'b') return 0x08 // `\b` is a backspace inside a class, as in JS
|
|
620
|
+
;
|
|
621
|
+
if (esc === 'u' || esc === 'x') {
|
|
622
|
+
const width = esc === 'u' ? 4 : 2;
|
|
623
|
+
const hex = this.src.slice(this.pos, this.pos + width);
|
|
624
|
+
if (!new RegExp(`^[0-9a-fA-F]{${width}}$`).test(hex)) {
|
|
625
|
+
throw new PatternError(`malformed \\${esc} escape`);
|
|
626
|
+
}
|
|
627
|
+
this.pos += width;
|
|
628
|
+
return parseInt(hex, 16);
|
|
629
|
+
}
|
|
630
|
+
const control = controlEscape(esc);
|
|
631
|
+
if (control !== null) return control;
|
|
632
|
+
return esc.charCodeAt(0);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* `wordBoundaries` admits `\b` and `\B`. A zero-width test of the two
|
|
636
|
+
* characters around the position is as linear as `^`, so the engine can
|
|
637
|
+
* run one; redirect rules have always refused it, and still do.
|
|
638
|
+
*/ constructor(src, wordBoundaries = false){
|
|
639
|
+
this.src = src;
|
|
640
|
+
this.wordBoundaries = wordBoundaries;
|
|
641
|
+
this.pos = 0;
|
|
642
|
+
/** Group 0 is the whole match, so user groups start at 1. */ this.groupCount = 0;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
/** Capture-group indices appearing anywhere inside a subtree. */ function collectGroups(node, into) {
|
|
646
|
+
switch(node.t){
|
|
647
|
+
case 'group':
|
|
648
|
+
into.push(node.index);
|
|
649
|
+
collectGroups(node.node, into);
|
|
650
|
+
break;
|
|
651
|
+
case 'alt':
|
|
652
|
+
for (const option of node.options)collectGroups(option, into);
|
|
653
|
+
break;
|
|
654
|
+
case 'cat':
|
|
655
|
+
for (const item of node.items)collectGroups(item, into);
|
|
656
|
+
break;
|
|
657
|
+
case 'rep':
|
|
658
|
+
collectGroups(node.node, into);
|
|
659
|
+
break;
|
|
660
|
+
default:
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
return into;
|
|
664
|
+
}
|
|
665
|
+
let Compiler = class Compiler {
|
|
666
|
+
/** Group 0 (the whole match) opens the program and closes it. */ compileProgram(node) {
|
|
667
|
+
this.emit({
|
|
668
|
+
op: 'save',
|
|
669
|
+
slot: 0
|
|
670
|
+
});
|
|
671
|
+
this.compile(node);
|
|
672
|
+
this.emit({
|
|
673
|
+
op: 'save',
|
|
674
|
+
slot: 1
|
|
675
|
+
});
|
|
676
|
+
this.emit({
|
|
677
|
+
op: 'match'
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
emit(inst) {
|
|
681
|
+
if (this.prog.length >= MAX_PROGRAM_LENGTH) {
|
|
682
|
+
throw new PatternError(`pattern compiles to more than ${MAX_PROGRAM_LENGTH} instructions`);
|
|
683
|
+
}
|
|
684
|
+
this.prog.push(inst);
|
|
685
|
+
return this.prog.length - 1;
|
|
686
|
+
}
|
|
687
|
+
compile(node) {
|
|
688
|
+
switch(node.t){
|
|
689
|
+
case 'empty':
|
|
690
|
+
return;
|
|
691
|
+
case 'char':
|
|
692
|
+
this.emit({
|
|
693
|
+
op: 'char',
|
|
694
|
+
c: this.flags.ignoreCase ? canonicalize(node.c) : node.c
|
|
695
|
+
});
|
|
696
|
+
return;
|
|
697
|
+
case 'class':
|
|
698
|
+
this.emit({
|
|
699
|
+
op: 'class',
|
|
700
|
+
ranges: this.flags.ignoreCase ? foldRanges(node.ranges) : node.ranges,
|
|
701
|
+
negated: node.negated
|
|
702
|
+
});
|
|
703
|
+
return;
|
|
704
|
+
case 'any':
|
|
705
|
+
this.emit({
|
|
706
|
+
op: 'any',
|
|
707
|
+
dotAll: this.flags.dotAll
|
|
708
|
+
});
|
|
709
|
+
return;
|
|
710
|
+
case 'assertStart':
|
|
711
|
+
this.emit({
|
|
712
|
+
op: 'assertStart',
|
|
713
|
+
multiline: this.flags.multiline
|
|
714
|
+
});
|
|
715
|
+
return;
|
|
716
|
+
case 'assertEnd':
|
|
717
|
+
this.emit({
|
|
718
|
+
op: 'assertEnd',
|
|
719
|
+
multiline: this.flags.multiline
|
|
720
|
+
});
|
|
721
|
+
return;
|
|
722
|
+
case 'wordBoundary':
|
|
723
|
+
this.emit({
|
|
724
|
+
op: 'assertWordBoundary',
|
|
725
|
+
negated: node.negated
|
|
726
|
+
});
|
|
727
|
+
return;
|
|
728
|
+
case 'cat':
|
|
729
|
+
for (const item of node.items)this.compile(item);
|
|
730
|
+
return;
|
|
731
|
+
case 'group':
|
|
732
|
+
this.emit({
|
|
733
|
+
op: 'save',
|
|
734
|
+
slot: node.index * 2
|
|
735
|
+
});
|
|
736
|
+
this.compile(node.node);
|
|
737
|
+
this.emit({
|
|
738
|
+
op: 'save',
|
|
739
|
+
slot: node.index * 2 + 1
|
|
740
|
+
});
|
|
741
|
+
return;
|
|
742
|
+
case 'alt':
|
|
743
|
+
this.compileAlt(node.options);
|
|
744
|
+
return;
|
|
745
|
+
case 'rep':
|
|
746
|
+
this.compileRep(node);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
compileAlt(options) {
|
|
751
|
+
// Chain of splits; every branch jumps to a shared end.
|
|
752
|
+
const jumpsToEnd = [];
|
|
753
|
+
for(let i = 0; i < options.length; i++){
|
|
754
|
+
const last = i === options.length - 1;
|
|
755
|
+
let split = -1;
|
|
756
|
+
if (!last) split = this.emit({
|
|
757
|
+
op: 'split',
|
|
758
|
+
x: 0,
|
|
759
|
+
y: 0
|
|
760
|
+
});
|
|
761
|
+
if (split >= 0) {
|
|
762
|
+
;
|
|
763
|
+
this.prog[split].x = this.prog.length;
|
|
764
|
+
}
|
|
765
|
+
this.compile(options[i]);
|
|
766
|
+
if (!last) jumpsToEnd.push(this.emit({
|
|
767
|
+
op: 'jmp',
|
|
768
|
+
x: 0
|
|
769
|
+
}));
|
|
770
|
+
if (split >= 0) {
|
|
771
|
+
;
|
|
772
|
+
this.prog[split].y = this.prog.length;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const end = this.prog.length;
|
|
776
|
+
for (const jump of jumpsToEnd)this.prog[jump].x = end;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Emits one *optional* iteration of a repeated body, applying the two
|
|
780
|
+
* ECMAScript RepeatMatcher rules that a naive NFA gets wrong:
|
|
781
|
+
*
|
|
782
|
+
* - captures inside the body are reset at the start of every iteration,
|
|
783
|
+
* so `/^(?:(a)|b)*$/.exec('ab')` leaves group 1 `undefined`;
|
|
784
|
+
* - an iteration that consumes nothing is discarded once `min` is
|
|
785
|
+
* satisfied, so `/^(-|\d*)?/.exec('')` also leaves group 1 `undefined`
|
|
786
|
+
* rather than capturing an empty string.
|
|
787
|
+
*
|
|
788
|
+
* Both are capture-visible only — they never change whether a pattern
|
|
789
|
+
* matches — but getting them right keeps this engine a drop-in for the
|
|
790
|
+
* patterns customers already have.
|
|
791
|
+
*/ compileOptionalBody(body, captures) {
|
|
792
|
+
if (captures.length > 0) {
|
|
793
|
+
this.emit({
|
|
794
|
+
op: 'clear',
|
|
795
|
+
slots: captures.flatMap((index)=>[
|
|
796
|
+
index * 2,
|
|
797
|
+
index * 2 + 1
|
|
798
|
+
])
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
const slot = this.markSlots++;
|
|
802
|
+
this.emit({
|
|
803
|
+
op: 'mark',
|
|
804
|
+
slot
|
|
805
|
+
});
|
|
806
|
+
this.compile(body);
|
|
807
|
+
this.emit({
|
|
808
|
+
op: 'progress',
|
|
809
|
+
slot
|
|
810
|
+
});
|
|
811
|
+
return slot;
|
|
812
|
+
}
|
|
813
|
+
compileRep(node) {
|
|
814
|
+
const { min, max, greedy } = node;
|
|
815
|
+
const captures = collectGroups(node.node, []);
|
|
816
|
+
// The mandatory copies: `min` is not yet satisfied, so the
|
|
817
|
+
// empty-iteration rule does not apply to them.
|
|
818
|
+
for(let i = 0; i < min; i++)this.compile(node.node);
|
|
819
|
+
if (max === Infinity) {
|
|
820
|
+
// L: split(body, end); body; jmp L; end:
|
|
821
|
+
const split = this.emit({
|
|
822
|
+
op: 'split',
|
|
823
|
+
x: 0,
|
|
824
|
+
y: 0
|
|
825
|
+
});
|
|
826
|
+
const bodyStart = this.prog.length;
|
|
827
|
+
this.compileOptionalBody(node.node, captures);
|
|
828
|
+
this.emit({
|
|
829
|
+
op: 'jmp',
|
|
830
|
+
x: split
|
|
831
|
+
});
|
|
832
|
+
const end = this.prog.length;
|
|
833
|
+
this.setSplit(split, bodyStart, end, greedy);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
// Bounded: `max - min` optional copies, each able to skip to the end.
|
|
837
|
+
const splits = [];
|
|
838
|
+
for(let i = min; i < max; i++){
|
|
839
|
+
const split = this.emit({
|
|
840
|
+
op: 'split',
|
|
841
|
+
x: 0,
|
|
842
|
+
y: 0
|
|
843
|
+
});
|
|
844
|
+
splits.push(split);
|
|
845
|
+
const bodyStart = this.prog.length;
|
|
846
|
+
this.compileOptionalBody(node.node, captures);
|
|
847
|
+
// Patch `x` now; `y` (the skip target) is patched to the shared end.
|
|
848
|
+
if (greedy) this.prog[split].x = bodyStart;
|
|
849
|
+
else this.prog[split].y = bodyStart;
|
|
850
|
+
}
|
|
851
|
+
const end = this.prog.length;
|
|
852
|
+
for (const split of splits){
|
|
853
|
+
if (greedy) this.prog[split].y = end;
|
|
854
|
+
else this.prog[split].x = end;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/** `x` is tried before `y`, so greedy puts the body first. */ setSplit(at, body, end, greedy) {
|
|
858
|
+
const inst = this.prog[at];
|
|
859
|
+
inst.x = greedy ? body : end;
|
|
860
|
+
inst.y = greedy ? end : body;
|
|
861
|
+
}
|
|
862
|
+
constructor(flags = NO_FLAGS){
|
|
863
|
+
this.flags = flags;
|
|
864
|
+
this.prog = [];
|
|
865
|
+
/**
|
|
866
|
+
* Scratch slots (allocated above the capture slots) holding the offset an
|
|
867
|
+
* optional iteration began at, for the empty-iteration rule below.
|
|
868
|
+
*/ this.markSlots = 0;
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
/**
|
|
872
|
+
* Simulates every live NFA state in lockstep across the input.
|
|
873
|
+
*
|
|
874
|
+
* The `visited` generation array is what makes this linear: within a single
|
|
875
|
+
* input position each program counter is added at most once, so the whole
|
|
876
|
+
* run costs at most `inputLength × programLength` steps regardless of what
|
|
877
|
+
* the pattern looks like.
|
|
878
|
+
*/ function run(prog, slotCount, markBase, input, flags, mode) {
|
|
879
|
+
const visited = new Int32Array(prog.length).fill(-1);
|
|
880
|
+
let generation = 0;
|
|
881
|
+
const length = input.length;
|
|
882
|
+
// Under `i` every input unit is compared by its folded form, which the
|
|
883
|
+
// program's characters and classes were compiled against.
|
|
884
|
+
let folded;
|
|
885
|
+
if (flags.ignoreCase) {
|
|
886
|
+
folded = new Uint16Array(length);
|
|
887
|
+
for(let at = 0; at < length; at++){
|
|
888
|
+
folded[at] = canonicalize(input.charCodeAt(at));
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const addThread = (list, pc, caps, sp, mark)=>{
|
|
892
|
+
// Iterative epsilon closure — a deep pattern must not blow the JS stack.
|
|
893
|
+
const stack = [
|
|
894
|
+
{
|
|
895
|
+
pc,
|
|
896
|
+
caps
|
|
897
|
+
}
|
|
898
|
+
];
|
|
899
|
+
while(stack.length > 0){
|
|
900
|
+
const entry = stack.pop();
|
|
901
|
+
if (visited[entry.pc] === mark) continue;
|
|
902
|
+
visited[entry.pc] = mark;
|
|
903
|
+
const inst = prog[entry.pc];
|
|
904
|
+
if (inst.op === 'jmp') {
|
|
905
|
+
stack.push({
|
|
906
|
+
pc: inst.x,
|
|
907
|
+
caps: entry.caps
|
|
908
|
+
});
|
|
909
|
+
} else if (inst.op === 'split') {
|
|
910
|
+
// Push `y` first so `x` (the higher priority branch) pops first.
|
|
911
|
+
stack.push({
|
|
912
|
+
pc: inst.y,
|
|
913
|
+
caps: entry.caps
|
|
914
|
+
});
|
|
915
|
+
stack.push({
|
|
916
|
+
pc: inst.x,
|
|
917
|
+
caps: entry.caps
|
|
918
|
+
});
|
|
919
|
+
} else if (inst.op === 'save') {
|
|
920
|
+
const next = entry.caps.slice();
|
|
921
|
+
next[inst.slot] = sp;
|
|
922
|
+
stack.push({
|
|
923
|
+
pc: entry.pc + 1,
|
|
924
|
+
caps: next
|
|
925
|
+
});
|
|
926
|
+
} else if (inst.op === 'mark') {
|
|
927
|
+
const next = entry.caps.slice();
|
|
928
|
+
next[markBase + inst.slot] = sp;
|
|
929
|
+
stack.push({
|
|
930
|
+
pc: entry.pc + 1,
|
|
931
|
+
caps: next
|
|
932
|
+
});
|
|
933
|
+
} else if (inst.op === 'progress') {
|
|
934
|
+
// Discard an iteration that consumed nothing (ECMAScript
|
|
935
|
+
// RepeatMatcher step 2.b). This also guarantees the epsilon closure
|
|
936
|
+
// cannot cycle forever through an empty-matching body.
|
|
937
|
+
if (entry.caps[markBase + inst.slot] < sp) {
|
|
938
|
+
stack.push({
|
|
939
|
+
pc: entry.pc + 1,
|
|
940
|
+
caps: entry.caps
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
} else if (inst.op === 'clear') {
|
|
944
|
+
const next = entry.caps.slice();
|
|
945
|
+
for (const slot of inst.slots)next[slot] = -1;
|
|
946
|
+
stack.push({
|
|
947
|
+
pc: entry.pc + 1,
|
|
948
|
+
caps: next
|
|
949
|
+
});
|
|
950
|
+
} else if (inst.op === 'assertStart') {
|
|
951
|
+
if (sp === 0 || inst.multiline && isLineTerminator(input.charCodeAt(sp - 1))) {
|
|
952
|
+
stack.push({
|
|
953
|
+
pc: entry.pc + 1,
|
|
954
|
+
caps: entry.caps
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
} else if (inst.op === 'assertEnd') {
|
|
958
|
+
if (sp === length || inst.multiline && isLineTerminator(input.charCodeAt(sp))) {
|
|
959
|
+
stack.push({
|
|
960
|
+
pc: entry.pc + 1,
|
|
961
|
+
caps: entry.caps
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
} else if (inst.op === 'assertWordBoundary') {
|
|
965
|
+
// Read from the input as written: without the `u` flag a word
|
|
966
|
+
// character is `[A-Za-z0-9_]`, and case folding does not move it.
|
|
967
|
+
const before = sp > 0 && rangesContain(WORD_RANGES, input.charCodeAt(sp - 1));
|
|
968
|
+
const after = sp < length && rangesContain(WORD_RANGES, input.charCodeAt(sp));
|
|
969
|
+
const boundary = before !== after;
|
|
970
|
+
if (boundary !== inst.negated) {
|
|
971
|
+
stack.push({
|
|
972
|
+
pc: entry.pc + 1,
|
|
973
|
+
caps: entry.caps
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
} else {
|
|
977
|
+
list.push({
|
|
978
|
+
pc: entry.pc,
|
|
979
|
+
caps: entry.caps
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
// NOTE: the iterative closure above uses a LIFO stack, which reverses the
|
|
985
|
+
// order sibling epsilon branches are appended in. Priority is restored by
|
|
986
|
+
// pushing the lower-priority branch first (see the `split` case), so `x`
|
|
987
|
+
// is always explored before `y`.
|
|
988
|
+
const initial = new Array(slotCount).fill(-1);
|
|
989
|
+
let current = [];
|
|
990
|
+
addThread(current, 0, initial, 0, generation++);
|
|
991
|
+
let matched = null;
|
|
992
|
+
for(let sp = 0; sp <= length; sp++){
|
|
993
|
+
// With nothing live, a match found is final and a sticky pattern has
|
|
994
|
+
// nowhere else to start. Otherwise only `stopWhenIdle` ends the scan
|
|
995
|
+
// here; see `ScanMode` for what that costs.
|
|
996
|
+
if (current.length === 0 && (matched !== null || flags.sticky || mode === 'stopWhenIdle')) {
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
const code = sp < length ? folded ? folded[sp] : input.charCodeAt(sp) : -1;
|
|
1000
|
+
const next = [];
|
|
1001
|
+
const mark = generation++;
|
|
1002
|
+
for(let i = 0; i < current.length; i++){
|
|
1003
|
+
const thread = current[i];
|
|
1004
|
+
const inst = prog[thread.pc];
|
|
1005
|
+
if (inst.op === 'match') {
|
|
1006
|
+
// Leftmost-first: this thread outranks every thread after it, so its
|
|
1007
|
+
// result is the one a backtracking engine would return. Threads
|
|
1008
|
+
// *ahead* of it are still live and may overwrite this on a later
|
|
1009
|
+
// step — which is exactly the greedy preference JS applies.
|
|
1010
|
+
matched = thread.caps;
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
if (code < 0) continue;
|
|
1014
|
+
let consumes = false;
|
|
1015
|
+
if (inst.op === 'char') {
|
|
1016
|
+
consumes = code === inst.c;
|
|
1017
|
+
} else if (inst.op === 'any') {
|
|
1018
|
+
// A line terminator folds to itself, so the folded unit reads true.
|
|
1019
|
+
consumes = inst.dotAll || !isLineTerminator(code);
|
|
1020
|
+
} else if (inst.op === 'class') {
|
|
1021
|
+
const inRanges = rangesContain(inst.ranges, code);
|
|
1022
|
+
consumes = inst.negated ? !inRanges : inRanges;
|
|
1023
|
+
}
|
|
1024
|
+
if (consumes) addThread(next, thread.pc + 1, thread.caps, sp + 1, mark);
|
|
1025
|
+
}
|
|
1026
|
+
// `exec` searches: JS retries the whole pattern at each later offset.
|
|
1027
|
+
// That matters whenever `^` binds to only part of the pattern, e.g.
|
|
1028
|
+
// `^/a|/b$`. Seeding a fresh start thread *after* the continuing ones
|
|
1029
|
+
// keeps it lower priority, so an earlier start always wins (leftmost).
|
|
1030
|
+
// Once a match exists no later start could be more leftmost, so seeding
|
|
1031
|
+
// stops — which is also what bounds this to one pass. A sticky pattern
|
|
1032
|
+
// (`y`) is only ever tried where the input starts.
|
|
1033
|
+
if (matched === null && sp + 1 <= length && !flags.sticky) {
|
|
1034
|
+
addThread(next, 0, initial, sp + 1, mark);
|
|
1035
|
+
}
|
|
1036
|
+
current = next;
|
|
1037
|
+
}
|
|
1038
|
+
return matched;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Compiles a pattern and its flags. Throws `PatternError` for either.
|
|
1042
|
+
*
|
|
1043
|
+
* The `test` dialect ({@link compileLinearTest}) also reads `\b` and `\B`;
|
|
1044
|
+
* the `pattern` dialect is the one redirect rules have always been held to.
|
|
1045
|
+
*/ function compileSource(pattern, flags, dialect) {
|
|
1046
|
+
const mode = parseFlags(flags);
|
|
1047
|
+
const parser = new Parser(pattern, dialect === 'test');
|
|
1048
|
+
const ast = parser.parse();
|
|
1049
|
+
const compiler = new Compiler(mode);
|
|
1050
|
+
compiler.compileProgram(ast);
|
|
1051
|
+
// Capture slots first, then one scratch slot per optional iteration.
|
|
1052
|
+
const markBase = (parser.groupCount + 1) * 2;
|
|
1053
|
+
return {
|
|
1054
|
+
prog: compiler.prog,
|
|
1055
|
+
flags: mode,
|
|
1056
|
+
groupCount: parser.groupCount,
|
|
1057
|
+
markBase,
|
|
1058
|
+
slotCount: markBase + compiler.markSlots
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
/** Why `compileSource` refused, or null when it did not. */ function explain(pattern, flags, dialect) {
|
|
1062
|
+
try {
|
|
1063
|
+
compileSource(pattern, flags, dialect);
|
|
1064
|
+
return null;
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
if (error instanceof PatternError) return error.message;
|
|
1067
|
+
return 'the pattern could not be understood';
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Compiles a pattern for linear-time matching, or returns null when it is
|
|
1072
|
+
* malformed or uses syntax outside the supported subset.
|
|
1073
|
+
*
|
|
1074
|
+
* Takes no flags, and scans as redirect rules always have (see `ScanMode`).
|
|
1075
|
+
* Never throws, and never hands the pattern to `new RegExp`.
|
|
1076
|
+
*/ export function compileLinearPattern(pattern) {
|
|
1077
|
+
try {
|
|
1078
|
+
const { prog, flags, groupCount, markBase, slotCount } = compileSource(pattern, '', 'pattern');
|
|
1079
|
+
return {
|
|
1080
|
+
source: pattern,
|
|
1081
|
+
programLength: prog.length,
|
|
1082
|
+
exec (input) {
|
|
1083
|
+
const caps = run(prog, slotCount, markBase, String(input != null ? input : ''), flags, 'stopWhenIdle');
|
|
1084
|
+
if (caps === null) return null;
|
|
1085
|
+
const result = [];
|
|
1086
|
+
for(let group = 0; group <= groupCount; group++){
|
|
1087
|
+
const start = caps[group * 2];
|
|
1088
|
+
const end = caps[group * 2 + 1];
|
|
1089
|
+
result.push(start < 0 || end < 0 || end < start ? undefined : input.slice(start, end));
|
|
1090
|
+
}
|
|
1091
|
+
return result;
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
if (error instanceof PatternError) return null;
|
|
1096
|
+
// A genuinely unexpected fault must still fail closed rather than let an
|
|
1097
|
+
// unvalidated pattern through.
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Explains why {@link compileLinearPattern} refuses a pattern, for
|
|
1103
|
+
* console-side validation. Returns null when the pattern is fine.
|
|
1104
|
+
*/ export function explainLinearPattern(pattern) {
|
|
1105
|
+
return explain(pattern, '', 'pattern');
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Compiles a pattern and its flags into a {@link LinearTest}, or returns null
|
|
1109
|
+
* when either is malformed or outside the supported subset: the syntax in
|
|
1110
|
+
* `UNSUPPORTED_SYNTAX` except word boundaries, which this reads, and any flag
|
|
1111
|
+
* outside {@link FLAG_CHARS}.
|
|
1112
|
+
*
|
|
1113
|
+
* Unlike {@link compileLinearPattern} it scans to the end of the input (see
|
|
1114
|
+
* `ScanMode`), so every pattern it accepts answers what `RegExp` answers.
|
|
1115
|
+
* Never throws, and never hands the pattern to `new RegExp`.
|
|
1116
|
+
*/ export function compileLinearTest(pattern, flags = '') {
|
|
1117
|
+
try {
|
|
1118
|
+
const compiled = compileSource(pattern, flags, 'test');
|
|
1119
|
+
return {
|
|
1120
|
+
source: pattern,
|
|
1121
|
+
flags,
|
|
1122
|
+
programLength: compiled.prog.length,
|
|
1123
|
+
test (input) {
|
|
1124
|
+
return run(compiled.prog, compiled.slotCount, compiled.markBase, String(input != null ? input : ''), compiled.flags, 'searchToEnd') !== null;
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
} catch (unused) {
|
|
1128
|
+
// A PatternError is a refusal, and a genuinely unexpected fault must
|
|
1129
|
+
// still fail closed rather than let an unvalidated pattern through.
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Explains why {@link compileLinearTest} refuses a pattern or its flags.
|
|
1135
|
+
* Returns null when both are fine.
|
|
1136
|
+
*/ export function explainLinearTest(pattern, flags = '') {
|
|
1137
|
+
return explain(pattern, flags, 'test');
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
//# sourceMappingURL=linear-regex.js.map
|