@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.
Files changed (288) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +39 -0
  4. package/src/index.d.ts +72 -0
  5. package/src/index.js +87 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/apply-mixins.d.ts +24 -0
  8. package/src/lib/apply-mixins.js +31 -0
  9. package/src/lib/apply-mixins.js.map +1 -0
  10. package/src/lib/array/array-copy-deep.d.ts +6 -0
  11. package/src/lib/array/array-copy-deep.js +25 -0
  12. package/src/lib/array/array-copy-deep.js.map +1 -0
  13. package/src/lib/array/array-copy-shallow.d.ts +19 -0
  14. package/src/lib/array/array-copy-shallow.js +20 -0
  15. package/src/lib/array/array-copy-shallow.js.map +1 -0
  16. package/src/lib/array/array-from-length.d.ts +18 -0
  17. package/src/lib/array/array-from-length.js +23 -0
  18. package/src/lib/array/array-from-length.js.map +1 -0
  19. package/src/lib/array/array-from.d.ts +24 -0
  20. package/src/lib/array/array-from.js +26 -0
  21. package/src/lib/array/array-from.js.map +1 -0
  22. package/src/lib/array/array-move-at-index.d.ts +24 -0
  23. package/src/lib/array/array-move-at-index.js +32 -0
  24. package/src/lib/array/array-move-at-index.js.map +1 -0
  25. package/src/lib/array/array-of-entries-to-object.d.ts +23 -0
  26. package/src/lib/array/array-of-entries-to-object.js +26 -0
  27. package/src/lib/array/array-of-entries-to-object.js.map +1 -0
  28. package/src/lib/array/array-overrides.d.ts +29 -0
  29. package/src/lib/array/array-overrides.js +71 -0
  30. package/src/lib/array/array-overrides.js.map +1 -0
  31. package/src/lib/array/array-push-at-index.d.ts +28 -0
  32. package/src/lib/array/array-push-at-index.js +32 -0
  33. package/src/lib/array/array-push-at-index.js.map +1 -0
  34. package/src/lib/array/array-remove-at-index.d.ts +23 -0
  35. package/src/lib/array/array-remove-at-index.js +26 -0
  36. package/src/lib/array/array-remove-at-index.js.map +1 -0
  37. package/src/lib/array/array-remove-item.d.ts +23 -0
  38. package/src/lib/array/array-remove-item.js +27 -0
  39. package/src/lib/array/array-remove-item.js.map +1 -0
  40. package/src/lib/array/array-safe.d.ts +21 -0
  41. package/src/lib/array/array-safe.js +29 -0
  42. package/src/lib/array/array-safe.js.map +1 -0
  43. package/src/lib/array/array-sort-by-deep-property.d.ts +26 -0
  44. package/src/lib/array/array-sort-by-deep-property.js +39 -0
  45. package/src/lib/array/array-sort-by-deep-property.js.map +1 -0
  46. package/src/lib/array/array-sort-by.d.ts +26 -0
  47. package/src/lib/array/array-sort-by.js +36 -0
  48. package/src/lib/array/array-sort-by.js.map +1 -0
  49. package/src/lib/array/array-update-at-index.d.ts +18 -0
  50. package/src/lib/array/array-update-at-index.js +22 -0
  51. package/src/lib/array/array-update-at-index.js.map +1 -0
  52. package/src/lib/array/array-update.d.ts +23 -0
  53. package/src/lib/array/array-update.js +25 -0
  54. package/src/lib/array/array-update.js.map +1 -0
  55. package/src/lib/bitwise/bitwise-has-all-attributes.d.ts +18 -0
  56. package/src/lib/bitwise/bitwise-has-all-attributes.js +21 -0
  57. package/src/lib/bitwise/bitwise-has-all-attributes.js.map +1 -0
  58. package/src/lib/bitwise/bitwise-has-attribute.d.ts +18 -0
  59. package/src/lib/bitwise/bitwise-has-attribute.js +21 -0
  60. package/src/lib/bitwise/bitwise-has-attribute.js.map +1 -0
  61. package/src/lib/bitwise/bitwise-has-only-attributes.d.ts +18 -0
  62. package/src/lib/bitwise/bitwise-has-only-attributes.js +21 -0
  63. package/src/lib/bitwise/bitwise-has-only-attributes.js.map +1 -0
  64. package/src/lib/contrast.d.ts +65 -0
  65. package/src/lib/contrast.js +78 -0
  66. package/src/lib/contrast.js.map +1 -0
  67. package/src/lib/copy-shallow.d.ts +19 -0
  68. package/src/lib/copy-shallow.js +20 -0
  69. package/src/lib/copy-shallow.js.map +1 -0
  70. package/src/lib/copy.d.ts +19 -0
  71. package/src/lib/copy.js +20 -0
  72. package/src/lib/copy.js.map +1 -0
  73. package/src/lib/create-chained-function.d.ts +38 -0
  74. package/src/lib/create-chained-function.js +46 -0
  75. package/src/lib/create-chained-function.js.map +1 -0
  76. package/src/lib/crud.d.ts +46 -0
  77. package/src/lib/crud.js +55 -0
  78. package/src/lib/crud.js.map +1 -0
  79. package/src/lib/css.d.ts +41 -0
  80. package/src/lib/css.js +54 -0
  81. package/src/lib/css.js.map +1 -0
  82. package/src/lib/escape-html.d.ts +38 -0
  83. package/src/lib/escape-html.js +47 -0
  84. package/src/lib/escape-html.js.map +1 -0
  85. package/src/lib/falsy.d.ts +23 -0
  86. package/src/lib/falsy.js +25 -0
  87. package/src/lib/falsy.js.map +1 -0
  88. package/src/lib/get-display-name.d.ts +46 -0
  89. package/src/lib/get-display-name.js +64 -0
  90. package/src/lib/get-display-name.js.map +1 -0
  91. package/src/lib/get-property.d.ts +30 -0
  92. package/src/lib/get-property.js +36 -0
  93. package/src/lib/get-property.js.map +1 -0
  94. package/src/lib/get-static-field.d.ts +23 -0
  95. package/src/lib/get-static-field.js +25 -0
  96. package/src/lib/get-static-field.js.map +1 -0
  97. package/src/lib/guards/index.d.ts +46 -0
  98. package/src/lib/guards/index.js +47 -0
  99. package/src/lib/guards/index.js.map +1 -0
  100. package/src/lib/guards/lib/_has-own-property.d.ts +24 -0
  101. package/src/lib/guards/lib/_has-own-property.js +27 -0
  102. package/src/lib/guards/lib/_has-own-property.js.map +1 -0
  103. package/src/lib/guards/lib/_is-arr-empty.d.ts +23 -0
  104. package/src/lib/guards/lib/_is-arr-empty.js +25 -0
  105. package/src/lib/guards/lib/_is-arr-empty.js.map +1 -0
  106. package/src/lib/guards/lib/_is-arr-of-arr.d.ts +25 -0
  107. package/src/lib/guards/lib/_is-arr-of-arr.js +31 -0
  108. package/src/lib/guards/lib/_is-arr-of-arr.js.map +1 -0
  109. package/src/lib/guards/lib/_is-arr.d.ts +21 -0
  110. package/src/lib/guards/lib/_is-arr.js +23 -0
  111. package/src/lib/guards/lib/_is-arr.js.map +1 -0
  112. package/src/lib/guards/lib/_is-big.d.ts +25 -0
  113. package/src/lib/guards/lib/_is-big.js +27 -0
  114. package/src/lib/guards/lib/_is-big.js.map +1 -0
  115. package/src/lib/guards/lib/_is-bool.d.ts +25 -0
  116. package/src/lib/guards/lib/_is-bool.js +27 -0
  117. package/src/lib/guards/lib/_is-bool.js.map +1 -0
  118. package/src/lib/guards/lib/_is-buff.d.ts +25 -0
  119. package/src/lib/guards/lib/_is-buff.js +29 -0
  120. package/src/lib/guards/lib/_is-buff.js.map +1 -0
  121. package/src/lib/guards/lib/_is-ctor.d.ts +25 -0
  122. package/src/lib/guards/lib/_is-ctor.js +30 -0
  123. package/src/lib/guards/lib/_is-ctor.js.map +1 -0
  124. package/src/lib/guards/lib/_is-empty-str-or-arr.d.ts +25 -0
  125. package/src/lib/guards/lib/_is-empty-str-or-arr.js +29 -0
  126. package/src/lib/guards/lib/_is-empty-str-or-arr.js.map +1 -0
  127. package/src/lib/guards/lib/_is-equality-same-type.d.ts +25 -0
  128. package/src/lib/guards/lib/_is-equality-same-type.js +33 -0
  129. package/src/lib/guards/lib/_is-equality-same-type.js.map +1 -0
  130. package/src/lib/guards/lib/_is-fn-t.d.ts +25 -0
  131. package/src/lib/guards/lib/_is-fn-t.js +27 -0
  132. package/src/lib/guards/lib/_is-fn-t.js.map +1 -0
  133. package/src/lib/guards/lib/_is-length-of.d.ts +47 -0
  134. package/src/lib/guards/lib/_is-length-of.js +49 -0
  135. package/src/lib/guards/lib/_is-length-of.js.map +1 -0
  136. package/src/lib/guards/lib/_is-length.d.ts +25 -0
  137. package/src/lib/guards/lib/_is-length.js +34 -0
  138. package/src/lib/guards/lib/_is-length.js.map +1 -0
  139. package/src/lib/guards/lib/_is-null.d.ts +25 -0
  140. package/src/lib/guards/lib/_is-null.js +27 -0
  141. package/src/lib/guards/lib/_is-null.js.map +1 -0
  142. package/src/lib/guards/lib/_is-num-neg.d.ts +25 -0
  143. package/src/lib/guards/lib/_is-num-neg.js +28 -0
  144. package/src/lib/guards/lib/_is-num-neg.js.map +1 -0
  145. package/src/lib/guards/lib/_is-num-pos.d.ts +25 -0
  146. package/src/lib/guards/lib/_is-num-pos.js +28 -0
  147. package/src/lib/guards/lib/_is-num-pos.js.map +1 -0
  148. package/src/lib/guards/lib/_is-num-t.d.ts +25 -0
  149. package/src/lib/guards/lib/_is-num-t.js +27 -0
  150. package/src/lib/guards/lib/_is-num-t.js.map +1 -0
  151. package/src/lib/guards/lib/_is-num-zero.d.ts +25 -0
  152. package/src/lib/guards/lib/_is-num-zero.js +28 -0
  153. package/src/lib/guards/lib/_is-num-zero.js.map +1 -0
  154. package/src/lib/guards/lib/_is-num.d.ts +26 -0
  155. package/src/lib/guards/lib/_is-num.js +29 -0
  156. package/src/lib/guards/lib/_is-num.js.map +1 -0
  157. package/src/lib/guards/lib/_is-obj-t.d.ts +26 -0
  158. package/src/lib/guards/lib/_is-obj-t.js +28 -0
  159. package/src/lib/guards/lib/_is-obj-t.js.map +1 -0
  160. package/src/lib/guards/lib/_is-obj.d.ts +27 -0
  161. package/src/lib/guards/lib/_is-obj.js +32 -0
  162. package/src/lib/guards/lib/_is-obj.js.map +1 -0
  163. package/src/lib/guards/lib/_is-prim.d.ts +27 -0
  164. package/src/lib/guards/lib/_is-prim.js +35 -0
  165. package/src/lib/guards/lib/_is-prim.js.map +1 -0
  166. package/src/lib/guards/lib/_is-promise-like.d.ts +24 -0
  167. package/src/lib/guards/lib/_is-promise-like.js +28 -0
  168. package/src/lib/guards/lib/_is-promise-like.js.map +1 -0
  169. package/src/lib/guards/lib/_is-str-empty.d.ts +25 -0
  170. package/src/lib/guards/lib/_is-str-empty.js +28 -0
  171. package/src/lib/guards/lib/_is-str-empty.js.map +1 -0
  172. package/src/lib/guards/lib/_is-str-t.d.ts +26 -0
  173. package/src/lib/guards/lib/_is-str-t.js +28 -0
  174. package/src/lib/guards/lib/_is-str-t.js.map +1 -0
  175. package/src/lib/guards/lib/_is-sym-t.d.ts +25 -0
  176. package/src/lib/guards/lib/_is-sym-t.js +27 -0
  177. package/src/lib/guards/lib/_is-sym-t.js.map +1 -0
  178. package/src/lib/guards/lib/_is-und-or-null.d.ts +25 -0
  179. package/src/lib/guards/lib/_is-und-or-null.js +29 -0
  180. package/src/lib/guards/lib/_is-und-or-null.js.map +1 -0
  181. package/src/lib/guards/lib/_is-und-t.d.ts +25 -0
  182. package/src/lib/guards/lib/_is-und-t.js +27 -0
  183. package/src/lib/guards/lib/_is-und-t.js.map +1 -0
  184. package/src/lib/interop-default.d.ts +18 -0
  185. package/src/lib/interop-default.js +21 -0
  186. package/src/lib/interop-default.js.map +1 -0
  187. package/src/lib/length.d.ts +22 -0
  188. package/src/lib/length.js +28 -0
  189. package/src/lib/length.js.map +1 -0
  190. package/src/lib/linear-regex.d.ts +136 -0
  191. package/src/lib/linear-regex.js +1140 -0
  192. package/src/lib/linear-regex.js.map +1 -0
  193. package/src/lib/no-side-effects.d.ts +29 -0
  194. package/src/lib/no-side-effects.js +33 -0
  195. package/src/lib/no-side-effects.js.map +1 -0
  196. package/src/lib/noop.d.ts +21 -0
  197. package/src/lib/noop.js +22 -0
  198. package/src/lib/noop.js.map +1 -0
  199. package/src/lib/normalized.d.ts +164 -0
  200. package/src/lib/normalized.js +232 -0
  201. package/src/lib/normalized.js.map +1 -0
  202. package/src/lib/number/number-from-hexadecimal.d.ts +24 -0
  203. package/src/lib/number/number-from-hexadecimal.js +29 -0
  204. package/src/lib/number/number-from-hexadecimal.js.map +1 -0
  205. package/src/lib/number/number-to-hexadecimal.d.ts +22 -0
  206. package/src/lib/number/number-to-hexadecimal.js +24 -0
  207. package/src/lib/number/number-to-hexadecimal.js.map +1 -0
  208. package/src/lib/number/to-num.d.ts +41 -0
  209. package/src/lib/number/to-num.js +41 -0
  210. package/src/lib/number/to-num.js.map +1 -0
  211. package/src/lib/numeronym.d.ts +41 -0
  212. package/src/lib/numeronym.js +54 -0
  213. package/src/lib/numeronym.js.map +1 -0
  214. package/src/lib/object/object-clone-deep.d.ts +21 -0
  215. package/src/lib/object/object-clone-deep.js +24 -0
  216. package/src/lib/object/object-clone-deep.js.map +1 -0
  217. package/src/lib/object/object-clone.d.ts +21 -0
  218. package/src/lib/object/object-clone.js +24 -0
  219. package/src/lib/object/object-clone.js.map +1 -0
  220. package/src/lib/object/object-delete-property.d.ts +28 -0
  221. package/src/lib/object/object-delete-property.js +32 -0
  222. package/src/lib/object/object-delete-property.js.map +1 -0
  223. package/src/lib/object/object-get-deep-property.d.ts +35 -0
  224. package/src/lib/object/object-get-deep-property.js +40 -0
  225. package/src/lib/object/object-get-deep-property.js.map +1 -0
  226. package/src/lib/object/object-get-keys-and-symbol-properties.d.ts +23 -0
  227. package/src/lib/object/object-get-keys-and-symbol-properties.js +30 -0
  228. package/src/lib/object/object-get-keys-and-symbol-properties.js.map +1 -0
  229. package/src/lib/object/object-remap.d.ts +33 -0
  230. package/src/lib/object/object-remap.js +36 -0
  231. package/src/lib/object/object-remap.js.map +1 -0
  232. package/src/lib/object/object-safe.d.ts +27 -0
  233. package/src/lib/object/object-safe.js +30 -0
  234. package/src/lib/object/object-safe.js.map +1 -0
  235. package/src/lib/object/object-set-deep-property.d.ts +35 -0
  236. package/src/lib/object/object-set-deep-property.js +44 -0
  237. package/src/lib/object/object-set-deep-property.js.map +1 -0
  238. package/src/lib/object/object-update.d.ts +28 -0
  239. package/src/lib/object/object-update.js +32 -0
  240. package/src/lib/object/object-update.js.map +1 -0
  241. package/src/lib/operator/compare.d.ts +26 -0
  242. package/src/lib/operator/compare.js +42 -0
  243. package/src/lib/operator/compare.js.map +1 -0
  244. package/src/lib/operator/definitions.d.ts +184 -0
  245. package/src/lib/operator/definitions.js +110 -0
  246. package/src/lib/operator/definitions.js.map +1 -0
  247. package/src/lib/operator/index.d.ts +19 -0
  248. package/src/lib/operator/index.js +20 -0
  249. package/src/lib/operator/index.js.map +1 -0
  250. package/src/lib/operator/inference.d.ts +66 -0
  251. package/src/lib/operator/inference.js +60 -0
  252. package/src/lib/operator/inference.js.map +1 -0
  253. package/src/lib/secret-box.d.ts +152 -0
  254. package/src/lib/secret-box.js +263 -0
  255. package/src/lib/secret-box.js.map +1 -0
  256. package/src/lib/serialize/base64-isomorphic-decode.d.ts +23 -0
  257. package/src/lib/serialize/base64-isomorphic-decode.js +28 -0
  258. package/src/lib/serialize/base64-isomorphic-decode.js.map +1 -0
  259. package/src/lib/serialize/base64-isomorphic-encode.d.ts +23 -0
  260. package/src/lib/serialize/base64-isomorphic-encode.js +28 -0
  261. package/src/lib/serialize/base64-isomorphic-encode.js.map +1 -0
  262. package/src/lib/serialize/json-deserialize.d.ts +25 -0
  263. package/src/lib/serialize/json-deserialize.js +27 -0
  264. package/src/lib/serialize/json-deserialize.js.map +1 -0
  265. package/src/lib/serialize/json-serialize.d.ts +25 -0
  266. package/src/lib/serialize/json-serialize.js +27 -0
  267. package/src/lib/serialize/json-serialize.js.map +1 -0
  268. package/src/lib/serialize/php-serial-deserialize-from-byte-stream.d.ts +77 -0
  269. package/src/lib/serialize/php-serial-deserialize-from-byte-stream.js +77 -0
  270. package/src/lib/serialize/php-serial-deserialize-from-byte-stream.js.map +1 -0
  271. package/src/lib/serialize/php-serial-is-serialized-byte-stream.d.ts +77 -0
  272. package/src/lib/serialize/php-serial-is-serialized-byte-stream.js +77 -0
  273. package/src/lib/serialize/php-serial-is-serialized-byte-stream.js.map +1 -0
  274. package/src/lib/serialize/php-serial-serialize-to-byte-stream.d.ts +77 -0
  275. package/src/lib/serialize/php-serial-serialize-to-byte-stream.js +77 -0
  276. package/src/lib/serialize/php-serial-serialize-to-byte-stream.js.map +1 -0
  277. package/src/lib/split-display-name.d.ts +36 -0
  278. package/src/lib/split-display-name.js +47 -0
  279. package/src/lib/split-display-name.js.map +1 -0
  280. package/src/lib/str.d.ts +21 -0
  281. package/src/lib/str.js +23 -0
  282. package/src/lib/str.js.map +1 -0
  283. package/src/lib/trim.d.ts +25 -0
  284. package/src/lib/trim.js +28 -0
  285. package/src/lib/trim.js.map +1 -0
  286. package/src/lib/truthy.d.ts +23 -0
  287. package/src/lib/truthy.js +30 -0
  288. 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