parseman 0.46.0 → 0.47.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +10339 -7787
  9. package/dist/analysis/diagnostics.cjs.map +3 -4
  10. package/dist/analysis/diagnostics.d.ts +2 -2
  11. package/dist/analysis/diagnostics.js +10339 -7787
  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 +10924 -8506
  24. package/dist/cli/index.cjs.map +3 -4
  25. package/dist/cli/index.js +10924 -8506
  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 +45 -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 +9290 -7181
  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 +9300 -7191
  106. package/dist/index.js.map +3 -4
  107. package/dist/language-service/index.cjs +313 -121
  108. package/dist/language-service/index.cjs.map +3 -4
  109. package/dist/language-service/index.js +313 -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 +11175 -9534
  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 +11175 -9534
  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 +2934 -1611
  131. package/dist/spec/index.cjs.map +3 -4
  132. package/dist/spec/index.js +2934 -1611
  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 +11463 -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 +11440 -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 +453 -0
  171. package/dist/table/ops.d.ts.map +1 -0
  172. package/dist/table/program.d.ts +496 -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 +26 -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 +348 -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 +237 -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 +852 -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 +3006 -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 +1777 -0
  286. package/src/table/emit.ts +429 -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 +1680 -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 +467 -0
  295. package/src/table/program.ts +876 -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,131 @@
1
+ import type { Span } from '../types.ts'
2
+
3
+ /** One captured trivia token (a whitespace run or a comment) with its source span. */
4
+ export type TriviaToken = { readonly value: string; readonly span: Span }
5
+
6
+ /**
7
+ * Before/after offset index of captured trivia. Built from a tree whose nodes
8
+ * carry a `triviaLog` property (the flat `[start, end, insertIdx, ...]` array
9
+ * produced by parseman when `parser({ captureTrivia: true })` wraps a `node()` grammar).
10
+ *
11
+ * index.before.get(node.span.start) // trivia immediately before a node
12
+ * index.after.get(node.span.end) // trivia immediately after a node
13
+ *
14
+ * A given trivia entry is registered under BOTH the following item's start
15
+ * (`before`) and the preceding item's end (`after`), so either lookup finds it.
16
+ */
17
+ export type TriviaIndex = {
18
+ readonly before: Map<number, TriviaToken[]>
19
+ readonly after: Map<number, TriviaToken[]>
20
+ }
21
+
22
+ type Spanned = { readonly span: Span }
23
+ type NodeWithTrivia = {
24
+ readonly span?: Span
25
+ readonly children?: ReadonlyArray<unknown>
26
+ readonly rawChildren?: ReadonlyArray<Spanned>
27
+ readonly triviaLog?: readonly number[]
28
+ }
29
+
30
+ function merge(map: Map<number, TriviaToken[]>, key: number, run: TriviaToken[]): void {
31
+ if (run.length === 0) return
32
+ const existing = map.get(key)
33
+ if (existing) existing.push(...run)
34
+ else map.set(key, [...run])
35
+ }
36
+
37
+ /**
38
+ * Options for trailing/leading trivia at the document boundary. A repeating root
39
+ * (e.g. `many()`) rolls back the trivia after its last item — it's "trailing" and
40
+ * belongs to the enclosing context, which at the document root is the document
41
+ * itself. Pass `{ trivia }` to re-scan and register that boundary trivia,
42
+ * so a round-trippable trivia map isn't missing the run before EOF.
43
+ */
44
+ export type TriviaIndexOptions = {
45
+ /**
46
+ * A regex matching ONE trivia token (a whitespace run or a comment). Applied
47
+ * repeatedly to tokenize the boundary trivia. Each match becomes one token, so
48
+ * write it to match a maximal run, e.g.
49
+ * `/[ \t\n\r\f]+|\/\*(?:[^*]|\*(?!\/))*\*\//`.
50
+ */
51
+ trivia: RegExp
52
+ }
53
+
54
+ /**
55
+ * Walk a tree whose nodes carry `triviaLog` (from `node()` + `captureTrivia`)
56
+ * and build a before/after trivia index. Each node must have
57
+ * `triviaLog: readonly number[]` (flat `[start, end, insertIdx, …]` —
58
+ * three numbers per entry) and `rawChildren` (structural items with `.span`).
59
+ * `input` is the source string used to materialize trivia values.
60
+ *
61
+ * With `opts`, also captures leading trivia (before the root's content) and
62
+ * trailing trivia (after it, up to EOF) — the document boundaries a repeating
63
+ * root rolls back.
64
+ */
65
+ export function buildTriviaIndex(root: unknown, input = '', opts?: TriviaIndexOptions): TriviaIndex {
66
+ const before = new Map<number, TriviaToken[]>()
67
+ const after = new Map<number, TriviaToken[]>()
68
+
69
+ const visit = (node: NodeWithTrivia): void => {
70
+ const log = node.triviaLog
71
+ const raw = node.rawChildren
72
+
73
+ if (log && log.length && raw) {
74
+ for (let i = 0; i < log.length; i += 3) {
75
+ const tStart = log[i]!, tEnd = log[i + 1]!, insertIdx = log[i + 2]!
76
+ if (tEnd <= tStart) continue
77
+ const run: TriviaToken[] = [{ value: input.slice(tStart, tEnd), span: { start: tStart, end: tEnd } }]
78
+ const next = raw[insertIdx]
79
+ if (next) merge(before, next.span.start, run)
80
+ const prev = insertIdx > 0 ? raw[insertIdx - 1] : undefined
81
+ if (prev) merge(after, prev.span.end, run)
82
+ }
83
+ }
84
+
85
+ const ch = node.children
86
+ if (ch) {
87
+ for (const c of ch) {
88
+ if (typeof c === 'object' && c !== null && (c as NodeWithTrivia).children !== undefined) {
89
+ visit(c as NodeWithTrivia)
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ if (root) visit(root as NodeWithTrivia)
96
+
97
+ // Document-boundary trivia: re-scan leading (offset 0) and trailing (root end
98
+ // → EOF) trivia that a repeating root rolled back.
99
+ if (opts && input) {
100
+ const rootSpan = (root as NodeWithTrivia | null)?.span
101
+ const re = new RegExp(opts.trivia.source, opts.trivia.flags.replace(/[gy]/g, '') + 'y')
102
+ const scanFrom = (from: number): TriviaToken[] => {
103
+ const run: TriviaToken[] = []
104
+ let pos = from
105
+ re.lastIndex = pos
106
+ let m = re.exec(input)
107
+ while (m && m.index === pos && m[0].length > 0) {
108
+ run.push({ value: m[0], span: { start: pos, end: pos + m[0].length } })
109
+ pos += m[0].length
110
+ re.lastIndex = pos
111
+ m = re.exec(input)
112
+ }
113
+ return run
114
+ }
115
+
116
+ if (rootSpan && rootSpan.start > 0) {
117
+ const lead = scanFrom(0)
118
+ if (lead.length) merge(before, rootSpan.start, lead)
119
+ }
120
+ const end = rootSpan ? rootSpan.end : 0
121
+ if (end < input.length) {
122
+ const trail = scanFrom(end)
123
+ if (trail.length) {
124
+ merge(after, end, trail)
125
+ merge(before, end + trail.reduce((n, t) => n + (t.span.end - t.span.start), 0), trail)
126
+ }
127
+ }
128
+ }
129
+
130
+ return { before, after }
131
+ }
@@ -0,0 +1,358 @@
1
+ import type { Combinator, ParseContext } from '../types.ts'
2
+ import { pushCstTriviaEntry, pushTriviaLogEntry } from './capture-buffer.ts'
3
+ import { startsFirstSet } from '../combinators/first-set.ts'
4
+ import { createDetachedParseContext } from '../parse-context.ts'
5
+ import { charArmsFor, charTriviaEnd, charTriviaKindMask, charTriviaVisit } from './trivia-charscan.ts'
6
+
7
+ export type TriviaChunk = { start: number; end: number; kindIndex: number }
8
+
9
+ export type LabeledTriviaSpec = {
10
+ readonly labels: readonly string[]
11
+ readonly arms: ReadonlyArray<{ label: string; kindIndex: number; parser: Combinator<unknown> }>
12
+ readonly minRepeats: number
13
+ }
14
+
15
+ /** See `trivia-skip.ts`: bounded scalar memoisation avoids a WeakMap in the
16
+ * shipped table runtime while retaining the common trivia grammars. */
17
+ let labeledTrivia0: Combinator<unknown> | undefined
18
+ let labeledTrivia1: Combinator<unknown> | undefined
19
+ let labeledTrivia2: Combinator<unknown> | undefined
20
+ let labeledTrivia3: Combinator<unknown> | undefined
21
+ let labeledSpec0: LabeledTriviaSpec | null | undefined
22
+ let labeledSpec1: LabeledTriviaSpec | null | undefined
23
+ let labeledSpec2: LabeledTriviaSpec | null | undefined
24
+ let labeledSpec3: LabeledTriviaSpec | null | undefined
25
+
26
+ function cacheLabeledTrivia(trivia: Combinator<unknown>, spec: LabeledTriviaSpec | null): LabeledTriviaSpec | null {
27
+ labeledTrivia3 = labeledTrivia2
28
+ labeledSpec3 = labeledSpec2
29
+ labeledTrivia2 = labeledTrivia1
30
+ labeledSpec2 = labeledSpec1
31
+ labeledTrivia1 = labeledTrivia0
32
+ labeledSpec1 = labeledSpec0
33
+ labeledTrivia0 = trivia
34
+ labeledSpec0 = spec
35
+ return spec
36
+ }
37
+
38
+ function unwrapTrivia(p: Combinator<unknown>): Combinator<unknown> {
39
+ let cur = p
40
+ while (cur._def.tag === 'trivia') cur = cur._def.parser
41
+ return cur
42
+ }
43
+
44
+ /** Strip `label()` wrapper; returns inner parser for matching. */
45
+ export function peelLabel(p: Combinator<unknown>): { label: string; parser: Combinator<unknown> } | null {
46
+ if (p._def.tag === 'label') return { label: p._def.label, parser: p._def.parser }
47
+ return null
48
+ }
49
+
50
+ /**
51
+ * When every trivia arm is `label(name, parser)` inside `oneOrMore(choice(…))`
52
+ * (or a single labeled arm), return the label table and matchers.
53
+ */
54
+ export function analyzeLabeledTrivia(trivia: Combinator<unknown>): LabeledTriviaSpec | null {
55
+ if (trivia === labeledTrivia0) return labeledSpec0!
56
+ if (trivia === labeledTrivia1) return labeledSpec1!
57
+ if (trivia === labeledTrivia2) return labeledSpec2!
58
+ if (trivia === labeledTrivia3) return labeledSpec3!
59
+
60
+ let core = unwrapTrivia(trivia)
61
+ let minRepeats = 1
62
+
63
+ if (core._def.tag === 'oneOrMore') {
64
+ const min = core._def.min
65
+ core = core._def.parser
66
+ minRepeats = min
67
+ } else if (core._def.tag === 'many') {
68
+ core = core._def.parser
69
+ minRepeats = 0
70
+ }
71
+
72
+ const arms: LabeledTriviaSpec['arms'][number][] = []
73
+
74
+ if (core._def.tag === 'choice') {
75
+ for (let i = 0; i < core._def.parsers.length; i++) {
76
+ const peeled = peelLabel(core._def.parsers[i]!)
77
+ if (!peeled) {
78
+ return cacheLabeledTrivia(trivia, null)
79
+ }
80
+ arms.push({ label: peeled.label, kindIndex: i, parser: peeled.parser })
81
+ }
82
+ } else {
83
+ const peeled = peelLabel(core)
84
+ if (!peeled) {
85
+ return cacheLabeledTrivia(trivia, null)
86
+ }
87
+ arms.push({ label: peeled.label, kindIndex: 0, parser: peeled.parser })
88
+ }
89
+
90
+ const spec = {
91
+ labels: arms.map(a => a.label),
92
+ arms,
93
+ minRepeats,
94
+ }
95
+ return cacheLabeledTrivia(trivia, spec)
96
+ }
97
+
98
+ /** Label table on a `trivia()` combinator, if all arms are labeled. */
99
+ export function triviaKindLabels(trivia: Combinator<unknown> | undefined): readonly string[] | undefined {
100
+ if (!trivia) return undefined
101
+ const fromMeta = trivia._meta.triviaKindLabels
102
+ if (fromMeta) return fromMeta
103
+ return analyzeLabeledTrivia(trivia)?.labels
104
+ }
105
+
106
+ function matchArmAt(
107
+ input: string,
108
+ pos: number,
109
+ arm: Combinator<unknown>,
110
+ state: unknown,
111
+ ): { end: number } | null {
112
+ // The first-set check is structural Parseman metadata. It neither assigns
113
+ // meaning to a label nor recognizes a particular comment form; it avoids
114
+ // entering arms whose own grammar proves they cannot start at this offset.
115
+ if (!startsFirstSet(arm, input, pos)) return null
116
+ const r = arm.parse(input, pos, createDetachedParseContext(false, state))
117
+ if (!r.ok || r.span.end <= pos) return null
118
+ return { end: r.span.end }
119
+ }
120
+
121
+ /**
122
+ * Scan maximal labeled trivia chunks (PEG `oneOrMore(choice(…))` semantics).
123
+ * Each successful arm match becomes one entry with that arm's kind index.
124
+ */
125
+ export function scanLabeledTriviaChunks(
126
+ input: string,
127
+ cur: number,
128
+ spec: LabeledTriviaSpec,
129
+ state?: unknown,
130
+ ): { end: number; chunks: TriviaChunk[] } {
131
+ const chunks: TriviaChunk[] = []
132
+ let pos = cur
133
+
134
+ while (pos < input.length) {
135
+ let matched: { end: number; kindIndex: number } | null = null
136
+ for (const arm of spec.arms) {
137
+ const m = matchArmAt(input, pos, arm.parser, state)
138
+ if (m) {
139
+ matched = { end: m.end, kindIndex: arm.kindIndex }
140
+ break
141
+ }
142
+ }
143
+ if (!matched) break
144
+ chunks.push({ start: pos, end: matched.end, kindIndex: matched.kindIndex })
145
+ pos = matched.end
146
+ }
147
+
148
+ if (chunks.length < spec.minRepeats) {
149
+ return { end: cur, chunks: [] }
150
+ }
151
+ return { end: pos, chunks }
152
+ }
153
+
154
+ /**
155
+ * Scan labeled trivia when no caller will retain individual chunks. This is the
156
+ * ordinary skip path for a classified grammar: labels remain available for an
157
+ * explicit capture request, but merely having labels must not allocate one
158
+ * `{ start, end, kindIndex }` object per whitespace/comment run.
159
+ */
160
+ export function scanLabeledTriviaEnd(
161
+ input: string,
162
+ cur: number,
163
+ spec: LabeledTriviaSpec,
164
+ state?: unknown,
165
+ ): number {
166
+ // `charArmsFor` only lowers `minRepeats <= 1`, where "no chunk matched" and
167
+ // "the minimum was not met" are the same position — so an end IS the answer.
168
+ const chars = charArmsFor(spec)
169
+ if (chars !== null) return charTriviaEnd(input, cur, chars)
170
+
171
+ let pos = cur
172
+ let count = 0
173
+
174
+ while (pos < input.length) {
175
+ let matched: { end: number } | null = null
176
+ for (const arm of spec.arms) {
177
+ matched = matchArmAt(input, pos, arm.parser, state)
178
+ if (matched) break
179
+ }
180
+ if (!matched) break
181
+ pos = matched.end
182
+ count++
183
+ }
184
+
185
+ return count < spec.minRepeats ? cur : pos
186
+ }
187
+
188
+ /**
189
+ * Visit classified trivia matches without constructing per-match objects. The
190
+ * visitor is only invoked after one or more arms matched, which is the shape
191
+ * used by `classifiedTrivia()`; callers with a stronger repeat minimum keep
192
+ * the buffered scanner so an unsuccessful minimum cannot leak a partial run.
193
+ */
194
+ export function visitLabeledTrivia(
195
+ input: string,
196
+ cur: number,
197
+ spec: LabeledTriviaSpec,
198
+ state: unknown | undefined,
199
+ visit: (start: number, end: number, kindIndex: number) => void,
200
+ ): number | undefined {
201
+ if (spec.minRepeats > 1) return undefined
202
+ const chars = charArmsFor(spec)
203
+ if (chars !== null) return charTriviaVisit(input, cur, chars, visit)
204
+
205
+ let pos = cur
206
+ let count = 0
207
+
208
+ while (pos < input.length) {
209
+ let matched: { end: number; kindIndex: number } | null = null
210
+ for (const arm of spec.arms) {
211
+ const match = matchArmAt(input, pos, arm.parser, state)
212
+ if (match) {
213
+ matched = { end: match.end, kindIndex: arm.kindIndex }
214
+ break
215
+ }
216
+ }
217
+ if (!matched) break
218
+ visit(pos, matched.end, matched.kindIndex)
219
+ pos = matched.end
220
+ count++
221
+ }
222
+
223
+ return count < spec.minRepeats ? cur : pos
224
+ }
225
+
226
+ export function recordTriviaChunks(ctx: ParseContext, chunks: readonly TriviaChunk[]): void {
227
+ const kinds = ctx.triviaKindLabels
228
+ const mask = ctx._triviaCaptureMask
229
+ const rootLog = ctx._rootTriviaLog
230
+ const rootKinds = ctx._rootTriviaKindIndex
231
+ const rootMark = rootLog?.length ?? 0
232
+ for (const ch of chunks) {
233
+ // Global trivia log: always complete (never kind-filtered).
234
+ pushTriviaLogEntry(ctx, ch.start, ch.end, kinds ? ch.kindIndex : undefined)
235
+ const rootKindIndex = ctx._rootTriviaCapture === false || rootLog === undefined || rootKinds === undefined || kinds === undefined
236
+ ? -1
237
+ : (rootKinds[kinds[ch.kindIndex] ?? ''] ?? -1)
238
+ if (rootKindIndex >= 0) {
239
+ // Fill the enclosing committed gap after the scanner has consumed every
240
+ // chunk. A selected marker therefore carries its exact authored context
241
+ // without recording any whitespace-only chunk.
242
+ rootLog!.push(0, 0, ch.start, ch.end, rootKindIndex)
243
+ }
244
+ // Per-node CST log: honour the kind mask when both a mask and labels are
245
+ // present, so a host can capture (e.g.) comments only without logging every
246
+ // whitespace run. No mask / no labels → capture everything, as before.
247
+ if (ctx.captureTrivia && (ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined)) {
248
+ if (mask === undefined || kinds === undefined || (mask & (1 << ch.kindIndex)) !== 0) {
249
+ pushCstTriviaEntry(ctx, ch.start, ch.end, kinds ? ch.kindIndex : undefined)
250
+ }
251
+ }
252
+ }
253
+ if (rootLog !== undefined && rootLog.length !== rootMark && chunks.length > 0) {
254
+ const start = chunks[0]!.start
255
+ const end = chunks[chunks.length - 1]!.end
256
+ for (let offset = rootMark; offset < rootLog.length; offset += 5) {
257
+ rootLog[offset] = start
258
+ rootLog[offset + 1] = end
259
+ }
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Resolve a per-node trivia capture mask (`ctx._triviaCaptureMask`) from a trivia
265
+ * label table and the kind names a host wants to keep. Returns `undefined` when
266
+ * `labels` is absent (nothing to key on → capture everything).
267
+ *
268
+ * Names in `keep` that aren't in `labels` are **ignored by design**: `keep` is
269
+ * often a dialect-independent list (e.g. `['blockComment', 'lineComment']`) applied
270
+ * to a grammar whose trivia only defines some of them (CSS has no `lineComment`) —
271
+ * so an unknown name is a normal cross-dialect no-op, not necessarily a typo, and
272
+ * is not worth a warning that would cry wolf on legitimate superset lists. An empty
273
+ * `keep` (or one where nothing matches) yields `0`, which means "capture NO trivia
274
+ * into per-node logs" — an intentional, valid state (the global `_triviaLog` is
275
+ * unaffected either way). If you expect comments and get an empty per-node log,
276
+ * check the name against the grammar's `triviaKindLabels`.
277
+ */
278
+ /**
279
+ * Resolve an ADJACENCY kind filter against the ACTIVE trivia table — the strict
280
+ * counterpart of `triviaKindMask` below.
281
+ *
282
+ * Deliberately NOT lenient. `triviaKindMask` is a capture preference, where an
283
+ * unknown name is a legitimate cross-dialect no-op; this is a RECOGNITION
284
+ * constraint, and a silently-dropped name turns `notAdjacent({kinds:['whitespace']})`
285
+ * into a bare `notAdjacent()` — i.e. `calc()` quietly accepting a comment where
286
+ * css-values-4 10.1 demands real whitespace, the exact defect the filter exists to
287
+ * prevent. Both failure modes are hard errors.
288
+ */
289
+ export function resolveAdjacencyKindMask(
290
+ trivia: Combinator<unknown> | undefined,
291
+ kinds: readonly string[],
292
+ ): number {
293
+ const spec = trivia ? analyzeLabeledTrivia(trivia) : null
294
+ if (!spec) {
295
+ throw new TypeError(
296
+ `notAdjacent({ kinds: [${kinds.map(k => JSON.stringify(k)).join(', ')}] }) requires classified trivia: `
297
+ + 'the active trivia table has no category labels. Build it with classifiedTrivia({ whitespace: …, comment: … }).',
298
+ )
299
+ }
300
+ let mask = 0
301
+ for (const name of kinds) {
302
+ const idx = spec.labels.indexOf(name)
303
+ if (idx < 0) {
304
+ throw new TypeError(
305
+ `notAdjacent(): unknown trivia kind ${JSON.stringify(name)}. `
306
+ + `The active trivia table declares: ${spec.labels.map(l => JSON.stringify(l)).join(', ')}.`,
307
+ )
308
+ }
309
+ mask |= 1 << idx
310
+ }
311
+ return mask
312
+ }
313
+
314
+ /**
315
+ * Bitmask of the trivia CATEGORIES occurring in the run starting at `pos`
316
+ * (`1 << kindIndex` per matched chunk); `0` when no trivia is there at all.
317
+ * The single source of truth for the interpreter's adjacency kind test — the
318
+ * compiled probe (`_akN`) mirrors this loop arm for arm.
319
+ */
320
+ export function triviaKindMaskAt(
321
+ input: string,
322
+ pos: number,
323
+ spec: LabeledTriviaSpec,
324
+ state?: unknown,
325
+ ): number {
326
+ const chars = charArmsFor(spec)
327
+ if (chars !== null) return charTriviaKindMask(input, pos, chars)
328
+
329
+ let cur = pos
330
+ let mask = 0
331
+ while (cur < input.length) {
332
+ let matched: { end: number; kindIndex: number } | null = null
333
+ for (const arm of spec.arms) {
334
+ const m = matchArmAt(input, cur, arm.parser, state)
335
+ if (m) {
336
+ matched = { end: m.end, kindIndex: arm.kindIndex }
337
+ break
338
+ }
339
+ }
340
+ if (!matched) break
341
+ mask |= 1 << matched.kindIndex
342
+ cur = matched.end
343
+ }
344
+ return mask
345
+ }
346
+
347
+ export function triviaKindMask(
348
+ labels: readonly string[] | undefined,
349
+ keep: readonly string[],
350
+ ): number | undefined {
351
+ if (!labels) return undefined
352
+ let mask = 0
353
+ for (const name of keep) {
354
+ const idx = labels.indexOf(name)
355
+ if (idx >= 0) mask |= 1 << idx
356
+ }
357
+ return mask
358
+ }
@@ -0,0 +1,76 @@
1
+ import type { Span } from '../types.ts'
2
+
3
+ /**
4
+ * Minimal interface a node must satisfy for incremental re-parse to navigate and
5
+ * re-parse the tree. Any object with `_tag: 'node'`, `type`, `span`, `state`,
6
+ * and `children` works — whether from `node()` build callbacks or custom ASTs.
7
+ *
8
+ * `children` only needs items with `_tag` so traversal can distinguish sub-nodes
9
+ * from leaves. `parseDoc` uses an optional `rebuild` callback (or a
10
+ * shallow spread) when grafting a re-parsed subtree.
11
+ */
12
+ export type NodeLike = {
13
+ readonly _tag: 'node'
14
+ readonly type: string
15
+ readonly span: Span
16
+ readonly state: unknown
17
+ readonly children: ReadonlyArray<{ readonly _tag: string }>
18
+ }
19
+
20
+ /**
21
+ * A named CST node from a `node()` rule (or equivalent build callback).
22
+ * Children are in parse order: sub-nodes interspersed with CSTLeaf terminals.
23
+ */
24
+ export type CSTNode = {
25
+ readonly _tag: 'node'
26
+ /** Rule name — the method name that produced this node. */
27
+ readonly type: string
28
+ /** Optional grammar categories materialized by `cstBuildHost({ tags: true })`. */
29
+ readonly tags?: readonly string[]
30
+ readonly span: Span
31
+ readonly children: CSTChild[]
32
+ /**
33
+ * Shallow clone of ctx.state at the moment this node's parse began.
34
+ * Used as the re-entry point for incremental re-parsing.
35
+ * Only meaningful when ctx.state is primitives-only (no mutable objects).
36
+ */
37
+ readonly state: unknown
38
+ }
39
+
40
+ /**
41
+ * A terminal token — the result of a literal() or regex() match inside a node rule.
42
+ */
43
+ export type CSTLeaf = {
44
+ readonly _tag: 'leaf'
45
+ readonly value: string
46
+ readonly span: Span
47
+ }
48
+
49
+ /**
50
+ * A recovered parse error embedded in the tree at a recovery point (tolerant
51
+ * parsing). Spans exactly the skipped text; `expected` is what the grammar wanted
52
+ * there. Identical shape to the runtime `ParseError`, so recovery emits ONE value
53
+ * into both the CST `children` and the flat `ctx._errors` channel. A tree walk
54
+ * finds every diagnostic, and — because it rides in the tree — an incremental
55
+ * reuse keeps it attached to the untouched subtree it belongs to.
56
+ */
57
+ export type CSTError = {
58
+ readonly _tag: 'parseError'
59
+ readonly span: Span
60
+ readonly expected: string[]
61
+ }
62
+
63
+ export type CSTChild = CSTNode | CSTLeaf | CSTError
64
+
65
+ /**
66
+ * A trivia token — whitespace or comment consumed between terms during parsing.
67
+ * Only present in `rawChildren`; never in the structural `children` array.
68
+ */
69
+ export type CSTTrivia = {
70
+ readonly _tag: 'trivia'
71
+ readonly value: string
72
+ readonly span: Span
73
+ }
74
+
75
+ /** Full child union including trivia — used in the `rawChildren` arg of `buildNode`. */
76
+ export type CSTRawChild = CSTNode | CSTLeaf | CSTTrivia | CSTError
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Grammar-aware CST traversal. `createVisitor(grammar, handlers)` consumes the
3
+ * reflection stamped on interpreted `rules()` maps and macro/compiled grammar
4
+ * maps, precomputes tag dispatch once, and returns a reusable `visit(root)` fn.
5
+ */
6
+ import type { CSTChild, CSTNode } from './types.ts'
7
+ import type { Combinator } from '../types.ts'
8
+ import {
9
+ collectGrammarReflection,
10
+ grammarReflectionOf,
11
+ type GrammarWithReflection,
12
+ } from './reflection.ts'
13
+
14
+ export type Walkable = {
15
+ readonly _tag: string
16
+ readonly type?: string
17
+ readonly children?: ReadonlyArray<Walkable>
18
+ }
19
+
20
+ type GrammarNodeType<G> =
21
+ G extends GrammarWithReflection<infer NodeType, string>
22
+ ? NodeType
23
+ : G extends Record<string, unknown>
24
+ ? Extract<keyof G, string>
25
+ : string
26
+
27
+ type GrammarNodeTag<G> =
28
+ G extends GrammarWithReflection<string, infer NodeTag> ? NodeTag : never
29
+
30
+ type NodeForType<N extends Walkable, Type extends string> =
31
+ Extract<N, { readonly _tag: 'node' }> extends never
32
+ ? CSTNode & { readonly type: Type }
33
+ : Extract<N, { readonly _tag: 'node' }> & { readonly type: Type }
34
+
35
+ export type VisitorHandler<N extends Walkable, Root extends Walkable = CSTChild, C = undefined> =
36
+ (node: N, parent: Root | null, ctx: C) => void
37
+
38
+ export type VisitorSpec<G, N extends Walkable = CSTChild, C = undefined> = {
39
+ enter?(node: N, parent: N | null, ctx: C): boolean | void
40
+ leave?(node: N, parent: N | null, ctx: C): void
41
+ type?: {
42
+ [K in GrammarNodeType<G>]?: VisitorHandler<NodeForType<N, K>, N, C>
43
+ }
44
+ tag?: {
45
+ [K in GrammarNodeTag<G>]?: VisitorHandler<NodeForType<N, GrammarNodeType<G>>, N, C>
46
+ }
47
+ }
48
+
49
+ function isCombinator(value: unknown): value is Combinator<unknown> {
50
+ return !!value && typeof value === 'object'
51
+ && '_def' in value
52
+ && '_meta' in value
53
+ && typeof (value as { parse?: unknown }).parse === 'function'
54
+ }
55
+
56
+ function reflectionFor(grammar: object) {
57
+ const stamped = grammarReflectionOf(grammar)
58
+ if (stamped) return stamped
59
+ const entries = Object.entries(grammar).filter((entry): entry is [string, Combinator<unknown>] => isCombinator(entry[1]))
60
+ return collectGrammarReflection(entries)
61
+ }
62
+
63
+ export function createVisitor<G extends object, N extends Walkable = CSTChild, C = undefined>(
64
+ grammar: G,
65
+ spec: VisitorSpec<G, N, C>,
66
+ ): (root: N, ctx?: C) => void {
67
+ const reflection = reflectionFor(grammar)
68
+ const typeHandlers = spec.type as Record<string, VisitorHandler<N, N, C> | undefined> | undefined
69
+ const tagSpec = spec.tag as Record<string, VisitorHandler<N, N, C> | undefined> | undefined
70
+ const tagHandlersByType = new Map<string, Array<VisitorHandler<N, N, C>>>()
71
+
72
+ if (tagSpec !== undefined) {
73
+ for (const node of reflection.nodes) {
74
+ const handlers: Array<VisitorHandler<N, N, C>> = []
75
+ for (const tag of node.tags) {
76
+ const handler = tagSpec[tag]
77
+ if (handler) handlers.push(handler)
78
+ }
79
+ if (handlers.length > 0) tagHandlersByType.set(node.type, handlers)
80
+ }
81
+ }
82
+
83
+ return (root: N, ctx: C = undefined as C): void => {
84
+ const go = (node: N, parent: N | null): void => {
85
+ const descend = spec.enter ? spec.enter(node, parent, ctx) : undefined
86
+ if (node._tag === 'node' && node.type !== undefined) {
87
+ typeHandlers?.[node.type]?.(node, parent, ctx)
88
+ const tagHandlers = tagHandlersByType.get(node.type)
89
+ if (tagHandlers !== undefined) {
90
+ for (const handler of tagHandlers) handler(node, parent, ctx)
91
+ }
92
+ }
93
+ const children = node.children as ReadonlyArray<N> | undefined
94
+ if (descend !== false && Array.isArray(children)) {
95
+ for (const child of children) go(child, node)
96
+ }
97
+ spec.leave?.(node, parent, ctx)
98
+ }
99
+ go(root, null)
100
+ }
101
+ }