parseman 0.46.0 → 0.47.0

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 (316) hide show
  1. package/README.md +8 -3
  2. package/dist/analysis/choice-cost-render.d.ts +12 -4
  3. package/dist/analysis/choice-cost-render.d.ts.map +1 -1
  4. package/dist/analysis/choice-cost.d.ts +2 -2
  5. package/dist/analysis/choice-cost.d.ts.map +1 -1
  6. package/dist/analysis/commitment.d.ts +131 -0
  7. package/dist/analysis/commitment.d.ts.map +1 -0
  8. package/dist/analysis/diagnostics.cjs +9929 -7465
  9. package/dist/analysis/diagnostics.cjs.map +3 -4
  10. package/dist/analysis/diagnostics.d.ts +2 -2
  11. package/dist/analysis/diagnostics.js +9929 -7465
  12. package/dist/analysis/diagnostics.js.map +3 -4
  13. package/dist/analysis/duplication.d.ts.map +1 -1
  14. package/dist/analysis/fix.d.ts +3 -3
  15. package/dist/analysis/fix.d.ts.map +1 -1
  16. package/dist/analysis/format-number.d.ts +23 -0
  17. package/dist/analysis/format-number.d.ts.map +1 -0
  18. package/dist/analysis/gating.d.ts +39 -2
  19. package/dist/analysis/gating.d.ts.map +1 -1
  20. package/dist/analysis/rebuild.d.ts.map +1 -1
  21. package/dist/analysis/terminal.d.ts +5 -2
  22. package/dist/analysis/terminal.d.ts.map +1 -1
  23. package/dist/cli/index.cjs +10865 -8535
  24. package/dist/cli/index.cjs.map +3 -4
  25. package/dist/cli/index.js +10865 -8535
  26. package/dist/cli/index.js.map +3 -4
  27. package/dist/combinators/adjacency.d.ts +100 -0
  28. package/dist/combinators/adjacency.d.ts.map +1 -0
  29. package/dist/combinators/choice.d.ts.map +1 -1
  30. package/dist/combinators/completions.d.ts.map +1 -1
  31. package/dist/combinators/dispatch.d.ts +8 -1
  32. package/dist/combinators/dispatch.d.ts.map +1 -1
  33. package/dist/combinators/expect.d.ts.map +1 -1
  34. package/dist/combinators/first-set.d.ts.map +1 -1
  35. package/dist/combinators/gate.d.ts.map +1 -1
  36. package/dist/combinators/grammar.d.ts +22 -0
  37. package/dist/combinators/grammar.d.ts.map +1 -1
  38. package/dist/combinators/literal.d.ts +8 -0
  39. package/dist/combinators/literal.d.ts.map +1 -1
  40. package/dist/combinators/map.d.ts +0 -1
  41. package/dist/combinators/map.d.ts.map +1 -1
  42. package/dist/combinators/node.d.ts +13 -3
  43. package/dist/combinators/node.d.ts.map +1 -1
  44. package/dist/combinators/parser.d.ts +1 -1
  45. package/dist/combinators/parser.d.ts.map +1 -1
  46. package/dist/combinators/ref.d.ts.map +1 -1
  47. package/dist/combinators/regex.d.ts +1 -16
  48. package/dist/combinators/regex.d.ts.map +1 -1
  49. package/dist/combinators/repeat.d.ts +30 -2
  50. package/dist/combinators/repeat.d.ts.map +1 -1
  51. package/dist/combinators/scanTo.d.ts +71 -1
  52. package/dist/combinators/scanTo.d.ts.map +1 -1
  53. package/dist/combinators/sequence.d.ts.map +1 -1
  54. package/dist/combinators/token.d.ts.map +1 -1
  55. package/dist/combinators/trivia-boundary.d.ts +26 -0
  56. package/dist/combinators/trivia-boundary.d.ts.map +1 -0
  57. package/dist/combinators/trivia-skip.d.ts +40 -1
  58. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  59. package/dist/combinators/withCtx.d.ts.map +1 -1
  60. package/dist/compiler/compile-linkable-table.d.ts +104 -0
  61. package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
  62. package/dist/compiler/degradation.d.ts +0 -2
  63. package/dist/compiler/degradation.d.ts.map +1 -1
  64. package/dist/compiler/fields.d.ts +13 -0
  65. package/dist/compiler/fields.d.ts.map +1 -1
  66. package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
  67. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  68. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  69. package/dist/compiler/linker.d.ts +47 -53
  70. package/dist/compiler/linker.d.ts.map +1 -1
  71. package/dist/compiler/token-alphabet.d.ts +81 -0
  72. package/dist/compiler/token-alphabet.d.ts.map +1 -0
  73. package/dist/compiler/token-dispatch.d.ts +95 -0
  74. package/dist/compiler/token-dispatch.d.ts.map +1 -0
  75. package/dist/compiler/token-scanner.d.ts +53 -0
  76. package/dist/compiler/token-scanner.d.ts.map +1 -0
  77. package/dist/compiler/value-usage.d.ts.map +1 -1
  78. package/dist/coverage.d.ts +1 -1
  79. package/dist/coverage.d.ts.map +1 -1
  80. package/dist/cst/capture-buffer.d.ts +35 -0
  81. package/dist/cst/capture-buffer.d.ts.map +1 -1
  82. package/dist/cst/reflection-attach.d.ts +8 -0
  83. package/dist/cst/reflection-attach.d.ts.map +1 -0
  84. package/dist/cst/reflection-symbols.d.ts +10 -0
  85. package/dist/cst/reflection-symbols.d.ts.map +1 -0
  86. package/dist/cst/reflection.d.ts +2 -4
  87. package/dist/cst/reflection.d.ts.map +1 -1
  88. package/dist/cst/root-trivia-scope.d.ts +26 -0
  89. package/dist/cst/root-trivia-scope.d.ts.map +1 -0
  90. package/dist/cst/trivia-charscan.d.ts +18 -0
  91. package/dist/cst/trivia-charscan.d.ts.map +1 -0
  92. package/dist/cst/trivia-kinds.d.ts +19 -0
  93. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  94. package/dist/functional/doc.d.ts.map +1 -1
  95. package/dist/functional/run-tabled.d.ts +12 -0
  96. package/dist/functional/run-tabled.d.ts.map +1 -0
  97. package/dist/functional/run.d.ts +25 -42
  98. package/dist/functional/run.d.ts.map +1 -1
  99. package/dist/grammar-metadata.d.ts +3 -0
  100. package/dist/grammar-metadata.d.ts.map +1 -0
  101. package/dist/index.cjs +9224 -7203
  102. package/dist/index.cjs.map +3 -4
  103. package/dist/index.d.ts +18 -9
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +9219 -7198
  106. package/dist/index.js.map +3 -4
  107. package/dist/language-service/index.cjs +311 -121
  108. package/dist/language-service/index.cjs.map +3 -4
  109. package/dist/language-service/index.js +311 -121
  110. package/dist/language-service/index.js.map +3 -4
  111. package/dist/oracle/index.cjs.map +0 -1
  112. package/dist/oracle/index.js.map +0 -1
  113. package/dist/parse-context.d.ts +49 -0
  114. package/dist/parse-context.d.ts.map +1 -0
  115. package/dist/plugin/evaluator.d.ts +40 -1
  116. package/dist/plugin/evaluator.d.ts.map +1 -1
  117. package/dist/plugin/index.cjs +11146 -9593
  118. package/dist/plugin/index.cjs.map +3 -4
  119. package/dist/plugin/index.d.ts.map +1 -1
  120. package/dist/plugin/index.js +11146 -9593
  121. package/dist/plugin/index.js.map +3 -4
  122. package/dist/regex/classes.d.ts +28 -0
  123. package/dist/regex/classes.d.ts.map +1 -1
  124. package/dist/regex/first-set.d.ts +14 -0
  125. package/dist/regex/first-set.d.ts.map +1 -1
  126. package/dist/run/index.cjs +136 -51
  127. package/dist/run/index.cjs.map +2 -3
  128. package/dist/run/index.js +136 -51
  129. package/dist/run/index.js.map +2 -3
  130. package/dist/spec/index.cjs +2898 -1623
  131. package/dist/spec/index.cjs.map +3 -4
  132. package/dist/spec/index.js +2898 -1623
  133. package/dist/spec/index.js.map +3 -4
  134. package/dist/spec/model.d.ts +8 -0
  135. package/dist/spec/model.d.ts.map +1 -1
  136. package/dist/table/assemble.d.ts +307 -0
  137. package/dist/table/assemble.d.ts.map +1 -0
  138. package/dist/table/cell.d.ts +42 -0
  139. package/dist/table/cell.d.ts.map +1 -0
  140. package/dist/table/child-slots.d.ts +30 -0
  141. package/dist/table/child-slots.d.ts.map +1 -0
  142. package/dist/table/compile-rule-map.d.ts +136 -0
  143. package/dist/table/compile-rule-map.d.ts.map +1 -0
  144. package/dist/table/compile.d.ts +56 -0
  145. package/dist/table/compile.d.ts.map +1 -0
  146. package/dist/table/duplication-hook.d.ts +32 -0
  147. package/dist/table/duplication-hook.d.ts.map +1 -0
  148. package/dist/table/emit-assembly.d.ts +139 -0
  149. package/dist/table/emit-assembly.d.ts.map +1 -0
  150. package/dist/table/emit.d.ts +113 -0
  151. package/dist/table/emit.d.ts.map +1 -0
  152. package/dist/table/encode-baseline.d.ts +16 -0
  153. package/dist/table/encode-baseline.d.ts.map +1 -0
  154. package/dist/table/encode.d.ts +71 -0
  155. package/dist/table/encode.d.ts.map +1 -0
  156. package/dist/table/exec-baseline.d.ts +9 -0
  157. package/dist/table/exec-baseline.d.ts.map +1 -0
  158. package/dist/table/exec.d.ts +72 -0
  159. package/dist/table/exec.d.ts.map +1 -0
  160. package/dist/table/fold.d.ts +25 -0
  161. package/dist/table/fold.d.ts.map +1 -0
  162. package/dist/table/index.cjs +11375 -0
  163. package/dist/table/index.cjs.map +6 -0
  164. package/dist/table/index.d.ts +100 -0
  165. package/dist/table/index.d.ts.map +1 -0
  166. package/dist/table/index.js +11352 -0
  167. package/dist/table/index.js.map +6 -0
  168. package/dist/table/inspect.d.ts +17 -0
  169. package/dist/table/inspect.d.ts.map +1 -0
  170. package/dist/table/ops.d.ts +446 -0
  171. package/dist/table/ops.d.ts.map +1 -0
  172. package/dist/table/program.d.ts +493 -0
  173. package/dist/table/program.d.ts.map +1 -0
  174. package/dist/table/run-support.d.ts +38 -0
  175. package/dist/table/run-support.d.ts.map +1 -0
  176. package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
  177. package/dist/table/scan-shapes.d.ts.map +1 -0
  178. package/dist/table/site-labels.d.ts +58 -0
  179. package/dist/table/site-labels.d.ts.map +1 -0
  180. package/dist/table/stamp.d.ts +70 -0
  181. package/dist/table/stamp.d.ts.map +1 -0
  182. package/dist/types.d.ts +80 -24
  183. package/dist/types.d.ts.map +1 -1
  184. package/dist/version.d.ts +1 -1
  185. package/package.json +25 -2
  186. package/src/analysis/choice-cost-gate.ts +359 -0
  187. package/src/analysis/choice-cost-render.ts +293 -0
  188. package/src/analysis/choice-cost.ts +1008 -0
  189. package/src/analysis/commitment.ts +557 -0
  190. package/src/analysis/corpus.ts +130 -0
  191. package/src/analysis/diagnose-render.ts +625 -0
  192. package/src/analysis/diagnose.ts +317 -0
  193. package/src/analysis/diagnostics.ts +33 -0
  194. package/src/analysis/duplication.ts +1955 -0
  195. package/src/analysis/fix-render.ts +217 -0
  196. package/src/analysis/fix.ts +669 -0
  197. package/src/analysis/format-number.ts +31 -0
  198. package/src/analysis/gating.ts +792 -0
  199. package/src/analysis/grammar.ts +72 -0
  200. package/src/analysis/rebuild.ts +287 -0
  201. package/src/analysis/terminal.ts +336 -0
  202. package/src/cli/index.ts +413 -0
  203. package/src/combinators/adjacency.ts +171 -0
  204. package/src/combinators/attempt.ts +41 -0
  205. package/src/combinators/case-fold.ts +90 -0
  206. package/src/combinators/choice.ts +466 -0
  207. package/src/combinators/completions.ts +64 -0
  208. package/src/combinators/dispatch.ts +358 -0
  209. package/src/combinators/expect.ts +159 -0
  210. package/src/combinators/first-set.ts +322 -0
  211. package/src/combinators/gate.ts +50 -0
  212. package/src/combinators/grammar.ts +272 -0
  213. package/src/combinators/keywords.ts +145 -0
  214. package/src/combinators/literal.ts +102 -0
  215. package/src/combinators/map.ts +106 -0
  216. package/src/combinators/node.ts +345 -0
  217. package/src/combinators/not.ts +55 -0
  218. package/src/combinators/parser.ts +266 -0
  219. package/src/combinators/peek.ts +63 -0
  220. package/src/combinators/probe.ts +25 -0
  221. package/src/combinators/ref.ts +109 -0
  222. package/src/combinators/regex.ts +170 -0
  223. package/src/combinators/repeat.ts +561 -0
  224. package/src/combinators/scanTo.ts +393 -0
  225. package/src/combinators/sequence.ts +221 -0
  226. package/src/combinators/token.ts +120 -0
  227. package/src/combinators/trivia-boundary.ts +48 -0
  228. package/src/combinators/trivia-skip.ts +821 -0
  229. package/src/combinators/withCtx.ts +45 -0
  230. package/src/compiler/build-arity.ts +330 -0
  231. package/src/compiler/compile-linkable-table.ts +209 -0
  232. package/src/compiler/degradation.ts +266 -0
  233. package/src/compiler/fields.ts +191 -0
  234. package/src/compiler/grammar-coverage-ids.ts +129 -0
  235. package/src/compiler/ir-serialize.ts +615 -0
  236. package/src/compiler/linker.ts +954 -0
  237. package/src/compiler/token-alphabet.ts +209 -0
  238. package/src/compiler/token-dispatch.ts +353 -0
  239. package/src/compiler/token-scanner.ts +318 -0
  240. package/src/compiler/value-usage.ts +147 -0
  241. package/src/coverage.ts +383 -0
  242. package/src/cst/capture-buffer.ts +297 -0
  243. package/src/cst/host-mode.ts +69 -0
  244. package/src/cst/reflection-attach.ts +11 -0
  245. package/src/cst/reflection-symbols.ts +9 -0
  246. package/src/cst/reflection.ts +149 -0
  247. package/src/cst/relative-spans.ts +207 -0
  248. package/src/cst/root-trivia-scope.ts +30 -0
  249. package/src/cst/trivia-charscan.ts +312 -0
  250. package/src/cst/trivia-entries.ts +285 -0
  251. package/src/cst/trivia-index.ts +131 -0
  252. package/src/cst/trivia-kinds.ts +358 -0
  253. package/src/cst/types.ts +76 -0
  254. package/src/cst/walk.ts +101 -0
  255. package/src/functional/doc.ts +959 -0
  256. package/src/functional/run-tabled.ts +153 -0
  257. package/src/functional/run.ts +558 -0
  258. package/src/grammar-metadata.ts +2 -0
  259. package/src/index.ts +189 -0
  260. package/src/language-service/index.ts +221 -0
  261. package/src/line-index.ts +131 -0
  262. package/src/oracle/digest.ts +620 -0
  263. package/src/oracle/index.ts +55 -0
  264. package/src/parse-context.ts +91 -0
  265. package/src/plugin/direct-builder-static.ts +102 -0
  266. package/src/plugin/evaluator.ts +1505 -0
  267. package/src/plugin/free-identifiers.ts +338 -0
  268. package/src/plugin/index.ts +2281 -0
  269. package/src/plugin/reducer-resolver.ts +606 -0
  270. package/src/recovery/scan.ts +155 -0
  271. package/src/regex/classes.ts +183 -0
  272. package/src/regex/first-set.ts +316 -0
  273. package/src/run/index.ts +31 -0
  274. package/src/spec/ebnf.ts +128 -0
  275. package/src/spec/index.ts +54 -0
  276. package/src/spec/model.ts +636 -0
  277. package/src/spec/railroad-lib.ts +559 -0
  278. package/src/spec/railroad.ts +256 -0
  279. package/src/table/assemble.ts +2994 -0
  280. package/src/table/cell.ts +42 -0
  281. package/src/table/child-slots.ts +113 -0
  282. package/src/table/compile-rule-map.ts +336 -0
  283. package/src/table/compile.ts +244 -0
  284. package/src/table/duplication-hook.ts +89 -0
  285. package/src/table/emit-assembly.ts +1766 -0
  286. package/src/table/emit.ts +418 -0
  287. package/src/table/encode-baseline.ts +340 -0
  288. package/src/table/encode.ts +1569 -0
  289. package/src/table/exec-baseline.ts +537 -0
  290. package/src/table/exec.ts +1672 -0
  291. package/src/table/fold.ts +38 -0
  292. package/src/table/index.ts +108 -0
  293. package/src/table/inspect.ts +52 -0
  294. package/src/table/ops.ts +459 -0
  295. package/src/table/program.ts +855 -0
  296. package/src/table/run-support.ts +66 -0
  297. package/src/table/scan-shapes.ts +1578 -0
  298. package/src/table/site-labels.ts +228 -0
  299. package/src/table/stamp.ts +229 -0
  300. package/src/types.ts +563 -0
  301. package/src/version.ts +16 -0
  302. package/dist/compiler/codegen.d.ts +0 -334
  303. package/dist/compiler/codegen.d.ts.map +0 -1
  304. package/dist/compiler/inline-build.d.ts +0 -11
  305. package/dist/compiler/inline-build.d.ts.map +0 -1
  306. package/dist/compiler/inline-callback.d.ts +0 -21
  307. package/dist/compiler/inline-callback.d.ts.map +0 -1
  308. package/dist/compiler/line-index.d.ts +0 -2
  309. package/dist/compiler/line-index.d.ts.map +0 -1
  310. package/dist/compiler/module-hoist.d.ts +0 -106
  311. package/dist/compiler/module-hoist.d.ts.map +0 -1
  312. package/dist/compiler/scannable-run.d.ts.map +0 -1
  313. package/dist/compiler/scannable-terminal.d.ts +0 -23
  314. package/dist/compiler/scannable-terminal.d.ts.map +0 -1
  315. package/dist/compiler/trivia-fast-path.d.ts +0 -38
  316. package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
@@ -0,0 +1,145 @@
1
+ import type { Combinator, ParseContext, ParseResult, ParserMeta, FirstSet } from '../types.ts'
2
+ import { fromChar, union, empty } from './first-set.ts'
3
+ import { caseFoldVariants } from './case-fold.ts'
4
+ import { failAt } from './probe.ts'
5
+ import { pushCstLeaf, cstCaptureActive } from '../cst/capture-buffer.ts'
6
+
7
+ export type KeywordsOptions = {
8
+ /** Match case-insensitively. */
9
+ caseInsensitive?: boolean
10
+ /**
11
+ * Character class (regex source, e.g. `'A-Za-z0-9_-'`) that must NOT follow a
12
+ * match — a word boundary. Prevents matching `red` inside `redish`.
13
+ */
14
+ boundary?: string
15
+ }
16
+
17
+ export type WordOptions = Omit<KeywordsOptions, 'boundary'>
18
+
19
+ /**
20
+ * Match a single keyword with an automatic word-boundary guard. Prevents
21
+ * matching `true` inside `trueish`. The boundary defaults to `_0-9A-Za-z`,
22
+ * which covers most programming-language identifiers.
23
+ *
24
+ * word('true') // matches "true" but not "trueish"
25
+ * word('color', 'A-Za-z-') // CSS-style identifier boundary
26
+ * word('media', 'A-Za-z0-9_-', { caseInsensitive: true })
27
+ * word('media', { caseInsensitive: true }) // default boundary + options
28
+ *
29
+ * `caseInsensitive` matches `keywords()` and exists because it is REQUIRED, not a
30
+ * nicety: CSS at-keywords, function names and units are ASCII case-insensitive per
31
+ * spec (CSS Syntax §3). Without it the only conforming spellings were
32
+ * `regex(/media/i)` — which `analyzeGating` correctly flags as the `keyword-regex`
33
+ * anti-pattern — or `keywords(['media'], …)` for a single word.
34
+ */
35
+ export function word(str: string, boundary?: string, opts?: WordOptions): Combinator<string>
36
+ export function word(str: string, opts: WordOptions): Combinator<string>
37
+ export function word(
38
+ str: string,
39
+ boundaryOrOpts?: string | WordOptions,
40
+ opts?: WordOptions,
41
+ ): Combinator<string> {
42
+ const boundary = typeof boundaryOrOpts === 'string' ? boundaryOrOpts : '_0-9A-Za-z'
43
+ const rest = typeof boundaryOrOpts === 'object' && boundaryOrOpts !== null ? boundaryOrOpts : opts
44
+ return keywords([str], { ...rest, boundary })
45
+ }
46
+
47
+ /**
48
+ * Create a keyword factory with a fixed word-boundary class. Use when many
49
+ * keywords share the same boundary (e.g. CSS identifiers). For a single keyword,
50
+ * `word(str, boundary?)` is enough.
51
+ *
52
+ * const kw = makeWord() // default boundary
53
+ * const cssKw = makeWord('A-Za-z0-9_-', { caseInsensitive: true })
54
+ *
55
+ * const query = kw('query')
56
+ * const color = cssKw('color') // matches "COLOR" but not "color-scheme"
57
+ */
58
+ export function makeWord(boundary?: string, opts?: WordOptions): (str: string) => Combinator<string>
59
+ export function makeWord(opts: WordOptions): (str: string) => Combinator<string>
60
+ export function makeWord(
61
+ boundaryOrOpts: string | WordOptions = '_0-9A-Za-z',
62
+ opts?: WordOptions,
63
+ ): (str: string) => Combinator<string> {
64
+ const boundary = typeof boundaryOrOpts === 'string' ? boundaryOrOpts : '_0-9A-Za-z'
65
+ const rest = typeof boundaryOrOpts === 'object' && boundaryOrOpts !== null ? boundaryOrOpts : opts
66
+ return (str: string) => keywords([str], { ...rest, boundary })
67
+ }
68
+
69
+ function escapeRe(s: string): string {
70
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
71
+ }
72
+
73
+ /**
74
+ * Match any one of a set of fixed keywords, longest-first (so `border` wins over
75
+ * `bord`), with an optional trailing word-boundary guard. Compiles to a single
76
+ * sticky regex — far less error-prone than a hand-maintained alternation, and
77
+ * the common case (keyword sets: CSS colors, units, at-rule names, HTML tags).
78
+ *
79
+ * keywords(CSS_COLOR_NAMES, { caseInsensitive: true, boundary: 'A-Za-z0-9_-' })
80
+ *
81
+ * On success the matched text is recorded as a CSTLeaf (like literal()/regex()).
82
+ */
83
+ export function keywords(words: readonly string[], opts: KeywordsOptions = {}): Combinator<string> {
84
+ // Longest-first keeps the alternation greedy-correct (regex alternation is
85
+ // first-match, not longest-match).
86
+ const sorted = [...new Set(words)].sort((a, b) => b.length - a.length)
87
+ const alt = sorted.map(escapeRe).join('|')
88
+ const boundary = opts.boundary ? `(?![${opts.boundary}])` : ''
89
+ // Case-INSENSITIVE drops `u` deliberately, so that MATCHING and the first-set
90
+ // below fold the SAME set of characters — the invariant `regex()` established in
91
+ // 0.32.0 (see the flag-aware widening in `src/combinators/regex.ts`):
92
+ // - `/i` WITHOUT `u` folds only pairs that stay on the SAME side of the ASCII
93
+ // boundary — every Basic-Latin pair (a↔A … z↔Z), and also the non-ASCII ones
94
+ // (ä↔Ä, σ↔Σ↔ς), but never a pair that crosses it. `caseFoldVariants` below
95
+ // enumerates exactly that relation → the first-set is a tight, SOUND superset
96
+ // and dispatch stays exact. (Widening by toUpperCase/toLowerCase alone would
97
+ // NOT be: 67 BMP code points sit in fold classes those two miss.)
98
+ // - `/iu` folds by Unicode *simple case folding*, so `/(?:stroke)/iu` also
99
+ // matches `ſtroke` (U+017F → s). An ASCII-only first-set would then dispatch
100
+ // that input AWAY from this arm — an unsound gate. `regex()` answers that by
101
+ // falling back to `any()` (forfeiting the gate); `keywords()` answers it by
102
+ // not entering Unicode mode at all, which keeps the gate.
103
+ // Every keyword is fully escaped by `escapeRe`, so `u` mode changes nothing about
104
+ // what a case-SENSITIVE set matches; that path keeps `u` unchanged.
105
+ const flags = opts.caseInsensitive ? 'iy' : 'uy'
106
+ const re = new RegExp(`(?:${alt})${boundary}`, flags)
107
+
108
+ // First-set: the set of first code points across all keywords (and their
109
+ // case-folded variants when case-insensitive), for choice() dispatch.
110
+ //
111
+ // The fold must cover the SAME pairs the `iy` matcher above accepts, or the gate
112
+ // dispatches valid input away from this arm. `/i` folds non-ASCII pairs too
113
+ // (`/ärger/i` matches `Ärger`, `/σ/i` matches both `Σ` and `ς`) — it only refuses
114
+ // folds that would CROSS the ASCII boundary. `caseFoldVariants` is that exact
115
+ // relation, so the widening stays a tight, sound superset for every first char,
116
+ // not just the Basic-Latin ones.
117
+ let firstSet: FirstSet = empty()
118
+ for (const w of sorted) {
119
+ if (w.length === 0) continue
120
+ const cp = w.codePointAt(0)!
121
+ firstSet = union(firstSet, fromChar(cp))
122
+ if (opts.caseInsensitive) {
123
+ for (const v of caseFoldVariants(cp)) firstSet = union(firstSet, fromChar(v))
124
+ }
125
+ }
126
+
127
+ const meta: ParserMeta = { firstSet, canMatchNewline: false, isTrivia: false }
128
+
129
+ return {
130
+ _tag: 'keywords',
131
+ _meta: meta,
132
+ _def: { tag: 'keywords', words: sorted, caseInsensitive: opts.caseInsensitive ?? false, boundary: opts.boundary },
133
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<string> {
134
+ re.lastIndex = pos
135
+ const m = re.exec(input)
136
+ if (m === null || m.index !== pos) {
137
+ return failAt(ctx, ['keyword'], pos)
138
+ }
139
+ const value = m[0]!
140
+ const span = { start: pos, end: pos + value.length }
141
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value, span })
142
+ return { ok: true, value, span }
143
+ },
144
+ }
145
+ }
@@ -0,0 +1,102 @@
1
+ import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
2
+ import { fromChar, empty } from './first-set.ts'
3
+ import { failAt } from './probe.ts'
4
+ import { pushCstLeaf, cstCaptureActive } from '../cst/capture-buffer.ts'
5
+ import { recordLineRangeFromContext } from '../line-index.ts'
6
+
7
+ /**
8
+ * ASCII case-fold equality — the interpreter twin of the compiler's `foldEq`
9
+ * (and `/i`-flag regex lowering). Folds ASCII letters via bit 0x20, exact-matches
10
+ * everything else. Deliberately NOT `Intl.Collator` (measured ~9× slower, and its
11
+ * Unicode accent-folding is the wrong semantic for a parser). Keeps interpreter /
12
+ * compiled / macro output identical for case-insensitive `literal()`.
13
+ */
14
+ export function asciiFoldEq(a: string, b: string): boolean {
15
+ if (a.length !== b.length) return false
16
+ for (let i = 0; i < a.length; i++) {
17
+ let ca = a.charCodeAt(i)
18
+ let cb = b.charCodeAt(i)
19
+ if (ca >= 65 && ca <= 90) ca += 32
20
+ if (cb >= 65 && cb <= 90) cb += 32
21
+ if (ca !== cb) return false
22
+ }
23
+ return true
24
+ }
25
+
26
+ export type LiteralOptions = {
27
+ caseInsensitive?: boolean
28
+ }
29
+
30
+ export function literal(value: string, opts: LiteralOptions = {}): Combinator<string> {
31
+ const caseInsensitive = opts.caseInsensitive ?? false
32
+
33
+ const firstSet = value.length > 0
34
+ ? fromChar(value.codePointAt(0)!)
35
+ : empty()
36
+
37
+ const meta: ParserMeta = {
38
+ firstSet,
39
+ canMatchNewline: value.includes('\n'),
40
+ isTrivia: false,
41
+ }
42
+ const canMatchNewline = meta.canMatchNewline
43
+
44
+ if (caseInsensitive) {
45
+ const upper = value.toUpperCase()
46
+ const lower = value.toLowerCase()
47
+ const firstUpper = upper.codePointAt(0)
48
+ const firstLower = lower.codePointAt(0)
49
+ // Two discrete points, not one spanning range — `asciiFoldEq` accepts only
50
+ // the char and its fold twin, so `[min, max]` would over-approximate (e.g.
51
+ // 'a'/'A' → the whole 65..97 span, pulling B–Z/`[\]^_\`` into dispatch).
52
+ meta.firstSet = firstLower !== undefined && firstUpper !== undefined
53
+ ? firstLower === firstUpper
54
+ ? { kind: 'ranges', ranges: [{ lo: firstLower, hi: firstLower }] }
55
+ : { kind: 'ranges', ranges: [
56
+ { lo: firstLower, hi: firstLower },
57
+ { lo: firstUpper, hi: firstUpper },
58
+ ]}
59
+ : firstSet
60
+ }
61
+
62
+ const expected = [JSON.stringify(value)]
63
+
64
+ // Single-char case-sensitive literals are the bulk of punctuation-heavy
65
+ // grammars (GraphQL `{ } ( ) : $ @ [ ] ! =`, JSON, CSS). A bare `charCodeAt`
66
+ // compare beats the generic `startsWith` builtin call, and an out-of-bounds
67
+ // `charCodeAt` returns NaN — so the length check folds into the compare.
68
+ const parse = !caseInsensitive && value.length === 1
69
+ ? (() => {
70
+ const code = value.charCodeAt(0)
71
+ return function parse(input: string, pos: number, ctx: ParseContext): ParseResult<string> {
72
+ if (input.charCodeAt(pos) === code) {
73
+ const span = { start: pos, end: pos + 1 }
74
+ if (canMatchNewline && ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, span.end)
75
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value, span })
76
+ return { ok: true, value, span }
77
+ }
78
+ return failAt(ctx, expected, pos)
79
+ }
80
+ })()
81
+ : function parse(input: string, pos: number, ctx: ParseContext): ParseResult<string> {
82
+ const end = pos + value.length
83
+ if (end > input.length) {
84
+ return failAt(ctx, expected, pos)
85
+ }
86
+ const matchedValue = caseInsensitive ? input.slice(pos, end) : value
87
+ if (caseInsensitive ? asciiFoldEq(matchedValue, value) : input.startsWith(value, pos)) {
88
+ const span = { start: pos, end }
89
+ if (canMatchNewline && ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, end)
90
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value: matchedValue, span })
91
+ return { ok: true, value: matchedValue, span }
92
+ }
93
+ return failAt(ctx, expected, pos)
94
+ }
95
+
96
+ return {
97
+ _tag: 'literal',
98
+ _meta: meta,
99
+ _def: { tag: 'literal', value, caseInsensitive },
100
+ parse,
101
+ }
102
+ }
@@ -0,0 +1,106 @@
1
+ import type { Combinator, ParseContext, ParseResult } from '../types.ts'
2
+ import { analyzeLabeledTrivia } from '../cst/trivia-kinds.ts'
3
+ import { choice } from './choice.ts'
4
+ import { matchesEmpty } from './first-set.ts'
5
+ import { oneOrMore } from './repeat.ts'
6
+
7
+ export function transform<T, U>(
8
+ combinator: Combinator<T>,
9
+ fn: (value: T, span: { start: number; end: number }) => U
10
+ ): Combinator<U> {
11
+ return {
12
+ _tag: 'transform',
13
+ _meta: combinator._meta,
14
+ _def: { tag: 'transform', parser: combinator as Combinator<unknown>, fn: fn as (v: unknown, span: { start: number; end: number }) => unknown },
15
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<U> {
16
+ const result = combinator.parse(input, pos, ctx)
17
+ if (!result.ok) return result
18
+ return { ok: true, value: fn(result.value, result.span), span: result.span }
19
+ },
20
+ }
21
+ }
22
+
23
+ export function trivia<T>(combinator: Combinator<T>): Combinator<T> {
24
+ const kindLabels = analyzeLabeledTrivia(combinator as Combinator<unknown>)?.labels
25
+ return {
26
+ _tag: combinator._tag,
27
+ _meta: {
28
+ ...combinator._meta,
29
+ isTrivia: true,
30
+ ...(kindLabels ? { triviaKindLabels: kindLabels } : {}),
31
+ },
32
+ _def: { tag: 'trivia', parser: combinator as Combinator<unknown> },
33
+ parse: combinator.parse.bind(combinator),
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Build trivia whose category labels are part of its recognition structure.
39
+ *
40
+ * Use this for a grammar that exposes selected root trivia. In contrast to
41
+ * `trivia(label('categoryA', broadRegex))`, every category here owns one arm,
42
+ * so one selected category cannot be silently consumed under another category's
43
+ * broad matcher.
44
+ * `rootTrivia: { select }` rejects ordinary scoped trivia
45
+ * unless that scope explicitly declares itself opaque.
46
+ */
47
+ export function classifiedTrivia(
48
+ arms: Readonly<Record<string, Combinator<unknown>>>,
49
+ ): Combinator<unknown> {
50
+ const entries = Object.entries(arms)
51
+ if (entries.length === 0) {
52
+ throw new TypeError('classifiedTrivia() requires at least one named trivia arm.')
53
+ }
54
+ for (let i = 0; i < entries.length; i++) {
55
+ const [name, arm] = entries[i]!
56
+ const first = arm._meta.firstSet
57
+ if (matchesEmpty(arm) || first.kind !== 'ranges' || first.ranges.length === 0) {
58
+ throw new TypeError(
59
+ `classifiedTrivia(): ${JSON.stringify(name)} must be non-nullable with a concrete finite first set.`,
60
+ )
61
+ }
62
+ }
63
+ const labeledArms = entries.map(([name, arm]) => label(name, arm)) as [
64
+ Combinator<unknown>,
65
+ ...Combinator<unknown>[],
66
+ ]
67
+ const result = trivia(oneOrMore(choice(...labeledArms)))
68
+ result._meta.rootTriviaClassified = true
69
+ return result
70
+ }
71
+
72
+ /**
73
+ * Attach a string label to a parser arm (e.g. trivia `choice` branches).
74
+ * Parse behavior is unchanged; the label is metadata for tooling and future
75
+ * trivia-kind capture (`'whitespace'`, `'blockComment'`, …).
76
+ */
77
+ export function label<T>(name: string, combinator: Combinator<T>): Combinator<T> {
78
+ return {
79
+ _tag: combinator._tag,
80
+ _meta: combinator._meta,
81
+ _def: { tag: 'label', label: name, parser: combinator as Combinator<unknown> },
82
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<T> {
83
+ const result = combinator.parse(input, pos, ctx)
84
+ if (!result.ok) return { ok: false, expected: [name], span: result.span }
85
+ return result
86
+ },
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Capture a named parse result for the nearest enclosing node() builder.
92
+ * Parse behavior and the normal returned value are unchanged.
93
+ */
94
+ export function field<T>(name: string, combinator: Combinator<T>): Combinator<T> {
95
+ return {
96
+ _tag: combinator._tag,
97
+ _meta: combinator._meta,
98
+ _def: { tag: 'field', name, parser: combinator as Combinator<unknown> },
99
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<T> {
100
+ const result = combinator.parse(input, pos, ctx)
101
+ if (!result.ok) return result
102
+ ctx._fields?.push({ name, value: result.value, span: result.span })
103
+ return result
104
+ },
105
+ }
106
+ }