parseman 0.45.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 (333) hide show
  1. package/README.md +8 -3
  2. package/dist/analysis/choice-cost-gate.d.ts +158 -0
  3. package/dist/analysis/choice-cost-gate.d.ts.map +1 -0
  4. package/dist/analysis/choice-cost-render.d.ts +96 -0
  5. package/dist/analysis/choice-cost-render.d.ts.map +1 -0
  6. package/dist/analysis/choice-cost.d.ts +383 -0
  7. package/dist/analysis/choice-cost.d.ts.map +1 -0
  8. package/dist/analysis/commitment.d.ts +131 -0
  9. package/dist/analysis/commitment.d.ts.map +1 -0
  10. package/dist/analysis/corpus.d.ts +73 -0
  11. package/dist/analysis/corpus.d.ts.map +1 -0
  12. package/dist/analysis/diagnose-render.d.ts +65 -0
  13. package/dist/analysis/diagnose-render.d.ts.map +1 -0
  14. package/dist/analysis/diagnose.d.ts +15 -0
  15. package/dist/analysis/diagnose.d.ts.map +1 -1
  16. package/dist/analysis/diagnostics.cjs +13736 -0
  17. package/dist/analysis/diagnostics.cjs.map +6 -0
  18. package/dist/analysis/diagnostics.d.ts +27 -0
  19. package/dist/analysis/diagnostics.d.ts.map +1 -0
  20. package/dist/analysis/diagnostics.js +13713 -0
  21. package/dist/analysis/diagnostics.js.map +6 -0
  22. package/dist/analysis/duplication.d.ts.map +1 -1
  23. package/dist/analysis/fix-render.d.ts +43 -0
  24. package/dist/analysis/fix-render.d.ts.map +1 -0
  25. package/dist/analysis/fix.d.ts +172 -0
  26. package/dist/analysis/fix.d.ts.map +1 -0
  27. package/dist/analysis/format-number.d.ts +23 -0
  28. package/dist/analysis/format-number.d.ts.map +1 -0
  29. package/dist/analysis/gating.d.ts +47 -2
  30. package/dist/analysis/gating.d.ts.map +1 -1
  31. package/dist/analysis/rebuild.d.ts +61 -0
  32. package/dist/analysis/rebuild.d.ts.map +1 -0
  33. package/dist/analysis/terminal.d.ts +145 -0
  34. package/dist/analysis/terminal.d.ts.map +1 -0
  35. package/dist/cli/index.cjs +14837 -0
  36. package/dist/cli/index.cjs.map +6 -0
  37. package/dist/cli/index.d.ts +3 -0
  38. package/dist/cli/index.d.ts.map +1 -0
  39. package/dist/cli/index.js +14814 -0
  40. package/dist/cli/index.js.map +6 -0
  41. package/dist/combinators/adjacency.d.ts +100 -0
  42. package/dist/combinators/adjacency.d.ts.map +1 -0
  43. package/dist/combinators/choice.d.ts.map +1 -1
  44. package/dist/combinators/completions.d.ts.map +1 -1
  45. package/dist/combinators/dispatch.d.ts +8 -1
  46. package/dist/combinators/dispatch.d.ts.map +1 -1
  47. package/dist/combinators/expect.d.ts.map +1 -1
  48. package/dist/combinators/first-set.d.ts.map +1 -1
  49. package/dist/combinators/gate.d.ts.map +1 -1
  50. package/dist/combinators/grammar.d.ts +22 -0
  51. package/dist/combinators/grammar.d.ts.map +1 -1
  52. package/dist/combinators/literal.d.ts +8 -0
  53. package/dist/combinators/literal.d.ts.map +1 -1
  54. package/dist/combinators/map.d.ts +0 -1
  55. package/dist/combinators/map.d.ts.map +1 -1
  56. package/dist/combinators/node.d.ts +13 -3
  57. package/dist/combinators/node.d.ts.map +1 -1
  58. package/dist/combinators/parser.d.ts +1 -1
  59. package/dist/combinators/parser.d.ts.map +1 -1
  60. package/dist/combinators/ref.d.ts.map +1 -1
  61. package/dist/combinators/regex.d.ts +1 -16
  62. package/dist/combinators/regex.d.ts.map +1 -1
  63. package/dist/combinators/repeat.d.ts +30 -2
  64. package/dist/combinators/repeat.d.ts.map +1 -1
  65. package/dist/combinators/scanTo.d.ts +71 -1
  66. package/dist/combinators/scanTo.d.ts.map +1 -1
  67. package/dist/combinators/sequence.d.ts.map +1 -1
  68. package/dist/combinators/token.d.ts.map +1 -1
  69. package/dist/combinators/trivia-boundary.d.ts +26 -0
  70. package/dist/combinators/trivia-boundary.d.ts.map +1 -0
  71. package/dist/combinators/trivia-skip.d.ts +40 -1
  72. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  73. package/dist/combinators/withCtx.d.ts.map +1 -1
  74. package/dist/compiler/build-arity.d.ts.map +1 -1
  75. package/dist/compiler/compile-linkable-table.d.ts +104 -0
  76. package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
  77. package/dist/compiler/degradation.d.ts +0 -2
  78. package/dist/compiler/degradation.d.ts.map +1 -1
  79. package/dist/compiler/fields.d.ts +13 -0
  80. package/dist/compiler/fields.d.ts.map +1 -1
  81. package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
  82. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  83. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  84. package/dist/compiler/linker.d.ts +79 -47
  85. package/dist/compiler/linker.d.ts.map +1 -1
  86. package/dist/compiler/token-alphabet.d.ts +81 -0
  87. package/dist/compiler/token-alphabet.d.ts.map +1 -0
  88. package/dist/compiler/token-dispatch.d.ts +95 -0
  89. package/dist/compiler/token-dispatch.d.ts.map +1 -0
  90. package/dist/compiler/token-scanner.d.ts +53 -0
  91. package/dist/compiler/token-scanner.d.ts.map +1 -0
  92. package/dist/compiler/value-usage.d.ts.map +1 -1
  93. package/dist/coverage.d.ts +1 -1
  94. package/dist/coverage.d.ts.map +1 -1
  95. package/dist/cst/capture-buffer.d.ts +35 -0
  96. package/dist/cst/capture-buffer.d.ts.map +1 -1
  97. package/dist/cst/reflection-attach.d.ts +8 -0
  98. package/dist/cst/reflection-attach.d.ts.map +1 -0
  99. package/dist/cst/reflection-symbols.d.ts +10 -0
  100. package/dist/cst/reflection-symbols.d.ts.map +1 -0
  101. package/dist/cst/reflection.d.ts +2 -4
  102. package/dist/cst/reflection.d.ts.map +1 -1
  103. package/dist/cst/root-trivia-scope.d.ts +26 -0
  104. package/dist/cst/root-trivia-scope.d.ts.map +1 -0
  105. package/dist/cst/trivia-charscan.d.ts +18 -0
  106. package/dist/cst/trivia-charscan.d.ts.map +1 -0
  107. package/dist/cst/trivia-kinds.d.ts +19 -0
  108. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  109. package/dist/functional/doc.d.ts.map +1 -1
  110. package/dist/functional/run-tabled.d.ts +12 -0
  111. package/dist/functional/run-tabled.d.ts.map +1 -0
  112. package/dist/functional/run.d.ts +25 -42
  113. package/dist/functional/run.d.ts.map +1 -1
  114. package/dist/grammar-metadata.d.ts +3 -0
  115. package/dist/grammar-metadata.d.ts.map +1 -0
  116. package/dist/index.cjs +10499 -7478
  117. package/dist/index.cjs.map +3 -4
  118. package/dist/index.d.ts +24 -9
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +10499 -7478
  121. package/dist/index.js.map +3 -4
  122. package/dist/language-service/index.cjs +311 -121
  123. package/dist/language-service/index.cjs.map +3 -4
  124. package/dist/language-service/index.js +311 -121
  125. package/dist/language-service/index.js.map +3 -4
  126. package/dist/oracle/index.cjs.map +0 -1
  127. package/dist/oracle/index.js.map +0 -1
  128. package/dist/parse-context.d.ts +49 -0
  129. package/dist/parse-context.d.ts.map +1 -0
  130. package/dist/plugin/evaluator.d.ts +41 -2
  131. package/dist/plugin/evaluator.d.ts.map +1 -1
  132. package/dist/plugin/free-identifiers.d.ts +23 -0
  133. package/dist/plugin/free-identifiers.d.ts.map +1 -0
  134. package/dist/plugin/index.cjs +11381 -9442
  135. package/dist/plugin/index.cjs.map +3 -4
  136. package/dist/plugin/index.d.ts.map +1 -1
  137. package/dist/plugin/index.js +11381 -9442
  138. package/dist/plugin/index.js.map +3 -4
  139. package/dist/plugin/reducer-resolver.d.ts +17 -1
  140. package/dist/plugin/reducer-resolver.d.ts.map +1 -1
  141. package/dist/regex/classes.d.ts +28 -0
  142. package/dist/regex/classes.d.ts.map +1 -1
  143. package/dist/regex/first-set.d.ts +14 -0
  144. package/dist/regex/first-set.d.ts.map +1 -1
  145. package/dist/run/index.cjs +136 -51
  146. package/dist/run/index.cjs.map +2 -3
  147. package/dist/run/index.js +136 -51
  148. package/dist/run/index.js.map +2 -3
  149. package/dist/spec/index.cjs +2898 -1622
  150. package/dist/spec/index.cjs.map +3 -4
  151. package/dist/spec/index.js +2898 -1622
  152. package/dist/spec/index.js.map +3 -4
  153. package/dist/spec/model.d.ts +8 -0
  154. package/dist/spec/model.d.ts.map +1 -1
  155. package/dist/table/assemble.d.ts +307 -0
  156. package/dist/table/assemble.d.ts.map +1 -0
  157. package/dist/table/cell.d.ts +42 -0
  158. package/dist/table/cell.d.ts.map +1 -0
  159. package/dist/table/child-slots.d.ts +30 -0
  160. package/dist/table/child-slots.d.ts.map +1 -0
  161. package/dist/table/compile-rule-map.d.ts +136 -0
  162. package/dist/table/compile-rule-map.d.ts.map +1 -0
  163. package/dist/table/compile.d.ts +56 -0
  164. package/dist/table/compile.d.ts.map +1 -0
  165. package/dist/table/duplication-hook.d.ts +32 -0
  166. package/dist/table/duplication-hook.d.ts.map +1 -0
  167. package/dist/table/emit-assembly.d.ts +139 -0
  168. package/dist/table/emit-assembly.d.ts.map +1 -0
  169. package/dist/table/emit.d.ts +113 -0
  170. package/dist/table/emit.d.ts.map +1 -0
  171. package/dist/table/encode-baseline.d.ts +16 -0
  172. package/dist/table/encode-baseline.d.ts.map +1 -0
  173. package/dist/table/encode.d.ts +71 -0
  174. package/dist/table/encode.d.ts.map +1 -0
  175. package/dist/table/exec-baseline.d.ts +9 -0
  176. package/dist/table/exec-baseline.d.ts.map +1 -0
  177. package/dist/table/exec.d.ts +72 -0
  178. package/dist/table/exec.d.ts.map +1 -0
  179. package/dist/table/fold.d.ts +25 -0
  180. package/dist/table/fold.d.ts.map +1 -0
  181. package/dist/table/index.cjs +11375 -0
  182. package/dist/table/index.cjs.map +6 -0
  183. package/dist/table/index.d.ts +100 -0
  184. package/dist/table/index.d.ts.map +1 -0
  185. package/dist/table/index.js +11352 -0
  186. package/dist/table/index.js.map +6 -0
  187. package/dist/table/inspect.d.ts +17 -0
  188. package/dist/table/inspect.d.ts.map +1 -0
  189. package/dist/table/ops.d.ts +446 -0
  190. package/dist/table/ops.d.ts.map +1 -0
  191. package/dist/table/program.d.ts +493 -0
  192. package/dist/table/program.d.ts.map +1 -0
  193. package/dist/table/run-support.d.ts +38 -0
  194. package/dist/table/run-support.d.ts.map +1 -0
  195. package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
  196. package/dist/table/scan-shapes.d.ts.map +1 -0
  197. package/dist/table/site-labels.d.ts +58 -0
  198. package/dist/table/site-labels.d.ts.map +1 -0
  199. package/dist/table/stamp.d.ts +70 -0
  200. package/dist/table/stamp.d.ts.map +1 -0
  201. package/dist/types.d.ts +80 -24
  202. package/dist/types.d.ts.map +1 -1
  203. package/dist/version.d.ts +1 -1
  204. package/package.json +41 -3
  205. package/src/analysis/choice-cost-gate.ts +359 -0
  206. package/src/analysis/choice-cost-render.ts +293 -0
  207. package/src/analysis/choice-cost.ts +1008 -0
  208. package/src/analysis/commitment.ts +557 -0
  209. package/src/analysis/corpus.ts +130 -0
  210. package/src/analysis/diagnose-render.ts +625 -0
  211. package/src/analysis/diagnose.ts +317 -0
  212. package/src/analysis/diagnostics.ts +33 -0
  213. package/src/analysis/duplication.ts +1955 -0
  214. package/src/analysis/fix-render.ts +217 -0
  215. package/src/analysis/fix.ts +669 -0
  216. package/src/analysis/format-number.ts +31 -0
  217. package/src/analysis/gating.ts +792 -0
  218. package/src/analysis/grammar.ts +72 -0
  219. package/src/analysis/rebuild.ts +287 -0
  220. package/src/analysis/terminal.ts +336 -0
  221. package/src/cli/index.ts +413 -0
  222. package/src/combinators/adjacency.ts +171 -0
  223. package/src/combinators/attempt.ts +41 -0
  224. package/src/combinators/case-fold.ts +90 -0
  225. package/src/combinators/choice.ts +466 -0
  226. package/src/combinators/completions.ts +64 -0
  227. package/src/combinators/dispatch.ts +358 -0
  228. package/src/combinators/expect.ts +159 -0
  229. package/src/combinators/first-set.ts +322 -0
  230. package/src/combinators/gate.ts +50 -0
  231. package/src/combinators/grammar.ts +272 -0
  232. package/src/combinators/keywords.ts +145 -0
  233. package/src/combinators/literal.ts +102 -0
  234. package/src/combinators/map.ts +106 -0
  235. package/src/combinators/node.ts +345 -0
  236. package/src/combinators/not.ts +55 -0
  237. package/src/combinators/parser.ts +266 -0
  238. package/src/combinators/peek.ts +63 -0
  239. package/src/combinators/probe.ts +25 -0
  240. package/src/combinators/ref.ts +109 -0
  241. package/src/combinators/regex.ts +170 -0
  242. package/src/combinators/repeat.ts +561 -0
  243. package/src/combinators/scanTo.ts +393 -0
  244. package/src/combinators/sequence.ts +221 -0
  245. package/src/combinators/token.ts +120 -0
  246. package/src/combinators/trivia-boundary.ts +48 -0
  247. package/src/combinators/trivia-skip.ts +821 -0
  248. package/src/combinators/withCtx.ts +45 -0
  249. package/src/compiler/build-arity.ts +330 -0
  250. package/src/compiler/compile-linkable-table.ts +209 -0
  251. package/src/compiler/degradation.ts +266 -0
  252. package/src/compiler/fields.ts +191 -0
  253. package/src/compiler/grammar-coverage-ids.ts +129 -0
  254. package/src/compiler/ir-serialize.ts +615 -0
  255. package/src/compiler/linker.ts +954 -0
  256. package/src/compiler/token-alphabet.ts +209 -0
  257. package/src/compiler/token-dispatch.ts +353 -0
  258. package/src/compiler/token-scanner.ts +318 -0
  259. package/src/compiler/value-usage.ts +147 -0
  260. package/src/coverage.ts +383 -0
  261. package/src/cst/capture-buffer.ts +297 -0
  262. package/src/cst/host-mode.ts +69 -0
  263. package/src/cst/reflection-attach.ts +11 -0
  264. package/src/cst/reflection-symbols.ts +9 -0
  265. package/src/cst/reflection.ts +149 -0
  266. package/src/cst/relative-spans.ts +207 -0
  267. package/src/cst/root-trivia-scope.ts +30 -0
  268. package/src/cst/trivia-charscan.ts +312 -0
  269. package/src/cst/trivia-entries.ts +285 -0
  270. package/src/cst/trivia-index.ts +131 -0
  271. package/src/cst/trivia-kinds.ts +358 -0
  272. package/src/cst/types.ts +76 -0
  273. package/src/cst/walk.ts +101 -0
  274. package/src/functional/doc.ts +959 -0
  275. package/src/functional/run-tabled.ts +153 -0
  276. package/src/functional/run.ts +558 -0
  277. package/src/grammar-metadata.ts +2 -0
  278. package/src/index.ts +189 -0
  279. package/src/language-service/index.ts +221 -0
  280. package/src/line-index.ts +131 -0
  281. package/src/oracle/digest.ts +620 -0
  282. package/src/oracle/index.ts +55 -0
  283. package/src/parse-context.ts +91 -0
  284. package/src/plugin/direct-builder-static.ts +102 -0
  285. package/src/plugin/evaluator.ts +1505 -0
  286. package/src/plugin/free-identifiers.ts +338 -0
  287. package/src/plugin/index.ts +2281 -0
  288. package/src/plugin/reducer-resolver.ts +606 -0
  289. package/src/recovery/scan.ts +155 -0
  290. package/src/regex/classes.ts +183 -0
  291. package/src/regex/first-set.ts +316 -0
  292. package/src/run/index.ts +31 -0
  293. package/src/spec/ebnf.ts +128 -0
  294. package/src/spec/index.ts +54 -0
  295. package/src/spec/model.ts +636 -0
  296. package/src/spec/railroad-lib.ts +559 -0
  297. package/src/spec/railroad.ts +256 -0
  298. package/src/table/assemble.ts +2994 -0
  299. package/src/table/cell.ts +42 -0
  300. package/src/table/child-slots.ts +113 -0
  301. package/src/table/compile-rule-map.ts +336 -0
  302. package/src/table/compile.ts +244 -0
  303. package/src/table/duplication-hook.ts +89 -0
  304. package/src/table/emit-assembly.ts +1766 -0
  305. package/src/table/emit.ts +418 -0
  306. package/src/table/encode-baseline.ts +340 -0
  307. package/src/table/encode.ts +1569 -0
  308. package/src/table/exec-baseline.ts +537 -0
  309. package/src/table/exec.ts +1672 -0
  310. package/src/table/fold.ts +38 -0
  311. package/src/table/index.ts +108 -0
  312. package/src/table/inspect.ts +52 -0
  313. package/src/table/ops.ts +459 -0
  314. package/src/table/program.ts +855 -0
  315. package/src/table/run-support.ts +66 -0
  316. package/src/table/scan-shapes.ts +1578 -0
  317. package/src/table/site-labels.ts +228 -0
  318. package/src/table/stamp.ts +229 -0
  319. package/src/types.ts +563 -0
  320. package/src/version.ts +16 -0
  321. package/dist/compiler/codegen.d.ts +0 -334
  322. package/dist/compiler/codegen.d.ts.map +0 -1
  323. package/dist/compiler/inline-build.d.ts +0 -11
  324. package/dist/compiler/inline-build.d.ts.map +0 -1
  325. package/dist/compiler/inline-callback.d.ts +0 -21
  326. package/dist/compiler/inline-callback.d.ts.map +0 -1
  327. package/dist/compiler/line-index.d.ts +0 -2
  328. package/dist/compiler/line-index.d.ts.map +0 -1
  329. package/dist/compiler/scannable-run.d.ts.map +0 -1
  330. package/dist/compiler/scannable-terminal.d.ts +0 -23
  331. package/dist/compiler/scannable-terminal.d.ts.map +0 -1
  332. package/dist/compiler/trivia-fast-path.d.ts +0 -38
  333. package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
@@ -0,0 +1,336 @@
1
+ /**
2
+ * The ONLY module in parseman that talks to a terminal.
3
+ *
4
+ * Everything above it produces DATA — lines made of spans, each span carrying text and a
5
+ * semantic tone — and this file turns lines into bytes. That split is not tidiness; it is
6
+ * what makes the rendering diffable. A renderer that emits escape sequences inline has
7
+ * two outputs (styled and plain) that can drift apart, and the plain one is what a
8
+ * snapshot, a CI log and `docs/samples/` all read.
9
+ *
10
+ * WHY LINECRAFT AND NOT HAND-ROLLED ANSI
11
+ * --------------------------------------
12
+ * The first cut of this CLI carried its own `ANSI` constant table. Writing that file
13
+ * silently dropped the ESC bytes, so colour was dead for an entire session and nothing
14
+ * said so — the output still looked structured, because an escape-less `[2m` prints as
15
+ * `[2m`. Escape sequences are invisible failure surface, and a library that owns them
16
+ * removes the surface. jess already renders its diagnostics through linecraft
17
+ * (`packages/compiler/src/diagnostics.ts`, `packages/lint/src/index.ts`), so following
18
+ * the same idiom also means a jess user and a parseman user see the same shape of output
19
+ * rather than two parallel inventions. Pinned to `0.2.6`, exactly matching jess's pins.
20
+ *
21
+ * SPANS, NOT WHOLE-LINE STYLES
22
+ * ----------------------------
23
+ * jess's lint table styles a whole row at a time, which is right for a table of one
24
+ * thing per line. A grammar finding is not that: within one line the arm index, the
25
+ * production, its first set and the annotation all mean different things, and the
26
+ * annotation is the part that should be shouting. So a line here is a list of SPANS, and
27
+ * a styled line is a linecraft `Grid` of `Styled` cells — which also gives the columns
28
+ * their alignment for free.
29
+ *
30
+ * THE INVARIANT THAT MUST NOT BREAK
31
+ * ---------------------------------
32
+ * Padding happens in ONE place, before either path sees it, and the plain form is the
33
+ * concatenation of the padded span texts. So the two paths differ in styling and in
34
+ * nothing else — not in width, not in alignment, not in content. No escape byte is
35
+ * produced at all when colour is off, so the diffable output is byte-stable by
36
+ * construction rather than by stripping. `width` is passed explicitly and defaults to 80
37
+ * off-TTY, so a piped run cannot vary with the terminal it was piped from.
38
+ */
39
+ import { CodeDebug, Region, Styled, type TextStyle } from 'linecraft'
40
+ import { groupDigits } from './format-number.ts'
41
+
42
+ export type Tone = TextStyle
43
+
44
+ /**
45
+ * Semantic tones, named once so the two renderers cannot drift apart.
46
+ *
47
+ * The ladder matters more than the individual colours: within a finding, exactly one
48
+ * thing should be the brightest, and provenance / accept keys / byte costs should
49
+ * recede. A report where everything is emphasised is a report where nothing is.
50
+ */
51
+ export const TONE = {
52
+ /** The single most important thing in its block. */
53
+ loud: { color: 'brightRed', bold: true } as Tone,
54
+ /** A heading, a count, the thing being named. */
55
+ strong: { bold: true } as Tone,
56
+ /** Supporting detail — never the finding itself. */
57
+ quiet: { color: 'brightBlack' } as Tone,
58
+ /** Recedes furthest: provenance, snapshot keys, byte costs. */
59
+ faint: { color: 'brightBlack', dim: true } as Tone,
60
+ /** This span IS the problem. */
61
+ bad: { color: 'red' } as Tone,
62
+ /** Real, but not blocking. */
63
+ warn: { color: 'yellow' } as Tone,
64
+ /** A location or an identifier the reader will search for. */
65
+ ident: { color: 'cyan', bold: true } as Tone,
66
+ /** Clean, or the thing to do. */
67
+ good: { color: 'green' } as Tone,
68
+ /** Structure: rules, box drawing, group frames. */
69
+ frame: { color: 'blue' } as Tone,
70
+ } as const
71
+
72
+ /** One styled run of text. `width` pads it, in BOTH paths, so columns line up. */
73
+ export type Span = {
74
+ text: string
75
+ style?: Tone
76
+ /** Pad to this column width. Applied once, before either path renders. */
77
+ width?: number
78
+ /** Text that ALREADY contains escapes (a linecraft component's own output). */
79
+ raw?: boolean
80
+ /**
81
+ * Absolute path this span points at. In the STYLED path it becomes a clickable
82
+ * terminal hyperlink; the plain path ignores it entirely, which is what keeps the
83
+ * diffable output free of escapes and of absolute paths.
84
+ */
85
+ link?: string
86
+ }
87
+
88
+ /** A line is its spans. An empty array is a blank line. */
89
+ export type Line = Span[]
90
+
91
+ export const t = (text: string, style?: Tone, width?: number): Span =>
92
+ ({ text, ...(style === undefined ? {} : { style }), ...(width === undefined ? {} : { width }) })
93
+
94
+ export const blank = (): Line => []
95
+
96
+ /** A horizontal rule. Segmentation is what turns eighty undifferentiated lines into a
97
+ * handful of blocks a reader can skip between. */
98
+ export const rule = (width: number, style: Tone = TONE.frame, char = '─'): Line =>
99
+ [{ text: char.repeat(Math.max(0, width)), style }]
100
+
101
+ export type RenderTarget = {
102
+ /** Emit ANSI. Default false — never sniffed here; the CLI decides. */
103
+ color?: boolean
104
+ /** Columns. Default 80, which is also what an off-TTY stream reports as nothing. */
105
+ width?: number
106
+ /**
107
+ * Emit OSC-8 terminal hyperlinks on file locations. Only meaningful with `color`,
108
+ * since both are escape sequences. Default: on when colouring. A terminal without
109
+ * OSC-8 support prints the URL as visible junk, so this is the escape hatch.
110
+ */
111
+ links?: boolean
112
+ }
113
+
114
+ export const DEFAULT_WIDTH = 80
115
+
116
+ const padTo = (s: string, w: number): string => (s.length >= w ? s : s + ' '.repeat(w - s.length))
117
+
118
+ /** The one place padding happens. Both render paths consume the result of this. */
119
+ const spanText = (s: Span): string => (s.width === undefined ? s.text : padTo(s.text, s.width))
120
+
121
+ /**
122
+ * The padded, trailing-trimmed spans of one line — the SINGLE source both paths read.
123
+ *
124
+ * Both `plain` and `render` consume exactly this, so they cannot disagree about content,
125
+ * padding or alignment. Only what wraps each span differs.
126
+ */
127
+ function cellsOf(line: Line): Span[] {
128
+ const cells = line.map(s => ({ ...s, text: spanText(s) })).filter(s => s.text !== '')
129
+ // Trailing whitespace is noise in a log and makes two otherwise-identical renderings
130
+ // compare unequal. Trim it off the END of the line, span by span.
131
+ for (let i = cells.length - 1; i >= 0; i--) {
132
+ const trimmed = cells[i]!.text.replace(/ +$/, '')
133
+ cells[i]!.text = trimmed
134
+ if (trimmed !== '') break
135
+ cells.pop()
136
+ }
137
+ return cells
138
+ }
139
+
140
+ /** Plain text of a line list — the byte-stable form, produced without a terminal. */
141
+ export const plain = (lines: readonly Line[]): string =>
142
+ lines.map(l => cellsOf(l).map(c => c.text).join('')).join('\n')
143
+
144
+ /**
145
+ * The escape codes linecraft emits for a tone, learned FROM linecraft and cached.
146
+ *
147
+ * The alternative — a `Grid` of `Styled` cells — loses to the component's own layout:
148
+ * `Styled` left-trims its content and the grid re-flows a long cell, so the styled line
149
+ * ended up disagreeing with the plain one in CONTENT, which is the one divergence this
150
+ * module exists to prevent. Asking the library what a tone looks like and wrapping
151
+ * UNCHANGED text in it makes parity structural rather than something a test has to
152
+ * catch: the text between the codes is byte-identical to the plain form because it is
153
+ * the same string.
154
+ *
155
+ * This is not hand-rolled ANSI. No escape sequence is written down anywhere in parseman;
156
+ * every byte of styling comes out of linecraft, once per distinct tone.
157
+ */
158
+ const CODE_CACHE = new Map<string, { pre: string; post: string }>()
159
+
160
+ function codesFor(style: Tone | undefined): { pre: string; post: string } {
161
+ if (style === undefined) return { pre: '', post: '' }
162
+ const key = JSON.stringify(style)
163
+ const hit = CODE_CACHE.get(key)
164
+ if (hit !== undefined) return hit
165
+ const region = Region({ disableRendering: true, width: 8 })
166
+ region.set(Styled(style, 'X'))
167
+ const line = region.getLine(1)
168
+ region.destroy(false)
169
+ const at = line.indexOf('X')
170
+ const codes = at === -1
171
+ ? { pre: '', post: '' }
172
+ : { pre: line.slice(0, at), post: line.slice(at + 1).replace(/ /g, '') }
173
+ CODE_CACHE.set(key, codes)
174
+ return codes
175
+ }
176
+
177
+ /**
178
+ * The OSC-8 hyperlink wrapper, also learned FROM linecraft rather than written down.
179
+ *
180
+ * linecraft has `fileLink()`, but its package `exports` map publishes only `.` and
181
+ * `./components`, so it cannot be imported. `CodeDebug` DOES emit one for its file
182
+ * header, so the sequence is recovered the same way tone codes are: render the component
183
+ * once with sentinel values and read back what it wrapped them in. If the shape ever
184
+ * changes — or is not found — links are simply not emitted, which degrades to plain text
185
+ * rather than to garbage on screen.
186
+ */
187
+ let LINK_TEMPLATE: { pre: string; mid: string; post: string } | null | undefined
188
+
189
+ function linkTemplate(): { pre: string; mid: string; post: string } | null {
190
+ if (LINK_TEMPLATE !== undefined) return LINK_TEMPLATE
191
+ const URL_SENTINEL = '/PM_LINK_URL'
192
+ const TEXT_SENTINEL = 'PM_LINK_TEXT'
193
+ const region = Region({ disableRendering: true, width: 120 })
194
+ region.set(CodeDebug({
195
+ startLine: 1, startColumn: 1, errorLine: 'x', message: 'm',
196
+ filePath: TEXT_SENTINEL, fullPath: URL_SENTINEL, type: 'info',
197
+ }))
198
+ let found: { pre: string; mid: string; post: string } | null = null
199
+ for (let i = 1; i <= region.height && found === null; i++) {
200
+ const line = region.getLine(i)
201
+ const u = line.indexOf(URL_SENTINEL)
202
+ const x = line.indexOf(TEXT_SENTINEL)
203
+ if (u === -1 || x === -1 || x < u) continue
204
+ // Everything from the last ESC before the URL, so the whole introducer is captured.
205
+ const start = line.lastIndexOf(ESC, u)
206
+ if (start === -1) continue
207
+ const after = x + TEXT_SENTINEL.length
208
+ const endEsc = line.indexOf(ESC, after)
209
+ if (endEsc === -1) continue
210
+ // The closing OSC-8 runs to the terminator after the empty URL.
211
+ const close = line.indexOf('\\', line.indexOf(ESC, endEsc + 1))
212
+ if (close === -1) continue
213
+ found = {
214
+ pre: line.slice(start, u),
215
+ mid: line.slice(u + URL_SENTINEL.length, x),
216
+ post: line.slice(after, close + 1),
217
+ }
218
+ }
219
+ region.destroy(false)
220
+ LINK_TEMPLATE = found
221
+ return found
222
+ }
223
+
224
+ /** Wrap `text` as a hyperlink to `path`. Zero-width: the visible text is unchanged, so
225
+ * every width and alignment calculation upstream stays correct. */
226
+ function linked(text: string, path: string): string {
227
+ const tpl = linkTemplate()
228
+ if (tpl === null) return text
229
+ // Link the VISIBLE text only; column padding stays outside the link so a reader is
230
+ // not clicking on empty space.
231
+ const body = text.replace(/ +$/, '')
232
+ const trail = text.slice(body.length)
233
+ return `${tpl.pre}${path}${tpl.mid}${body}${tpl.post}${trail}`
234
+ }
235
+
236
+ /**
237
+ * Lines → a string. Without colour this never constructs a Region at all, so nothing on
238
+ * the diffable path depends on linecraft's environment detection.
239
+ */
240
+ export function render(lines: readonly Line[], target: RenderTarget = {}): string {
241
+ if (target.color !== true) return plain(lines)
242
+ const wantLinks = target.links !== false
243
+ return lines.map((line) => {
244
+ // A pre-rendered component line (a code frame) already carries its own escapes.
245
+ if (line.length === 1 && line[0]!.raw === true) return line[0]!.text
246
+ return cellsOf(line).map((c) => {
247
+ const { pre, post } = codesFor(c.style)
248
+ const body = wantLinks && c.link !== undefined ? linked(c.text, c.link) : c.text
249
+ return `${pre}${body}${post}`
250
+ }).join('')
251
+ }).join('\n')
252
+ }
253
+
254
+ // linecraft's own escapes, for the one component whose plain form is not otherwise
255
+ // reachable. Mirrors `packages/compiler/src/diagnostics.ts:246`. Built from a char code
256
+ // so the ESC byte cannot be lost in transit the way the old constant table's was.
257
+ const ESC = String.fromCharCode(27)
258
+ const OSC8 = new RegExp(`${ESC}\\]8;;.*?${ESC}\\\\`, 'g')
259
+ const SGR = new RegExp(`${ESC}\\[[0-9;]*m`, 'g')
260
+
261
+ export type CodeFrame = {
262
+ /** Path as the reader should see it — relative, so no absolute path can be diffed. */
263
+ path: string
264
+ /** Absolute path, used ONLY for the terminal hyperlink, which the plain form strips. */
265
+ fullPath: string
266
+ line: number
267
+ column: number
268
+ endLine?: number
269
+ endColumn?: number
270
+ lineBefore?: string | null
271
+ lineText: string
272
+ lineAfter?: string | null
273
+ message: string
274
+ /** Short label attached to the underline — the one sentence the caret is making. */
275
+ shortMessage?: string
276
+ type?: 'error' | 'warning' | 'info'
277
+ }
278
+
279
+ /**
280
+ * A source frame with the caret under the offending span — linecraft's `CodeDebug`, the
281
+ * same component jess renders its compiler errors with.
282
+ *
283
+ * Its lines come back as RAW spans: the component styles per token, which a span list
284
+ * cannot describe, and it is already the shape a reader wants. The plain form is
285
+ * recovered by stripping — the one place in this file that has to — and is asserted
286
+ * escape-free and absolute-path-free by test.
287
+ */
288
+ export function codeFrame(frame: CodeFrame, target: RenderTarget = {}, indent = ''): Line[] {
289
+ const region = Region({ disableRendering: true, width: (target.width ?? DEFAULT_WIDTH) - indent.length })
290
+ region.set(CodeDebug({
291
+ startLine: frame.line,
292
+ startColumn: frame.column,
293
+ ...(frame.endLine === undefined ? {} : { endLine: frame.endLine }),
294
+ ...(frame.endColumn === undefined ? {} : { endColumn: frame.endColumn }),
295
+ errorLine: frame.lineText,
296
+ lineBefore: frame.lineBefore ?? null,
297
+ lineAfter: frame.lineAfter ?? null,
298
+ message: frame.message,
299
+ ...(frame.shortMessage === undefined ? {} : { shortMessage: frame.shortMessage }),
300
+ filePath: frame.path,
301
+ // The component links its own header. Without colour (or with links off) it is
302
+ // handed the relative path, so no absolute path can reach the diffable output.
303
+ fullPath: target.color === true && target.links !== false ? frame.fullPath : frame.path,
304
+ type: frame.type ?? 'error',
305
+ }))
306
+ const out: Line[] = []
307
+ for (let line = 1; line <= region.height; line++) {
308
+ const text = region.getLine(line)
309
+ const body = target.color === true ? text : text.replace(OSC8, '').replace(SGR, '').replace(/\s+$/, '')
310
+ out.push(body === '' ? [] : [{ text: indent + body, raw: target.color === true }])
311
+ }
312
+ region.destroy(false)
313
+ return out
314
+ }
315
+
316
+ /** Deterministic thousands grouping — `toLocaleString()` differs between machines.
317
+ * Defined in `./format-number.ts`, which depends on nothing, and re-exported here
318
+ * because this module's callers import the formatting primitives from here. */
319
+ export { groupDigits }
320
+
321
+ export const pad = padTo
322
+
323
+ /** Hard-wrap on spaces at `width`, prefixing every line with `indent`. */
324
+ export function wrap(text: string, width: number, indent: string): string[] {
325
+ const out: string[] = []
326
+ for (const para of text.split('\n')) {
327
+ let line = ''
328
+ for (const word of para.split(/\s+/).filter(w => w !== '')) {
329
+ if (line === '') line = word
330
+ else if (line.length + 1 + word.length <= width) line += ` ${word}`
331
+ else { out.push(indent + line); line = word }
332
+ }
333
+ out.push(indent + line)
334
+ }
335
+ return out
336
+ }