@bendyline/squisq 1.5.2 → 2.0.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 (263) hide show
  1. package/README.md +63 -10
  2. package/dist/{Doc-BBVGq1_9.d.ts → Doc-ylqWKDc_.d.ts} +248 -37
  3. package/dist/{ImageEditDoc-FuyGtt6o.d.ts → ImageEditDoc-D828IXJ1.d.ts} +1 -1
  4. package/dist/annotationCoercion-Dr_DY9Hx.d.ts +86 -0
  5. package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
  6. package/dist/chunk-3E5F2XMR.js.map +1 -0
  7. package/dist/chunk-4X3JQXNM.js +35 -0
  8. package/dist/chunk-4X3JQXNM.js.map +1 -0
  9. package/dist/{chunk-P3K7NLHF.js → chunk-5DKBFXPY.js} +31 -121
  10. package/dist/chunk-5DKBFXPY.js.map +1 -0
  11. package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
  12. package/dist/chunk-5J4HKRJF.js.map +1 -0
  13. package/dist/chunk-5UYI3RJB.js +2503 -0
  14. package/dist/chunk-5UYI3RJB.js.map +1 -0
  15. package/dist/{chunk-BEQODJWV.js → chunk-66QO7XFB.js} +11 -291
  16. package/dist/chunk-66QO7XFB.js.map +1 -0
  17. package/dist/{chunk-KKNUBQ6Y.js → chunk-6PJRB2CF.js} +156 -91
  18. package/dist/chunk-6PJRB2CF.js.map +1 -0
  19. package/dist/{chunk-HWVFJAAH.js → chunk-AMIR72JP.js} +28 -10
  20. package/dist/chunk-AMIR72JP.js.map +1 -0
  21. package/dist/chunk-FVJUUTEM.js +74 -0
  22. package/dist/chunk-FVJUUTEM.js.map +1 -0
  23. package/dist/{chunk-5TYS5V3G.js → chunk-GF3BVNLU.js} +4127 -3538
  24. package/dist/chunk-GF3BVNLU.js.map +1 -0
  25. package/dist/{chunk-Q3ROPT5H.js → chunk-GZY66SDN.js} +1 -1
  26. package/dist/{chunk-Q3ROPT5H.js.map → chunk-GZY66SDN.js.map} +1 -1
  27. package/dist/chunk-JOQYPRDZ.js +127 -0
  28. package/dist/chunk-JOQYPRDZ.js.map +1 -0
  29. package/dist/{chunk-A4O7GIWE.js → chunk-LYNJEOAC.js} +10 -73
  30. package/dist/chunk-LYNJEOAC.js.map +1 -0
  31. package/dist/{chunk-LDQ2HJIX.js → chunk-PXLVXTLW.js} +252 -54
  32. package/dist/chunk-PXLVXTLW.js.map +1 -0
  33. package/dist/{chunk-TQWLI6S2.js → chunk-Q7LROV2H.js} +4 -2
  34. package/dist/{chunk-TQWLI6S2.js.map → chunk-Q7LROV2H.js.map} +1 -1
  35. package/dist/{chunk-T5UK6YOB.js → chunk-QTSJOY54.js} +60 -17
  36. package/dist/chunk-QTSJOY54.js.map +1 -0
  37. package/dist/chunk-QVANWDPH.js +2358 -0
  38. package/dist/chunk-QVANWDPH.js.map +1 -0
  39. package/dist/{chunk-GPLTCSXO.js → chunk-R2GC6RLD.js} +7 -7
  40. package/dist/chunk-R2GC6RLD.js.map +1 -0
  41. package/dist/chunk-R2YZTCLO.js +292 -0
  42. package/dist/chunk-R2YZTCLO.js.map +1 -0
  43. package/dist/chunk-S7A5D7ZJ.js +174 -0
  44. package/dist/chunk-S7A5D7ZJ.js.map +1 -0
  45. package/dist/chunk-T3TI7GSZ.js +884 -0
  46. package/dist/chunk-T3TI7GSZ.js.map +1 -0
  47. package/dist/{chunk-LTN6I7QW.js → chunk-TUWMK62B.js} +55 -4
  48. package/dist/chunk-TUWMK62B.js.map +1 -0
  49. package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
  50. package/dist/chunk-VR7UPJ7C.js.map +1 -0
  51. package/dist/doc/index.d.ts +2334 -5
  52. package/dist/doc/index.js +127 -33
  53. package/dist/generate/index.d.ts +7 -2
  54. package/dist/generate/index.js +1 -1
  55. package/dist/imageEdit/index.d.ts +9 -6
  56. package/dist/imageEdit/index.js +3 -3
  57. package/dist/index.d.ts +12 -10
  58. package/dist/index.js +246 -80
  59. package/dist/jsonForm/index.d.ts +7 -3
  60. package/dist/jsonForm/index.js +9 -4
  61. package/dist/markdown/index.d.ts +6 -70
  62. package/dist/markdown/index.js +13 -9
  63. package/dist/narration/index.d.ts +618 -0
  64. package/dist/narration/index.js +82 -0
  65. package/dist/recommend/index.d.ts +15 -1
  66. package/dist/recommend/index.js +2 -2
  67. package/dist/schemas/index.d.ts +28 -6
  68. package/dist/schemas/index.js +15 -13
  69. package/dist/spatial/index.d.ts +0 -1
  70. package/dist/spatial/index.js +1 -1
  71. package/dist/storage/index.js +1 -1
  72. package/dist/{themeLibrary-CehcJhzz.d.ts → themeLibrary-RjVgLpUi.d.ts} +10 -11
  73. package/dist/timing/index.js +5 -3
  74. package/dist/transform/index.d.ts +35 -24
  75. package/dist/transform/index.js +9 -10
  76. package/dist/{types-DlAZ7MW4.d.ts → types-BwMPl2CB.d.ts} +1 -1
  77. package/dist/{types-D98li1gV.d.ts → types-C4kiRn_k.d.ts} +3 -1
  78. package/dist/versions/index.d.ts +2 -1
  79. package/dist/versions/index.js +2 -2
  80. package/package.json +6 -6
  81. package/src/__tests__/applyNarrationTiming.test.ts +190 -0
  82. package/src/__tests__/applyRenderStyle.test.ts +3 -3
  83. package/src/__tests__/asciiDiagramBattle.test.ts +246 -0
  84. package/src/__tests__/asciiDiagramDetect.test.ts +162 -0
  85. package/src/__tests__/asciiDiagramMapping.test.ts +38 -0
  86. package/src/__tests__/asciiDiagramParse.test.ts +233 -0
  87. package/src/__tests__/asciiDiagramPipeline.test.ts +273 -0
  88. package/src/__tests__/asciiDiagramRender.test.ts +162 -0
  89. package/src/__tests__/asciiDiagramRepair.test.ts +84 -0
  90. package/src/__tests__/asciiTimelineDetect.test.ts +157 -0
  91. package/src/__tests__/asciiTimelineParse.test.ts +205 -0
  92. package/src/__tests__/asciiTimelinePipeline.test.ts +349 -0
  93. package/src/__tests__/asciiTimelineRender.test.ts +376 -0
  94. package/src/__tests__/attrTokens.test.ts +5 -0
  95. package/src/__tests__/contentContainer.test.ts +43 -0
  96. package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -15
  97. package/src/__tests__/dataTable.test.ts +25 -15
  98. package/src/__tests__/diagramBlock.test.ts +3 -3
  99. package/src/__tests__/diagramText.test.ts +26 -0
  100. package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
  101. package/src/__tests__/drawingBlock.test.ts +3 -3
  102. package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
  103. package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
  104. package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
  105. package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
  106. package/src/__tests__/getLayers.test.ts +32 -18
  107. package/src/__tests__/getLayersFallback.test.ts +8 -8
  108. package/src/__tests__/imageEditVersions.test.ts +2 -0
  109. package/src/__tests__/jsonForm.pointer.test.ts +47 -1
  110. package/src/__tests__/layoutBlock.test.ts +3 -3
  111. package/src/__tests__/majorApiTypes.test.ts +18 -0
  112. package/src/__tests__/markdown.test.ts +41 -0
  113. package/src/__tests__/matchFontFamily.test.ts +114 -0
  114. package/src/__tests__/materializeBlockLayers.test.ts +135 -0
  115. package/src/__tests__/materializeTestUtils.ts +17 -0
  116. package/src/__tests__/narrationAlign.test.ts +150 -0
  117. package/src/__tests__/narrationFeatures.test.ts +71 -0
  118. package/src/__tests__/narrationNuclei.test.ts +45 -0
  119. package/src/__tests__/narrationPacing.test.ts +189 -0
  120. package/src/__tests__/narrationScript.test.ts +149 -0
  121. package/src/__tests__/narrationSessionRepro.test.ts +62 -0
  122. package/src/__tests__/narrationSidecar.test.ts +88 -0
  123. package/src/__tests__/narrationSyllables.test.ts +65 -0
  124. package/src/__tests__/narrationTestSignals.ts +167 -0
  125. package/src/__tests__/narrationVad.test.ts +66 -0
  126. package/src/__tests__/resolveDocTheme.test.ts +16 -2
  127. package/src/__tests__/rootExports.test.ts +18 -0
  128. package/src/__tests__/scopedContentContainer.test.ts +20 -0
  129. package/src/__tests__/shapeGeometry.test.ts +12 -0
  130. package/src/__tests__/templateAnnotationParse.test.ts +10 -0
  131. package/src/__tests__/templateInputs.test.ts +69 -0
  132. package/src/__tests__/templateMetadata.test.ts +2 -1
  133. package/src/__tests__/templates.test.ts +143 -17
  134. package/src/__tests__/themeCompile.test.ts +49 -22
  135. package/src/__tests__/transformProvenance.test.ts +160 -0
  136. package/src/__tests__/transformV2.test.ts +40 -14
  137. package/src/__tests__/treeviewBattle.test.ts +76 -0
  138. package/src/__tests__/treeviewDetect.test.ts +168 -0
  139. package/src/__tests__/treeviewParse.test.ts +118 -0
  140. package/src/__tests__/treeviewPipeline.test.ts +183 -0
  141. package/src/__tests__/treeviewRender.test.ts +91 -0
  142. package/src/__tests__/versions.test.ts +44 -0
  143. package/src/doc/applyNarrationTiming.ts +244 -0
  144. package/src/doc/asciiDiagram/chars.ts +210 -0
  145. package/src/doc/asciiDiagram/detect.ts +175 -0
  146. package/src/doc/asciiDiagram/index.ts +38 -0
  147. package/src/doc/asciiDiagram/mapping.ts +225 -0
  148. package/src/doc/asciiDiagram/parse.ts +1095 -0
  149. package/src/doc/asciiDiagram/render.ts +963 -0
  150. package/src/doc/asciiDiagram/repair.ts +400 -0
  151. package/src/doc/asciiDiagram/types.ts +65 -0
  152. package/src/doc/asciiTimeline/detect.ts +138 -0
  153. package/src/doc/asciiTimeline/index.ts +26 -0
  154. package/src/doc/asciiTimeline/mapping.ts +43 -0
  155. package/src/doc/asciiTimeline/parse.ts +870 -0
  156. package/src/doc/asciiTimeline/render.ts +427 -0
  157. package/src/doc/asciiTimeline/types.ts +73 -0
  158. package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
  159. package/src/doc/audioMapping.ts +60 -46
  160. package/src/doc/customTemplatesFrontmatter.ts +0 -41
  161. package/src/doc/docToMarkdown.ts +70 -2
  162. package/src/doc/index.ts +76 -4
  163. package/src/doc/markdownToDoc.ts +135 -1
  164. package/src/doc/materializeBlockLayers.ts +304 -0
  165. package/src/doc/mediaAnnotations.ts +20 -0
  166. package/src/doc/resolveDocTheme.ts +8 -7
  167. package/src/doc/templateInputs.ts +185 -2
  168. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
  169. package/src/doc/templates/diagramBlock.ts +237 -38
  170. package/src/doc/templates/fallbackBlock.ts +1 -1
  171. package/src/doc/templates/index.ts +134 -279
  172. package/src/doc/templates/inputDescriptors.ts +20 -1
  173. package/src/doc/templates/metadata.ts +10 -0
  174. package/src/doc/templates/persistentLayers.ts +12 -126
  175. package/src/doc/templates/registry.ts +88 -0
  176. package/src/doc/templates/templateNames.ts +22 -0
  177. package/src/doc/templates/timelineBlock.ts +613 -0
  178. package/src/doc/templates/treeBlock.ts +131 -0
  179. package/src/doc/treeview/chars.ts +72 -0
  180. package/src/doc/treeview/detect.ts +145 -0
  181. package/src/doc/treeview/index.ts +30 -0
  182. package/src/doc/treeview/mapping.ts +95 -0
  183. package/src/doc/treeview/parse.ts +201 -0
  184. package/src/doc/treeview/render.ts +41 -0
  185. package/src/doc/treeview/types.ts +43 -0
  186. package/src/doc/utils/applyRenderStyle.ts +2 -9
  187. package/src/doc/utils/diagramText.ts +78 -0
  188. package/src/doc/utils/shapeGeometry.ts +67 -5
  189. package/src/doc/validate.ts +1 -1
  190. package/src/generate/templateMapper.ts +7 -0
  191. package/src/imageEdit/export.ts +6 -5
  192. package/src/imageEdit/versionPaths.ts +13 -26
  193. package/src/imageEdit/versions.ts +21 -100
  194. package/src/index.ts +1 -0
  195. package/src/internal/immutable.ts +58 -0
  196. package/src/jsonForm/index.ts +9 -1
  197. package/src/jsonForm/pointer.ts +68 -23
  198. package/src/markdown/attrTokens.ts +7 -5
  199. package/src/markdown/index.ts +1 -0
  200. package/src/markdown/stringify.ts +4 -9
  201. package/src/markdown/utils.ts +36 -14
  202. package/src/narration/align.ts +514 -0
  203. package/src/narration/features.ts +231 -0
  204. package/src/narration/index.ts +86 -0
  205. package/src/narration/nuclei.ts +118 -0
  206. package/src/narration/pacing.ts +211 -0
  207. package/src/narration/script.ts +239 -0
  208. package/src/narration/session.ts +100 -0
  209. package/src/narration/sidecar.ts +187 -0
  210. package/src/narration/syllables.ts +85 -0
  211. package/src/narration/trace.ts +50 -0
  212. package/src/narration/types.ts +299 -0
  213. package/src/narration/vad.ts +131 -0
  214. package/src/recommend/numberHighlight.ts +25 -0
  215. package/src/recommend/templates.ts +108 -12
  216. package/src/schemas/BlockTemplates.ts +156 -7
  217. package/src/schemas/Doc.ts +72 -9
  218. package/src/schemas/Media.ts +12 -2
  219. package/src/schemas/Theme.ts +45 -25
  220. package/src/schemas/Viewport.ts +2 -2
  221. package/src/schemas/fontStacks.ts +79 -0
  222. package/src/schemas/themeCompile.ts +15 -0
  223. package/src/schemas/themeConstants.ts +2 -0
  224. package/src/schemas/themeLibrary.ts +25 -19
  225. package/src/schemas/themeValidator.ts +1 -1
  226. package/src/spatial/Geohash.ts +0 -3
  227. package/src/storage/ContentContainer.ts +17 -9
  228. package/src/storage/ScopedContentContainer.ts +45 -15
  229. package/src/transform/applyTransform.ts +5 -5
  230. package/src/transform/index.ts +3 -1
  231. package/src/transform/registry.ts +169 -49
  232. package/src/transform/templateSelector.ts +21 -2
  233. package/src/transform/timingAllocator.ts +152 -21
  234. package/src/transform/types.ts +14 -1
  235. package/src/versions/gitignore.ts +34 -0
  236. package/src/versions/index.ts +2 -0
  237. package/src/versions/operations.ts +18 -104
  238. package/src/versions/paths.ts +13 -28
  239. package/src/versions/snapshotStore.ts +202 -0
  240. package/src/versions/types.ts +3 -1
  241. package/dist/chunk-5TYS5V3G.js.map +0 -1
  242. package/dist/chunk-A4O7GIWE.js.map +0 -1
  243. package/dist/chunk-BEQODJWV.js.map +0 -1
  244. package/dist/chunk-DLXZMS5K.js.map +0 -1
  245. package/dist/chunk-GPLTCSXO.js.map +0 -1
  246. package/dist/chunk-HWVFJAAH.js.map +0 -1
  247. package/dist/chunk-IMSCRLLV.js.map +0 -1
  248. package/dist/chunk-JELIRVDP.js +0 -24
  249. package/dist/chunk-JELIRVDP.js.map +0 -1
  250. package/dist/chunk-KKNUBQ6Y.js.map +0 -1
  251. package/dist/chunk-LDQ2HJIX.js.map +0 -1
  252. package/dist/chunk-LTN6I7QW.js.map +0 -1
  253. package/dist/chunk-P3K7NLHF.js.map +0 -1
  254. package/dist/chunk-T5UK6YOB.js.map +0 -1
  255. package/dist/chunk-UY7KGQ5R.js +0 -192
  256. package/dist/chunk-UY7KGQ5R.js.map +0 -1
  257. package/dist/chunk-VLJJHDAC.js +0 -218
  258. package/dist/chunk-VLJJHDAC.js.map +0 -1
  259. package/dist/chunk-VWUFZ6ZG.js.map +0 -1
  260. package/dist/story/index.d.ts +0 -1801
  261. package/dist/story/index.js +0 -279
  262. package/src/doc/getLayers.ts +0 -195
  263. /package/dist/{story → narration}/index.js.map +0 -0
@@ -0,0 +1,427 @@
1
+ /**
2
+ * ASCII timeline renderer: semantic tracks/events -> canonical timeline art.
3
+ *
4
+ * The canonical authored form keeps the rail visually continuous and moves
5
+ * event payloads onto pointer callouts:
6
+ *
7
+ * ▲ T28 :: delta 28 {#t28 column=12}
8
+ * Kernel {#kernel start=8 end=72}: ───●────────────────○────►
9
+ * ▼ T29 {#t29 column=36}
10
+ *
11
+ * Links follow the tracks as declarations. This form is compact, readable in
12
+ * raw Markdown, and is the exact grammar consumed by `parse.ts`. Rendering is
13
+ * deterministic and normalizes parser-sensitive text/ids up front, so it is a
14
+ * byte fixpoint after one render:
15
+ *
16
+ * renderAsciiTimeline(parseAsciiTimeline(renderAsciiTimeline(t))) ===
17
+ * renderAsciiTimeline(t)
18
+ */
19
+
20
+ import type {
21
+ AsciiTimeline,
22
+ AsciiTimelineMarker,
23
+ AsciiTimelineSide,
24
+ AsciiTimelineStyle,
25
+ } from './types.js';
26
+
27
+ export interface RenderAsciiTimelineOptions {
28
+ /** Character vocabulary; defaults to the timeline's detected style. */
29
+ style?: AsciiTimelineStyle;
30
+ }
31
+
32
+ interface TimelineVocab {
33
+ rail: string;
34
+ arrow: string;
35
+ marker: Record<AsciiTimelineMarker, string>;
36
+ }
37
+
38
+ interface PreparedEvent {
39
+ id: string;
40
+ label: string;
41
+ description?: string;
42
+ descriptionSide?: AsciiTimelineSide;
43
+ column: number;
44
+ side: AsciiTimelineSide;
45
+ callout?: boolean;
46
+ marker: AsciiTimelineMarker;
47
+ }
48
+
49
+ interface PreparedTrack {
50
+ id: string;
51
+ label?: string;
52
+ endLabel?: string;
53
+ startColumn: number;
54
+ endColumn: number;
55
+ events: PreparedEvent[];
56
+ }
57
+
58
+ interface PreparedLink {
59
+ source: string;
60
+ target: string;
61
+ label?: string;
62
+ }
63
+
64
+ const UNICODE_VOCAB: TimelineVocab = {
65
+ rail: '\u2500',
66
+ arrow: '\u25ba',
67
+ marker: {
68
+ filled: '\u25cf',
69
+ hollow: '\u25cb',
70
+ diamond: '\u25c6',
71
+ },
72
+ };
73
+
74
+ const ASCII_VOCAB: TimelineVocab = {
75
+ rail: '-',
76
+ arrow: '>',
77
+ marker: {
78
+ filled: '*',
79
+ hollow: 'o',
80
+ // The parser's ASCII vocabulary has no distinct diamond point. Folding
81
+ // it to `*` is the canonical ASCII representation and is byte-stable.
82
+ diamond: '*',
83
+ },
84
+ };
85
+
86
+ const MIN_AXIS_WIDTH = 48;
87
+ const MAX_COORDINATE_AXIS_WIDTH = 64;
88
+ const EVENT_SPACING = 3;
89
+ const TERMINAL_RAIL_LENGTH = 3;
90
+
91
+ /** Render a timeline into deterministic, parser-canonical ASCII/Unicode art. */
92
+ export function renderAsciiTimeline(
93
+ timeline: AsciiTimeline,
94
+ options: RenderAsciiTimelineOptions = {},
95
+ ): string {
96
+ const style = options.style ?? timeline.style;
97
+ const vocab = style === 'ascii' ? ASCII_VOCAB : UNICODE_VOCAB;
98
+ const { tracks, links } = prepareTimeline(timeline, style);
99
+ if (tracks.length === 0) return '';
100
+
101
+ const globalStart = Math.min(...tracks.map((track) => track.startColumn));
102
+ const globalEnd = Math.max(...tracks.map((track) => track.endColumn));
103
+ const globalSpan = Math.max(1, globalEnd - globalStart);
104
+ const axisWidth = Math.max(
105
+ MIN_AXIS_WIDTH,
106
+ Math.min(MAX_COORDINATE_AXIS_WIDTH, Math.ceil(globalSpan)),
107
+ ...tracks.map((track) => track.events.length * EVENT_SPACING),
108
+ );
109
+ const prefixes = tracks.map((track) => renderTrackPrefix(track));
110
+ const prefixWidth = Math.max(...prefixes.map(displayWidth));
111
+ const lines: string[] = [];
112
+
113
+ tracks.forEach((track, index) => {
114
+ if (index > 0) lines.push('');
115
+ lines.push(
116
+ ...renderTrack(
117
+ track,
118
+ style,
119
+ vocab,
120
+ prefixes[index],
121
+ prefixWidth,
122
+ axisWidth,
123
+ globalStart,
124
+ globalSpan,
125
+ ),
126
+ );
127
+ });
128
+
129
+ if (links.length > 0) {
130
+ lines.push('');
131
+ for (const link of links) lines.push(renderLink(link, style));
132
+ }
133
+
134
+ return lines.join('\n');
135
+ }
136
+
137
+ function renderTrack(
138
+ track: PreparedTrack,
139
+ style: AsciiTimelineStyle,
140
+ vocab: TimelineVocab,
141
+ prefix: string,
142
+ prefixWidth: number,
143
+ axisWidth: number,
144
+ globalStart: number,
145
+ globalSpan: number,
146
+ ): string[] {
147
+ const paddedPrefix = `${prefix}${' '.repeat(Math.max(0, prefixWidth - displayWidth(prefix)))}`;
148
+ const positioned = positionEvents(track.events, axisWidth, globalStart, globalSpan);
149
+ const rail = Array.from({ length: axisWidth + 1 }, () => vocab.rail);
150
+ for (const { event, offset } of positioned) rail[offset] = vocab.marker[event.marker];
151
+
152
+ const callouts = positioned.map(({ event, offset }) => ({
153
+ event,
154
+ column: prefixWidth + offset,
155
+ text: renderEventPayload(event),
156
+ }));
157
+ const pointer = style === 'ascii' ? { above: '^', below: 'v' } : { above: '▲', below: '▼' };
158
+ const above = renderCalloutLanes(
159
+ callouts.filter(({ event }) => event.side === 'above'),
160
+ pointer.above,
161
+ );
162
+ const below = renderCalloutLanes(
163
+ callouts.filter(({ event }) => event.side === 'below'),
164
+ pointer.below,
165
+ );
166
+ const axis = `${paddedPrefix}${rail.join('')}${vocab.rail.repeat(TERMINAL_RAIL_LENGTH)}${vocab.arrow}${track.endLabel ? ` ${track.endLabel}` : ''}`;
167
+ return [...above, axis, ...below];
168
+ }
169
+
170
+ function renderEventPayload(event: PreparedEvent): string {
171
+ const body = event.description ? `${event.label} :: ${event.description}` : event.label;
172
+ const descriptionSide = event.descriptionSide ? ` descriptionSide=${event.descriptionSide}` : '';
173
+ const callout = event.callout === false ? ' callout=false' : '';
174
+ return `${body} {#${event.id} column=${formatCoordinate(event.column)}${descriptionSide}${callout}}`;
175
+ }
176
+
177
+ function renderTrackPrefix(track: PreparedTrack): string {
178
+ const metadata = `{#${track.id} start=${formatCoordinate(track.startColumn)} end=${formatCoordinate(track.endColumn)}}`;
179
+ return track.label ? `${track.label} ${metadata}: ` : `${metadata}: `;
180
+ }
181
+
182
+ function positionEvents(
183
+ events: readonly PreparedEvent[],
184
+ axisWidth: number,
185
+ globalStart: number,
186
+ globalSpan: number,
187
+ ): Array<{ event: PreparedEvent; offset: number }> {
188
+ let previous = -1;
189
+ return events.map((event, index) => {
190
+ const ideal = Math.round(((event.column - globalStart) / globalSpan) * axisWidth);
191
+ const remaining = events.length - index - 1;
192
+ const latest = Math.max(0, axisWidth - remaining);
193
+ const offset = Math.max(previous + 1, Math.min(latest, Math.max(0, ideal)));
194
+ previous = offset;
195
+ return { event, offset };
196
+ });
197
+ }
198
+
199
+ function renderCalloutLanes(
200
+ callouts: ReadonlyArray<{ column: number; text: string }>,
201
+ pointer: string,
202
+ ): string[] {
203
+ const lanes: Array<Array<{ column: number; text: string }>> = [];
204
+ const laneEnds: number[] = [];
205
+ for (const callout of callouts) {
206
+ const end = callout.column + 2 + displayWidth(callout.text);
207
+ let lane = laneEnds.findIndex((laneEnd) => callout.column > laneEnd + 1);
208
+ if (lane < 0) {
209
+ lane = lanes.length;
210
+ lanes.push([]);
211
+ laneEnds.push(-1);
212
+ }
213
+ lanes[lane].push(callout);
214
+ laneEnds[lane] = end;
215
+ }
216
+ return lanes.map((lane) => {
217
+ const chars: string[] = [];
218
+ for (const callout of lane) {
219
+ while (chars.length < callout.column) chars.push(' ');
220
+ chars.push(pointer, ' ', ...Array.from(callout.text));
221
+ }
222
+ return chars.join('').trimEnd();
223
+ });
224
+ }
225
+
226
+ function displayWidth(value: string): number {
227
+ return Array.from(value).length;
228
+ }
229
+
230
+ function renderLink(link: PreparedLink, style: AsciiTimelineStyle): string {
231
+ const arrow = style === 'ascii' ? '->' : '\u2192';
232
+ return `branch: ${link.source} ${arrow} ${link.target}${link.label ? ` : ${link.label}` : ''}`;
233
+ }
234
+
235
+ function prepareTimeline(
236
+ timeline: AsciiTimeline,
237
+ style: AsciiTimelineStyle,
238
+ ): { tracks: PreparedTrack[]; links: PreparedLink[] } {
239
+ const sourceTracks = timeline.tracks
240
+ .map((track, index) => ({ track, index }))
241
+ .filter(({ track }) => Array.isArray(track.events) && track.events.length > 0)
242
+ .sort(
243
+ (a, b) =>
244
+ finiteOr(a.track.row, a.index) - finiteOr(b.track.row, b.index) ||
245
+ a.index - b.index ||
246
+ a.track.id.localeCompare(b.track.id),
247
+ );
248
+
249
+ const usedTrackIds = new Set<string>();
250
+ const usedEventIds = new Set<string>();
251
+ const endpointIds = new Map<string, string>();
252
+ const labelIds = new Map<string, string>();
253
+ const tracks: PreparedTrack[] = [];
254
+ let fallbackIndex = 0;
255
+
256
+ for (const { track } of sourceTracks) {
257
+ const trackLabel = canonicalInlineText(track.label);
258
+ const endLabel = canonicalInlineText(track.endLabel);
259
+ const trackId = uniqueId(
260
+ canonicalId(track.id, canonicalId(trackLabel, `track_${tracks.length + 1}`)),
261
+ usedTrackIds,
262
+ );
263
+ const sortedEvents = track.events
264
+ .map((event, index) => ({ event, index }))
265
+ .sort(
266
+ (a, b) =>
267
+ finiteOr(a.event.column, a.index) - finiteOr(b.event.column, b.index) ||
268
+ a.index - b.index ||
269
+ a.event.id.localeCompare(b.event.id),
270
+ );
271
+ const events: PreparedEvent[] = [];
272
+
273
+ for (let index = 0; index < sortedEvents.length; index++) {
274
+ const event = sortedEvents[index].event;
275
+ const fallbackLabel = `Event ${index + 1}`;
276
+ const label = canonicalInlineText(event.label) || fallbackLabel;
277
+ const idBase = canonicalId(event.id, canonicalId(label, `event_${++fallbackIndex}`));
278
+ const id = uniqueId(idBase, usedEventIds);
279
+ const description = canonicalInlineText(event.description);
280
+ const descriptionSide =
281
+ description && (event.descriptionSide === 'above' || event.descriptionSide === 'below')
282
+ ? event.descriptionSide
283
+ : undefined;
284
+ const column = canonicalCoordinate(event.column, index);
285
+ const side: AsciiTimelineSide =
286
+ event.side === 'below'
287
+ ? 'below'
288
+ : event.side === 'above'
289
+ ? 'above'
290
+ : index % 2 === 0
291
+ ? 'above'
292
+ : 'below';
293
+ const marker = canonicalMarker(event.marker, style);
294
+
295
+ events.push({
296
+ id,
297
+ label,
298
+ ...(description ? { description } : {}),
299
+ ...(descriptionSide ? { descriptionSide } : {}),
300
+ column,
301
+ side,
302
+ ...(event.callout === false ? { callout: false } : {}),
303
+ marker,
304
+ });
305
+
306
+ // Source ids are normally unique. Preserve the first target when a
307
+ // malformed runtime value duplicates one; the rendered ids themselves
308
+ // are always unique and are also valid branch endpoints.
309
+ if (event.id && !endpointIds.has(event.id)) endpointIds.set(event.id, id);
310
+ endpointIds.set(id, id);
311
+ labelIds.set(slug(label), id);
312
+ }
313
+
314
+ const eventColumns = events.map((event) => event.column);
315
+ const fallbackStart = eventColumns.length > 0 ? Math.min(...eventColumns) : 0;
316
+ const fallbackEnd = eventColumns.length > 0 ? Math.max(...eventColumns) : fallbackStart + 1;
317
+ tracks.push({
318
+ id: trackId,
319
+ ...(trackLabel ? { label: trackLabel } : {}),
320
+ ...(endLabel ? { endLabel } : {}),
321
+ startColumn: canonicalCoordinate(track.startColumn, fallbackStart),
322
+ endColumn: canonicalCoordinate(track.endColumn, fallbackEnd),
323
+ events,
324
+ });
325
+ }
326
+
327
+ const resolveEndpoint = (value: string): string | undefined =>
328
+ endpointIds.get(value) ?? labelIds.get(slug(value));
329
+ const links: PreparedLink[] = [];
330
+ for (const link of Array.isArray(timeline.links) ? timeline.links : []) {
331
+ const source = resolveEndpoint(link.source);
332
+ const target = resolveEndpoint(link.target);
333
+ // Unresolved declarations would be dropped by the parser anyway. Drop
334
+ // them before the first render so the emitted form is already a fixpoint.
335
+ if (!source || !target) continue;
336
+ const label = canonicalBranchText(link.label);
337
+ links.push({ source, target, ...(label ? { label } : {}) });
338
+ }
339
+ links.sort(
340
+ (a, b) =>
341
+ a.source.localeCompare(b.source) ||
342
+ a.target.localeCompare(b.target) ||
343
+ (a.label ?? '').localeCompare(b.label ?? ''),
344
+ );
345
+
346
+ return { tracks, links };
347
+ }
348
+
349
+ function canonicalMarker(
350
+ marker: AsciiTimelineMarker | undefined,
351
+ style: AsciiTimelineStyle,
352
+ ): AsciiTimelineMarker {
353
+ if (marker === 'hollow') return 'hollow';
354
+ if (marker === 'diamond' && style !== 'ascii') return 'diamond';
355
+ return 'filled';
356
+ }
357
+
358
+ /**
359
+ * Normalize point glyphs that would accidentally create another event. Rails,
360
+ * prose arrows, and hyphens are preserved: `parse.ts` only strips separator
361
+ * rails at the payload edges, so `A > B` and `release--candidate` round-trip.
362
+ */
363
+ function canonicalInlineText(value: string | undefined): string {
364
+ if (!value) return '';
365
+ const normalized = value
366
+ .normalize('NFC')
367
+ .replace(/[\r\n\t]+/g, ' ')
368
+ .replace(/[\u25cf\u25cb\u25c9\u25c6\u25c7\u2022]+/gu, '\u00b7')
369
+ .replace(/\*/g, '\u2217')
370
+ .replace(/::/g, '\u2236')
371
+ .replace(/\s+/g, ' ')
372
+ .trim();
373
+ // A leading authored rail is otherwise indistinguishable from the
374
+ // renderer's separator prefix to `stripEdgeRails`. Keep the punctuation
375
+ // visible with non-structural lookalikes so `--watch`, `-1.5`, and
376
+ // `─phase` survive the very first render/parse normalization cycle.
377
+ return normalized.replace(/^[\u2500\u2501\u2550\u254c\u254d\u2504\u2505\u2508\u2509-]+/u, (run) =>
378
+ Array.from(run, (character) => (character === '-' ? '\u2212' : '\u2013')).join(''),
379
+ );
380
+ }
381
+
382
+ function canonicalBranchText(value: string | undefined): string {
383
+ return canonicalInlineText(value).replace(/\s+/g, ' ').trim();
384
+ }
385
+
386
+ function canonicalId(value: string | undefined, fallback: string): string {
387
+ const normalized = (value ?? '')
388
+ .normalize('NFC')
389
+ .replace(/[\r\n\t\s]+/g, '_')
390
+ .replace(/[^A-Za-z0-9_.~-]+/g, '_')
391
+ .replace(/^_+|_+$/g, '');
392
+ return normalized || fallback;
393
+ }
394
+
395
+ function uniqueId(base: string, used: Set<string>): string {
396
+ if (!used.has(base)) {
397
+ used.add(base);
398
+ return base;
399
+ }
400
+ let suffix = 2;
401
+ let candidate = `${base}_${suffix}`;
402
+ while (used.has(candidate)) candidate = `${base}_${++suffix}`;
403
+ used.add(candidate);
404
+ return candidate;
405
+ }
406
+
407
+ function slug(value: string): string {
408
+ return (
409
+ value
410
+ .toLowerCase()
411
+ .replace(/[^a-z0-9]+/g, '-')
412
+ .replace(/^-+|-+$/g, '') || 'event'
413
+ );
414
+ }
415
+
416
+ function finiteOr(value: number, fallback: number): number {
417
+ return Number.isFinite(value) ? value : fallback;
418
+ }
419
+
420
+ function canonicalCoordinate(value: number, fallback: number): number {
421
+ const finite = finiteOr(value, fallback);
422
+ return Math.max(0, Object.is(finite, -0) ? 0 : finite);
423
+ }
424
+
425
+ function formatCoordinate(value: number): string {
426
+ return String(Object.is(value, -0) ? 0 : value);
427
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Public model for authored ASCII timelines.
3
+ *
4
+ * Source columns and rows are retained so separate tracks share one spatial
5
+ * scale and pointer callouts can land between authored milestone dots. The
6
+ * template mapping normalizes columns only at the final rendering boundary.
7
+ */
8
+
9
+ export type AsciiTimelineStyle = 'unicode' | 'ascii';
10
+ export type AsciiTimelineSide = 'above' | 'below';
11
+ export type AsciiTimelineMarker = 'filled' | 'hollow' | 'diamond';
12
+
13
+ export interface AsciiTimelineEvent {
14
+ /** Stable slug id, deduplicated across every track. */
15
+ id: string;
16
+ /** Short callout heading. */
17
+ label: string;
18
+ /** Optional longer callout/body text. */
19
+ description?: string;
20
+ /** Zero-based source-grid column of the event anchor. */
21
+ column: number;
22
+ /** Authored/default callout placement around the track. */
23
+ side?: AsciiTimelineSide;
24
+ /** Placement for `description` when it differs from the label side. */
25
+ descriptionSide?: AsciiTimelineSide;
26
+ /** False renders a cadence/tick point without a text callout. */
27
+ callout?: boolean;
28
+ /** Visual point vocabulary inferred from the authored marker. */
29
+ marker?: AsciiTimelineMarker;
30
+ }
31
+
32
+ export interface AsciiTimelineTrack {
33
+ /** Stable track id. */
34
+ id: string;
35
+ /** Optional label shown to the left of the rendered track. */
36
+ label?: string;
37
+ /** Optional axis/end label authored after the terminal arrow. */
38
+ endLabel?: string;
39
+ /** Zero-based source row containing this track's horizontal axis. */
40
+ row: number;
41
+ /** Inclusive source-grid bounds of the axis. */
42
+ startColumn: number;
43
+ endColumn: number;
44
+ events: AsciiTimelineEvent[];
45
+ }
46
+
47
+ export interface AsciiTimelineLink {
48
+ /** Source event id. */
49
+ source: string;
50
+ /** Target event id. */
51
+ target: string;
52
+ /** Optional branch/link label. */
53
+ label?: string;
54
+ }
55
+
56
+ export interface AsciiTimeline {
57
+ tracks: AsciiTimelineTrack[];
58
+ links: AsciiTimelineLink[];
59
+ /** Source-grid dimensions, shared by every track. */
60
+ width: number;
61
+ height: number;
62
+ style: AsciiTimelineStyle;
63
+ /** Non-fatal parse notes (unresolved links, unlabeled events, and so on). */
64
+ warnings: string[];
65
+ }
66
+
67
+ export interface AsciiTimelineDetection {
68
+ isTimeline: boolean;
69
+ /** Present on acceptance so callers do not need to parse twice. */
70
+ timeline?: AsciiTimeline;
71
+ /** Machine-readable accept/reject reasons. */
72
+ reasons: string[];
73
+ }