@bendyline/squisq 1.5.2 → 2.0.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 (268) 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-T5UK6YOB.js → chunk-2PWOO4ZE.js} +60 -17
  6. package/dist/chunk-2PWOO4ZE.js.map +1 -0
  7. package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
  8. package/dist/chunk-3E5F2XMR.js.map +1 -0
  9. package/dist/chunk-4X3JQXNM.js +35 -0
  10. package/dist/chunk-4X3JQXNM.js.map +1 -0
  11. package/dist/{chunk-P3K7NLHF.js → chunk-5DKBFXPY.js} +31 -121
  12. package/dist/chunk-5DKBFXPY.js.map +1 -0
  13. package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
  14. package/dist/chunk-5J4HKRJF.js.map +1 -0
  15. package/dist/chunk-5UYI3RJB.js +2503 -0
  16. package/dist/chunk-5UYI3RJB.js.map +1 -0
  17. package/dist/{chunk-BEQODJWV.js → chunk-66QO7XFB.js} +11 -291
  18. package/dist/chunk-66QO7XFB.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-CY2R2VPR.js +884 -0
  22. package/dist/chunk-CY2R2VPR.js.map +1 -0
  23. package/dist/chunk-FVJUUTEM.js +74 -0
  24. package/dist/chunk-FVJUUTEM.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-MMPBOUK7.js} +10 -73
  30. package/dist/chunk-MMPBOUK7.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-GPLTCSXO.js → chunk-R2GC6RLD.js} +7 -7
  36. package/dist/chunk-R2GC6RLD.js.map +1 -0
  37. package/dist/chunk-R2YZTCLO.js +292 -0
  38. package/dist/chunk-R2YZTCLO.js.map +1 -0
  39. package/dist/chunk-S7A5D7ZJ.js +174 -0
  40. package/dist/chunk-S7A5D7ZJ.js.map +1 -0
  41. package/dist/chunk-TFJVXUHH.js +2358 -0
  42. package/dist/chunk-TFJVXUHH.js.map +1 -0
  43. package/dist/{chunk-LTN6I7QW.js → chunk-TUWMK62B.js} +55 -4
  44. package/dist/chunk-TUWMK62B.js.map +1 -0
  45. package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
  46. package/dist/chunk-VR7UPJ7C.js.map +1 -0
  47. package/dist/{chunk-KKNUBQ6Y.js → chunk-W4O2TX5I.js} +157 -92
  48. package/dist/chunk-W4O2TX5I.js.map +1 -0
  49. package/dist/{chunk-5TYS5V3G.js → chunk-X6DUONGT.js} +4204 -3560
  50. package/dist/chunk-X6DUONGT.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__/customThemesFrontmatter.test.ts +1 -0
  98. package/src/__tests__/dataTable.test.ts +25 -15
  99. package/src/__tests__/diagramBlock.test.ts +3 -3
  100. package/src/__tests__/diagramText.test.ts +26 -0
  101. package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
  102. package/src/__tests__/drawingBlock.test.ts +3 -3
  103. package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
  104. package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
  105. package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
  106. package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
  107. package/src/__tests__/getLayers.test.ts +32 -18
  108. package/src/__tests__/getLayersFallback.test.ts +8 -8
  109. package/src/__tests__/imageEditVersions.test.ts +2 -0
  110. package/src/__tests__/jsonForm.pointer.test.ts +47 -1
  111. package/src/__tests__/layoutBlock.test.ts +3 -3
  112. package/src/__tests__/majorApiTypes.test.ts +18 -0
  113. package/src/__tests__/markdown.test.ts +54 -4
  114. package/src/__tests__/markdownSanitize.test.ts +7 -5
  115. package/src/__tests__/matchFontFamily.test.ts +114 -0
  116. package/src/__tests__/materializeBlockLayers.test.ts +135 -0
  117. package/src/__tests__/materializeTestUtils.ts +17 -0
  118. package/src/__tests__/narrationAlign.test.ts +150 -0
  119. package/src/__tests__/narrationFeatures.test.ts +71 -0
  120. package/src/__tests__/narrationNuclei.test.ts +45 -0
  121. package/src/__tests__/narrationPacing.test.ts +189 -0
  122. package/src/__tests__/narrationScript.test.ts +149 -0
  123. package/src/__tests__/narrationSessionRepro.test.ts +62 -0
  124. package/src/__tests__/narrationSidecar.test.ts +88 -0
  125. package/src/__tests__/narrationSyllables.test.ts +65 -0
  126. package/src/__tests__/narrationTestSignals.ts +167 -0
  127. package/src/__tests__/narrationVad.test.ts +66 -0
  128. package/src/__tests__/resolveDocTheme.test.ts +16 -2
  129. package/src/__tests__/rootExports.test.ts +18 -0
  130. package/src/__tests__/scopedContentContainer.test.ts +20 -0
  131. package/src/__tests__/shapeGeometry.test.ts +12 -0
  132. package/src/__tests__/templateAnnotationParse.test.ts +10 -0
  133. package/src/__tests__/templateInputs.test.ts +69 -0
  134. package/src/__tests__/templateMetadata.test.ts +2 -1
  135. package/src/__tests__/templates.test.ts +143 -17
  136. package/src/__tests__/themeCompile.test.ts +49 -22
  137. package/src/__tests__/transformProvenance.test.ts +160 -0
  138. package/src/__tests__/transformV2.test.ts +40 -14
  139. package/src/__tests__/treeviewBattle.test.ts +76 -0
  140. package/src/__tests__/treeviewDetect.test.ts +168 -0
  141. package/src/__tests__/treeviewParse.test.ts +118 -0
  142. package/src/__tests__/treeviewPipeline.test.ts +183 -0
  143. package/src/__tests__/treeviewRender.test.ts +91 -0
  144. package/src/__tests__/versions.test.ts +44 -0
  145. package/src/doc/applyNarrationTiming.ts +244 -0
  146. package/src/doc/asciiDiagram/chars.ts +210 -0
  147. package/src/doc/asciiDiagram/detect.ts +175 -0
  148. package/src/doc/asciiDiagram/index.ts +38 -0
  149. package/src/doc/asciiDiagram/mapping.ts +225 -0
  150. package/src/doc/asciiDiagram/parse.ts +1095 -0
  151. package/src/doc/asciiDiagram/render.ts +963 -0
  152. package/src/doc/asciiDiagram/repair.ts +400 -0
  153. package/src/doc/asciiDiagram/types.ts +65 -0
  154. package/src/doc/asciiTimeline/detect.ts +138 -0
  155. package/src/doc/asciiTimeline/index.ts +26 -0
  156. package/src/doc/asciiTimeline/mapping.ts +43 -0
  157. package/src/doc/asciiTimeline/parse.ts +870 -0
  158. package/src/doc/asciiTimeline/render.ts +427 -0
  159. package/src/doc/asciiTimeline/types.ts +73 -0
  160. package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
  161. package/src/doc/audioMapping.ts +60 -46
  162. package/src/doc/customTemplatesFrontmatter.ts +0 -41
  163. package/src/doc/docToMarkdown.ts +70 -2
  164. package/src/doc/index.ts +76 -4
  165. package/src/doc/markdownToDoc.ts +138 -2
  166. package/src/doc/materializeBlockLayers.ts +304 -0
  167. package/src/doc/mediaAnnotations.ts +20 -0
  168. package/src/doc/resolveDocTheme.ts +8 -7
  169. package/src/doc/templateInputs.ts +185 -2
  170. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
  171. package/src/doc/templates/__tests__/listBlock.test.ts +64 -0
  172. package/src/doc/templates/diagramBlock.ts +237 -38
  173. package/src/doc/templates/fallbackBlock.ts +1 -1
  174. package/src/doc/templates/index.ts +134 -279
  175. package/src/doc/templates/inputDescriptors.ts +20 -1
  176. package/src/doc/templates/listBlock.ts +88 -30
  177. package/src/doc/templates/metadata.ts +10 -0
  178. package/src/doc/templates/persistentLayers.ts +12 -126
  179. package/src/doc/templates/registry.ts +88 -0
  180. package/src/doc/templates/templateNames.ts +22 -0
  181. package/src/doc/templates/timelineBlock.ts +613 -0
  182. package/src/doc/templates/treeBlock.ts +131 -0
  183. package/src/doc/treeview/chars.ts +72 -0
  184. package/src/doc/treeview/detect.ts +145 -0
  185. package/src/doc/treeview/index.ts +30 -0
  186. package/src/doc/treeview/mapping.ts +95 -0
  187. package/src/doc/treeview/parse.ts +201 -0
  188. package/src/doc/treeview/render.ts +41 -0
  189. package/src/doc/treeview/types.ts +43 -0
  190. package/src/doc/utils/applyRenderStyle.ts +2 -9
  191. package/src/doc/utils/diagramText.ts +78 -0
  192. package/src/doc/utils/shapeGeometry.ts +67 -5
  193. package/src/doc/validate.ts +1 -1
  194. package/src/generate/templateMapper.ts +7 -0
  195. package/src/imageEdit/export.ts +6 -5
  196. package/src/imageEdit/versionPaths.ts +13 -26
  197. package/src/imageEdit/versions.ts +21 -100
  198. package/src/index.ts +1 -0
  199. package/src/internal/immutable.ts +58 -0
  200. package/src/jsonForm/index.ts +9 -1
  201. package/src/jsonForm/pointer.ts +68 -23
  202. package/src/markdown/attrTokens.ts +7 -5
  203. package/src/markdown/index.ts +1 -0
  204. package/src/markdown/stringify.ts +4 -9
  205. package/src/markdown/utils.ts +36 -14
  206. package/src/narration/align.ts +514 -0
  207. package/src/narration/features.ts +231 -0
  208. package/src/narration/index.ts +86 -0
  209. package/src/narration/nuclei.ts +118 -0
  210. package/src/narration/pacing.ts +211 -0
  211. package/src/narration/script.ts +239 -0
  212. package/src/narration/session.ts +100 -0
  213. package/src/narration/sidecar.ts +187 -0
  214. package/src/narration/syllables.ts +85 -0
  215. package/src/narration/trace.ts +50 -0
  216. package/src/narration/types.ts +299 -0
  217. package/src/narration/vad.ts +131 -0
  218. package/src/recommend/numberHighlight.ts +25 -0
  219. package/src/recommend/templates.ts +108 -12
  220. package/src/schemas/BlockTemplates.ts +156 -7
  221. package/src/schemas/Doc.ts +72 -9
  222. package/src/schemas/Media.ts +12 -2
  223. package/src/schemas/Theme.ts +45 -25
  224. package/src/schemas/Viewport.ts +2 -2
  225. package/src/schemas/fontStacks.ts +79 -0
  226. package/src/schemas/themeCompile.ts +15 -0
  227. package/src/schemas/themeConstants.ts +2 -0
  228. package/src/schemas/themeLibrary.ts +25 -19
  229. package/src/schemas/themeValidator.ts +1 -1
  230. package/src/schemas/themes/gezellig.json +1 -1
  231. package/src/spatial/Geohash.ts +0 -3
  232. package/src/storage/ContentContainer.ts +17 -9
  233. package/src/storage/ScopedContentContainer.ts +45 -15
  234. package/src/transform/applyTransform.ts +5 -5
  235. package/src/transform/index.ts +3 -1
  236. package/src/transform/registry.ts +169 -49
  237. package/src/transform/templateSelector.ts +21 -2
  238. package/src/transform/timingAllocator.ts +152 -21
  239. package/src/transform/types.ts +14 -1
  240. package/src/versions/gitignore.ts +34 -0
  241. package/src/versions/index.ts +2 -0
  242. package/src/versions/operations.ts +18 -104
  243. package/src/versions/paths.ts +13 -28
  244. package/src/versions/snapshotStore.ts +202 -0
  245. package/src/versions/types.ts +3 -1
  246. package/dist/chunk-5TYS5V3G.js.map +0 -1
  247. package/dist/chunk-A4O7GIWE.js.map +0 -1
  248. package/dist/chunk-BEQODJWV.js.map +0 -1
  249. package/dist/chunk-DLXZMS5K.js.map +0 -1
  250. package/dist/chunk-GPLTCSXO.js.map +0 -1
  251. package/dist/chunk-HWVFJAAH.js.map +0 -1
  252. package/dist/chunk-IMSCRLLV.js.map +0 -1
  253. package/dist/chunk-JELIRVDP.js +0 -24
  254. package/dist/chunk-JELIRVDP.js.map +0 -1
  255. package/dist/chunk-KKNUBQ6Y.js.map +0 -1
  256. package/dist/chunk-LDQ2HJIX.js.map +0 -1
  257. package/dist/chunk-LTN6I7QW.js.map +0 -1
  258. package/dist/chunk-P3K7NLHF.js.map +0 -1
  259. package/dist/chunk-T5UK6YOB.js.map +0 -1
  260. package/dist/chunk-UY7KGQ5R.js +0 -192
  261. package/dist/chunk-UY7KGQ5R.js.map +0 -1
  262. package/dist/chunk-VLJJHDAC.js +0 -218
  263. package/dist/chunk-VLJJHDAC.js.map +0 -1
  264. package/dist/chunk-VWUFZ6ZG.js.map +0 -1
  265. package/dist/story/index.d.ts +0 -1801
  266. package/dist/story/index.js +0 -279
  267. package/src/doc/getLayers.ts +0 -195
  268. /package/dist/{story → narration}/index.js.map +0 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Tree Template
3
+ *
4
+ * Renders a hierarchical filesystem-style treeview (folder/file icons +
5
+ * connector rails, collapsible in the live player) from an ASCII tree fence.
6
+ * Emits a single `TreeLayer` (foreignObject HTML, like `dataTable`) plus an
7
+ * optional title + themed background.
8
+ *
9
+ * Data source: `input.items` (a nested `TreeLayerItem[]`), populated from
10
+ * `templateData.items` which the pipeline derives from the tree fence.
11
+ */
12
+
13
+ import type { Layer, TreeLayerItem } from '../../schemas/Doc.js';
14
+ import type { TreeBlockInput, TemplateContext } from '../../schemas/BlockTemplates.js';
15
+ import {
16
+ getThemeFont,
17
+ resolveColorScheme,
18
+ shouldUseShadow,
19
+ themedFontSize,
20
+ themedSurfaceGradient,
21
+ } from '../utils/themeUtils.js';
22
+ import { withAlpha } from '../../schemas/colorUtils.js';
23
+ import { createBackgroundLayer } from './captionUtils.js';
24
+
25
+ /** Defensive coercion of untyped `templateData.items` into valid tree items. */
26
+ function coerceItems(raw: unknown): TreeLayerItem[] {
27
+ if (!Array.isArray(raw)) return [];
28
+ const out: TreeLayerItem[] = [];
29
+ for (const r of raw) {
30
+ if (typeof r !== 'object' || r === null) continue;
31
+ const item = r as Record<string, unknown>;
32
+ if (typeof item.label !== 'string') continue;
33
+ const children = coerceItems(item.children);
34
+ out.push({
35
+ id: typeof item.id === 'string' && item.id ? item.id : slug(item.label),
36
+ label: item.label,
37
+ children,
38
+ ...(item.isDir === true || children.length > 0 ? { isDir: true } : {}),
39
+ ...(typeof item.comment === 'string' && item.comment ? { comment: item.comment } : {}),
40
+ });
41
+ }
42
+ return out;
43
+ }
44
+
45
+ function slug(s: string): string {
46
+ return (
47
+ s
48
+ .toLowerCase()
49
+ .replace(/[^a-z0-9]+/g, '-')
50
+ .replace(/^-+|-+$/g, '') || 'node'
51
+ );
52
+ }
53
+
54
+ export function treeBlock(input: TreeBlockInput, context: TemplateContext): Layer[] {
55
+ const { theme, viewport } = context;
56
+ const colors = resolveColorScheme(context, input.colorScheme);
57
+ const items = coerceItems(input.items);
58
+
59
+ const titleFontSize = themedFontSize(48, context, true);
60
+ const rowFontSize = themedFontSize(28, context, false);
61
+
62
+ const layers: Layer[] = [createBackgroundLayer('bg', themedSurfaceGradient(context, 170))];
63
+
64
+ if (items.length === 0) {
65
+ layers.push({
66
+ type: 'text',
67
+ id: 'tree-empty',
68
+ content: {
69
+ text: input.title ?? 'Empty tree',
70
+ style: {
71
+ fontSize: titleFontSize,
72
+ fontFamily: getThemeFont(context, 'title'),
73
+ color: theme.colors.textMuted,
74
+ textAlign: 'center',
75
+ },
76
+ },
77
+ position: { x: '50%', y: '50%', anchor: 'center' },
78
+ });
79
+ return layers;
80
+ }
81
+
82
+ const titleBandPct = input.title ? (titleFontSize * 2.2 * 100) / viewport.height : 0;
83
+
84
+ if (input.title) {
85
+ layers.push({
86
+ type: 'text',
87
+ id: 'title',
88
+ content: {
89
+ text: input.title,
90
+ style: {
91
+ fontSize: titleFontSize,
92
+ fontFamily: getThemeFont(context, 'title'),
93
+ fontWeight: 'bold',
94
+ color: theme.colors.text,
95
+ textAlign: 'center',
96
+ shadow: shouldUseShadow(context),
97
+ },
98
+ },
99
+ position: { x: '50%', y: `${8 + titleBandPct / 2}%`, width: '80%', anchor: 'center' },
100
+ animation: { type: 'fadeIn', duration: 0.8 },
101
+ });
102
+ }
103
+
104
+ layers.push({
105
+ type: 'tree',
106
+ id: 'tree',
107
+ content: {
108
+ items,
109
+ style: {
110
+ rowColor: theme.colors.text,
111
+ dirColor: colors.accent ?? theme.colors.primary,
112
+ connectorColor: withAlpha(theme.colors.text, 0.25),
113
+ iconColor: colors.accent ?? theme.colors.primary,
114
+ commentColor: theme.colors.textMuted,
115
+ fontSize: rowFontSize,
116
+ fontFamily: getThemeFont(context, 'body'),
117
+ monoFontFamily: getThemeFont(context, 'mono'),
118
+ indentPx: Math.round(rowFontSize * 1.4),
119
+ },
120
+ },
121
+ position: {
122
+ x: '10%',
123
+ y: `${8 + titleBandPct}%`,
124
+ width: '80%',
125
+ height: `${Math.max(60, 90 - titleBandPct)}%`,
126
+ },
127
+ animation: { type: 'fadeIn', duration: 1, delay: input.title ? 0.4 : 0 },
128
+ });
129
+
130
+ return layers;
131
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Character classification + render vocabularies for ASCII tree art.
3
+ *
4
+ * The parser accepts a superset (Unicode light/heavy/rounded branches plus
5
+ * ASCII `|-- ` / `` `-- `` / `+-- `); the renderer emits one `TreeVocab`.
6
+ * Internal to the treeview module.
7
+ */
8
+
9
+ /** Vertical rail glyphs (an ancestor line continuing downward). */
10
+ const RAIL_CHARS = new Set(['│', '┃', '╎', '╏', '║', '|']);
11
+ /** Branch-tee glyphs (a node that has a following sibling). */
12
+ const BRANCH_TEE = new Set(['├', '┣', '┠', '┝', '╠', '╞']);
13
+ /** Branch-elbow glyphs (the last child). */
14
+ const BRANCH_ELBOW = new Set(['└', '┗', '╰', '╚', '╘']);
15
+ /** Horizontal dash glyphs used in a branch arm. */
16
+ const DASH_CHARS = new Set(['─', '━', '═', '-']);
17
+
18
+ export function isRailChar(ch: string): boolean {
19
+ return RAIL_CHARS.has(ch);
20
+ }
21
+ export function isBranchTee(ch: string): boolean {
22
+ return BRANCH_TEE.has(ch);
23
+ }
24
+ export function isBranchElbow(ch: string): boolean {
25
+ return BRANCH_ELBOW.has(ch);
26
+ }
27
+ export function isUnicodeBranch(ch: string): boolean {
28
+ return BRANCH_TEE.has(ch) || BRANCH_ELBOW.has(ch);
29
+ }
30
+ export function isDashChar(ch: string): boolean {
31
+ return DASH_CHARS.has(ch);
32
+ }
33
+
34
+ /** Structural glyphs used for the style vote. */
35
+ export function isUnicodeTreeChar(ch: string): boolean {
36
+ return (
37
+ BRANCH_TEE.has(ch) ||
38
+ BRANCH_ELBOW.has(ch) ||
39
+ ch === '│' ||
40
+ ch === '┃' ||
41
+ ch === '─' ||
42
+ ch === '━'
43
+ );
44
+ }
45
+ export function isAsciiTreeChar(ch: string): boolean {
46
+ return ch === '|' || ch === '`' || ch === '+' || ch === '-';
47
+ }
48
+
49
+ export interface TreeVocab {
50
+ /** Branch to a node that has a later sibling (`├── `). */
51
+ tee: string;
52
+ /** Branch to the last child (`└── `). */
53
+ elbow: string;
54
+ /** Ancestor rail column (`│ `). */
55
+ rail: string;
56
+ /** Ended-ancestor gap column (` `). */
57
+ gap: string;
58
+ }
59
+
60
+ export const UNICODE_TREE_VOCAB: TreeVocab = {
61
+ tee: '├── ',
62
+ elbow: '└── ',
63
+ rail: '│ ',
64
+ gap: ' ',
65
+ };
66
+
67
+ export const ASCII_TREE_VOCAB: TreeVocab = {
68
+ tee: '|-- ',
69
+ elbow: '`-- ',
70
+ rail: '| ',
71
+ gap: ' ',
72
+ };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * ASCII tree detection: is this fence content a file-tree / outline?
3
+ *
4
+ * The mirror image of the diagram detector. A diagram has ≥2 closed boxes;
5
+ * a tree has ZERO closed boxes plus connector-branch structure. The two
6
+ * partition the `├──` glyph space cleanly, so a fence is at most one of
7
+ * them. Auto-detection is deliberately conservative (requires connector
8
+ * branches — the unambiguous AI file-tree signal); the parser still handles
9
+ * pure-indentation renditions for explicitly `{[tree]}`-annotated fences.
10
+ */
11
+
12
+ import type { MarkdownCodeBlock } from '../../markdown/types.js';
13
+ import { parseAsciiDiagramWithStats } from '../asciiDiagram/parse.js';
14
+ import { detectAsciiTimeline } from '../asciiTimeline/detect.js';
15
+ import { parseTreeWithStats } from './parse.js';
16
+ import type { TreeDetection } from './types.js';
17
+
18
+ /** Fence languages eligible for tree detection (plus no language at all). */
19
+ export const TREE_FENCE_LANGS: ReadonlySet<string> = new Set([
20
+ 'text',
21
+ 'txt',
22
+ 'plaintext',
23
+ 'plain',
24
+ 'ascii',
25
+ 'tree',
26
+ ]);
27
+
28
+ const MAX_LINES = 400;
29
+ const MIN_LINES = 2;
30
+ const MIN_CONNECTOR_LINES = 2;
31
+ const MIN_NODES = 2;
32
+
33
+ /**
34
+ * Box-drawing corners a file tree NEVER contains: top-left/right and
35
+ * bottom-right in light, heavy, double, and rounded weights. Bottom-LEFT
36
+ * corners (└ ┗ ╰ ╚ ╘) are excluded — the tree codec uses them as elbows.
37
+ */
38
+ const BOX_CORNER_RE = /[┌┐┘┏┓┛╔╗╝╭╮╯]/u;
39
+
40
+ export function isEligibleTreeFenceLang(lang: string | null | undefined): boolean {
41
+ if (lang === null || lang === undefined) return true;
42
+ const normalized = lang.trim().toLowerCase();
43
+ return normalized === '' || TREE_FENCE_LANGS.has(normalized);
44
+ }
45
+
46
+ /**
47
+ * True when the fence LANGUAGE is the explicit `tree` tag — an author-set
48
+ * "this is a tree" marker that survives markdown ↔ Tiptap round-trips (the
49
+ * language class round-trips; fence meta does not). Detection of a tagged
50
+ * fence is lenient (no connector requirement) so a flattened/degenerate
51
+ * tree stays a tree.
52
+ */
53
+ export function isExplicitTreeLang(lang: string | null | undefined): boolean {
54
+ return typeof lang === 'string' && lang.trim().toLowerCase() === 'tree';
55
+ }
56
+
57
+ export interface DetectTreeOptions {
58
+ /** The fence is explicitly `tree`-tagged: skip the connector requirement. */
59
+ explicit?: boolean;
60
+ }
61
+
62
+ export function detectTree(text: string, opts: DetectTreeOptions = {}): TreeDetection {
63
+ const explicit = opts.explicit === true;
64
+ const lines = text.replace(/\r\n?/g, '\n').split('\n');
65
+ const nonBlank = lines.filter((l) => l.trim().length > 0);
66
+ // A `tree`-tagged fence can be a single-node tree (e.g. the outline edited
67
+ // down to one item); an untagged fence needs ≥2 lines to auto-detect.
68
+ const minLines = explicit ? 1 : MIN_LINES;
69
+ if (nonBlank.length < minLines) {
70
+ return { isTree: false, reasons: [`too-few-lines(${nonBlank.length})`] };
71
+ }
72
+ if (lines.length > MAX_LINES) {
73
+ return { isTree: false, reasons: [`too-many-lines(${lines.length})`] };
74
+ }
75
+
76
+ // Markdown/GFM table shape → not a tree (even when explicitly tagged; it
77
+ // wouldn't parse into a sensible hierarchy anyway).
78
+ const pipeRows = nonBlank.filter((l) => /^\s*\|.*\|\s*$/.test(l)).length;
79
+ const hasGfmSeparator = nonBlank.some((l) => /^\s*\|?[\s:|-]+\|?\s*$/.test(l) && l.includes('-'));
80
+ if (pipeRows > nonBlank.length / 2 && hasGfmSeparator) {
81
+ return { isTree: false, reasons: ['markdown-table'] };
82
+ }
83
+
84
+ // Box-drawing CORNERS are an unambiguous box-diagram signal. A file tree /
85
+ // outline is built only from tees (├), elbows (└ ┗ ╰), rails (│), and dashes
86
+ // (─) — it never contains a TOP corner (┌ ┐) or a BOTTOM-RIGHT corner (┘).
87
+ // A fence with those is a diagram, even a MALFORMED one whose borders are
88
+ // misaligned so the tracer finds <2 closed boxes (e.g. hand-drawn art
89
+ // knocked out of alignment by a later rename that widened the labels).
90
+ // Without this, such art falls through to a mangled treeview. (Bottom-LEFT
91
+ // corners └ ┗ ╰ ╚ ╘ are deliberately excluded — they double as tree elbows.)
92
+ if (BOX_CORNER_RE.test(text)) {
93
+ return { isTree: false, reasons: ['has-box-corners'] };
94
+ }
95
+
96
+ // Timeline callout elbows can resemble tree branches. Give a qualifying
97
+ // marker+axis timeline ownership before the loose tree parser sees it.
98
+ if (!explicit && detectAsciiTimeline(text).isTimeline) {
99
+ return { isTree: false, reasons: ['is-timeline'] };
100
+ }
101
+
102
+ // Mutual exclusion with the diagram codec: a fence with ≥2 closed boxes is
103
+ // a diagram, not a tree.
104
+ const boxes = parseAsciiDiagramWithStats(text).diagram.nodes.length;
105
+ if (boxes >= 2) {
106
+ return { isTree: false, reasons: [`has-boxes(${boxes})`] };
107
+ }
108
+
109
+ const { tree, stats } = parseTreeWithStats(text);
110
+ const nodeCount = countNodes(tree.roots);
111
+ const minNodes = explicit ? 1 : MIN_NODES;
112
+ if (nodeCount < minNodes) {
113
+ return { isTree: false, reasons: [`too-few-nodes(${nodeCount})`] };
114
+ }
115
+ if (!explicit) {
116
+ // Auto-detection of an UNTAGGED fence requires the connector signal
117
+ // (pure-indentation art could be prose or a list). A `tree`-tagged fence
118
+ // trusts the author and skips this.
119
+ if (stats.connectorLines < MIN_CONNECTOR_LINES) {
120
+ return { isTree: false, reasons: [`too-few-connectors(${stats.connectorLines})`] };
121
+ }
122
+ if (stats.looseLines > stats.contentLines * 0.25) {
123
+ return { isTree: false, reasons: [`loose-ratio(${stats.looseLines}/${stats.contentLines})`] };
124
+ }
125
+ }
126
+
127
+ return {
128
+ isTree: true,
129
+ tree,
130
+ reasons: [`nodes(${nodeCount})`, explicit ? 'explicit' : `connectors(${stats.connectorLines})`],
131
+ };
132
+ }
133
+
134
+ /** Convenience gate for pipeline callers: lang allowlist + content detection. */
135
+ export function isTreeFence(node: MarkdownCodeBlock): boolean {
136
+ if (node.type !== 'code') return false;
137
+ if (!isEligibleTreeFenceLang(node.lang)) return false;
138
+ return detectTree(node.value, { explicit: isExplicitTreeLang(node.lang) }).isTree;
139
+ }
140
+
141
+ function countNodes(nodes: readonly import('./types.js').TreeNode[]): number {
142
+ let n = 0;
143
+ for (const node of nodes) n += 1 + countNodes(node.children);
144
+ return n;
145
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * ASCII treeview codec: detect / parse / render file-tree & outline art,
3
+ * plus mapping helpers between the tree model and the template/layer data.
4
+ *
5
+ * The fence text is the source of truth end-to-end: the editor parses it
6
+ * live for an interactive outline and re-renders it after each edit; the
7
+ * doc pipeline derives `templateData.items` from it so the player renders
8
+ * the same tree. Peer to the ASCII diagram codec.
9
+ */
10
+
11
+ export { parseTree, parseTreeWithStats } from './parse.js';
12
+ export type { TreeLineStats } from './parse.js';
13
+ export { renderTree } from './render.js';
14
+ export type { RenderTreeOptions } from './render.js';
15
+ export {
16
+ detectTree,
17
+ isTreeFence,
18
+ isEligibleTreeFenceLang,
19
+ isExplicitTreeLang,
20
+ TREE_FENCE_LANGS,
21
+ } from './detect.js';
22
+ export type { DetectTreeOptions } from './detect.js';
23
+ export {
24
+ treeToTemplateData,
25
+ treeFromTemplateData,
26
+ treeFromMarkdownList,
27
+ findFirstList,
28
+ } from './mapping.js';
29
+ export type { TreeItem } from './mapping.js';
30
+ export type { Tree, TreeNode, TreeDetection } from './types.js';
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Mapping between the tree model and the template/layer data model, plus a
3
+ * nested-markdown-list → tree walker (none exists in core — `extractListItems`
4
+ * flattens, losing depth).
5
+ */
6
+
7
+ import type { MarkdownBlockNode, MarkdownList, MarkdownListItem } from '../../markdown/types.js';
8
+ import { extractPlainText } from '../../markdown/utils.js';
9
+ import type { Tree, TreeNode } from './types.js';
10
+
11
+ /** JSON-serializable tree item carried on `templateData.items` / a `TreeLayer`. */
12
+ export interface TreeItem {
13
+ id: string;
14
+ label: string;
15
+ isDir?: boolean;
16
+ comment?: string;
17
+ children: TreeItem[];
18
+ }
19
+
20
+ /** Tree model → template/layer items (structure-preserving). */
21
+ export function treeToTemplateData(tree: Tree): { items: TreeItem[] } {
22
+ return { items: tree.roots.map(toItem) };
23
+ }
24
+
25
+ function toItem(node: TreeNode): TreeItem {
26
+ return {
27
+ id: node.id,
28
+ label: node.label,
29
+ ...(node.isDir ? { isDir: true } : {}),
30
+ ...(node.comment ? { comment: node.comment } : {}),
31
+ children: node.children.map(toItem),
32
+ };
33
+ }
34
+
35
+ /** Template/layer items → tree model (editor write-back / render). */
36
+ export function treeFromTemplateData(
37
+ items: readonly TreeItem[],
38
+ options: { style?: 'unicode' | 'ascii' } = {},
39
+ ): Tree {
40
+ const roots = items.map(fromItem);
41
+ return { roots, style: options.style ?? 'unicode', warnings: [] };
42
+ }
43
+
44
+ function fromItem(item: TreeItem): TreeNode {
45
+ const children = Array.isArray(item.children) ? item.children.map(fromItem) : [];
46
+ return {
47
+ id: typeof item.id === 'string' && item.id ? item.id : 'node',
48
+ label: typeof item.label === 'string' ? item.label : '',
49
+ children,
50
+ isDir: item.isDir === true || children.length > 0,
51
+ ...(item.comment ? { comment: item.comment } : {}),
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Recursively convert a nested markdown bullet list into a tree. Used by
57
+ * `deriveTemplateInputs` when an author explicitly `{[tree]}`-annotates a
58
+ * block whose body is a nested list rather than a fence.
59
+ */
60
+ export function treeFromMarkdownList(list: MarkdownList): Tree {
61
+ const usedIds = new Map<string, number>();
62
+ const slugify = (s: string): string =>
63
+ s
64
+ .toLowerCase()
65
+ .replace(/[^a-z0-9]+/g, '-')
66
+ .replace(/^-+|-+$/g, '') || 'node';
67
+ const idFor = (label: string): string => {
68
+ const base = slugify(label.replace(/\/$/, ''));
69
+ const count = usedIds.get(base) ?? 0;
70
+ usedIds.set(base, count + 1);
71
+ return count === 0 ? base : `${base}-${count + 1}`;
72
+ };
73
+
74
+ const itemToNode = (item: MarkdownListItem): TreeNode => {
75
+ // The item's own label = its first non-list block's plain text.
76
+ const labelNode = item.children.find((c) => c.type !== 'list');
77
+ const label = labelNode ? extractPlainText(labelNode).trim() : '';
78
+ const childLists = item.children.filter((c): c is MarkdownList => c.type === 'list');
79
+ const children: TreeNode[] = [];
80
+ for (const cl of childLists) for (const ci of cl.children) children.push(itemToNode(ci));
81
+ return {
82
+ id: idFor(label),
83
+ label,
84
+ children,
85
+ isDir: label.endsWith('/') || children.length > 0,
86
+ };
87
+ };
88
+
89
+ return { roots: list.children.map(itemToNode), style: 'unicode', warnings: [] };
90
+ }
91
+
92
+ /** Find the first top-level markdown list in a block's body, if any. */
93
+ export function findFirstList(contents: MarkdownBlockNode[] | undefined): MarkdownList | undefined {
94
+ return contents?.find((n): n is MarkdownList => n.type === 'list');
95
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * ASCII tree parser: file-tree / outline art → nested nodes.
3
+ *
4
+ * Pure, deterministic, never throws. Handles the many renditions AI emits:
5
+ * Unicode connectors (`├── `/`└── `/`│ `), ASCII (`|-- `/`` `-- ``/`+-- `),
6
+ * bullet-indented (`- `/`* `), and pure indentation. Depth is recovered by
7
+ * finding where each line's LABEL begins (after any connector/indent
8
+ * prefix) and running the standard outline indent-stack — this unifies all
9
+ * renditions and tolerates ragged dedents.
10
+ */
11
+
12
+ import {
13
+ isBranchElbow,
14
+ isBranchTee,
15
+ isDashChar,
16
+ isRailChar,
17
+ isAsciiTreeChar,
18
+ isUnicodeTreeChar,
19
+ } from './chars.js';
20
+ import type { Tree, TreeNode } from './types.js';
21
+
22
+ /** Internal per-line analysis fed to the reconstruction stack. */
23
+ export interface TreeLineStats {
24
+ /** Non-blank content lines. */
25
+ contentLines: number;
26
+ /** Lines carrying a connector branch (`├──`/`└──`/`|--`/`` `--``/`+--`). */
27
+ connectorLines: number;
28
+ /** Lines that could not be analyzed as a node (loose text). */
29
+ looseLines: number;
30
+ /** Distinct label-start columns observed (a hierarchy has ≥2). */
31
+ indentLevels: number;
32
+ }
33
+
34
+ interface AnalyzedLine {
35
+ indent: number; // column where the label begins
36
+ label: string;
37
+ comment?: string;
38
+ hadConnector: boolean;
39
+ }
40
+
41
+ export function parseTree(text: string): Tree {
42
+ return parseTreeWithStats(text).tree;
43
+ }
44
+
45
+ export function parseTreeWithStats(text: string): { tree: Tree; stats: TreeLineStats } {
46
+ const warnings: string[] = [];
47
+ const rawLines = text.replace(/\r\n?/g, '\n').split('\n');
48
+
49
+ const style = detectStyle(rawLines);
50
+ const stats: TreeLineStats = {
51
+ contentLines: 0,
52
+ connectorLines: 0,
53
+ looseLines: 0,
54
+ indentLevels: 0,
55
+ };
56
+ const indentSet = new Set<number>();
57
+
58
+ const analyzed: AnalyzedLine[] = [];
59
+ for (const raw of rawLines) {
60
+ if (raw.trim().length === 0) continue;
61
+ stats.contentLines++;
62
+ const a = analyzeLine(raw);
63
+ if (!a || a.label.length === 0) {
64
+ stats.looseLines++;
65
+ continue;
66
+ }
67
+ if (a.hadConnector) stats.connectorLines++;
68
+ indentSet.add(a.indent);
69
+ analyzed.push(a);
70
+ }
71
+ stats.indentLevels = indentSet.size;
72
+
73
+ // ---- Reconstruct the hierarchy via an indent stack --------------------
74
+ const roots: TreeNode[] = [];
75
+ const stack: Array<{ indent: number; node: TreeNode }> = [];
76
+ for (const a of analyzed) {
77
+ const node: TreeNode = {
78
+ id: '',
79
+ label: a.label,
80
+ children: [],
81
+ ...(a.comment ? { comment: a.comment } : {}),
82
+ };
83
+ // Pop ancestors that are at the same or deeper indent.
84
+ while (stack.length > 0 && stack[stack.length - 1].indent >= a.indent) stack.pop();
85
+ if (stack.length === 0) {
86
+ roots.push(node);
87
+ } else {
88
+ stack[stack.length - 1].node.children.push(node);
89
+ }
90
+ stack.push({ indent: a.indent, node });
91
+ }
92
+
93
+ // ---- Post-pass: isDir + stable ids ------------------------------------
94
+ const usedIds = new Map<string, number>();
95
+ const finalize = (node: TreeNode): void => {
96
+ node.isDir = node.label.endsWith('/') || node.children.length > 0;
97
+ const base = slugify(stripTrailingSlash(node.label));
98
+ const count = usedIds.get(base) ?? 0;
99
+ usedIds.set(base, count + 1);
100
+ node.id = count === 0 ? base : `${base}-${count + 1}`;
101
+ for (const child of node.children) finalize(child);
102
+ };
103
+ for (const root of roots) finalize(root);
104
+
105
+ if (analyzed.length > 0 && stats.looseLines > 0) {
106
+ warnings.push(`loose-lines(${stats.looseLines})`);
107
+ }
108
+
109
+ return { tree: { roots, style, warnings }, stats };
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Line analysis
114
+ // ---------------------------------------------------------------------------
115
+
116
+ /**
117
+ * Strip a line's connector / indent prefix and return the column where the
118
+ * label begins plus the cleaned label + trailing comment. Returns null for
119
+ * a rails-only spacer line.
120
+ */
121
+ function analyzeLine(line: string): AnalyzedLine | null {
122
+ const chars = Array.from(line);
123
+ let i = 0;
124
+ let hadConnector = false;
125
+
126
+ // Consume leading whitespace + vertical rails. A rail is a rail glyph NOT
127
+ // immediately followed by a dash (which would make it an ASCII branch).
128
+ for (;;) {
129
+ while (i < chars.length && (chars[i] === ' ' || chars[i] === '\t')) i++;
130
+ const ch = chars[i];
131
+ if (ch === undefined) return null; // blank / rails-only
132
+ if (isRailChar(ch) && !isDashChar(chars[i + 1] ?? '')) {
133
+ i++;
134
+ continue;
135
+ }
136
+ break;
137
+ }
138
+
139
+ // Optional branch: a Unicode tee/elbow + dashes, or ASCII `|`/`` ` ``/`+`
140
+ // + dashes. The label follows (after any trailing spaces).
141
+ const ch = chars[i];
142
+ if (ch !== undefined) {
143
+ if (isBranchTee(ch) || isBranchElbow(ch)) {
144
+ i++;
145
+ while (i < chars.length && (isDashChar(chars[i]) || isRailChar(chars[i]))) i++;
146
+ hadConnector = true;
147
+ } else if ((ch === '|' || ch === '`' || ch === '+') && isDashChar(chars[i + 1] ?? '')) {
148
+ i++;
149
+ while (i < chars.length && isDashChar(chars[i])) i++;
150
+ hadConnector = true;
151
+ } else if ((ch === '-' || ch === '*' || ch === '+') && (chars[i + 1] ?? '') === ' ') {
152
+ // Markdown-style bullet (indent-based rendition).
153
+ i++;
154
+ hadConnector = false;
155
+ }
156
+ }
157
+ while (i < chars.length && (chars[i] === ' ' || chars[i] === '\t')) i++;
158
+
159
+ const indent = i;
160
+ let label = chars.slice(i).join('').replace(/\s+$/u, '');
161
+ if (label.length === 0) return null;
162
+
163
+ // Trailing comment: ` # …`, ` <-- …`, or ` // …` (needs a leading space).
164
+ let comment: string | undefined;
165
+ const commentMatch = /\s+(?:#|<--|\/\/)\s?(.*)$/.exec(label);
166
+ if (commentMatch) {
167
+ comment = commentMatch[1].trim() || undefined;
168
+ label = label.slice(0, commentMatch.index).replace(/\s+$/u, '');
169
+ }
170
+ if (label.length === 0) return null;
171
+
172
+ return { indent, label, hadConnector, ...(comment ? { comment } : {}) };
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Helpers
177
+ // ---------------------------------------------------------------------------
178
+
179
+ function detectStyle(lines: string[]): 'unicode' | 'ascii' {
180
+ let unicode = 0;
181
+ let ascii = 0;
182
+ for (const line of lines) {
183
+ for (const ch of line) {
184
+ if (isUnicodeTreeChar(ch)) unicode++;
185
+ else if (isAsciiTreeChar(ch)) ascii++;
186
+ }
187
+ }
188
+ return ascii > unicode ? 'ascii' : 'unicode';
189
+ }
190
+
191
+ function stripTrailingSlash(s: string): string {
192
+ return s.endsWith('/') ? s.slice(0, -1) : s;
193
+ }
194
+
195
+ function slugify(s: string): string {
196
+ const slug = s
197
+ .toLowerCase()
198
+ .replace(/[^a-z0-9]+/g, '-')
199
+ .replace(/^-+|-+$/g, '');
200
+ return slug || 'node';
201
+ }