@diagrammo/dgmo 0.47.0 → 0.48.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 (345) hide show
  1. package/dist/advanced.js +11 -11
  2. package/dist/auto.js +16 -16
  3. package/dist/auto.mjs +41 -12
  4. package/dist/block.js +4 -4
  5. package/dist/{chunk-FNB3PNGF.js → chunk-7DTWEFUE.js} +3 -3
  6. package/dist/{chunk-2DXMZRWJ.js → chunk-B65G5DNL.js} +8 -1
  7. package/dist/{chunk-RZCF2VVM.js → chunk-B7JUCQUA.js} +3 -1
  8. package/dist/{chunk-DU7W62N5.js → chunk-FANX6UJR.js} +3 -3
  9. package/dist/{chunk-6FZYCKXB.js → chunk-HSH73DP6.js} +2 -2
  10. package/dist/{chunk-P4RH4OSP.js → chunk-ILKORZ3H.js} +2 -2
  11. package/dist/{chunk-Y5QGVTVC.js → chunk-JTSMEYIZ.js} +7 -3
  12. package/dist/{chunk-RCERWOJR.js → chunk-ODZVHU3R.js} +1 -2
  13. package/dist/{chunk-DZAQBW35.js → chunk-SGZYXELT.js} +3 -7
  14. package/dist/{chunk-CUVHQHZ7.js → chunk-THXCFRTU.js} +3 -3
  15. package/dist/{chunk-IH6ZMVQ5.js → chunk-UC7TFHUM.js} +29 -11
  16. package/dist/{chunk-EMS2SK26.js → chunk-ZBWMSD5N.js} +3 -1
  17. package/dist/cli.cjs +15 -15
  18. package/dist/{dgmo-router-DZCIDUO5.js → dgmo-router-X35VLZLM.js} +3 -3
  19. package/dist/element.js +16 -16
  20. package/dist/element.mjs +41 -12
  21. package/dist/{flowchart-renderer-ZAEOL4DX.js → flowchart-renderer-SNDSII4D.js} +2 -2
  22. package/dist/index.d.ts +4 -2
  23. package/dist/index.js +4 -4
  24. package/dist/{parser-FUSKFGZN.js → parser-2EWECDSA.js} +2 -2
  25. package/dist/{renderer-UWA5XS74.js → renderer-4JHRUBE6.js} +3 -3
  26. package/dist/{renderer-WZRREQ2P.js → renderer-5MYBMBYO.js} +2 -2
  27. package/dist/{renderer-TOCOVLHS.js → renderer-B2OMQIQG.js} +3 -3
  28. package/dist/{renderer-2XQQQ6QR.js → renderer-N43WR5GV.js} +3 -3
  29. package/dist/{renderer-T7YKC5VP.js → renderer-ZRNG54CW.js} +3 -3
  30. package/dist/{state-renderer-RFYHSP4K.js → state-renderer-B4DQBM6M.js} +2 -2
  31. package/package.json +23 -71
  32. package/dist/advanced.cjs +0 -71648
  33. package/dist/advanced.d.cts +0 -5719
  34. package/dist/auto.cjs +0 -70648
  35. package/dist/auto.d.cts +0 -54
  36. package/dist/block.cjs +0 -70208
  37. package/dist/block.d.cts +0 -115
  38. package/dist/editor.cjs +0 -526
  39. package/dist/editor.d.cts +0 -26
  40. package/dist/element.cjs +0 -70530
  41. package/dist/element.d.cts +0 -62
  42. package/dist/highlight.cjs +0 -852
  43. package/dist/highlight.d.cts +0 -49
  44. package/dist/index.cjs +0 -70491
  45. package/dist/index.d.cts +0 -160
  46. package/dist/pert.cjs +0 -325
  47. package/dist/pert.d.cts +0 -559
  48. package/dist/themes-1-CuKpeH.d.cts +0 -908
  49. package/src/advanced.ts +0 -873
  50. package/src/arc/parser.ts +0 -15
  51. package/src/arc/renderer.ts +0 -633
  52. package/src/auto/index.ts +0 -621
  53. package/src/auto/shared.ts +0 -370
  54. package/src/auto/styles.ts +0 -73
  55. package/src/block/layout.ts +0 -198
  56. package/src/block/parser.ts +0 -427
  57. package/src/block/renderer.ts +0 -484
  58. package/src/block/types.ts +0 -67
  59. package/src/boxes-and-lines/collapse.ts +0 -96
  60. package/src/boxes-and-lines/focus.ts +0 -257
  61. package/src/boxes-and-lines/label-placement.ts +0 -339
  62. package/src/boxes-and-lines/layout-grouped.ts +0 -912
  63. package/src/boxes-and-lines/layout-layered.ts +0 -722
  64. package/src/boxes-and-lines/layout-search.ts +0 -1678
  65. package/src/boxes-and-lines/layout.ts +0 -398
  66. package/src/boxes-and-lines/parser.ts +0 -1181
  67. package/src/boxes-and-lines/renderer.ts +0 -1465
  68. package/src/boxes-and-lines/types.ts +0 -66
  69. package/src/c4/layout.ts +0 -2441
  70. package/src/c4/parser.ts +0 -1158
  71. package/src/c4/renderer.ts +0 -2008
  72. package/src/c4/types.ts +0 -89
  73. package/src/chart-type-registry.ts +0 -529
  74. package/src/chart-types.ts +0 -225
  75. package/src/chart.ts +0 -685
  76. package/src/charts-d3/bar.ts +0 -338
  77. package/src/charts-d3/chord.ts +0 -140
  78. package/src/charts-d3/function.ts +0 -147
  79. package/src/charts-d3/funnel.ts +0 -104
  80. package/src/charts-d3/heatmap.ts +0 -145
  81. package/src/charts-d3/index.ts +0 -337
  82. package/src/charts-d3/interactions.ts +0 -477
  83. package/src/charts-d3/line.ts +0 -344
  84. package/src/charts-d3/mount.ts +0 -151
  85. package/src/charts-d3/pie.ts +0 -117
  86. package/src/charts-d3/polar.ts +0 -89
  87. package/src/charts-d3/radar.ts +0 -0
  88. package/src/charts-d3/sankey.ts +0 -0
  89. package/src/charts-d3/scatter.ts +0 -185
  90. package/src/charts-d3/shared.ts +0 -208
  91. package/src/class/layout.ts +0 -326
  92. package/src/class/parser.ts +0 -635
  93. package/src/class/renderer.ts +0 -793
  94. package/src/class/types.ts +0 -58
  95. package/src/cli-banner.ts +0 -106
  96. package/src/cli.ts +0 -1080
  97. package/src/colors-diagnostics.ts +0 -35
  98. package/src/colors.ts +0 -379
  99. package/src/completion-types.ts +0 -27
  100. package/src/completion.ts +0 -2668
  101. package/src/cycle/layout.ts +0 -1026
  102. package/src/cycle/parser.ts +0 -387
  103. package/src/cycle/renderer.ts +0 -653
  104. package/src/cycle/types.ts +0 -101
  105. package/src/d3.ts +0 -1434
  106. package/src/data-chart-parser.ts +0 -1007
  107. package/src/dgmo-router.ts +0 -296
  108. package/src/diagnostics-registry.ts +0 -108
  109. package/src/diagnostics.ts +0 -0
  110. package/src/dimensions.ts +0 -20
  111. package/src/directives-registry.ts +0 -396
  112. package/src/editor/dgmo.grammar +0 -76
  113. package/src/editor/dgmo.grammar.d.ts +0 -2
  114. package/src/editor/dgmo.grammar.js +0 -18
  115. package/src/editor/dgmo.grammar.terms.d.ts +0 -5
  116. package/src/editor/dgmo.grammar.terms.js +0 -35
  117. package/src/editor/highlight-api.ts +0 -596
  118. package/src/editor/highlight.ts +0 -36
  119. package/src/editor/index.ts +0 -27
  120. package/src/editor/keywords.ts +0 -128
  121. package/src/editor/tokens.ts +0 -30
  122. package/src/element/index.ts +0 -432
  123. package/src/embed/css.ts +0 -312
  124. package/src/embed/escape.ts +0 -15
  125. package/src/embed/index.ts +0 -280
  126. package/src/er/classify.ts +0 -228
  127. package/src/er/layout.ts +0 -530
  128. package/src/er/parser.ts +0 -750
  129. package/src/er/renderer.ts +0 -725
  130. package/src/er/types.ts +0 -52
  131. package/src/error-card.ts +0 -269
  132. package/src/event-line/diagnostics.ts +0 -117
  133. package/src/event-line/parser.ts +0 -643
  134. package/src/event-line/renderer.ts +0 -1742
  135. package/src/event-line/types.ts +0 -74
  136. package/src/fonts.ts +0 -3
  137. package/src/gantt/calculator.ts +0 -979
  138. package/src/gantt/parser.ts +0 -1641
  139. package/src/gantt/renderer.ts +0 -3787
  140. package/src/gantt/resolver.ts +0 -172
  141. package/src/gantt/types.ts +0 -206
  142. package/src/graph/diagnostics.ts +0 -64
  143. package/src/graph/edge-spline.ts +0 -29
  144. package/src/graph/flowchart-parser.ts +0 -759
  145. package/src/graph/flowchart-renderer.ts +0 -817
  146. package/src/graph/layout.ts +0 -472
  147. package/src/graph/notes.ts +0 -21
  148. package/src/graph/state-collapse.ts +0 -78
  149. package/src/graph/state-parser.ts +0 -574
  150. package/src/graph/state-renderer.ts +0 -653
  151. package/src/graph/types.ts +0 -65
  152. package/src/index.ts +0 -261
  153. package/src/infra/compute.ts +0 -1385
  154. package/src/infra/layout.ts +0 -888
  155. package/src/infra/parser.ts +0 -1234
  156. package/src/infra/renderer.ts +0 -2534
  157. package/src/infra/roles.ts +0 -75
  158. package/src/infra/types.ts +0 -216
  159. package/src/infra/validation.ts +0 -201
  160. package/src/journey-map/layout.ts +0 -490
  161. package/src/journey-map/parser.ts +0 -557
  162. package/src/journey-map/renderer.ts +0 -1580
  163. package/src/journey-map/types.ts +0 -47
  164. package/src/kanban/mutations.ts +0 -175
  165. package/src/kanban/parser.ts +0 -509
  166. package/src/kanban/renderer.ts +0 -1449
  167. package/src/kanban/types.ts +0 -39
  168. package/src/label-layout.ts +0 -368
  169. package/src/map/colorize.ts +0 -54
  170. package/src/map/completion.ts +0 -316
  171. package/src/map/context-labels.ts +0 -609
  172. package/src/map/data/PROVENANCE.json +0 -1
  173. package/src/map/data/README.md +0 -40
  174. package/src/map/data/airport-collisions.json +0 -1
  175. package/src/map/data/airports.json +0 -1
  176. package/src/map/data/gazetteer.json +0 -1
  177. package/src/map/data/lakes.json +0 -1
  178. package/src/map/data/mountain-ranges.json +0 -1
  179. package/src/map/data/na-lakes.json +0 -1
  180. package/src/map/data/na-land.json +0 -1
  181. package/src/map/data/region-names.json +0 -1
  182. package/src/map/data/rivers.json +0 -1
  183. package/src/map/data/types.ts +0 -121
  184. package/src/map/data/us-states.json +0 -1
  185. package/src/map/data/water-bodies.json +0 -1
  186. package/src/map/data/world-coarse.json +0 -1
  187. package/src/map/data/world-detail.json +0 -1
  188. package/src/map/diagnostics.ts +0 -134
  189. package/src/map/dimensions.ts +0 -150
  190. package/src/map/geo-query.ts +0 -295
  191. package/src/map/geo.ts +0 -438
  192. package/src/map/invert.ts +0 -111
  193. package/src/map/layout.ts +0 -5069
  194. package/src/map/legend-band.ts +0 -127
  195. package/src/map/load-data.ts +0 -186
  196. package/src/map/parser.ts +0 -783
  197. package/src/map/renderer.ts +0 -1445
  198. package/src/map/resolved-types.ts +0 -160
  199. package/src/map/resolver.ts +0 -1146
  200. package/src/map/types.ts +0 -194
  201. package/src/mindmap/collapse.ts +0 -67
  202. package/src/mindmap/layout.ts +0 -649
  203. package/src/mindmap/parser.ts +0 -430
  204. package/src/mindmap/renderer.ts +0 -637
  205. package/src/mindmap/text-wrap.ts +0 -218
  206. package/src/mindmap/types.ts +0 -56
  207. package/src/org/collapse.ts +0 -151
  208. package/src/org/layout.ts +0 -1262
  209. package/src/org/parser.ts +0 -519
  210. package/src/org/renderer.ts +0 -866
  211. package/src/org/resolver.ts +0 -636
  212. package/src/palettes/atlas.ts +0 -77
  213. package/src/palettes/blueprint.ts +0 -73
  214. package/src/palettes/catppuccin.ts +0 -84
  215. package/src/palettes/color-utils.ts +0 -443
  216. package/src/palettes/index.ts +0 -67
  217. package/src/palettes/nord.ts +0 -67
  218. package/src/palettes/registry.ts +0 -114
  219. package/src/palettes/slate.ts +0 -73
  220. package/src/palettes/tidewater.ts +0 -73
  221. package/src/palettes/tokyo-night.ts +0 -86
  222. package/src/palettes/types.ts +0 -88
  223. package/src/pert/analyzer.ts +0 -1183
  224. package/src/pert/diagnostics.ts +0 -110
  225. package/src/pert/index.ts +0 -43
  226. package/src/pert/internal.ts +0 -252
  227. package/src/pert/layout.ts +0 -971
  228. package/src/pert/monte-carlo.ts +0 -446
  229. package/src/pert/parser.ts +0 -1606
  230. package/src/pert/renderer.ts +0 -4009
  231. package/src/pert/share-normalize.ts +0 -62
  232. package/src/pert/types.ts +0 -604
  233. package/src/pyramid/parser.ts +0 -185
  234. package/src/pyramid/renderer.ts +0 -768
  235. package/src/pyramid/types.ts +0 -28
  236. package/src/quadrant/parser.ts +0 -18
  237. package/src/quadrant/renderer.ts +0 -638
  238. package/src/raci/diagnostics.ts +0 -213
  239. package/src/raci/index.ts +0 -23
  240. package/src/raci/mutations.ts +0 -321
  241. package/src/raci/parser.ts +0 -699
  242. package/src/raci/renderer.ts +0 -1627
  243. package/src/raci/types.ts +0 -82
  244. package/src/raci/variants.ts +0 -320
  245. package/src/render.ts +0 -268
  246. package/src/ring/parser.ts +0 -212
  247. package/src/ring/renderer.ts +0 -490
  248. package/src/ring/types.ts +0 -27
  249. package/src/sequence/collapse.ts +0 -174
  250. package/src/sequence/parser.ts +0 -1596
  251. package/src/sequence/participant-inference.ts +0 -152
  252. package/src/sequence/renderer.ts +0 -3168
  253. package/src/sequence/tag-resolution.ts +0 -164
  254. package/src/sharing.ts +0 -211
  255. package/src/sitemap/collapse.ts +0 -100
  256. package/src/sitemap/layout.ts +0 -925
  257. package/src/sitemap/parser.ts +0 -755
  258. package/src/sitemap/renderer.ts +0 -828
  259. package/src/sitemap/types.ts +0 -42
  260. package/src/slope/parser.ts +0 -18
  261. package/src/slope/renderer.ts +0 -378
  262. package/src/swimlane/diagnostics.ts +0 -94
  263. package/src/swimlane/layout.ts +0 -597
  264. package/src/swimlane/parser.ts +0 -613
  265. package/src/swimlane/renderer.ts +0 -681
  266. package/src/swimlane/types.ts +0 -151
  267. package/src/tech-radar/interactive.ts +0 -1129
  268. package/src/tech-radar/layout.ts +0 -197
  269. package/src/tech-radar/parser.ts +0 -436
  270. package/src/tech-radar/renderer.ts +0 -1239
  271. package/src/tech-radar/shared.ts +0 -151
  272. package/src/tech-radar/types.ts +0 -86
  273. package/src/themes.ts +0 -22
  274. package/src/timeline/parser.ts +0 -472
  275. package/src/timeline/renderer.ts +0 -3024
  276. package/src/timeline/types.ts +0 -33
  277. package/src/timeline/viz-parser.ts +0 -21
  278. package/src/treemap/diagnostics.ts +0 -62
  279. package/src/treemap/layout-radial.ts +0 -233
  280. package/src/treemap/layout.ts +0 -189
  281. package/src/treemap/parser.ts +0 -463
  282. package/src/treemap/renderer-radial.ts +0 -438
  283. package/src/treemap/renderer.ts +0 -572
  284. package/src/treemap/treemap-shared.ts +0 -309
  285. package/src/treemap/types.ts +0 -60
  286. package/src/utils/arrow-markers.ts +0 -51
  287. package/src/utils/arrows-diagnostics.ts +0 -62
  288. package/src/utils/arrows.ts +0 -227
  289. package/src/utils/brand.ts +0 -61
  290. package/src/utils/card.ts +0 -183
  291. package/src/utils/collapse-engine/tree-edges.ts +0 -115
  292. package/src/utils/collapse-engine/tree.ts +0 -117
  293. package/src/utils/d3-helpers.ts +0 -219
  294. package/src/utils/d3-types.ts +0 -9
  295. package/src/utils/description-helpers.ts +0 -38
  296. package/src/utils/duration.ts +0 -312
  297. package/src/utils/export-container.ts +0 -41
  298. package/src/utils/expr-eval.ts +0 -27
  299. package/src/utils/fit-canvas.ts +0 -64
  300. package/src/utils/hover-styles.ts +0 -673
  301. package/src/utils/inline-markdown.ts +0 -96
  302. package/src/utils/legend-constants.ts +0 -48
  303. package/src/utils/legend-d3.ts +0 -618
  304. package/src/utils/legend-integration.ts +0 -107
  305. package/src/utils/legend-layout.ts +0 -776
  306. package/src/utils/legend-svg.ts +0 -214
  307. package/src/utils/legend-types.ts +0 -272
  308. package/src/utils/name-normalize.ts +0 -163
  309. package/src/utils/note-box/constants.ts +0 -25
  310. package/src/utils/note-box/index.ts +0 -11
  311. package/src/utils/note-box/metrics.ts +0 -90
  312. package/src/utils/note-box/svg.ts +0 -331
  313. package/src/utils/notes/bounds.ts +0 -30
  314. package/src/utils/notes/build.ts +0 -131
  315. package/src/utils/notes/index.ts +0 -18
  316. package/src/utils/notes/model.ts +0 -19
  317. package/src/utils/notes/parse.ts +0 -131
  318. package/src/utils/notes/place.ts +0 -177
  319. package/src/utils/notes/resolve.ts +0 -88
  320. package/src/utils/number-format.ts +0 -36
  321. package/src/utils/parsing.ts +0 -953
  322. package/src/utils/reserved-key-registry.ts +0 -239
  323. package/src/utils/safe-href.ts +0 -38
  324. package/src/utils/scaling.ts +0 -97
  325. package/src/utils/svg-embed.ts +0 -251
  326. package/src/utils/tag-groups.ts +0 -607
  327. package/src/utils/text-measure.ts +0 -122
  328. package/src/utils/time-ticks.ts +0 -253
  329. package/src/utils/title-constants.ts +0 -24
  330. package/src/utils/visual-conventions.ts +0 -61
  331. package/src/utils/wrapped-desc.ts +0 -87
  332. package/src/venn/parser.ts +0 -18
  333. package/src/venn/renderer.ts +0 -1047
  334. package/src/version-control/diagnostics.ts +0 -67
  335. package/src/version-control/parser.ts +0 -544
  336. package/src/version-control/renderer.ts +0 -724
  337. package/src/version-control/types.ts +0 -100
  338. package/src/visualizations/parse.ts +0 -1560
  339. package/src/visualizations/types.ts +0 -262
  340. package/src/wireframe/layout.ts +0 -479
  341. package/src/wireframe/parser.ts +0 -1114
  342. package/src/wireframe/renderer.ts +0 -1318
  343. package/src/wireframe/types.ts +0 -110
  344. package/src/wordcloud/parser.ts +0 -18
  345. package/src/wordcloud/renderer.ts +0 -357
package/src/completion.ts DELETED
@@ -1,2668 +0,0 @@
1
- /**
2
- * Diagram symbol extraction API + completion registry.
3
- *
4
- * Provides:
5
- * - DiagramSymbols interface + extractDiagramSymbols() dispatch
6
- * - COMPLETION_REGISTRY: chart-type → directives map (for editor autocomplete)
7
- * - CHART_TYPES: array of { name, description } for chart type completion
8
- * - METADATA_KEY_SET: derived set of all known directive keys
9
- *
10
- * Each diagram type registers its own extractor via registerExtractor().
11
- * All built-in extractors are registered at module init below.
12
- */
13
-
14
- import { extractSymbols as extractErSymbols } from './er/parser';
15
- import { extractSymbols as extractFlowchartSymbols } from './graph/flowchart-parser';
16
- import { extractSymbols as extractInfraSymbols } from './infra/parser';
17
- import { extractSymbols as extractClassSymbols } from './class/parser';
18
- import { extractPertSymbols } from './pert/parser';
19
- import {
20
- parseFirstLine,
21
- ALL_CHART_TYPES,
22
- measureIndent,
23
- } from './utils/parsing';
24
- import { RECOGNIZED_COLOR_NAMES } from './colors';
25
- // Closed enum sets owned by their respective parsers — imported (never
26
- // hand-copied) so completion can't drift from the grammar (one-oracle rule).
27
- import { ALL_MARKERS } from './raci/variants';
28
- import { STATE_KEYWORDS as WIREFRAME_STATE_KEYWORDS } from './wireframe/parser';
29
-
30
- const RECOGNIZED_COLOR_SET: ReadonlySet<string> = new Set(
31
- RECOGNIZED_COLOR_NAMES
32
- );
33
- // Read chart-type descriptions directly from the source-of-truth data
34
- // module instead of via dgmo-router.ts. dgmo-router imports every
35
- // parser, and the parsers (Class/ER/Infra/Pert/Flowchart) type-only
36
- // import DiagramSymbols back from this file — creating a hub of cycles
37
- // through completion ↔ dgmo-router. Going through chart-types.ts (a
38
- // leaf module with zero imports) breaks 7 of the 10 known cycles.
39
- import { chartTypes } from './chart-types';
40
-
41
- // ============================================================
42
- // Symbol extraction
43
- // ============================================================
44
-
45
- // Types live in ./completion-types so the chart-type parsers can
46
- // import them without taking a cycle through this file.
47
- import type { ChartType, DiagramSymbols, ExtractFn } from './completion-types';
48
- export type { ChartType, DiagramSymbols, ExtractFn };
49
-
50
- const extractorRegistry = new Map<ChartType, ExtractFn>();
51
-
52
- export function registerExtractor(kind: ChartType, fn: ExtractFn): void {
53
- extractorRegistry.set(kind, fn);
54
- }
55
-
56
- /**
57
- * Extract diagram symbols from document text.
58
- * Returns null if the chart type is unknown or has no registered extractor.
59
- */
60
- export function extractDiagramSymbols(docText: string): DiagramSymbols | null {
61
- // Parse chartType from first line — bare type name.
62
- let chartType: string | null = null;
63
- for (const line of docText.split('\n')) {
64
- const trimmed = line.trim();
65
- if (!trimmed || trimmed.startsWith('//')) continue;
66
- const result = parseFirstLine(trimmed);
67
- if (result) {
68
- chartType = result.chartType;
69
- }
70
- break; // only check the first non-empty, non-comment line
71
- }
72
- if (!chartType) return null;
73
- const fn = extractorRegistry.get(chartType);
74
- if (!fn) return null;
75
- const result = fn(docText);
76
- // Populate `aliases` uniformly for every chart type so downstream
77
- // editor surfaces don't need per-extractor branches.
78
- const aliases = extractAliasDeclarations(docText);
79
- return Object.keys(aliases).length > 0 ? { ...result, aliases } : result;
80
- }
81
-
82
- // ============================================================
83
- // Completion registry
84
- // ============================================================
85
-
86
- /** Specification for a single directive: description + optional enumerated values. */
87
- export interface DirectiveValueSpec {
88
- description: string;
89
- values?: string[];
90
- }
91
-
92
- /** Specification for a chart type's directives. */
93
- export interface DirectiveSpec {
94
- directives: Record<string, DirectiveValueSpec>;
95
- }
96
-
97
- // Global directives applied to every chart type
98
- const GLOBAL_DIRECTIVES: Record<string, DirectiveValueSpec> = {
99
- palette: {
100
- description: 'Color palette name',
101
- values: [
102
- 'nord',
103
- 'catppuccin',
104
- 'tokyo-night',
105
- 'atlas',
106
- 'blueprint',
107
- 'slate',
108
- 'tidewater',
109
- ],
110
- },
111
- theme: {
112
- description: 'Color theme',
113
- values: ['light', 'dark', 'transparent'],
114
- },
115
- 'no-title': {
116
- description: 'Hide the diagram title',
117
- },
118
- };
119
-
120
- function withGlobals(
121
- directives: Record<string, DirectiveValueSpec> = {}
122
- ): DirectiveSpec {
123
- return { directives: { ...GLOBAL_DIRECTIVES, ...directives } };
124
- }
125
-
126
- /** Chart-type → directive specifications. Every chart type has at least palette + theme. */
127
- export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
128
- // ── Data charts ──────────────────────────────────────────
129
- [
130
- 'bar',
131
- withGlobals({
132
- stack: {
133
- description:
134
- 'Multi-series block header → stacked bars (one bar/category)',
135
- },
136
- group: {
137
- description:
138
- 'Multi-series block header → clustered (side-by-side) bars',
139
- },
140
- 'x-label': { description: 'X-axis label' },
141
- 'y-label': { description: 'Y-axis label' },
142
- 'orientation-horizontal': { description: 'Switch to horizontal bars' },
143
- 'no-value': { description: 'Hide value labels atop each bar' },
144
- color: { description: 'Bar color override' },
145
- }),
146
- ],
147
- [
148
- 'line',
149
- withGlobals({
150
- series: { description: 'Series name(s)' },
151
- fill: { description: 'Fill under the line (area chart)' },
152
- 'x-label': { description: 'X-axis label' },
153
- 'y-label': { description: 'Y-axis label (left axis)' },
154
- 'y-right-label': {
155
- description: 'Right y-axis label (dual-axis; group series beneath it)',
156
- },
157
- 'no-value': { description: 'Hide value labels at each point' },
158
- }),
159
- ],
160
- [
161
- 'pie',
162
- withGlobals({
163
- hole: {
164
- description:
165
- 'Doughnut ring (optional ratio 0–0.9); shows the total in center',
166
- },
167
- 'no-center-total': {
168
- description: 'Hide the center total on a doughnut/hole pie',
169
- },
170
- 'no-name': { description: 'Hide name from segment labels' },
171
- 'no-value': { description: 'Hide value from segment labels' },
172
- 'no-percent': { description: 'Hide percent from segment labels' },
173
- }),
174
- ],
175
- [
176
- 'polar-area',
177
- withGlobals({
178
- 'no-name': { description: 'Hide name from segment labels' },
179
- 'no-value': { description: 'Hide value from segment labels' },
180
- 'no-percent': { description: 'Hide percent from segment labels' },
181
- }),
182
- ],
183
- [
184
- 'radar',
185
- withGlobals({
186
- 'no-value': { description: 'Hide value labels at each vertex' },
187
- }),
188
- ],
189
-
190
- // ── Extended charts ──────────────────────────────────────
191
- [
192
- 'scatter',
193
- withGlobals({
194
- 'no-name': { description: 'Hide point labels' },
195
- 'x-label': { description: 'X-axis label' },
196
- 'y-label': { description: 'Y-axis label' },
197
- 'size-label': { description: 'Size axis label' },
198
- }),
199
- ],
200
- [
201
- 'heatmap',
202
- withGlobals({
203
- columns: { description: 'Column labels (required)' },
204
- 'no-value': { description: 'Hide cell value text' },
205
- }),
206
- ],
207
- ['sankey', withGlobals()],
208
- [
209
- 'funnel',
210
- withGlobals({
211
- 'no-name': { description: 'Hide left-side name labels' },
212
- 'no-value': { description: 'Hide right-side value labels' },
213
- }),
214
- ],
215
- [
216
- 'function',
217
- withGlobals({
218
- x: { description: 'X-axis range (start to end)' },
219
- 'x-label': { description: 'X-axis label' },
220
- 'y-label': { description: 'Y-axis label' },
221
- shade: { description: 'Fill area below curves with translucent color' },
222
- }),
223
- ],
224
-
225
- // ── Visualizations ───────────────────────────────────────
226
- ['slope', withGlobals()],
227
- [
228
- 'wordcloud',
229
- withGlobals({
230
- rotate: {
231
- description: 'Word rotation',
232
- values: ['none', 'mixed', 'angled'],
233
- },
234
- max: { description: 'Maximum word count' },
235
- size: { description: 'Font size range (min, max)' },
236
- }),
237
- ],
238
- [
239
- 'arc',
240
- withGlobals({
241
- order: {
242
- description: 'Node ordering',
243
- values: ['appearance', 'name', 'group', 'degree'],
244
- },
245
- }),
246
- ],
247
- [
248
- 'timeline',
249
- withGlobals({
250
- 'active-tag': { description: 'Active tag group name' },
251
- }),
252
- ],
253
- [
254
- 'event-line',
255
- withGlobals({
256
- 'no-scale': { description: 'Space events evenly instead of by date' },
257
- side: {
258
- description: 'Card placement: side above | below (default alternate)',
259
- },
260
- 'no-box': {
261
- description: 'Card-less label/rule/description style (slides)',
262
- },
263
- 'no-legend': { description: 'Hide the tag legend' },
264
- TBD: {
265
- description:
266
- 'Date prefix for a not-yet-scheduled future event (e.g. "TBD Console Port")',
267
- },
268
- 'active-tag': { description: 'Active tag group name' },
269
- }),
270
- ],
271
- ['venn', withGlobals()],
272
- [
273
- 'quadrant',
274
- withGlobals({
275
- 'x-label': { description: 'X-axis labels (low, high)' },
276
- 'y-label': { description: 'Y-axis labels (low, high)' },
277
- }),
278
- ],
279
-
280
- // ── Diagrams ─────────────────────────────────────────────
281
- [
282
- 'sequence',
283
- withGlobals({
284
- activations: {
285
- description: 'Show activation bars',
286
- values: ['on', 'off'],
287
- },
288
- 'active-tag': { description: 'Active tag group name' },
289
- }),
290
- ],
291
- [
292
- 'flowchart',
293
- // Spec §5 §4.6: direction-lr, orientation-vertical, solid-fill, no-notes
294
- withGlobals({
295
- 'direction-lr': { description: 'Switch to left-to-right layout' },
296
- 'orientation-vertical': {
297
- description: 'Use vertical orientation for ranks',
298
- },
299
- 'no-notes': { description: 'Suppress all node note boxes' },
300
- }),
301
- ],
302
- ['class', withGlobals({})],
303
- [
304
- 'er',
305
- // Spec §9 §8.5: notation (chen/crow), active-tag.
306
- withGlobals({
307
- notation: {
308
- description: 'ER notation style',
309
- values: ['chen', 'crow'],
310
- },
311
- 'active-tag': { description: 'Active tag group name' },
312
- }),
313
- ],
314
- [
315
- 'org',
316
- // Spec §7 §6.5: direction-tb, sub-node-label, show-sub-node-count,
317
- // hide, active-tag. solid-fill via SOLID_FILL_CAPABLE.
318
- withGlobals({
319
- 'direction-tb': { description: 'Switch to top-to-bottom layout' },
320
- 'sub-node-label': { description: 'Label for sub-nodes' },
321
- 'show-sub-node-count': { description: 'Show sub-node counts' },
322
- hide: { description: 'Hide tag:value pairs' },
323
- 'active-tag': { description: 'Active tag group name' },
324
- }),
325
- ],
326
- [
327
- 'kanban',
328
- // Spec §11 §10.4: hide, active-tag.
329
- withGlobals({
330
- hide: { description: 'Hide tag:value pairs' },
331
- 'active-tag': { description: 'Active tag group name' },
332
- }),
333
- ],
334
- // RACI / RASCI / DACI — one chart type (`raci`); the variant (RACI, RASCI,
335
- // or DACI) is inferred from the markers used (D → DACI, S → RASCI).
336
- [
337
- 'raci',
338
- withGlobals({
339
- roles: {
340
- description:
341
- 'Declare role column order (inline `roles A, B, C` or indented block with per-role pipe metadata)',
342
- },
343
- 'active-tag': { description: 'Active tag group name' },
344
- }),
345
- ],
346
- [
347
- 'c4',
348
- // Spec §8 §7.7: direction-tb, active-tag.
349
- withGlobals({
350
- 'direction-tb': { description: 'Switch to top-to-bottom layout' },
351
- 'active-tag': { description: 'Active tag group name' },
352
- }),
353
- ],
354
- [
355
- 'state',
356
- // Spec §6 §5.6: direction-tb, solid-fill, no-notes.
357
- withGlobals({
358
- 'direction-tb': { description: 'Switch to top-to-bottom layout' },
359
- 'no-notes': { description: 'Suppress all state note boxes' },
360
- }),
361
- ],
362
- [
363
- 'sitemap',
364
- withGlobals({
365
- 'direction-tb': { description: 'Switch to top-to-bottom layout' },
366
- 'active-tag': { description: 'Active tag group name' },
367
- }),
368
- ],
369
- [
370
- 'infra',
371
- withGlobals({
372
- 'direction-tb': { description: 'Switch to top-to-bottom layout' },
373
- animate: { description: 'Enable traffic animation' },
374
- 'default-latency-ms': { description: 'Default latency for all nodes' },
375
- 'default-uptime': { description: 'Default uptime for all nodes' },
376
- 'default-rps': { description: 'Default RPS capacity for all nodes' },
377
- 'slo-availability': { description: 'SLO availability target (0-1)' },
378
- 'slo-p90-latency-ms': { description: 'SLO p90 latency target in ms' },
379
- 'slo-warning-margin': { description: 'SLO warning margin percentage' },
380
- 'active-tag': { description: 'Active tag group name' },
381
- }),
382
- ],
383
- [
384
- 'pert',
385
- withGlobals({
386
- 'time-unit': {
387
- description: 'Time unit for activity durations',
388
- values: ['min', 'h', 'd', 'bd', 'w', 'm', 'q', 'y'],
389
- },
390
- confidence: {
391
- description: 'Confidence factor for M-only durations',
392
- values: ['high', 'medium', 'low'],
393
- },
394
- direction: { description: 'Layout direction', values: ['LR', 'TB'] },
395
- 'node-detail': {
396
- description: 'Node visual density',
397
- values: ['compact', 'full'],
398
- },
399
- trials: {
400
- description:
401
- 'Monte Carlo trial count (auto-derived from activity count)',
402
- },
403
- seed: { description: 'Monte Carlo PRNG seed (auto-derived from title)' },
404
- 'scrubber-trials': {
405
- description: 'Fast-MC trials for the duration scrubber (default 300)',
406
- },
407
- 'start-date': { description: 'Project start date (YYYY-MM-DD or now)' },
408
- 'end-date': { description: 'Project end date (YYYY-MM-DD)' },
409
- 'sprint-length': { description: 'Sprint duration (e.g. 2w)' },
410
- 'sprint-number': { description: 'Starting sprint number' },
411
- 'sprint-start': { description: 'Sprint start date (YYYY-MM-DD)' },
412
- 'active-tag': { description: 'Active tag group name' },
413
- }),
414
- ],
415
- [
416
- 'gantt',
417
- // Spec §13 §12.2 Options.
418
- withGlobals({
419
- start: { description: 'Project start date (YYYY-MM-DD)' },
420
- 'today-marker': {
421
- description: 'Today marker (bare = on, or YYYY-MM-DD date)',
422
- },
423
- sort: { description: 'Sort order', values: ['time', 'group', 'tag'] },
424
- 'critical-path': { description: 'Show critical path' },
425
- 'no-dependencies': { description: 'Hide dependency arrows' },
426
- 'sprint-length': { description: 'Sprint duration (e.g. 2w)' },
427
- 'sprint-number': { description: 'Starting sprint number' },
428
- 'sprint-start': { description: 'Sprint start date (YYYY-MM-DD)' },
429
- 'active-tag': { description: 'Active tag group name' },
430
- // Legacy positive form `dependencies` — kept for back-compat. Use
431
- // `no-dependencies` to suppress dependency arrows in new code.
432
- dependencies: { description: 'Show dependencies (legacy form)' },
433
- }),
434
- ],
435
- [
436
- 'boxes-and-lines',
437
- withGlobals({
438
- direction: { description: 'Layout direction', values: ['LR', 'TB'] },
439
- 'active-tag': { description: 'Active tag group name' },
440
- hide: { description: 'Hide tag:value pairs' },
441
- heat: {
442
- description:
443
- 'Label for the value→colour ramp, with an optional trailing [low] [high] color pair (pairs with the `heat:` key)',
444
- },
445
- 'show-values': { description: 'Print box values as text' },
446
- }),
447
- ],
448
- [
449
- 'swimlane',
450
- withGlobals({
451
- direction: { description: 'Layout direction', values: ['LR', 'TB'] },
452
- lane: { description: 'Declare a lane (row) with an optional color' },
453
- 'active-tag': { description: 'Active tag group name' },
454
- }),
455
- ],
456
- [
457
- 'version-control',
458
- withGlobals({
459
- direction: {
460
- description: 'Layout direction',
461
- values: ['LR', 'TB', 'BT'],
462
- },
463
- merge: { description: 'Merge a branch into the active branch' },
464
- 'cherry-pick': { description: 'Copy a commit onto the active branch' },
465
- rebase: { description: 'Replay a branch onto another (rebase X onto Y)' },
466
- reset: { description: 'Move a branch pointer back (reset X to commit)' },
467
- revert: { description: 'Add an inverse commit undoing a commit' },
468
- ref: {
469
- description:
470
- 'Pointer at a commit (ref origin/main at commit) — remotes / HEAD',
471
- },
472
- note: { description: 'Numbered step annotation on the current commit' },
473
- 'no-labels': { description: 'Hide commit messages' },
474
- 'no-lanes': { description: 'Hide branch lanes' },
475
- 'no-head': { description: 'Hide the HEAD marker' },
476
- }),
477
- ],
478
- [
479
- 'mindmap',
480
- withGlobals({
481
- 'active-tag': { description: 'Active tag group name' },
482
- }),
483
- ],
484
- [
485
- 'wireframe',
486
- withGlobals({
487
- mobile: { description: 'Use mobile (narrow vertical) layout' },
488
- 'active-tag': { description: 'Active tag group name' },
489
- }),
490
- ],
491
- [
492
- 'tech-radar',
493
- // Spec §20 documents one directive: `show-blip-legend`. `rings` is a
494
- // structural block keyword; quadrant/ring/trend/color are pipe metadata
495
- // that live in PIPE_METADATA.
496
- withGlobals({
497
- 'show-blip-legend': {
498
- description: 'Render the four-column blip listing alongside the radar',
499
- },
500
- }),
501
- ],
502
- [
503
- 'cycle',
504
- withGlobals({
505
- 'direction-counterclockwise': {
506
- description: 'Reverse cycle direction to counterclockwise',
507
- },
508
- 'circle-nodes': {
509
- description: 'Render nodes as circles instead of rectangles',
510
- },
511
- }),
512
- ],
513
- [
514
- 'journey-map',
515
- // Spec §22 directives: `active-tag`. `persona` is a
516
- // structural keyword (like `tag` / `roles`), not a directive.
517
- // `solid-fill` is added via SOLID_FILL_CAPABLE below.
518
- withGlobals({
519
- 'active-tag': { description: 'Active tag group name' },
520
- }),
521
- ],
522
- [
523
- 'pyramid',
524
- // Spec §23.5 documents `inverted`; `solid-fill` is added via
525
- // SOLID_FILL_CAPABLE below (working but not yet in spec §23.5).
526
- // `color`/`description` are layer pipe-metadata, not directives.
527
- withGlobals({
528
- inverted: { description: 'Flip apex to the bottom (funnel orientation)' },
529
- }),
530
- ],
531
- [
532
- 'ring',
533
- // Per spec §24.5 the only chart-specific directive is `solid-fill`,
534
- // applied via SOLID_FILL_CAPABLE below. `color`/`description` are
535
- // layer pipe-metadata, not directives — they live in PIPE_METADATA.
536
- withGlobals({}),
537
- ],
538
- [
539
- 'treemap',
540
- // Hierarchy sized by a bare trailing number; color modes + opt-outs.
541
- withGlobals({
542
- heat: {
543
- description:
544
- 'Name the color-by-value ramp (`heat <Label> [low] [high]`); pairs with the per-node `heat:` key',
545
- },
546
- depth: {
547
- description:
548
- 'Render N levels; deeper subtrees collapse to a drillable block',
549
- },
550
- 'no-values': { description: 'Hide value labels' },
551
- 'no-percent': { description: 'Hide percentage labels' },
552
- 'no-headers': { description: 'Hide parent header bars' },
553
- 'no-legend': { description: 'Hide the legend' },
554
- radial: {
555
- description:
556
- 'Render as a sunburst / hierarchical pie (concentric rings)',
557
- },
558
- }),
559
- ],
560
- [
561
- 'block',
562
- // Author-controlled grid; columns are inferred from placement, so the only
563
- // directives are an explicit `columns` override and `no-legend`.
564
- withGlobals({
565
- columns: {
566
- description:
567
- 'Grid width (columns); inferred from the widest row if omitted',
568
- },
569
- 'no-legend': { description: 'Hide the tag legend' },
570
- }),
571
- ],
572
- [
573
- 'map',
574
- // Geographic map directives (§24B.2/.7). Cosmetics are ON by default — the
575
- // only switches are bare `no-*` opt-outs, surfaced proactively so a
576
- // zero-config map still hints at what can be turned off. `poi`/`route` are
577
- // content keywords, not directives; metadata keys (heat/size/width/label/style)
578
- // live in the reserved-key registry.
579
- withGlobals({
580
- 'region-heat': {
581
- description:
582
- 'Label for the region value→colour ramp, with an optional trailing [low] [high] color pair (pairs with the `heat:` key)',
583
- },
584
- 'poi-size': {
585
- description:
586
- 'Label for the POI value→marker-size channel (pairs with the `size:` key)',
587
- },
588
- 'flow-width': {
589
- description:
590
- 'Label for the edge/leg value→line-thickness channel (pairs with the `width:` key)',
591
- },
592
- locale: {
593
- description:
594
- 'Default country/state for bare place names, e.g. locale US-GA',
595
- },
596
- 'active-tag': {
597
- description: 'Which tag group leads when several are present',
598
- },
599
- caption: { description: 'Caption line (data-source attribution)' },
600
- 'no-title': { description: 'Suppress the title banner' },
601
- 'no-legend': { description: 'Suppress the legend' },
602
- 'no-coastline': {
603
- description: 'Turn off coastal water-lines (on by default)',
604
- },
605
- 'no-relief': {
606
- description: 'Turn off mountain-range relief shading (on by default)',
607
- },
608
- 'no-context-labels': {
609
- description: 'Turn off orientation labels for water + nearby countries',
610
- },
611
- 'no-region-labels': {
612
- description: 'Turn off subdivision name labels (on by default)',
613
- },
614
- 'no-region-heat-value': {
615
- description:
616
- 'Turn off the heat value shown under each region (on by default)',
617
- },
618
- 'no-poi-labels': { description: 'Turn off POI labels (on by default)' },
619
- 'no-colorize': {
620
- description:
621
- 'Force plain green-land reference dress (regions are auto-coloured by default)',
622
- },
623
- 'no-cities': {
624
- description:
625
- 'Turn off the subtle city dots scattered across the basemap (on by default)',
626
- },
627
- 'no-cluster-pois': {
628
- description:
629
- 'Always fan out coincident POI markers instead of collapsing them into a count badge',
630
- },
631
- }),
632
- ],
633
- ]);
634
-
635
- // ── Cross-chart-type bare-keyword option: `solid-fill` ──────────
636
- // Adds the directive to every chart type whose renderer actually responds to
637
- // it (renders shapes at full intent saturation when set). Chart types where
638
- // the keyword is a no-op (gantt/infra/tech-radar/event-line deliberately opt
639
- // out; quadrant/scatter-dot/wordcloud/line/arc/slope have no tinted shape
640
- // fill) intentionally don't list it — keeps the completion popup honest.
641
- const SOLID_FILL_CAPABLE = new Set([
642
- 'flowchart',
643
- 'state',
644
- 'sequence',
645
- 'c4',
646
- 'org',
647
- 'kanban',
648
- 'journey-map',
649
- 'mindmap',
650
- 'cycle',
651
- 'pyramid',
652
- 'ring',
653
- 'funnel',
654
- 'class',
655
- 'er',
656
- 'sitemap',
657
- 'boxes-and-lines',
658
- 'wireframe',
659
- 'bar',
660
- 'pie',
661
- 'polar-area',
662
- 'radar',
663
- 'scatter',
664
- 'pert',
665
- 'block',
666
- 'swimlane',
667
- 'treemap',
668
- 'heatmap',
669
- 'venn',
670
- 'timeline',
671
- ]);
672
- for (const [type, spec] of COMPLETION_REGISTRY) {
673
- if (SOLID_FILL_CAPABLE.has(type)) {
674
- spec.directives['solid-fill'] = {
675
- description:
676
- 'Render shapes with full intent color instead of the default 25% tint',
677
- };
678
- }
679
- }
680
-
681
- // ============================================================
682
- // Chart types array (for chart type completion popup)
683
- // ============================================================
684
-
685
- /** All chart types with descriptions, for chart type autocomplete. */
686
- const CHART_TYPE_DESCRIPTIONS: Record<string, string> = Object.fromEntries(
687
- chartTypes.map((c) => [c.id, c.description])
688
- );
689
-
690
- export const CHART_TYPES: ReadonlyArray<{ name: string; description: string }> =
691
- [...ALL_CHART_TYPES].map((name) => ({
692
- name,
693
- description: CHART_TYPE_DESCRIPTIONS[name] ?? name,
694
- }));
695
-
696
- // ============================================================
697
- // Entity types for `is a` declarations
698
- // ============================================================
699
-
700
- /**
701
- * Entity types for `Name is a <type>` declarations, keyed by chart type.
702
- * Values are sourced from parser constants (VALID_PARTICIPANT_TYPES,
703
- * C4_IS_A_RE).
704
- */
705
- export const ENTITY_TYPES = new Map<string, string[]>([
706
- ['sequence', ['actor', 'database', 'queue', 'cache']],
707
- [
708
- 'c4',
709
- ['person', 'system', 'container', 'component', 'external', 'database'],
710
- ],
711
- ]);
712
-
713
- // ============================================================
714
- // Structural keywords for line-leading completion
715
- // ============================================================
716
-
717
- /**
718
- * Chart-type-specific structural keywords offered on an empty/start-of-line in
719
- * the data zone (block openers like `loop`, section headers like `containers`,
720
- * the `tag` block declaration, etc.). This is the single source of truth for
721
- * the editor's structural-keyword popup — every entry MUST be a token the
722
- * corresponding parser actually recognizes (validated by the
723
- * completion-conformance suite). Do NOT add removed/diagnostic-only tokens
724
- * (e.g. cycle's `no-descriptions`) or tokens the parser ignores.
725
- *
726
- * Chart types not listed here have no structural keywords (most data charts).
727
- */
728
- export const STRUCTURAL_KEYWORDS = new Map<string, string[]>([
729
- ['sequence', ['if', 'else', 'loop', 'parallel', 'note', 'tag']],
730
- ['gantt', ['era', 'marker', 'holiday', 'workweek', 'parallel', 'tag']],
731
- ['c4', ['containers', 'components', 'deployment', 'tag']],
732
- ['timeline', ['era', 'marker', 'tag']],
733
- ['org', ['tag']],
734
- ['kanban', ['tag']],
735
- ['sitemap', ['tag']],
736
- ['infra', ['tag']],
737
- ['pert', ['tag']],
738
- ['mindmap', ['tag']],
739
- ['treemap', ['tag']],
740
- ['block', ['tag']],
741
- ['boxes-and-lines', ['tag']],
742
- ['swimlane', ['lane', 'tag']],
743
- [
744
- 'version-control',
745
- ['merge', 'cherry-pick', 'rebase', 'reset', 'revert', 'ref', 'note'],
746
- ],
747
- ['er', ['tag']],
748
- ['cycle', ['direction-counterclockwise', 'circle-nodes']],
749
- ['journey-map', ['persona', 'tag']],
750
- ['raci', ['roles']],
751
- ['tech-radar', ['rings']],
752
- [
753
- 'wireframe',
754
- [
755
- 'nav',
756
- 'tabs',
757
- 'table',
758
- 'image',
759
- 'modal',
760
- 'skeleton',
761
- 'alert',
762
- 'progress',
763
- 'chart',
764
- 'mobile',
765
- 'tag',
766
- ],
767
- ],
768
- ['class', ['abstract', 'interface', 'enum', 'extends', 'implements']],
769
- ]);
770
-
771
- /**
772
- * Chart types that support `tag` block declarations (and thus the
773
- * `alias`/`default` sub-keywords inside a tag block). Derived from
774
- * STRUCTURAL_KEYWORDS so the two can never drift — a chart supports tag blocks
775
- * iff it offers the `tag` keyword.
776
- */
777
- export const TAG_SUPPORTING_TYPES: ReadonlySet<string> = new Set(
778
- [...STRUCTURAL_KEYWORDS]
779
- .filter(([, kws]) => kws.includes('tag'))
780
- .map(([type]) => type)
781
- );
782
-
783
- // ============================================================
784
- // Reference grammar descriptor (slot 5 — entity reference)
785
- // ============================================================
786
-
787
- /** Whether a chart type has a reference position (arrow/operator → a prior
788
- * declaration) and which operators open it. The single oracle that drives
789
- * the library extractor audit, the app's entity-trigger map, and the
790
- * conformance drift-guard — so the three can never disagree. v1 models
791
- * reference-after-operator only (NOT metadata-target references). */
792
- export interface ReferenceGrammar {
793
- hasReferenceGrammar: boolean;
794
- /** Literal operator tokens that introduce a reference (e.g. `->`, `<->`,
795
- * `~>`, `+`). The app derives its trigger regex from these (allowing the
796
- * labeled `-label->` / `~label~>` variants where applicable). */
797
- referenceOperators: string[];
798
- }
799
-
800
- /**
801
- * Per-type reference grammar. Grounded in each parser's arrow/operator
802
- * grammar — NOT hand-typed blind. Types with no reference position (org,
803
- * kanban, mindmap, all data charts, the radial/visualization types) declare
804
- * `hasReferenceGrammar: false`. `map` references (`route A ~> B`) are owned by
805
- * the app's bespoke geo-completion path, so it stays `false` here to avoid
806
- * double-handling.
807
- */
808
- export const REFERENCE_GRAMMAR = new Map<string, ReferenceGrammar>([
809
- // Diagrams with a genuine reference position
810
- ['sequence', { hasReferenceGrammar: true, referenceOperators: ['->', '~>'] }],
811
- ['flowchart', { hasReferenceGrammar: true, referenceOperators: ['->'] }],
812
- ['state', { hasReferenceGrammar: true, referenceOperators: ['->'] }],
813
- ['sitemap', { hasReferenceGrammar: true, referenceOperators: ['->'] }],
814
- [
815
- 'c4',
816
- {
817
- hasReferenceGrammar: true,
818
- referenceOperators: ['->', '<->', '~>', '<~>'],
819
- },
820
- ],
821
- // ER/class/infra reference grammars are irregular (cardinality / typed
822
- // relationship operators / indented arrows); the app keeps a special-cased
823
- // trigger for those, but they're still reference-grammar types here so the
824
- // drift-guard covers their extractors.
825
- ['er', { hasReferenceGrammar: true, referenceOperators: ['--'] }],
826
- [
827
- 'class',
828
- {
829
- hasReferenceGrammar: true,
830
- referenceOperators: ['-->', '--|>', '..|>', '*--', 'o--', '..>'],
831
- },
832
- ],
833
- ['infra', { hasReferenceGrammar: true, referenceOperators: ['->'] }],
834
- ['gantt', { hasReferenceGrammar: true, referenceOperators: ['->'] }],
835
- ['pert', { hasReferenceGrammar: true, referenceOperators: ['->'] }],
836
- ['arc', { hasReferenceGrammar: true, referenceOperators: ['->'] }],
837
- ['sankey', { hasReferenceGrammar: true, referenceOperators: ['->', '--'] }],
838
- [
839
- 'boxes-and-lines',
840
- { hasReferenceGrammar: true, referenceOperators: ['->', '<->'] },
841
- ],
842
- // Venn references prior sets via the `+` intersection operator (not an arrow).
843
- ['venn', { hasReferenceGrammar: true, referenceOperators: ['+'] }],
844
- ]);
845
-
846
- // Every other registered chart type has no reference position.
847
- for (const type of ALL_CHART_TYPES) {
848
- if (!REFERENCE_GRAMMAR.has(type)) {
849
- REFERENCE_GRAMMAR.set(type, {
850
- hasReferenceGrammar: false,
851
- referenceOperators: [],
852
- });
853
- }
854
- }
855
-
856
- // ============================================================
857
- // Closed value enums (slot 7) sourced from parser constants
858
- // ============================================================
859
-
860
- /**
861
- * RACI marker alphabet (slot-7 enum in the role-assignment value position).
862
- * Consumed by editor completion. The chart type is always `raci`; the variant
863
- * (RACI / RASCI / DACI) is inferred from the markers used, so completion offers
864
- * the full union (R / A / S / C / I / D) sourced from `raci/variants.ts`'s
865
- * `ALL_MARKERS` — the marker set can never drift from the parser.
866
- */
867
- export const RACI_MARKER_ALPHABETS: ReadonlyMap<string, readonly string[]> =
868
- new Map([['raci', [...ALL_MARKERS]]]);
869
-
870
- /**
871
- * Closed set of wireframe element state flags (slot-7 trailing enum, e.g.
872
- * `(Submit) primary destructive`). Sourced from the wireframe parser's
873
- * `STATE_KEYWORDS` — exported there and consumed here, never re-typed.
874
- */
875
- export const WIREFRAME_FLAGS: readonly string[] = [...WIREFRAME_STATE_KEYWORDS];
876
-
877
- // ============================================================
878
- // Pipe metadata for inline `| key value` on data lines
879
- // ============================================================
880
-
881
- /** Specification for a single pipe metadata key. */
882
- export interface PipeKeySpec {
883
- description: string;
884
- values?: string[];
885
- }
886
-
887
- /**
888
- * Pipe metadata keys for inline `| key value` on data lines.
889
- * Keyed by chart type → { context-name: keys }.
890
- *
891
- * Contexts are open-ended. The two universal ones are:
892
- * - `node` — the default for any non-arrow line
893
- * - `edge` — lines containing an arrow (`->`, `--`)
894
- *
895
- * Charts with richer line types declare additional contexts:
896
- * - raci: `role`, `phase`, `assignment`
897
- * - ring / pyramid: `layer`
898
- * - tech-radar: `quadrant`, `blip`
899
- * - journey-map: `step`
900
- *
901
- * IMPORTANT: NEVER add 'sequence' here. The `|` character in sequence
902
- * diagrams separates display names from identifiers and tag metadata.
903
- * Adding sequence would trigger false pipe-metadata completions on every `|`.
904
- */
905
- export type PipeContextMap = Record<string, Record<string, PipeKeySpec>>;
906
-
907
- export const PIPE_METADATA = new Map<string, PipeContextMap>([
908
- [
909
- 'infra',
910
- {
911
- node: {
912
- description: { description: 'Node description text' },
913
- instances: {
914
- description: 'Instance count or auto-scaling range (N-M)',
915
- },
916
- 'latency-ms': { description: 'Per-request latency in milliseconds' },
917
- 'max-rps': { description: 'Max requests per second per instance' },
918
- 'cache-hit': { description: 'Cache hit percentage (0-100)' },
919
- 'firewall-block': { description: 'Traffic blocked percentage' },
920
- 'ratelimit-rps': { description: 'Max RPS allowed through' },
921
- 'cb-error-threshold': {
922
- description: 'Circuit breaker error threshold %',
923
- },
924
- 'cb-latency-threshold-ms': {
925
- description: 'Circuit breaker latency threshold',
926
- },
927
- uptime: { description: 'Component availability (0-1)' },
928
- concurrency: { description: 'Concurrent request limit' },
929
- 'duration-ms': { description: 'Processing duration' },
930
- 'cold-start-ms': { description: 'Function cold-start time' },
931
- buffer: { description: 'Queue/buffer capacity' },
932
- 'drain-rate': { description: 'Queue drain rate' },
933
- 'retention-hours': { description: 'Data retention period' },
934
- partitions: { description: 'Queue/stream partition count' },
935
- 'slo-availability': { description: 'Node availability target (0-1)' },
936
- 'slo-p90-latency-ms': { description: 'Node p90 latency target' },
937
- 'slo-warning-margin': { description: 'Node SLO warning margin' },
938
- },
939
- edge: {
940
- split: { description: 'Traffic split percentage (e.g., 60%)' },
941
- fanout: { description: 'Fanout multiplier (integer >= 1)' },
942
- },
943
- },
944
- ],
945
- [
946
- 'c4',
947
- {
948
- node: {
949
- description: { description: 'Element description' },
950
- tech: { description: 'Technology stack' },
951
- technology: { description: 'Technology stack (alias for tech)' },
952
- },
953
- edge: {},
954
- },
955
- ],
956
- [
957
- 'gantt',
958
- {
959
- node: {
960
- duration: { description: 'Task duration (e.g., 30bd, 5d, 1.5w)' },
961
- start: { description: 'Explicit start date (e.g., 2024-01-15)' },
962
- progress: { description: 'Task progress (0–100)' },
963
- offset: { description: 'Task start offset (e.g., 8bd, -3bd)' },
964
- },
965
- edge: {
966
- // Gantt "edge" = dependency arrow (TaskA -> TaskB | offset 2bd)
967
- offset: { description: 'Dependency offset (e.g., 2bd, -1w)' },
968
- },
969
- },
970
- ],
971
- [
972
- 'boxes-and-lines',
973
- {
974
- node: {
975
- description: { description: 'Node description text' },
976
- heat: { description: 'Numeric value for the heat (colour) ramp' },
977
- },
978
- edge: {},
979
- },
980
- ],
981
- [
982
- 'mindmap',
983
- {
984
- node: {
985
- description: { description: 'Node description text' },
986
- collapsed: { description: 'Collapse node subtree by default' },
987
- },
988
- edge: {},
989
- },
990
- ],
991
- [
992
- // Tech-radar pipe metadata (spec §20). Two contexts:
993
- // - quadrant: top-level quadrant headers (`Tools | quadrant: top-left, color: blue`)
994
- // - blip: indented blip lines (` Vite | ring: Adopt, trend: up`)
995
- 'tech-radar',
996
- {
997
- quadrant: {
998
- quadrant: {
999
- description: 'Quadrant position',
1000
- values: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
1001
- },
1002
- color: { description: 'Override quadrant color' },
1003
- },
1004
- blip: {
1005
- ring: { description: 'Ring assignment (must match a declared ring)' },
1006
- trend: {
1007
- description: 'Blip trend indicator',
1008
- values: ['new', 'up', 'down', 'stable'],
1009
- },
1010
- },
1011
- },
1012
- ],
1013
- [
1014
- 'cycle',
1015
- {
1016
- node: {
1017
- color: { description: 'Node fill color (palette name)' },
1018
- span: { description: 'Relative arc distance to next node' },
1019
- description: { description: 'Node description text' },
1020
- },
1021
- edge: {
1022
- color: { description: 'Edge stroke color (palette name)' },
1023
- width: { description: 'Edge stroke width in pixels' },
1024
- },
1025
- },
1026
- ],
1027
- [
1028
- // RACI pipe metadata (spec §24A). Two contexts:
1029
- // - role: declarations inside the `roles` block (`Cap | color: red`)
1030
- // - phase: bracketed phase headers (`[Departure] | color: teal`)
1031
- 'raci',
1032
- {
1033
- role: {
1034
- color: { description: 'Role column tint (palette name)' },
1035
- },
1036
- phase: {
1037
- color: { description: 'Phase bar tint (palette name)' },
1038
- },
1039
- },
1040
- ],
1041
- [
1042
- // Ring layer pipe metadata (spec §24.4). One context: `layer`.
1043
- 'ring',
1044
- {
1045
- layer: {
1046
- color: { description: 'Ring color (palette name)' },
1047
- description: { description: 'Layer description (one-liner shorthand)' },
1048
- },
1049
- },
1050
- ],
1051
- [
1052
- // Pyramid layer pipe metadata (spec §23.4). Identical surface to ring.
1053
- 'pyramid',
1054
- {
1055
- layer: {
1056
- color: { description: 'Layer color (palette name)' },
1057
- description: { description: 'Layer description (one-liner shorthand)' },
1058
- },
1059
- },
1060
- ],
1061
- [
1062
- // Journey-map step pipe metadata (spec §22).
1063
- // Tag aliases (e.g. `ch: Web`) are user-defined via the `tag` block
1064
- // and resolved dynamically — not part of the static map.
1065
- 'journey-map',
1066
- {
1067
- step: {
1068
- score: { description: 'Step score (1–5 integer; high = good)' },
1069
- emotion: { description: 'Emotional state at this step' },
1070
- description: { description: 'Step description text' },
1071
- pain: { description: 'Pain point at this step' },
1072
- opportunity: { description: 'Opportunity at this step' },
1073
- thought: { description: 'User thought at this step' },
1074
- },
1075
- },
1076
- ],
1077
- [
1078
- 'org',
1079
- {
1080
- node: {
1081
- description: { description: 'Person/team description' },
1082
- role: { description: 'Role or job title' },
1083
- location: { description: 'Office location' },
1084
- email: { description: 'Email address' },
1085
- phone: { description: 'Phone number' },
1086
- },
1087
- },
1088
- ],
1089
- [
1090
- 'er',
1091
- {
1092
- node: {
1093
- description: { description: 'Entity description' },
1094
- domain: { description: 'Domain grouping' },
1095
- },
1096
- },
1097
- ],
1098
- [
1099
- 'class',
1100
- {
1101
- node: {
1102
- description: { description: 'Class description' },
1103
- },
1104
- },
1105
- ],
1106
- [
1107
- 'kanban',
1108
- {
1109
- node: {
1110
- description: { description: 'Card description' },
1111
- assignee: { description: 'Assigned person' },
1112
- due: { description: 'Due date (YYYY-MM-DD)' },
1113
- },
1114
- },
1115
- ],
1116
- [
1117
- 'sitemap',
1118
- {
1119
- node: {
1120
- description: { description: 'Page description' },
1121
- status: { description: 'Page status' },
1122
- },
1123
- },
1124
- ],
1125
- [
1126
- 'pert',
1127
- {
1128
- node: {
1129
- description: { description: 'Activity description' },
1130
- confidence: {
1131
- description: 'Confidence factor',
1132
- values: ['high', 'medium', 'low'],
1133
- },
1134
- collapsed: { description: 'Collapse activity detail' },
1135
- },
1136
- },
1137
- ],
1138
- [
1139
- 'timeline',
1140
- {
1141
- node: {
1142
- description: { description: 'Event description' },
1143
- duration: { description: 'Event duration (e.g., 30d, 1.5y)' },
1144
- },
1145
- },
1146
- ],
1147
- ]);
1148
-
1149
- // ============================================================
1150
- // Derived metadata key set
1151
- // ============================================================
1152
-
1153
- /** All known directive keys, derived from COMPLETION_REGISTRY. Includes implicit keys. */
1154
- export const METADATA_KEY_SET: ReadonlySet<string> = new Set([
1155
- 'chart',
1156
- 'title', // implicit directives recognized as metadata
1157
- ...[...COMPLETION_REGISTRY.values()].flatMap((spec) =>
1158
- Object.keys(spec.directives)
1159
- ),
1160
- ]);
1161
-
1162
- // ============================================================
1163
- // Sequence extractor
1164
- // ============================================================
1165
-
1166
- // Universal Name Handling: source/target accept multi-word + "quoted" names.
1167
- // `[^|]+?` captures greedy-to-pipe-or-arrow; the arrow alternation acts as
1168
- // the boundary. Caller strips quotes via stripQuotes().
1169
- const SEQ_ARROW_RE =
1170
- /^(?:"([^"]+)"|([^|"]+?))\s+(->|-.*->|~>|~.*~>)\s+(?:"([^"]+)"|([^|"]+?))(?:\s|\|.*)?$/;
1171
- const SEQ_IS_A_RE = /^(?:"([^"]+)"|([^|":]+?))\s+is\s+an?\s+/i;
1172
- const SEQ_SECTION_RE = /^==/;
1173
- const SEQ_STRUCTURAL_RE = /^(if|else|loop|parallel|end)\b/i;
1174
-
1175
- function extractSequenceSymbols(docText: string): DiagramSymbols {
1176
- const lines = docText.split('\n');
1177
- const entities: string[] = [];
1178
- let pastFirstLine = false;
1179
-
1180
- for (const line of lines) {
1181
- const trimmed = line.trim();
1182
- if (!trimmed || trimmed.startsWith('//')) continue;
1183
-
1184
- // Skip first line (chart type)
1185
- if (!pastFirstLine) {
1186
- pastFirstLine = true;
1187
- continue;
1188
- }
1189
-
1190
- // Skip metadata lines
1191
- // split(/\s+/) on non-empty `trimmed` always yields at least one element.
1192
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
1193
- if (METADATA_KEY_SET.has(firstToken)) continue;
1194
-
1195
- // Skip sections, structural keywords
1196
- if (SEQ_SECTION_RE.test(trimmed)) continue;
1197
- if (SEQ_STRUCTURAL_RE.test(trimmed)) continue;
1198
-
1199
- // Arrow lines: A -> B, A -label-> B, A ~> B
1200
- const arrowMatch = trimmed.match(SEQ_ARROW_RE);
1201
- if (arrowMatch) {
1202
- const src = (arrowMatch[1] ?? arrowMatch[2] ?? '').trim();
1203
- const dst = (arrowMatch[4] ?? arrowMatch[5] ?? '').trim();
1204
- if (src && !entities.includes(src)) entities.push(src);
1205
- if (dst && !entities.includes(dst)) entities.push(dst);
1206
- continue;
1207
- }
1208
-
1209
- // Type declarations: A is a person, A is an actor
1210
- const isAMatch = trimmed.match(SEQ_IS_A_RE);
1211
- if (isAMatch) {
1212
- const name = (isAMatch[1] ?? isAMatch[2] ?? '').trim();
1213
- if (name && !entities.includes(name)) entities.push(name);
1214
- continue;
1215
- }
1216
- }
1217
-
1218
- return {
1219
- kind: 'sequence',
1220
- entities,
1221
- };
1222
- }
1223
-
1224
- // ============================================================
1225
- // State extractor
1226
- // ============================================================
1227
-
1228
- const STATE_ARROW_RE = /^(\S+)\s+->\s+(\S+)/;
1229
-
1230
- function extractStateSymbols(docText: string): DiagramSymbols {
1231
- const lines = docText.split('\n');
1232
- const entities: string[] = [];
1233
- let pastFirstLine = false;
1234
-
1235
- for (const line of lines) {
1236
- const trimmed = line.trim();
1237
- if (!trimmed || trimmed.startsWith('//')) continue;
1238
-
1239
- if (!pastFirstLine) {
1240
- pastFirstLine = true;
1241
- continue;
1242
- }
1243
-
1244
- // Skip metadata lines
1245
- // split(/\s+/) on non-empty `trimmed` always yields at least one element.
1246
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
1247
- if (METADATA_KEY_SET.has(firstToken)) continue;
1248
-
1249
- const arrowMatch = trimmed.match(STATE_ARROW_RE);
1250
- if (arrowMatch) {
1251
- // Regex captured groups 1 and 2 by successful match; split('|')[0] always defined.
1252
- const src = arrowMatch[1]!.split('|')[0]!.trim();
1253
- const dst = arrowMatch[2]!.split('|')[0]!.trim();
1254
- if (src && !entities.includes(src)) entities.push(src);
1255
- if (dst && !entities.includes(dst)) entities.push(dst);
1256
- }
1257
- }
1258
-
1259
- return { kind: 'state', entities };
1260
- }
1261
-
1262
- // ============================================================
1263
- // Tag declaration extraction
1264
- // ============================================================
1265
-
1266
- // Matches tag declarations in both forms:
1267
- // - `tag Name alias x` (explicit alias keyword)
1268
- // - `tag Name x` (shorthand: 1-4 lowercase chars = alias, matching parser's isAliasToken)
1269
- const TAG_DECL_EXPLICIT_RE = /^tag\s+(\S+)\s+alias\s+(\S+)/i;
1270
- const TAG_DECL_SHORT_RE = /^tag\s+(\S+)\s+([a-z]{1,4})(?:\s|$)/;
1271
-
1272
- /**
1273
- * Extract tag declarations from document text.
1274
- * Returns a map of alias (or full name) → array of tag values.
1275
- * Keys preserve original case for display; use case-insensitive lookup.
1276
- */
1277
- export function extractTagDeclarations(docText: string): Map<string, string[]> {
1278
- const result = new Map<string, string[]>();
1279
- const lines = docText.split('\n');
1280
- let currentAlias: string | null = null;
1281
- let currentValues: string[] = [];
1282
-
1283
- for (let i = 0; i < lines.length; i++) {
1284
- // In-bounds by loop guard.
1285
- const raw = lines[i]!;
1286
- const trimmed = raw.trim();
1287
-
1288
- // Check for tag declaration — try explicit `alias` keyword first, then shorthand
1289
- const tagMatch =
1290
- trimmed.match(TAG_DECL_EXPLICIT_RE) ?? trimmed.match(TAG_DECL_SHORT_RE);
1291
- if (tagMatch) {
1292
- // Save previous tag group
1293
- if (currentAlias !== null) {
1294
- result.set(currentAlias, currentValues);
1295
- }
1296
- // Both regexes capture groups 1 (and 2 for explicit) on successful match.
1297
- const name = tagMatch[1]!;
1298
- const alias = tagMatch[2] ?? name;
1299
- currentAlias = alias;
1300
- currentValues = [];
1301
- continue;
1302
- }
1303
- // Also match bare `tag Name` (no alias) — fall through with name as key
1304
- if (/^tag\s+(\S+)\s*$/i.test(trimmed)) {
1305
- if (currentAlias !== null) {
1306
- result.set(currentAlias, currentValues);
1307
- }
1308
- // Regex captured group 1 by successful re-match (test passed above).
1309
- currentAlias = trimmed.match(/^tag\s+(\S+)/i)![1]!;
1310
- currentValues = [];
1311
- continue;
1312
- }
1313
-
1314
- // Collect indented tag values
1315
- if (
1316
- currentAlias !== null &&
1317
- raw.length > 0 &&
1318
- (raw[0] === ' ' || raw[0] === '\t')
1319
- ) {
1320
- if (trimmed && !trimmed.startsWith('//')) {
1321
- // Strip trailing-token color (§1.5): `Frontend blue` → `Frontend`.
1322
- // Whitespace-split; if the last token is a recognized color word,
1323
- // drop it; otherwise the whole trimmed string is the value.
1324
- const lastSpaceIdx = trimmed.lastIndexOf(' ');
1325
- const value =
1326
- lastSpaceIdx > 0 &&
1327
- RECOGNIZED_COLOR_SET.has(trimmed.substring(lastSpaceIdx + 1))
1328
- ? trimmed.substring(0, lastSpaceIdx).trim()
1329
- : trimmed;
1330
- if (value) currentValues.push(value);
1331
- }
1332
- continue;
1333
- }
1334
-
1335
- // Non-indented non-tag line ends the current tag block
1336
- if (currentAlias !== null && trimmed) {
1337
- result.set(currentAlias, currentValues);
1338
- currentAlias = null;
1339
- currentValues = [];
1340
- }
1341
- }
1342
-
1343
- // Save last tag group
1344
- if (currentAlias !== null) {
1345
- result.set(currentAlias, currentValues);
1346
- }
1347
-
1348
- return result;
1349
- }
1350
-
1351
- // ============================================================
1352
- // Universal alias extractor (`Name as <alias>` postfix)
1353
- // ============================================================
1354
-
1355
- // Postfix-alias form on any name-slot line. Caller-agnostic — runs
1356
- // over the full document so every chart-type extractor can populate
1357
- // `DiagramSymbols.aliases` consistently.
1358
- const ALIAS_POSTFIX_DECL_RE =
1359
- /(?:^|[^|/])\s*(.+?)\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11})\s*(?:\|.*)?$/;
1360
-
1361
- /**
1362
- * Scan document text for `Name as <alias>` declarations.
1363
- *
1364
- * Returns `Record<alias, canonical-fragment>`. The canonical may
1365
- * still carry color/type modifiers (the per-parser logic peels
1366
- * those off at parse time); for autocomplete display purposes the
1367
- * raw fragment is good enough.
1368
- *
1369
- * Pure helper — does NOT enforce strict-ordering, collisions, or
1370
- * other semantic rules. Those are parser-side checks.
1371
- */
1372
- export function extractAliasDeclarations(
1373
- docText: string
1374
- ): Record<string, string> {
1375
- const aliases: Record<string, string> = {};
1376
- for (const raw of docText.split('\n')) {
1377
- const trimmed = raw.trim();
1378
- if (!trimmed || trimmed.startsWith('//')) continue;
1379
- const match = trimmed.match(ALIAS_POSTFIX_DECL_RE);
1380
- if (!match) continue;
1381
- // Regex captured groups 1 and 2 by successful match.
1382
- const canonical = match[1]!.trim();
1383
- const alias = match[2]!;
1384
- // Skip if canonical itself looks structural (arrow / pipe-only / brackets-only)
1385
- if (!canonical || canonical === '[' || canonical === ']') continue;
1386
- if (!(alias in aliases)) {
1387
- aliases[alias] = canonical;
1388
- }
1389
- }
1390
- return aliases;
1391
- }
1392
-
1393
- // ============================================================
1394
- // Sitemap extractor
1395
- // ============================================================
1396
-
1397
- const SITEMAP_CONTAINER_RE = /^\[([^\]]+)\]/;
1398
- const SITEMAP_ARROW_RE = /^-.*->\s*(.+)$/;
1399
- const SITEMAP_BARE_ARROW_RE = /^->\s*(.+)$/;
1400
- const SITEMAP_METADATA_RE = /^([^:]+):\s*(.+)$/;
1401
-
1402
- function extractSitemapSymbols(docText: string): DiagramSymbols {
1403
- const lines = docText.split('\n');
1404
- const entities: string[] = [];
1405
- let pastFirstLine = false;
1406
- let inTagBlock = false;
1407
- let lastNodeIndent = -1;
1408
-
1409
- for (const line of lines) {
1410
- const trimmed = line.trim();
1411
- if (!trimmed || trimmed.startsWith('//')) continue;
1412
-
1413
- if (!pastFirstLine) {
1414
- pastFirstLine = true;
1415
- continue;
1416
- }
1417
-
1418
- // Skip metadata lines
1419
- // split(/\s+/) on non-empty `trimmed` always yields at least one element.
1420
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
1421
- if (METADATA_KEY_SET.has(firstToken)) continue;
1422
-
1423
- // Track tag blocks
1424
- if (/^tag\s+/i.test(trimmed)) {
1425
- inTagBlock = true;
1426
- continue;
1427
- }
1428
- const indent = line.search(/\S/);
1429
- if (inTagBlock) {
1430
- if (indent > 0) continue;
1431
- inTagBlock = false;
1432
- }
1433
-
1434
- // Containers: [GroupName]
1435
- const containerMatch = trimmed.match(SITEMAP_CONTAINER_RE);
1436
- if (containerMatch) {
1437
- // Regex captured group 1 by successful match; split('|')[0] always defined.
1438
- const name = containerMatch[1]!.split('|')[0]!.trim();
1439
- if (name && !entities.includes(name)) entities.push(name);
1440
- lastNodeIndent = indent;
1441
- continue;
1442
- }
1443
-
1444
- // Arrows: -> Target or -label-> Target
1445
- const bareArrow = trimmed.match(SITEMAP_BARE_ARROW_RE);
1446
- const labeledArrow = !bareArrow ? trimmed.match(SITEMAP_ARROW_RE) : null;
1447
- if (bareArrow || labeledArrow) {
1448
- // split('|')[0] always defined on any string.
1449
- const target = (bareArrow?.[1] ?? labeledArrow?.[1] ?? '')
1450
- .split('|')[0]!
1451
- .trim();
1452
- if (target && !entities.includes(target)) entities.push(target);
1453
- continue;
1454
- }
1455
-
1456
- // Indented metadata under a node (key: value) — skip
1457
- if (
1458
- indent > 0 &&
1459
- lastNodeIndent >= 0 &&
1460
- indent > lastNodeIndent &&
1461
- SITEMAP_METADATA_RE.test(trimmed)
1462
- ) {
1463
- continue;
1464
- }
1465
-
1466
- // Page label (anything else that's not special)
1467
- // split('|')[0] always defined on any string.
1468
- const label = trimmed.split('|')[0]!.trim();
1469
- if (label) {
1470
- if (!entities.includes(label)) entities.push(label);
1471
- lastNodeIndent = indent;
1472
- }
1473
- }
1474
-
1475
- return { kind: 'sitemap', entities };
1476
- }
1477
-
1478
- // ============================================================
1479
- // C4 extractor
1480
- // ============================================================
1481
-
1482
- const C4_ELEMENT_RE = /^(person|system|container|component)\s+(.+)$/i;
1483
- const C4_IS_A_RE =
1484
- /^(.+?)\s+is\s+an?\s+(person|system|container|component|external|database)\b/i;
1485
- const C4_ARROW_RE =
1486
- /^(\S+)\s+(?:->|-.*->|~>|~.*~>|<->|<-.*->|<~>|<~.*~>)\s+(\S+)/;
1487
- const C4_SECTION_RE = /^(containers|components|deployment)\s*$/i;
1488
-
1489
- function extractC4Symbols(docText: string): DiagramSymbols {
1490
- const lines = docText.split('\n');
1491
- const entities: string[] = [];
1492
- let pastFirstLine = false;
1493
- let inTagBlock = false;
1494
-
1495
- for (const line of lines) {
1496
- const trimmed = line.trim();
1497
- if (!trimmed || trimmed.startsWith('//')) continue;
1498
-
1499
- if (!pastFirstLine) {
1500
- pastFirstLine = true;
1501
- continue;
1502
- }
1503
-
1504
- // split(/\s+/) on non-empty `trimmed` always yields at least one element.
1505
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
1506
- if (METADATA_KEY_SET.has(firstToken)) continue;
1507
-
1508
- if (/^tag\s+/i.test(trimmed)) {
1509
- inTagBlock = true;
1510
- continue;
1511
- }
1512
- const indent = line.search(/\S/);
1513
- if (inTagBlock) {
1514
- if (indent > 0) continue;
1515
- inTagBlock = false;
1516
- }
1517
-
1518
- // Skip section headers
1519
- if (C4_SECTION_RE.test(trimmed)) continue;
1520
-
1521
- // Element declaration: person Name, system Name, etc.
1522
- const elemMatch = trimmed.match(C4_ELEMENT_RE);
1523
- if (elemMatch) {
1524
- // Regex captured group 2 by successful match; split('|')[0] always defined.
1525
- const name = elemMatch[2]!.split('|')[0]!.trim();
1526
- if (name && !entities.includes(name)) entities.push(name);
1527
- continue;
1528
- }
1529
-
1530
- // Is-a declaration: Name is a person
1531
- const isAMatch = trimmed.match(C4_IS_A_RE);
1532
- if (isAMatch) {
1533
- // Regex captured group 1 by successful match; split('|')[0] always defined.
1534
- const name = isAMatch[1]!.split('|')[0]!.trim();
1535
- if (name && !entities.includes(name)) entities.push(name);
1536
- continue;
1537
- }
1538
-
1539
- // Arrow lines: Source -> Target, Source ~> Target, etc.
1540
- const arrowMatch = trimmed.match(C4_ARROW_RE);
1541
- if (arrowMatch) {
1542
- // Regex captured groups 1 and 2 by successful match; split('|')[0] always defined.
1543
- const src = arrowMatch[1]!.split('|')[0]!.trim();
1544
- const dst = arrowMatch[2]!.split('|')[0]!.trim();
1545
- if (src && !entities.includes(src)) entities.push(src);
1546
- if (dst && !entities.includes(dst)) entities.push(dst);
1547
- continue;
1548
- }
1549
- }
1550
-
1551
- return {
1552
- kind: 'c4',
1553
- entities,
1554
- };
1555
- }
1556
-
1557
- // ============================================================
1558
- // Gantt extractor
1559
- // ============================================================
1560
-
1561
- const GANTT_LEGACY_DURATION_RE =
1562
- /^(\d+(?:\.\d+)?)(min|bd|d|w|m|q|y|h|s)\??\s+(.+)$/;
1563
- const GANTT_LEGACY_DATE_RE = /^(\d{4}-\d{2}-\d{2}(?:\s\d{2}:\d{2})?)\s+(.+)$/;
1564
- const GANTT_GROUP_RE = /^\[(.+?)\]/;
1565
- const GANTT_STRUCTURAL_RE = /^(era|marker|holiday|workweek|parallel)\b/i;
1566
- const GANTT_META_KEY_RE = /\b(?:duration|start):\s/;
1567
-
1568
- function extractGanttSymbols(docText: string): DiagramSymbols {
1569
- const lines = docText.split('\n');
1570
- const entities: string[] = [];
1571
- let pastFirstLine = false;
1572
- let inTagBlock = false;
1573
-
1574
- for (const line of lines) {
1575
- const trimmed = line.trim();
1576
- if (!trimmed || trimmed.startsWith('//')) continue;
1577
-
1578
- if (!pastFirstLine) {
1579
- pastFirstLine = true;
1580
- continue;
1581
- }
1582
-
1583
- // split(/\s+/) on non-empty `trimmed` always yields at least one element.
1584
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
1585
- if (METADATA_KEY_SET.has(firstToken)) continue;
1586
-
1587
- if (/^tag\s+/i.test(trimmed)) {
1588
- inTagBlock = true;
1589
- continue;
1590
- }
1591
- const indent = line.search(/\S/);
1592
- if (inTagBlock) {
1593
- if (indent > 0) continue;
1594
- inTagBlock = false;
1595
- }
1596
-
1597
- // Groups: [GroupName]
1598
- const groupMatch = trimmed.match(GANTT_GROUP_RE);
1599
- if (groupMatch) {
1600
- const name = groupMatch[1]!.trim();
1601
- if (name && !entities.includes(name)) entities.push(name);
1602
- continue;
1603
- }
1604
-
1605
- // New syntax: Task Name duration: 5d or Task Name start: 2024-01-15
1606
- // (checked before structural keyword skip so "Era of Innovation duration: 5d" isn't skipped)
1607
- if (GANTT_META_KEY_RE.test(trimmed)) {
1608
- const cutIdx = trimmed.search(
1609
- /\b(?:duration|start|progress|offset|color|description):\s/
1610
- );
1611
- if (cutIdx > 0) {
1612
- let taskName = trimmed.substring(0, cutIdx).trim();
1613
- const arrowIdx = taskName.indexOf('->');
1614
- if (arrowIdx > 0)
1615
- taskName = taskName
1616
- .substring(0, arrowIdx)
1617
- .replace(/-[^>]*$/, '')
1618
- .trim();
1619
- if (taskName && !entities.includes(taskName)) entities.push(taskName);
1620
- continue;
1621
- }
1622
- }
1623
-
1624
- // Skip structural keywords (after new-syntax check so "Era of Innovation duration: 5d" isn't skipped)
1625
- if (GANTT_STRUCTURAL_RE.test(trimmed)) continue;
1626
-
1627
- // Legacy: Tasks by duration: 30d Task Name
1628
- const durMatch = trimmed.match(GANTT_LEGACY_DURATION_RE);
1629
- if (durMatch) {
1630
- let taskName = durMatch[3]!.split('|')[0]!.trim();
1631
- const arrowIdx = taskName.indexOf('->');
1632
- if (arrowIdx > 0)
1633
- taskName = taskName
1634
- .substring(0, arrowIdx)
1635
- .replace(/-[^>]*$/, '')
1636
- .trim();
1637
- if (taskName && !entities.includes(taskName)) entities.push(taskName);
1638
- continue;
1639
- }
1640
-
1641
- // Legacy: Tasks by date: 2024-01-15 Task Name
1642
- const dateMatch = trimmed.match(GANTT_LEGACY_DATE_RE);
1643
- if (dateMatch) {
1644
- let taskName = dateMatch[2]!.split('|')[0]!.trim();
1645
- const arrowIdx = taskName.indexOf('->');
1646
- if (arrowIdx > 0)
1647
- taskName = taskName
1648
- .substring(0, arrowIdx)
1649
- .replace(/-[^>]*$/, '')
1650
- .trim();
1651
- if (taskName && !entities.includes(taskName)) entities.push(taskName);
1652
- continue;
1653
- }
1654
- }
1655
-
1656
- return { kind: 'gantt', entities };
1657
- }
1658
-
1659
- // ============================================================
1660
- // Boxes-and-lines extractor
1661
- // ============================================================
1662
-
1663
- const BL_ARROW_RE = /^(\S+)\s+(?:-.*)?(?:->|<->)\s+(\S+)/;
1664
-
1665
- function extractBoxesAndLinesSymbols(docText: string): DiagramSymbols {
1666
- const lines = docText.split('\n');
1667
- const entities: string[] = [];
1668
- let pastFirstLine = false;
1669
- let inTagBlock = false;
1670
-
1671
- for (const line of lines) {
1672
- const trimmed = line.trim();
1673
- if (!trimmed || trimmed.startsWith('//')) continue;
1674
-
1675
- if (!pastFirstLine) {
1676
- pastFirstLine = true;
1677
- continue;
1678
- }
1679
-
1680
- // split(/\s+/) on non-empty `trimmed` always yields at least one element.
1681
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
1682
- if (METADATA_KEY_SET.has(firstToken)) continue;
1683
-
1684
- if (/^tag\s+/i.test(trimmed)) {
1685
- inTagBlock = true;
1686
- continue;
1687
- }
1688
- const indent = line.search(/\S/);
1689
- if (inTagBlock) {
1690
- if (indent > 0) continue;
1691
- inTagBlock = false;
1692
- }
1693
-
1694
- // Skip groups
1695
- if (/^\[.+?\]/.test(trimmed)) continue;
1696
-
1697
- // Edge lines
1698
- const arrowMatch = trimmed.match(BL_ARROW_RE);
1699
- if (arrowMatch) {
1700
- // Regex captured groups 1 and 2 by successful match; split('|')[0] always defined.
1701
- const src = arrowMatch[1]!.split('|')[0]!.trim();
1702
- const dst = arrowMatch[2]!.split('|')[0]!.trim();
1703
- if (src && !entities.includes(src)) entities.push(src);
1704
- if (dst && !entities.includes(dst)) entities.push(dst);
1705
- continue;
1706
- }
1707
-
1708
- // Node lines
1709
- // split('|')[0] and chained split('[')[0] always defined on any string.
1710
- const label = trimmed.split('|')[0]!.split('[')[0]!.trim();
1711
- if (label && !entities.includes(label)) entities.push(label);
1712
- }
1713
-
1714
- return { kind: 'boxes-and-lines', entities };
1715
- }
1716
-
1717
- // ============================================================
1718
- // Org extractor
1719
- // ============================================================
1720
-
1721
- const ORG_GROUP_RE = /^\[(.+?)\]/;
1722
-
1723
- function extractOrgSymbols(docText: string): DiagramSymbols {
1724
- const lines = docText.split('\n');
1725
- const entities: string[] = [];
1726
- let pastFirstLine = false;
1727
- let inTagBlock = false;
1728
-
1729
- for (const line of lines) {
1730
- const trimmed = line.trim();
1731
- if (!trimmed || trimmed.startsWith('//')) continue;
1732
-
1733
- if (!pastFirstLine) {
1734
- pastFirstLine = true;
1735
- continue;
1736
- }
1737
-
1738
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
1739
- if (METADATA_KEY_SET.has(firstToken)) continue;
1740
-
1741
- if (/^tag\s+/i.test(trimmed)) {
1742
- inTagBlock = true;
1743
- continue;
1744
- }
1745
- const indent = line.search(/\S/);
1746
- if (inTagBlock) {
1747
- if (indent > 0) continue;
1748
- inTagBlock = false;
1749
- }
1750
-
1751
- // Team/group headers: [Team Name]
1752
- const groupMatch = trimmed.match(ORG_GROUP_RE);
1753
- if (groupMatch) {
1754
- const name = groupMatch[1]!.split('|')[0]!.trim();
1755
- if (name && !entities.includes(name)) entities.push(name);
1756
- continue;
1757
- }
1758
-
1759
- // Skip indented metadata lines (key: value)
1760
- if (indent > 0 && /^[a-z]+\s*:/.test(trimmed)) continue;
1761
-
1762
- // Person name (indent 0 or direct child)
1763
- const label = trimmed.split('|')[0]!.trim();
1764
- if (label && !entities.includes(label)) entities.push(label);
1765
- }
1766
-
1767
- return { kind: 'org', entities };
1768
- }
1769
-
1770
- // ============================================================
1771
- // Kanban extractor
1772
- // ============================================================
1773
-
1774
- const KANBAN_COLUMN_RE = /^\[(.+?)\]/;
1775
-
1776
- function extractKanbanSymbols(docText: string): DiagramSymbols {
1777
- const lines = docText.split('\n');
1778
- const entities: string[] = [];
1779
- let pastFirstLine = false;
1780
- let inTagBlock = false;
1781
-
1782
- for (const line of lines) {
1783
- const trimmed = line.trim();
1784
- if (!trimmed || trimmed.startsWith('//')) continue;
1785
-
1786
- if (!pastFirstLine) {
1787
- pastFirstLine = true;
1788
- continue;
1789
- }
1790
-
1791
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
1792
- if (METADATA_KEY_SET.has(firstToken)) continue;
1793
-
1794
- if (/^tag\s+/i.test(trimmed)) {
1795
- inTagBlock = true;
1796
- continue;
1797
- }
1798
- const indent = line.search(/\S/);
1799
- if (inTagBlock) {
1800
- if (indent > 0) continue;
1801
- inTagBlock = false;
1802
- }
1803
-
1804
- // Column headers: [Column Name]
1805
- const colMatch = trimmed.match(KANBAN_COLUMN_RE);
1806
- if (colMatch) {
1807
- const name = colMatch[1]!.split('|')[0]!.trim();
1808
- if (name && !entities.includes(name)) entities.push(name);
1809
- continue;
1810
- }
1811
-
1812
- // Card names (indented under columns)
1813
- if (indent > 0) {
1814
- const label = trimmed.split('|')[0]!.trim();
1815
- if (label && !entities.includes(label)) entities.push(label);
1816
- }
1817
- }
1818
-
1819
- return { kind: 'kanban', entities };
1820
- }
1821
-
1822
- // ============================================================
1823
- // Mindmap extractor
1824
- // ============================================================
1825
-
1826
- function extractMindmapSymbols(docText: string): DiagramSymbols {
1827
- const lines = docText.split('\n');
1828
- const entities: string[] = [];
1829
- let pastFirstLine = false;
1830
- let inTagBlock = false;
1831
-
1832
- for (const line of lines) {
1833
- const trimmed = line.trim();
1834
- if (!trimmed || trimmed.startsWith('//')) continue;
1835
-
1836
- if (!pastFirstLine) {
1837
- pastFirstLine = true;
1838
- continue;
1839
- }
1840
-
1841
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
1842
- if (METADATA_KEY_SET.has(firstToken)) continue;
1843
-
1844
- if (/^tag\s+/i.test(trimmed)) {
1845
- inTagBlock = true;
1846
- continue;
1847
- }
1848
- const indent = line.search(/\S/);
1849
- if (inTagBlock) {
1850
- if (indent > 0) continue;
1851
- inTagBlock = false;
1852
- }
1853
-
1854
- // Skip indented metadata (description:, collapsed:)
1855
- if (/^(description|collapsed)\s*:/i.test(trimmed)) continue;
1856
-
1857
- // Node name (at any indent level)
1858
- const label = trimmed.split('|')[0]!.trim();
1859
- if (label && !entities.includes(label)) entities.push(label);
1860
- }
1861
-
1862
- return { kind: 'mindmap', entities };
1863
- }
1864
-
1865
- // ============================================================
1866
- // Treemap extractor
1867
- // ============================================================
1868
-
1869
- function extractTreemapSymbols(docText: string): DiagramSymbols {
1870
- const lines = docText.split('\n');
1871
- const entities: string[] = [];
1872
- let pastFirstLine = false;
1873
- let inTagBlock = false;
1874
-
1875
- for (const line of lines) {
1876
- const trimmed = line.trim();
1877
- if (!trimmed || trimmed.startsWith('//')) continue;
1878
-
1879
- if (!pastFirstLine) {
1880
- pastFirstLine = true;
1881
- continue;
1882
- }
1883
-
1884
- // Directives and tag blocks are not node entities.
1885
- if (
1886
- /^(depth|heat|no-[a-z]+)\s/i.test(trimmed) ||
1887
- /^(no-[a-z]+|radial)$/i.test(trimmed)
1888
- )
1889
- continue;
1890
- if (/^tag\s+/i.test(trimmed)) {
1891
- inTagBlock = true;
1892
- continue;
1893
- }
1894
- const indent = line.search(/\S/);
1895
- if (inTagBlock) {
1896
- if (indent > 0) continue;
1897
- inTagBlock = false;
1898
- }
1899
-
1900
- // Node name: strip same-line metadata + the bare trailing value number.
1901
- let label = trimmed.split(/\s+\w+:/)[0]!.trim();
1902
- label = label.replace(/\s+-?\d[\d_,.]*$/, '').trim();
1903
- if (label && !entities.includes(label)) entities.push(label);
1904
- }
1905
-
1906
- return { kind: 'treemap', entities };
1907
- }
1908
-
1909
- /** Block-diagram symbols: every `[Label]` on a grid row (skips tag blocks and
1910
- * the `columns` / `no-*` directives). */
1911
- function extractBlockSymbols(docText: string): DiagramSymbols {
1912
- const lines = docText.split('\n');
1913
- const entities: string[] = [];
1914
- let pastFirstLine = false;
1915
- let inTagBlock = false;
1916
-
1917
- for (const line of lines) {
1918
- const trimmed = line.trim();
1919
- if (!trimmed || trimmed.startsWith('//')) continue;
1920
- if (!pastFirstLine) {
1921
- pastFirstLine = true;
1922
- continue;
1923
- }
1924
- if (/^(columns|no-[a-z]+)\b/i.test(trimmed)) continue;
1925
- if (/^tag\s+/i.test(trimmed)) {
1926
- inTagBlock = true;
1927
- continue;
1928
- }
1929
- const indent = line.search(/\S/);
1930
- if (inTagBlock) {
1931
- if (indent > 0) continue;
1932
- inTagBlock = false;
1933
- }
1934
- const re = /\[([^\]]*)\]/g;
1935
- let m: RegExpExecArray | null;
1936
- while ((m = re.exec(trimmed)) !== null) {
1937
- const label = m[1]!.trim();
1938
- if (label && !entities.includes(label)) entities.push(label);
1939
- }
1940
- }
1941
-
1942
- return { kind: 'block', entities };
1943
- }
1944
-
1945
- // ============================================================
1946
- // Pyramid extractor
1947
- // ============================================================
1948
-
1949
- function extractPyramidSymbols(docText: string): DiagramSymbols {
1950
- const lines = docText.split('\n');
1951
- const entities: string[] = [];
1952
- let pastFirstLine = false;
1953
-
1954
- for (const line of lines) {
1955
- const trimmed = line.trim();
1956
- if (!trimmed || trimmed.startsWith('//')) continue;
1957
-
1958
- if (!pastFirstLine) {
1959
- pastFirstLine = true;
1960
- continue;
1961
- }
1962
-
1963
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
1964
- if (METADATA_KEY_SET.has(firstToken)) continue;
1965
- if (firstToken === 'inverted' || firstToken === 'solid-fill') continue;
1966
-
1967
- // Skip indented description lines
1968
- if (line[0] === ' ' || line[0] === '\t') continue;
1969
-
1970
- // Layer name (strip pipe metadata)
1971
- const label = trimmed.split('|')[0]!.trim();
1972
- if (label && !entities.includes(label)) entities.push(label);
1973
- }
1974
-
1975
- return { kind: 'pyramid', entities };
1976
- }
1977
-
1978
- // ============================================================
1979
- // Ring extractor
1980
- // ============================================================
1981
-
1982
- function extractRingSymbols(docText: string): DiagramSymbols {
1983
- const lines = docText.split('\n');
1984
- const entities: string[] = [];
1985
- let pastFirstLine = false;
1986
-
1987
- for (const line of lines) {
1988
- const trimmed = line.trim();
1989
- if (!trimmed || trimmed.startsWith('//')) continue;
1990
-
1991
- if (!pastFirstLine) {
1992
- pastFirstLine = true;
1993
- continue;
1994
- }
1995
-
1996
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
1997
- if (METADATA_KEY_SET.has(firstToken)) continue;
1998
- if (firstToken === 'solid-fill') continue;
1999
-
2000
- // Skip indented description lines
2001
- if (line[0] === ' ' || line[0] === '\t') continue;
2002
-
2003
- // Layer name (strip pipe metadata)
2004
- const label = trimmed.split('|')[0]!.trim();
2005
- if (label && !entities.includes(label)) entities.push(label);
2006
- }
2007
-
2008
- return { kind: 'ring', entities };
2009
- }
2010
-
2011
- // ============================================================
2012
- // Arc extractor
2013
- // ============================================================
2014
-
2015
- const ARC_ARROW_RE = /^(\S+)\s+(?:->|-[^>]*->)\s+(\S+)/;
2016
-
2017
- function extractArcSymbols(docText: string): DiagramSymbols {
2018
- const lines = docText.split('\n');
2019
- const entities: string[] = [];
2020
- let pastFirstLine = false;
2021
-
2022
- for (const line of lines) {
2023
- const trimmed = line.trim();
2024
- if (!trimmed || trimmed.startsWith('//')) continue;
2025
-
2026
- if (!pastFirstLine) {
2027
- pastFirstLine = true;
2028
- continue;
2029
- }
2030
-
2031
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
2032
- if (METADATA_KEY_SET.has(firstToken)) continue;
2033
-
2034
- const arrowMatch = trimmed.match(ARC_ARROW_RE);
2035
- if (arrowMatch) {
2036
- const src = arrowMatch[1]!.split('|')[0]!.trim();
2037
- const dst = arrowMatch[2]!.split('|')[0]!.trim();
2038
- if (src && !entities.includes(src)) entities.push(src);
2039
- if (dst && !entities.includes(dst)) entities.push(dst);
2040
- }
2041
- }
2042
-
2043
- return { kind: 'arc', entities };
2044
- }
2045
-
2046
- // ============================================================
2047
- // Sankey extractor
2048
- // ============================================================
2049
-
2050
- // Sankey links accept both directed `->` and undirected `--` (echarts.ts §1.5).
2051
- const SANKEY_ARROW_RE = /^(.+?)\s+(?:->|--)\s+(.+?)\s+(\d[\d,_.]*)/;
2052
-
2053
- function extractSankeySymbols(docText: string): DiagramSymbols {
2054
- const lines = docText.split('\n');
2055
- const entities: string[] = [];
2056
- let pastFirstLine = false;
2057
-
2058
- for (const line of lines) {
2059
- const trimmed = line.trim();
2060
- if (!trimmed || trimmed.startsWith('//')) continue;
2061
-
2062
- if (!pastFirstLine) {
2063
- pastFirstLine = true;
2064
- continue;
2065
- }
2066
-
2067
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
2068
- if (METADATA_KEY_SET.has(firstToken)) continue;
2069
-
2070
- const arrowMatch = trimmed.match(SANKEY_ARROW_RE);
2071
- if (arrowMatch) {
2072
- const src = arrowMatch[1]!.split('|')[0]!.trim();
2073
- const dst = arrowMatch[2]!.split('|')[0]!.trim();
2074
- if (src && !entities.includes(src)) entities.push(src);
2075
- if (dst && !entities.includes(dst)) entities.push(dst);
2076
- } else {
2077
- // Standalone node declaration (just a name, possibly with color)
2078
- const label = trimmed.split('|')[0]!.trim();
2079
- if (label && !entities.includes(label)) entities.push(label);
2080
- }
2081
- }
2082
-
2083
- return { kind: 'sankey', entities };
2084
- }
2085
-
2086
- // ============================================================
2087
- // Timeline extractor
2088
- // ============================================================
2089
-
2090
- const TIMELINE_ERA_RE = /^era\s+/i;
2091
- const TIMELINE_MARKER_RE = /^marker\s+/i;
2092
-
2093
- const TIMELINE_SCHEDULING_RE = /\b(?:start|end|duration)\s*:/;
2094
-
2095
- function extractTimelineSymbols(docText: string): DiagramSymbols {
2096
- const lines = docText.split('\n');
2097
- const entities: string[] = [];
2098
- let pastFirstLine = false;
2099
- let inTagBlock = false;
2100
-
2101
- for (const line of lines) {
2102
- const trimmed = line.trim();
2103
- if (!trimmed || trimmed.startsWith('//')) continue;
2104
-
2105
- if (!pastFirstLine) {
2106
- pastFirstLine = true;
2107
- continue;
2108
- }
2109
-
2110
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
2111
- if (METADATA_KEY_SET.has(firstToken)) continue;
2112
- if (TIMELINE_ERA_RE.test(trimmed) || TIMELINE_MARKER_RE.test(trimmed))
2113
- continue;
2114
-
2115
- if (/^tag\s+/i.test(trimmed)) {
2116
- inTagBlock = true;
2117
- continue;
2118
- }
2119
- const indent = line.search(/\S/);
2120
- if (inTagBlock) {
2121
- if (indent > 0) continue;
2122
- inTagBlock = false;
2123
- }
2124
-
2125
- let label: string;
2126
- if (TIMELINE_SCHEDULING_RE.test(trimmed)) {
2127
- label = trimmed
2128
- .replace(/\b(?:start|end|duration|color|description)\s*:.*$/, '')
2129
- .split('|')[0]!
2130
- .trim();
2131
- } else {
2132
- label = trimmed
2133
- .replace(
2134
- /^\d{4}(?:-\d{2}(?:-\d{2})?)?\s*(?:->\s*\d{4}(?:-\d{2}(?:-\d{2})?)?)?\s*/,
2135
- ''
2136
- )
2137
- .split('|')[0]!
2138
- .trim();
2139
- }
2140
- if (label && !entities.includes(label)) entities.push(label);
2141
- }
2142
-
2143
- return { kind: 'timeline', entities };
2144
- }
2145
-
2146
- // ============================================================
2147
- // Venn extractor
2148
- // ============================================================
2149
-
2150
- function extractVennSymbols(docText: string): DiagramSymbols {
2151
- const lines = docText.split('\n');
2152
- const entities: string[] = [];
2153
- let pastFirstLine = false;
2154
-
2155
- for (const line of lines) {
2156
- const trimmed = line.trim();
2157
- if (!trimmed || trimmed.startsWith('//')) continue;
2158
-
2159
- if (!pastFirstLine) {
2160
- pastFirstLine = true;
2161
- continue;
2162
- }
2163
-
2164
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
2165
- if (METADATA_KEY_SET.has(firstToken)) continue;
2166
-
2167
- // Skip indented intersection lines
2168
- if (line[0] === ' ' || line[0] === '\t') continue;
2169
-
2170
- // Skip intersection rows — those are references to prior sets, not
2171
- // declarations. Set NAMES + aliases come from the declaration lines below
2172
- // (the `+`-completion offers those). Match the parser's detection (d3.ts:
2173
- // any `+` on the line = intersection), not just the spaced form.
2174
- if (trimmed.includes('+')) continue;
2175
-
2176
- // Set declaration: `Name [as <alias>] [color]`. Emit both the clean name
2177
- // and the alias so `Set + ` reference completion can offer either token.
2178
- const work = trimmed.split('|')[0]!.trim();
2179
- const asMatch = work.match(/^(.+?)\s+as\s+([A-Za-z][\w-]*)\b/i);
2180
- if (asMatch) {
2181
- const name = asMatch[1]!.trim();
2182
- const alias = asMatch[2]!;
2183
- if (name && !entities.includes(name)) entities.push(name);
2184
- if (alias && !entities.includes(alias)) entities.push(alias);
2185
- continue;
2186
- }
2187
- // No alias — strip a trailing color token if present.
2188
- const colorMatch = work.match(/^(.+?)\s+(\S+)$/);
2189
- const label =
2190
- colorMatch && RECOGNIZED_COLOR_SET.has(colorMatch[2]!.toLowerCase())
2191
- ? colorMatch[1]!.trim()
2192
- : work;
2193
- if (label && !entities.includes(label)) entities.push(label);
2194
- }
2195
-
2196
- return { kind: 'venn', entities };
2197
- }
2198
-
2199
- // ============================================================
2200
- // Quadrant extractor
2201
- // ============================================================
2202
-
2203
- const QUADRANT_POSITION_RE =
2204
- /^(top-right|top-left|bottom-right|bottom-left)\s+/i;
2205
-
2206
- function extractQuadrantSymbols(docText: string): DiagramSymbols {
2207
- const lines = docText.split('\n');
2208
- const entities: string[] = [];
2209
- let pastFirstLine = false;
2210
-
2211
- for (const line of lines) {
2212
- const trimmed = line.trim();
2213
- if (!trimmed || trimmed.startsWith('//')) continue;
2214
-
2215
- if (!pastFirstLine) {
2216
- pastFirstLine = true;
2217
- continue;
2218
- }
2219
-
2220
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
2221
- if (METADATA_KEY_SET.has(firstToken)) continue;
2222
- if (QUADRANT_POSITION_RE.test(trimmed)) continue;
2223
-
2224
- // Point name (may have coordinates: Name x,y)
2225
- const parts = trimmed.split(/\s+\d/);
2226
- const label = (parts[0] ?? '').split('|')[0]!.trim();
2227
- if (label && !entities.includes(label)) entities.push(label);
2228
- }
2229
-
2230
- return { kind: 'quadrant', entities };
2231
- }
2232
-
2233
- // ============================================================
2234
- // Slope extractor
2235
- // ============================================================
2236
-
2237
- function extractSlopeSymbols(docText: string): DiagramSymbols {
2238
- const lines = docText.split('\n');
2239
- const entities: string[] = [];
2240
- let pastFirstLine = false;
2241
-
2242
- for (const line of lines) {
2243
- const trimmed = line.trim();
2244
- if (!trimmed || trimmed.startsWith('//')) continue;
2245
-
2246
- if (!pastFirstLine) {
2247
- pastFirstLine = true;
2248
- continue;
2249
- }
2250
-
2251
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
2252
- if (METADATA_KEY_SET.has(firstToken)) continue;
2253
- if (firstToken === 'period') continue;
2254
-
2255
- // Data row: Label value1 value2 [color]
2256
- // Extract just the label (everything before first number)
2257
- const numIdx = trimmed.search(/\s\d/);
2258
- const label =
2259
- numIdx > 0
2260
- ? trimmed.slice(0, numIdx).trim()
2261
- : trimmed.split('|')[0]!.trim();
2262
- if (label && !entities.includes(label)) entities.push(label);
2263
- }
2264
-
2265
- return { kind: 'slope', entities };
2266
- }
2267
-
2268
- // ============================================================
2269
- // Generic data chart extractor
2270
- // ============================================================
2271
-
2272
- const SERIES_RE = /^series\s+(.+)$/i;
2273
- // "A -> B value" / "A -- B value" flow-link rows (chord/sankey via the shared
2274
- // extractor). Both endpoints captured; trailing weight optional.
2275
- const DATA_EDGE_RE = /^(.+?)\s+(?:->|--)\s+(.+?)(?:\s+-?\d[\d,_.]*)?\s*$/;
2276
-
2277
- function extractDataChartSymbols(docText: string): DiagramSymbols {
2278
- const lines = docText.split('\n');
2279
- const entities: string[] = [];
2280
- let chartType = 'bar';
2281
- let pastFirstLine = false;
2282
-
2283
- for (const line of lines) {
2284
- const trimmed = line.trim();
2285
- if (!trimmed || trimmed.startsWith('//')) continue;
2286
-
2287
- if (!pastFirstLine) {
2288
- pastFirstLine = true;
2289
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
2290
- if (firstToken) chartType = firstToken;
2291
- continue;
2292
- }
2293
-
2294
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
2295
- if (METADATA_KEY_SET.has(firstToken)) continue;
2296
-
2297
- // Series declarations: "series Revenue, Expenses"
2298
- const seriesMatch = trimmed.match(SERIES_RE);
2299
- if (seriesMatch) {
2300
- for (const s of seriesMatch[1]!.split(',')) {
2301
- const name = s.trim().split(/\s+/)[0]!;
2302
- if (name && !entities.includes(name)) entities.push(name);
2303
- }
2304
- continue;
2305
- }
2306
-
2307
- // Edge rows (chord / flow links): "A -> B value" / "A -- B value" — recover
2308
- // BOTH endpoints. Without this the trailing-number scan below mangles the
2309
- // whole "A -> B" into a single junk entity.
2310
- const edgeMatch = trimmed.match(DATA_EDGE_RE);
2311
- if (edgeMatch) {
2312
- const src = edgeMatch[1]!.trim();
2313
- const dst = edgeMatch[2]!.trim();
2314
- if (src && !entities.includes(src)) entities.push(src);
2315
- if (dst && !entities.includes(dst)) entities.push(dst);
2316
- continue;
2317
- }
2318
-
2319
- // Label-expression rows (function curves §15.4): "Name: expression". ONLY
2320
- // for `function` — other data charts have no colon-form and would otherwise
2321
- // truncate legitimate colon-bearing labels (`12:00 5` → `12`). Column-0
2322
- // only, so indented `key: value` metadata is never misread as an entity.
2323
- if (chartType === 'function') {
2324
- const indent = line.length - line.trimStart().length;
2325
- const colonIdx = trimmed.indexOf(':');
2326
- if (indent === 0 && colonIdx > 0) {
2327
- const beforeColon = trimmed.slice(0, colonIdx).trim();
2328
- if (beforeColon && !METADATA_KEY_SET.has(beforeColon.toLowerCase())) {
2329
- if (!entities.includes(beforeColon)) entities.push(beforeColon);
2330
- continue;
2331
- }
2332
- }
2333
- }
2334
-
2335
- // Data rows: "Label value [value...] [color]"
2336
- const numIdx = trimmed.search(/\s-?\d/);
2337
- if (numIdx > 0) {
2338
- const label = trimmed.slice(0, numIdx).trim();
2339
- if (label && !entities.includes(label)) entities.push(label);
2340
- }
2341
- }
2342
-
2343
- return { kind: chartType, entities };
2344
- }
2345
-
2346
- // ============================================================
2347
- // Wireframe extractor
2348
- // ============================================================
2349
-
2350
- // Wireframe element grammar is not a numeric-row variant, so it earns its own
2351
- // extractor: `[label]` fields, `(button)`, `{a | b}` dropdowns/selects, with
2352
- // possibly several elements per line. Returns the element labels as entities.
2353
- const WF_FIELD_RE = /\[([^\]]*)\]/g;
2354
- const WF_BUTTON_RE = /\(([^)]+)\)/g;
2355
- const WF_DROPDOWN_RE = /\{([^}]+)\}/g;
2356
-
2357
- function extractWireframeSymbols(docText: string): DiagramSymbols {
2358
- const lines = docText.split('\n');
2359
- const entities: string[] = [];
2360
- let pastFirstLine = false;
2361
-
2362
- const push = (s: string): void => {
2363
- // Strip any legacy trailing `| meta` inside a bracket; the `{a|b}` pipe is
2364
- // handled separately by the dropdown split below.
2365
- const t = s.split('|')[0]!.trim();
2366
- if (t && !entities.includes(t)) entities.push(t);
2367
- };
2368
-
2369
- for (const line of lines) {
2370
- const trimmed = line.trim();
2371
- if (!trimmed || trimmed.startsWith('//')) continue;
2372
-
2373
- if (!pastFirstLine) {
2374
- pastFirstLine = true;
2375
- continue;
2376
- }
2377
-
2378
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
2379
- if (METADATA_KEY_SET.has(firstToken)) continue;
2380
-
2381
- // `[label]` input/group fields.
2382
- for (const m of trimmed.matchAll(WF_FIELD_RE)) {
2383
- const inner = m[1]!.trim();
2384
- if (inner) push(inner);
2385
- }
2386
- // `(button)` — skip radio markers `(*)` / `( )`.
2387
- for (const m of trimmed.matchAll(WF_BUTTON_RE)) {
2388
- const inner = m[1]!.trim();
2389
- if (inner === '*' || inner === '') continue;
2390
- push(inner);
2391
- }
2392
- // `{opt1 | opt2}` dropdown/select — each option is an entity (carve-out pipe).
2393
- for (const m of trimmed.matchAll(WF_DROPDOWN_RE)) {
2394
- for (const opt of m[1]!.split('|')) {
2395
- const t = opt.trim();
2396
- if (t && !entities.includes(t)) entities.push(t);
2397
- }
2398
- }
2399
- }
2400
-
2401
- return { kind: 'wireframe', entities };
2402
- }
2403
-
2404
- // ============================================================
2405
- // Register built-in extractors
2406
- // ============================================================
2407
-
2408
- registerExtractor('er', extractErSymbols);
2409
- registerExtractor('flowchart', extractFlowchartSymbols);
2410
- registerExtractor('infra', extractInfraSymbols);
2411
- registerExtractor('class', extractClassSymbols);
2412
- registerExtractor('sequence', extractSequenceSymbols);
2413
- registerExtractor('state', extractStateSymbols);
2414
- registerExtractor('sitemap', extractSitemapSymbols);
2415
- registerExtractor('c4', extractC4Symbols);
2416
- registerExtractor('gantt', extractGanttSymbols);
2417
- registerExtractor('pert', extractPertSymbols);
2418
- registerExtractor('boxes-and-lines', extractBoxesAndLinesSymbols);
2419
- registerExtractor('tech-radar', extractTechRadarSymbols);
2420
- registerExtractor('cycle', extractCycleSymbols);
2421
- registerExtractor('journey-map', extractJourneyMapSymbols);
2422
- registerExtractor('raci', extractRaciSymbols);
2423
- registerExtractor('org', extractOrgSymbols);
2424
- registerExtractor('kanban', extractKanbanSymbols);
2425
- registerExtractor('mindmap', extractMindmapSymbols);
2426
- registerExtractor('treemap', extractTreemapSymbols);
2427
- registerExtractor('block', extractBlockSymbols);
2428
- registerExtractor('pyramid', extractPyramidSymbols);
2429
- registerExtractor('ring', extractRingSymbols);
2430
- registerExtractor('arc', extractArcSymbols);
2431
- registerExtractor('sankey', extractSankeySymbols);
2432
- registerExtractor('timeline', extractTimelineSymbols);
2433
- registerExtractor('venn', extractVennSymbols);
2434
- registerExtractor('quadrant', extractQuadrantSymbols);
2435
- registerExtractor('slope', extractSlopeSymbols);
2436
- registerExtractor('bar', extractDataChartSymbols);
2437
- registerExtractor('line', extractDataChartSymbols);
2438
- registerExtractor('pie', extractDataChartSymbols);
2439
- registerExtractor('polar-area', extractDataChartSymbols);
2440
- registerExtractor('radar', extractDataChartSymbols);
2441
- registerExtractor('scatter', extractDataChartSymbols);
2442
- registerExtractor('heatmap', extractDataChartSymbols);
2443
- registerExtractor('funnel', extractDataChartSymbols);
2444
- // `function` (`Name: expr`) and `wordcloud` (`Word weight`) had NO extractor
2445
- // registered — extractDiagramSymbols returned null for them. The generalized
2446
- // shared extractor now handles the colon-label form, so register both.
2447
- registerExtractor('function', extractDataChartSymbols);
2448
- registerExtractor('wordcloud', extractDataChartSymbols);
2449
- registerExtractor('wireframe', extractWireframeSymbols);
2450
-
2451
- function extractTechRadarSymbols(docText: string): DiagramSymbols {
2452
- const entities: string[] = [];
2453
-
2454
- // Extract ring names and aliases from the rings block
2455
- const lines = docText.split('\n');
2456
- let inRings = false;
2457
- for (const line of lines) {
2458
- const trimmed = line.trim();
2459
- if (trimmed.toLowerCase() === 'rings') {
2460
- inRings = true;
2461
- continue;
2462
- }
2463
- if (inRings) {
2464
- if (!trimmed || (line[0] !== ' ' && line[0] !== '\t')) {
2465
- inRings = false;
2466
- continue;
2467
- }
2468
- // Parse ring name (and alias)
2469
- const aliasMatch = trimmed.match(/^(.+?)\s+(?:alias|aka)\s+(\S+)\s*$/i);
2470
- if (aliasMatch) {
2471
- // Regex captured groups 1 and 2 by successful match.
2472
- entities.push(aliasMatch[1]!.trim());
2473
- entities.push(aliasMatch[2]!.trim());
2474
- } else {
2475
- entities.push(trimmed);
2476
- }
2477
- }
2478
- }
2479
-
2480
- return { kind: 'tech-radar', entities };
2481
- }
2482
-
2483
- // ============================================================
2484
- // Cycle extractor
2485
- // ============================================================
2486
-
2487
- function extractCycleSymbols(docText: string): DiagramSymbols {
2488
- const lines = docText.split('\n');
2489
- const entities: string[] = [];
2490
- let pastFirstLine = false;
2491
-
2492
- for (const line of lines) {
2493
- const trimmed = line.trim();
2494
- if (!trimmed || trimmed.startsWith('//')) continue;
2495
-
2496
- if (!pastFirstLine) {
2497
- pastFirstLine = true;
2498
- continue;
2499
- }
2500
-
2501
- // Skip directives/metadata
2502
- // split(/\s+/) on non-empty `trimmed` always yields at least one element.
2503
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
2504
- if (METADATA_KEY_SET.has(firstToken)) continue;
2505
- if (
2506
- firstToken === 'direction-counterclockwise' ||
2507
- firstToken === 'circle-nodes'
2508
- )
2509
- continue;
2510
-
2511
- // Skip indented lines (descriptions, edges)
2512
- if (line[0] === ' ' || line[0] === '\t') continue;
2513
-
2514
- // Node label (strip pipe metadata)
2515
- // split('|')[0] always defined on any string.
2516
- const label = trimmed.split('|')[0]!.trim();
2517
- if (label && !entities.includes(label)) entities.push(label);
2518
- }
2519
-
2520
- return {
2521
- kind: 'cycle',
2522
- entities,
2523
- };
2524
- }
2525
-
2526
- // ============================================================
2527
- // RACI / RASCI / DACI extractor
2528
- // ============================================================
2529
- //
2530
- // Extract role names, task names, and phase labels for editor
2531
- // autocomplete. Mirrors the lightweight per-line scan pattern used
2532
- // by other extractors (cycle / journey-map) — does NOT rebuild the
2533
- // full AST.
2534
-
2535
- const RACI_PHASE_RE = /^\[(.+)\]\s*$/;
2536
- const RACI_ROLES_DIRECTIVE_RE = /^roles\s+(.+)$/i;
2537
- const RACI_VARIANT_DIRECTIVE_RE = /^variant\s+(.+)$/i;
2538
- const RACI_ROLE_ASSIGNMENT_RE = /^([^:]+):\s*(.*)$/;
2539
-
2540
- function extractRaciSymbols(docText: string): DiagramSymbols {
2541
- const lines = docText.split('\n');
2542
- const entities: string[] = [];
2543
- let chartType = 'raci';
2544
- let pastFirstLine = false;
2545
- let underTask = false;
2546
-
2547
- const push = (s: string): void => {
2548
- const trimmed = s.trim();
2549
- if (trimmed && !entities.includes(trimmed)) entities.push(trimmed);
2550
- };
2551
-
2552
- for (const line of lines) {
2553
- const trimmed = line.trim();
2554
- if (!trimmed || trimmed.startsWith('//')) continue;
2555
-
2556
- if (!pastFirstLine) {
2557
- pastFirstLine = true;
2558
- // split(/\s+/) on non-empty `trimmed` always yields at least one element.
2559
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
2560
- if (firstToken === 'raci') {
2561
- chartType = firstToken;
2562
- }
2563
- continue;
2564
- }
2565
-
2566
- const indent = measureIndent(line);
2567
-
2568
- // Header directives
2569
- if (indent === 0) {
2570
- const rolesMatch = trimmed.match(RACI_ROLES_DIRECTIVE_RE);
2571
- if (rolesMatch) {
2572
- // Regex captured group 1 by successful match.
2573
- for (const r of rolesMatch[1]!.split(',')) push(r);
2574
- continue;
2575
- }
2576
- if (RACI_VARIANT_DIRECTIVE_RE.test(trimmed)) continue;
2577
- // split(/\s+/) on non-empty `trimmed` always yields at least one element.
2578
- if (METADATA_KEY_SET.has(trimmed.split(/\s+/)[0]!.toLowerCase()))
2579
- continue;
2580
- if (
2581
- trimmed.toLowerCase() === 'draft' ||
2582
- trimmed.toLowerCase() === 'solid-fill'
2583
- )
2584
- continue;
2585
- }
2586
-
2587
- // [Phase Label]
2588
- const phaseMatch = trimmed.match(RACI_PHASE_RE);
2589
- if (phaseMatch && indent === 0) {
2590
- // Regex captured group 1 by successful match.
2591
- push(phaseMatch[1]!);
2592
- underTask = false;
2593
- continue;
2594
- }
2595
-
2596
- // Role assignment (Role: markers) — only valid under a task
2597
- const roleMatch = trimmed.match(RACI_ROLE_ASSIGNMENT_RE);
2598
- if (underTask && roleMatch) {
2599
- // Strip a possible trailing `# annotation`
2600
- // Regex captured group 1 by successful match.
2601
- const rolePart = roleMatch[1]!.trim();
2602
- push(rolePart);
2603
- continue;
2604
- }
2605
-
2606
- // Otherwise: treat the line as a task name. `#` is NOT a comment
2607
- // character in DGMO (`//` is) — task names are used verbatim.
2608
- push(trimmed);
2609
- underTask = true;
2610
- }
2611
-
2612
- return {
2613
- kind: chartType,
2614
- entities,
2615
- };
2616
- }
2617
-
2618
- function extractJourneyMapSymbols(docText: string): DiagramSymbols {
2619
- const lines = docText.split('\n');
2620
- const entities: string[] = [];
2621
- let pastFirstLine = false;
2622
-
2623
- for (const line of lines) {
2624
- const trimmed = line.trim();
2625
- if (!trimmed || trimmed.startsWith('//')) continue;
2626
-
2627
- if (!pastFirstLine) {
2628
- pastFirstLine = true;
2629
- continue;
2630
- }
2631
-
2632
- // Skip directives/metadata at indent 0
2633
- // split(/\s+/) on non-empty `trimmed` always yields at least one element.
2634
- const firstToken = trimmed.split(/\s+/)[0]!.toLowerCase();
2635
- if (METADATA_KEY_SET.has(firstToken)) continue;
2636
- if (firstToken === 'persona' || firstToken === 'tag') continue;
2637
-
2638
- const isIndented = line[0] === ' ' || line[0] === '\t';
2639
-
2640
- // Skip deep-indented lines (annotations, descriptions under steps)
2641
- // but keep singly-indented lines (steps within phases)
2642
- if (isIndented) {
2643
- // Annotation/description keywords — skip
2644
- if (/^(pain|opportunity|thought|description)\s*:/i.test(trimmed))
2645
- continue;
2646
- // Tag group entries — skip
2647
- if (/^\S+\([^)]+\)/.test(trimmed)) continue;
2648
- }
2649
-
2650
- // Phase header
2651
- const phaseMatch = trimmed.match(/^\[(.+?)\]$/);
2652
- if (phaseMatch) {
2653
- // Regex captured group 1 by successful match.
2654
- entities.push(phaseMatch[1]!.trim());
2655
- continue;
2656
- }
2657
-
2658
- // Step label (strip pipe metadata) — works for both indent 0 and indented steps
2659
- // split('|')[0] always defined on any string.
2660
- const label = trimmed.split('|')[0]!.trim();
2661
- if (label && !entities.includes(label)) entities.push(label);
2662
- }
2663
-
2664
- return {
2665
- kind: 'journey-map',
2666
- entities,
2667
- };
2668
- }