@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/c4/parser.ts DELETED
@@ -1,1158 +0,0 @@
1
- // ============================================================
2
- // C4 Architecture Diagram — Parser
3
- // ============================================================
4
-
5
- import type { PaletteColors } from '../palettes';
6
- import {
7
- formatDgmoError,
8
- makeDgmoError,
9
- makeFail,
10
- suggest,
11
- } from '../diagnostics';
12
- import { parseInArrowLabel } from '../utils/arrows';
13
- import { normalizeName } from '../utils/name-normalize';
14
- import type { TagGroup } from '../utils/tag-groups';
15
- import type { Writable } from '../utils/brand';
16
- import {
17
- matchTagBlockHeading,
18
- stripDefaultModifier,
19
- validateTagGroupNames,
20
- finalizeAutoTagColors,
21
- AUTO_TAG_COLOR_SENTINEL,
22
- tagAttrKey,
23
- } from '../utils/tag-groups';
24
- import { inferParticipantType } from '../sequence/participant-inference';
25
- import {
26
- measureIndent,
27
- extractColor,
28
- splitNameAndMeta,
29
- parseFirstLine,
30
- OPTION_NOCOLON_RE,
31
- warnUnknownMetaKeys,
32
- } from '../utils/parsing';
33
- import { C4_REGISTRY, withTagAliases } from '../utils/reserved-key-registry';
34
- import { tryStripDescriptionKeyword } from '../utils/description-helpers';
35
- import type {
36
- ParsedC4,
37
- C4Element,
38
- C4ElementType,
39
- C4Shape,
40
- C4ArrowType,
41
- C4Relationship,
42
- C4Group,
43
- C4DeploymentNode,
44
- } from './types';
45
-
46
- // ============================================================
47
- // Regex patterns
48
- // ============================================================
49
-
50
- const CONTAINER_RE = /^\[([^\]]+)\]$/;
51
-
52
- /** Matches element declarations: `person Name`, `system Name | k: v` */
53
- const ELEMENT_RE = /^(person|system|container|component)\s+(.+)$/i;
54
-
55
- /** Matches `is a <shape>` in the element name portion */
56
- const IS_A_RE = /\s+is\s+a(?:n)?\s+(\w+)\s*$/i;
57
-
58
- /** Matches `Name is a <type>` declarations (new preferred syntax) */
59
- const C4_IS_A_RE =
60
- /^([^:]+?)\s+is\s+an?\s+(person|system|container|component|external|database)\b(.*)$/i;
61
-
62
- /** Matches relationship arrows: `->`, `~>`, `<->`, `<~>` */
63
- const RELATIONSHIP_RE = /^(<?-?>|<?~?>)\s*(.+)$/;
64
-
65
- /**
66
- * Labeled arrow relationships: -label->, ~label~>, <-label->, <~label~>.
67
- * Label captured lazily so a line splits at the FIRST arrow — matches every
68
- * other chart parser (which use lazy `.+?`); greedy `.+` split at the last.
69
- */
70
- const C4_LABELED_SYNC_RE = /^-(.+?)->\s*(.+)$/;
71
- const C4_LABELED_ASYNC_RE = /^~(.+?)~>\s*(.+)$/;
72
- const C4_LABELED_BIDI_SYNC_RE = /^<-(.+?)->\s*(.+)$/;
73
- const C4_LABELED_BIDI_ASYNC_RE = /^<~(.+?)~>\s*(.+)$/;
74
-
75
- /** Matches section headers: `containers`, `components`, `deployment` (bare keyword) */
76
- const SECTION_HEADER_RE = /^(containers|components|deployment)\s*$/i;
77
-
78
- /** Matches `container X` references inside deployment nodes */
79
- const CONTAINER_REF_RE = /^container\s+(.+)$/i;
80
-
81
- /** Matches indented metadata: `key: value` (colon-separated) */
82
- const METADATA_RE = /^([a-z][a-z0-9-]*):\s+(.+)$/i;
83
-
84
- // ============================================================
85
- // Helpers
86
- // ============================================================
87
-
88
- const VALID_ELEMENT_TYPES = new Set<string>([
89
- 'person',
90
- 'system',
91
- 'container',
92
- 'component',
93
- ]);
94
-
95
- const VALID_SHAPES = new Set<string>([
96
- 'default',
97
- 'database',
98
- 'cache',
99
- 'queue',
100
- 'cloud',
101
- 'external',
102
- ]);
103
-
104
- /** Known top-level option keys for C4 diagrams. */
105
- const KNOWN_C4_OPTIONS = new Set<string>(['layout', 'active-tag']);
106
-
107
- /** Known C4 boolean options (bare keyword = on). */
108
- const KNOWN_C4_BOOLEANS = new Set<string>([
109
- 'direction-tb',
110
- 'solid-fill',
111
- 'no-title',
112
- ]);
113
-
114
- const ALL_CHART_TYPES = [
115
- 'c4',
116
- 'org',
117
- 'class',
118
- 'flowchart',
119
- 'sequence',
120
- 'er',
121
- 'bar',
122
- 'line',
123
- 'pie',
124
- 'scatter',
125
- 'sankey',
126
- 'venn',
127
- 'timeline',
128
- 'arc',
129
- 'slope',
130
- 'kanban',
131
- ];
132
-
133
- /** Map from ParticipantType inference → C4Shape */
134
- function participantTypeToC4Shape(pType: string): C4Shape {
135
- switch (pType) {
136
- case 'database':
137
- return 'database';
138
- case 'cache':
139
- return 'cache';
140
- case 'queue':
141
- return 'queue';
142
- case 'external':
143
- return 'external';
144
- case 'networking':
145
- return 'cloud';
146
- default:
147
- return 'default';
148
- }
149
- }
150
-
151
- /** Infer C4Shape from element name and optional technology value. */
152
- function inferC4Shape(name: string, tech?: string): C4Shape {
153
- // Try tech value first (more specific)
154
- if (tech) {
155
- const techShape = participantTypeToC4Shape(inferParticipantType(tech));
156
- if (techShape !== 'default') return techShape;
157
- }
158
- // Fall back to name inference
159
- return participantTypeToC4Shape(inferParticipantType(name));
160
- }
161
-
162
- /**
163
- * Pull metadata out of a c4 line tail (the part after `name is a type`
164
- * or after a target identifier), accepting both legacy and new forms:
165
- * - legacy: `| k: v, k: v`
166
- * - new (§1.4): `k: v, k: v`
167
- * Returns `{}` when the tail has no metadata.
168
- */
169
- function parseC4MetaTail(
170
- tail: string,
171
- metaAliasMap: Map<string, string>
172
- ): Record<string, string> {
173
- const trimmed = tail.trim();
174
- if (!trimmed) return {};
175
- if (!trimmed.includes(':')) {
176
- // Non-empty tail with no `key:` — dropped silently (data loss
177
- // accepted pre-1.0; the legacy bare-tail diagnostic was removed).
178
- return {};
179
- }
180
- const registry = withTagAliases(C4_REGISTRY, new Set(metaAliasMap.keys()));
181
- // Force the whole tail into the metadata region by leading with the
182
- // always-reserved `color:` key (sentinel), then drop that pair. This
183
- // parses every `key: value` pair regardless of whether the first key
184
- // is reserved (matching the prior same-line metadata behavior).
185
- const split = splitNameAndMeta(
186
- `color: __c4ph, ${trimmed}`,
187
- registry,
188
- metaAliasMap
189
- );
190
- const meta = split.meta;
191
- if (meta['color'] === '__c4ph') delete meta['color'];
192
- return meta;
193
- }
194
-
195
- /**
196
- * Split a name-prefixed line into `{ name, metadata }`. Accepts both:
197
- * - legacy: `Name | k: v, k: v` (first-`|` cut)
198
- * - new (§1.4): `Name k: v, k: v` (first reserved-key cut via
199
- * `splitNameAndMeta` against `C4_REGISTRY` plus declared tag
200
- * aliases)
201
- */
202
- function parseC4NameAndMeta(
203
- text: string,
204
- metaAliasMap: Map<string, string>
205
- ): { name: string; metadata: Record<string, string> } {
206
- const registry = withTagAliases(C4_REGISTRY, new Set(metaAliasMap.keys()));
207
- const split = splitNameAndMeta(text, registry, metaAliasMap);
208
- return { name: split.name, metadata: split.meta };
209
- }
210
-
211
- function parseArrowType(arrow: string): C4ArrowType | null {
212
- switch (arrow) {
213
- case '->':
214
- return 'sync';
215
- case '~>':
216
- return 'async';
217
- case '<->':
218
- return 'bidirectional';
219
- case '<~>':
220
- return 'bidirectional-async';
221
- default:
222
- return null;
223
- }
224
- }
225
-
226
- // ============================================================
227
- // Stack entry types
228
- // ============================================================
229
-
230
- interface ElementStackEntry {
231
- kind: 'element';
232
- element: Writable<C4Element>;
233
- indent: number;
234
- }
235
-
236
- interface GroupStackEntry {
237
- kind: 'group';
238
- group: Writable<C4Group>;
239
- parentElement: Writable<C4Element>;
240
- indent: number;
241
- }
242
-
243
- interface SectionStackEntry {
244
- kind: 'section';
245
- sectionType: 'containers' | 'components';
246
- parentElement: Writable<C4Element>;
247
- indent: number;
248
- }
249
-
250
- interface DeploymentStackEntry {
251
- kind: 'deployment';
252
- node: Writable<C4DeploymentNode>;
253
- indent: number;
254
- }
255
-
256
- type StackEntry =
257
- | ElementStackEntry
258
- | GroupStackEntry
259
- | SectionStackEntry
260
- | DeploymentStackEntry;
261
-
262
- // ============================================================
263
- // Parser
264
- // ============================================================
265
-
266
- export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
267
- const options: Record<string, string> = {};
268
- const result: Writable<ParsedC4> = {
269
- title: null,
270
- titleLineNumber: null,
271
- options,
272
- tagGroups: [],
273
- elements: [],
274
- relationships: [],
275
- deployment: [],
276
- diagnostics: [],
277
- error: null,
278
- };
279
-
280
- const pushError = (
281
- line: number,
282
- message: string,
283
- severity: 'error' | 'warning' = 'error'
284
- ): void => {
285
- const diag = makeDgmoError(line, message, severity);
286
- result.diagnostics.push(diag);
287
- if (!result.error && severity === 'error')
288
- result.error = formatDgmoError(diag);
289
- };
290
-
291
- const fail = makeFail(result);
292
-
293
- if (!content?.trim()) {
294
- return fail(0, 'No content provided');
295
- }
296
-
297
- const lines = content.split('\n');
298
- let contentStarted = false;
299
- let sawChartType = false;
300
- let inDeployment = false;
301
-
302
- // Tag group parsing state
303
- let currentTagGroup: Writable<TagGroup> | null = null;
304
- // metaAliasMap: pipe-metadata-key aliases (`tech:` → `technology:`).
305
- // Distinct from nameAliasMap below per the A1 naming convention.
306
- const metaAliasMap = new Map<string, string>();
307
-
308
- // nameAliasMap: TD-18 entity-name aliases (`os` → `Order Service`).
309
- // Per C8: per-parse, never persisted, fresh each parse.
310
- const nameAliasMap = new Map<string, string>();
311
- function resolveNameRef(rawName: string): string {
312
- return nameAliasMap.get(rawName.trim()) ?? rawName;
313
- }
314
-
315
- // Name uniqueness tracking
316
- const knownNames = new Map<string, number>(); // name → lineNumber
317
-
318
- // Sidecar: each element's metadata is a mutable Record we own. The
319
- // C4Element.metadata field types it as Readonly, so post-construction
320
- // mutation goes through this map.
321
- const elementMetadata = new WeakMap<
322
- Writable<C4Element>,
323
- Record<string, string>
324
- >();
325
- // Sidecar: each element's description is a mutable string[] we own.
326
- const elementDescription = new WeakMap<Writable<C4Element>, string[]>();
327
-
328
- // Indent stack for hierarchy tracking
329
- const stack: StackEntry[] = [];
330
-
331
- // Deployment indent stack
332
- const deployStack: { node: Writable<C4DeploymentNode>; indent: number }[] =
333
- [];
334
-
335
- for (let i = 0; i < lines.length; i++) {
336
- // In-bounds by loop guard.
337
- const line = lines[i]!;
338
- const lineNumber = i + 1;
339
- const trimmed = line.trim();
340
-
341
- // Skip empty lines
342
- if (!trimmed) {
343
- if (currentTagGroup) currentTagGroup = null;
344
- continue;
345
- }
346
-
347
- // Skip comments
348
- if (trimmed.startsWith('//')) continue;
349
-
350
- // --- Header phase ---
351
-
352
- // First line: `c4` or `c4 My Title`
353
- if (!contentStarted && !sawChartType) {
354
- const firstLine = parseFirstLine(trimmed);
355
- if (firstLine) {
356
- if (firstLine.chartType !== 'c4') {
357
- let msg = `Expected chart type "c4", got "${firstLine.chartType}"`;
358
- const hint = suggest(firstLine.chartType, ALL_CHART_TYPES);
359
- if (hint) msg += `. ${hint}`;
360
- return fail(lineNumber, msg);
361
- }
362
- sawChartType = true;
363
- if (firstLine.title) {
364
- result.title = firstLine.title;
365
- result.titleLineNumber = lineNumber;
366
- }
367
- continue;
368
- }
369
- }
370
-
371
- // Tag group heading — `tag Name as <alias>`
372
- // Must be checked BEFORE OPTION_RE to prevent `tag: Rank` being swallowed as option
373
- const tagBlockMatch = matchTagBlockHeading(trimmed);
374
- if (tagBlockMatch) {
375
- if (contentStarted) {
376
- pushError(lineNumber, 'Tag groups must appear before content');
377
- continue;
378
- }
379
- currentTagGroup = {
380
- name: tagBlockMatch.name,
381
- ...(tagBlockMatch.alias !== undefined && {
382
- alias: tagBlockMatch.alias,
383
- }),
384
- entries: [],
385
- lineNumber,
386
- };
387
- if (tagBlockMatch.alias) {
388
- metaAliasMap.set(
389
- normalizeName(tagBlockMatch.alias),
390
- tagAttrKey(tagBlockMatch.name)
391
- );
392
- }
393
- metaAliasMap.set(
394
- normalizeName(tagBlockMatch.name),
395
- tagAttrKey(tagBlockMatch.name)
396
- );
397
- result.tagGroups.push(currentTagGroup);
398
- continue;
399
- }
400
-
401
- // Generic header options (space-separated: `key value` or bare boolean)
402
- if (!contentStarted && !currentTagGroup && measureIndent(line) === 0) {
403
- // Bare boolean options
404
- if (KNOWN_C4_BOOLEANS.has(trimmed.toLowerCase())) {
405
- options[trimmed.toLowerCase()] = 'on';
406
- continue;
407
- }
408
-
409
- const optMatch = trimmed.match(OPTION_NOCOLON_RE);
410
- if (optMatch) {
411
- // Capture groups [1] and [2] guaranteed by regex match.
412
- const key = optMatch[1]!.trim().toLowerCase();
413
- if (KNOWN_C4_OPTIONS.has(key)) {
414
- options[key] = optMatch[2]!.trim();
415
- continue;
416
- }
417
- }
418
- }
419
-
420
- // Tag group entries — first entry is the default unless another is marked `default`
421
- if (currentTagGroup && !contentStarted) {
422
- const indent = measureIndent(line);
423
- if (indent > 0) {
424
- const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
425
- const { label, color } = extractColor(
426
- cleanEntry,
427
- palette,
428
- result.diagnostics,
429
- lineNumber
430
- );
431
- // Bare value (no explicit color) → keep it; the post-parse
432
- // finalize pass assigns a deterministic palette color.
433
- if (isDefault) {
434
- currentTagGroup.defaultValue = label;
435
- } else if (currentTagGroup.entries.length === 0) {
436
- currentTagGroup.defaultValue = label;
437
- }
438
- currentTagGroup.entries.push({
439
- value: label,
440
- color: color ?? AUTO_TAG_COLOR_SENTINEL,
441
- lineNumber,
442
- });
443
- continue;
444
- }
445
- currentTagGroup = null; // eslint-disable-line no-useless-assignment
446
- }
447
-
448
- // --- Content phase ---
449
- contentStarted = true;
450
- currentTagGroup = null;
451
-
452
- if (!sawChartType) {
453
- return fail(lineNumber, 'Missing "c4" header');
454
- }
455
-
456
- const indent = measureIndent(line);
457
-
458
- // ── Deployment section ──────────────────────────────────
459
- if (inDeployment) {
460
- // Pop deployment stack for decreased indent
461
- while (deployStack.length > 0) {
462
- // In-bounds by length > 0 check.
463
- const top = deployStack[deployStack.length - 1]!;
464
- if (top.indent < indent) break;
465
- deployStack.pop();
466
- }
467
-
468
- // Check for top-level non-deployment content (section ended)
469
- if (
470
- indent === 0 &&
471
- (C4_IS_A_RE.test(trimmed) || ELEMENT_RE.test(trimmed))
472
- ) {
473
- inDeployment = false;
474
- // Fall through to element parsing below
475
- } else {
476
- // container X reference?
477
- const refMatch = trimmed.match(CONTAINER_REF_RE);
478
- if (refMatch) {
479
- // Capture group [1] guaranteed by regex match.
480
- const refName = refMatch[1]!.trim();
481
- if (deployStack.length > 0) {
482
- // In-bounds by length > 0 check.
483
- deployStack[deployStack.length - 1]!.node.containerRefs.push(
484
- refName
485
- );
486
- } else {
487
- pushError(
488
- lineNumber,
489
- `"container ${refName}" must be indented under a deployment node (e.g. under 'Prod is a deployment-node')`
490
- );
491
- }
492
- continue;
493
- }
494
-
495
- // Otherwise it's a deployment node (possibly with metadata).
496
- // Accepts both `Name | k: v` (legacy) and `Name k: v` (§1.4).
497
- const { name: nodeName, metadata } = parseC4NameAndMeta(
498
- trimmed,
499
- metaAliasMap
500
- );
501
- warnUnknownMetaKeys(
502
- metadata,
503
- withTagAliases(C4_REGISTRY, new Set(metaAliasMap.keys())),
504
- (msg) => pushError(lineNumber, msg, 'warning'),
505
- nodeName
506
- );
507
- const shape = inferC4Shape(
508
- nodeName,
509
- metadata['tech'] ?? metadata['technology']
510
- );
511
-
512
- const dNode: Writable<C4DeploymentNode> = {
513
- name: nodeName,
514
- metadata,
515
- shape,
516
- children: [],
517
- containerRefs: [],
518
- lineNumber,
519
- };
520
-
521
- if (deployStack.length > 0) {
522
- // In-bounds by length > 0 check.
523
- deployStack[deployStack.length - 1]!.node.children.push(dNode);
524
- } else {
525
- result.deployment.push(dNode);
526
- }
527
- deployStack.push({ node: dNode, indent });
528
- continue;
529
- }
530
- }
531
-
532
- // ── Section headers ─────────────────────────────────────
533
- const sectionMatch = trimmed.match(SECTION_HEADER_RE);
534
- if (sectionMatch) {
535
- // Capture group [1] guaranteed by regex match.
536
- const sectionType = sectionMatch[1]!.toLowerCase();
537
-
538
- if (sectionType === 'deployment') {
539
- inDeployment = true;
540
- continue;
541
- }
542
-
543
- // containers / components must be inside an element
544
- const parentEntry = findParentElement(indent, stack);
545
- if (parentEntry) {
546
- parentEntry.element.sectionHeader = sectionType as
547
- | 'containers'
548
- | 'components';
549
- parentEntry.element.sectionHeaderLineNumber = lineNumber;
550
- stack.push({
551
- kind: 'section',
552
- sectionType: sectionType as 'containers' | 'components',
553
- parentElement: parentEntry.element,
554
- indent,
555
- });
556
- } else {
557
- pushError(
558
- lineNumber,
559
- `"${sectionType}" must be indented under an element (e.g. under 'Web App is a container')`
560
- );
561
- }
562
- continue;
563
- }
564
-
565
- // ── Pop stack for decreased indent ──────────────────────
566
- while (stack.length > 0) {
567
- // In-bounds by length > 0 check.
568
- const top = stack[stack.length - 1]!;
569
- if (top.indent < indent) break;
570
- stack.pop();
571
- }
572
-
573
- // ── Group boundaries: [Group Name] ──────────────────────
574
- const containerMatch = trimmed.match(CONTAINER_RE);
575
- if (containerMatch) {
576
- // Capture group [1] guaranteed by regex match.
577
- const groupName = containerMatch[1]!.trim();
578
- const parentEntry = findParentElement(indent, stack);
579
- if (parentEntry) {
580
- const group: Writable<C4Group> = {
581
- name: groupName,
582
- children: [],
583
- lineNumber,
584
- };
585
- parentEntry.element.groups.push(group);
586
- stack.push({
587
- kind: 'group',
588
- group,
589
- parentElement: parentEntry.element,
590
- indent,
591
- });
592
- } else {
593
- pushError(
594
- lineNumber,
595
- `Group [${groupName}] must be indented under an element (e.g. under 'Web App is a container')`
596
- );
597
- }
598
- continue;
599
- }
600
-
601
- // ── Labeled arrow relationships: -label->, ~label~>, <-label->, <~label~> ──
602
- // Must be checked BEFORE plain RELATIONSHIP_RE to avoid partial matches
603
- {
604
- const labeledPatterns: {
605
- re: RegExp;
606
- arrowType: C4ArrowType;
607
- }[] = [
608
- { re: C4_LABELED_BIDI_SYNC_RE, arrowType: 'bidirectional' },
609
- { re: C4_LABELED_BIDI_ASYNC_RE, arrowType: 'bidirectional-async' },
610
- { re: C4_LABELED_SYNC_RE, arrowType: 'sync' },
611
- { re: C4_LABELED_ASYNC_RE, arrowType: 'async' },
612
- ];
613
- let labeledHandled = false;
614
- for (const { re, arrowType } of labeledPatterns) {
615
- const m = trimmed.match(re);
616
- if (!m) continue;
617
- // Capture groups [1] and [2] guaranteed by regex match.
618
- const rawLabel = m[1]!.trim();
619
- const targetBody = m[2]!.trim();
620
- if (!rawLabel) break; // empty label — fall through to plain arrow
621
-
622
- // Reject bidirectional arrows
623
- if (
624
- arrowType === 'bidirectional' ||
625
- arrowType === 'bidirectional-async'
626
- ) {
627
- const source = findParentElement(indent, stack)?.element.name ?? '?';
628
- pushError(
629
- lineNumber,
630
- `Bidirectional arrows are no longer supported. Replace with two separate arrows:\n -${rawLabel}-> ${targetBody}\n ${targetBody} -${rawLabel}-> ${source}`
631
- );
632
- labeledHandled = true;
633
- break;
634
- }
635
-
636
- // TD-5: the trailing `[tech]` sugar is no longer extracted from the
637
- // in-arrow label. The entire label stays as the label. Technology
638
- // metadata comes from post-colon or pipe metadata on the target.
639
- // Also run TD-13/TD-14 validation on the label characters.
640
- const labelResult = parseInArrowLabel(rawLabel, lineNumber);
641
- labelResult.diagnostics.forEach((d) => result.diagnostics.push(d));
642
- const label: string | undefined = labelResult.label;
643
- let technology: string | undefined;
644
-
645
- // Extract metadata from target body. Accepts both
646
- // `Database | tech: SQL` (legacy) and `Database tech: SQL`
647
- // (§1.4). tech/technology on metadata override `[tech]` in
648
- // the label.
649
- const targetParsed = parseC4NameAndMeta(targetBody, metaAliasMap);
650
- warnUnknownMetaKeys(
651
- targetParsed.metadata,
652
- withTagAliases(C4_REGISTRY, new Set(metaAliasMap.keys())),
653
- (msg) => pushError(lineNumber, msg, 'warning'),
654
- targetParsed.name
655
- );
656
- const target = targetParsed.name;
657
- if (targetParsed.metadata['tech']) {
658
- technology = targetParsed.metadata['tech'];
659
- }
660
- if (targetParsed.metadata['technology']) {
661
- technology = targetParsed.metadata['technology'];
662
- }
663
-
664
- const rel: C4Relationship = {
665
- target: resolveNameRef(target),
666
- ...(label !== undefined && { label }),
667
- ...(technology !== undefined && { technology }),
668
- arrowType,
669
- lineNumber,
670
- };
671
-
672
- const parentEntry = findParentElement(indent, stack);
673
- if (parentEntry) {
674
- parentEntry.element.relationships.push(rel);
675
- } else {
676
- result.relationships.push(rel);
677
- }
678
- labeledHandled = true;
679
- break;
680
- }
681
- if (labeledHandled) continue;
682
- }
683
-
684
- // ── Relationships (plain arrows: ->, ~>) ─────────────────
685
- const relMatch = trimmed.match(RELATIONSHIP_RE);
686
- if (relMatch) {
687
- // Capture groups [1] and [2] guaranteed by regex match.
688
- const arrowType = parseArrowType(relMatch[1]!);
689
- if (arrowType) {
690
- // Reject bidirectional arrows
691
- if (
692
- arrowType === 'bidirectional' ||
693
- arrowType === 'bidirectional-async'
694
- ) {
695
- const arrow = relMatch[1]!;
696
- const target = relMatch[2]!.trim();
697
- const source = findParentElement(indent, stack)?.element.name ?? '?';
698
- pushError(
699
- lineNumber,
700
- `'${arrow}' bidirectional arrows are no longer supported. Replace with two separate arrows:\n -> ${target}\n ${target} -> ${source}`
701
- );
702
- continue;
703
- }
704
-
705
- // Plain arrow: entire body is the target (no colon label, no technology)
706
- const target = relMatch[2]!.trim();
707
- const rel: C4Relationship = {
708
- target: resolveNameRef(target),
709
- arrowType,
710
- lineNumber,
711
- };
712
-
713
- // Attach to nearest parent element
714
- const parentEntry = findParentElement(indent, stack);
715
- if (parentEntry) {
716
- parentEntry.element.relationships.push(rel);
717
- } else {
718
- // Top-level relationship (orphan) — add to result-level relationships
719
- result.relationships.push(rel);
720
- }
721
- continue;
722
- }
723
- }
724
-
725
- // ── "Name is a type" declarations (preferred syntax) ────
726
- const isATypeMatch = trimmed.match(C4_IS_A_RE);
727
- if (isATypeMatch) {
728
- // Capture groups [1], [2], [3] guaranteed by regex match.
729
- let namePart = isATypeMatch[1]!.trim();
730
- const rawType = isATypeMatch[2]!.toLowerCase();
731
- let remainder = isATypeMatch[3]!;
732
-
733
- // TD-18: peel optional `as <alias>` from the trailing remainder
734
- // (modifier order: `Name is a TYPE [is a SHAPE] as <alias> [| meta]`).
735
- const asPostfix = remainder.match(
736
- /^(.*?)\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11})\s*(\|.*)?$/
737
- );
738
- if (asPostfix) {
739
- // Capture groups [1] and [2] guaranteed by regex match.
740
- nameAliasMap.set(asPostfix[2]!, namePart);
741
- remainder = (asPostfix[1]! + (asPostfix[3] ?? '')).trim();
742
- } else {
743
- // Or peel from the namePart itself in `Name as <alias> is a TYPE` form.
744
- const asInName = namePart.match(
745
- /^(.*?)\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11})\s*$/
746
- );
747
- if (asInName) {
748
- // Capture groups [1] and [2] guaranteed by regex match.
749
- namePart = asInName[1]!.trim();
750
- nameAliasMap.set(asInName[2]!, namePart);
751
- }
752
- }
753
-
754
- // Map external/database to shape overrides with default element type
755
- let elementType: C4ElementType;
756
- let explicitShape: C4Shape | null = null;
757
- if (rawType === 'external') {
758
- elementType = 'system';
759
- explicitShape = 'external';
760
- } else if (rawType === 'database') {
761
- elementType = 'container';
762
- explicitShape = 'database';
763
- } else {
764
- elementType = rawType as C4ElementType;
765
- }
766
-
767
- // Parse metadata from remainder. Accepts both legacy
768
- // (`| k: v, ...`) and new §1.4 (`k: v, ...`) forms.
769
- const remainderTrimmed = remainder.trim();
770
- let metaTail = remainderTrimmed;
771
-
772
- // Check for additional `is a <shape>` in the name (e.g., already stripped by C4_IS_A_RE won't happen,
773
- // but handle remainder like "is a cylinder" after type)
774
- const remainderIsA = remainderTrimmed.match(
775
- /^\s*is\s+a(?:n)?\s+(\w+)\s*(.*)$/i
776
- );
777
- if (remainderIsA) {
778
- // Capture groups [1] and [2] guaranteed by regex match.
779
- const shapeName = remainderIsA[1]!.toLowerCase();
780
- if (VALID_SHAPES.has(shapeName)) {
781
- explicitShape = shapeName as C4Shape;
782
- } else {
783
- pushError(
784
- lineNumber,
785
- `Unknown shape "${remainderIsA[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}`
786
- );
787
- }
788
- // Re-parse remainder after shape
789
- metaTail = remainderIsA[2]!.trim();
790
- }
791
-
792
- // Also check for `is a <shape>` within the name part itself
793
- const nameIsAMatch = namePart.match(IS_A_RE);
794
- if (nameIsAMatch) {
795
- // Capture group [1] guaranteed by regex match.
796
- const shapeName = nameIsAMatch[1]!.toLowerCase();
797
- if (VALID_SHAPES.has(shapeName)) {
798
- explicitShape = shapeName as C4Shape;
799
- } else {
800
- pushError(
801
- lineNumber,
802
- `Unknown shape "${nameIsAMatch[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}`
803
- );
804
- }
805
- namePart = namePart.substring(0, nameIsAMatch.index!).trim();
806
- }
807
-
808
- const metadata = parseC4MetaTail(metaTail, metaAliasMap);
809
-
810
- const shape =
811
- explicitShape ??
812
- inferC4Shape(namePart, metadata['tech'] ?? metadata['technology']);
813
-
814
- // Extract description from pipe metadata into dedicated field
815
- let isADescription: string[] | undefined;
816
- if ('description' in metadata) {
817
- const descVal = metadata['description'].trim();
818
- if (descVal) isADescription = [descVal];
819
- delete metadata['description'];
820
- }
821
-
822
- const element: Writable<C4Element> = {
823
- name: namePart,
824
- type: elementType,
825
- shape,
826
- metadata,
827
- ...(isADescription !== undefined && { description: isADescription }),
828
- children: [],
829
- groups: [],
830
- relationships: [],
831
- lineNumber,
832
- };
833
- elementMetadata.set(element, metadata);
834
- if (isADescription !== undefined)
835
- elementDescription.set(element, isADescription);
836
-
837
- // Check for duplicate name (forgiving: case + whitespace insensitive)
838
- const key = normalizeName(namePart);
839
- const existingLine = knownNames.get(key);
840
- if (existingLine !== undefined) {
841
- pushError(
842
- lineNumber,
843
- `Duplicate element name "${namePart}" (first defined on line ${existingLine})`
844
- );
845
- } else {
846
- knownNames.set(key, lineNumber);
847
- }
848
-
849
- attachElement(element, indent, stack, result);
850
- continue;
851
- }
852
-
853
- // ── Element declarations (deprecated prefix syntax) ─────
854
- const elementMatch = trimmed.match(ELEMENT_RE);
855
- if (elementMatch) {
856
- // Capture groups [1] and [2] guaranteed by regex match.
857
- const elementType = elementMatch[1]!.toLowerCase() as C4ElementType;
858
- const nameAndRest = elementMatch[2]!;
859
-
860
- const parsed = parseC4NameAndMeta(nameAndRest, metaAliasMap);
861
- warnUnknownMetaKeys(
862
- parsed.metadata,
863
- withTagAliases(C4_REGISTRY, new Set(metaAliasMap.keys())),
864
- (msg) => pushError(lineNumber, msg, 'warning'),
865
- parsed.name
866
- );
867
- let namePart = parsed.name;
868
- const metadata = parsed.metadata;
869
-
870
- // Check for `is a <shape>` in the name portion
871
- let explicitShape: C4Shape | null = null;
872
- const isAMatch = namePart.match(IS_A_RE);
873
- if (isAMatch) {
874
- // Capture group [1] guaranteed by regex match.
875
- const shapeName = isAMatch[1]!.toLowerCase();
876
- if (VALID_SHAPES.has(shapeName)) {
877
- explicitShape = shapeName as C4Shape;
878
- } else {
879
- pushError(
880
- lineNumber,
881
- `Unknown shape "${isAMatch[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}`
882
- );
883
- }
884
- namePart = namePart.substring(0, isAMatch.index!).trim();
885
- }
886
-
887
- // Emit deprecation error with migration hint
888
- pushError(
889
- lineNumber,
890
- `'${elementMatch[1]} ${namePart}' prefix syntax is no longer supported — use '${namePart} is a ${elementType}' instead`
891
- );
892
-
893
- // Determine shape: explicit > inference
894
- const shape =
895
- explicitShape ??
896
- inferC4Shape(namePart, metadata['tech'] ?? metadata['technology']);
897
-
898
- // Extract description from pipe metadata into dedicated field
899
- let prefixDescription: string[] | undefined;
900
- if ('description' in metadata) {
901
- const descVal = metadata['description'].trim();
902
- if (descVal) prefixDescription = [descVal];
903
- delete metadata['description'];
904
- }
905
-
906
- const element: Writable<C4Element> = {
907
- name: namePart,
908
- type: elementType,
909
- shape,
910
- metadata,
911
- ...(prefixDescription !== undefined && {
912
- description: prefixDescription,
913
- }),
914
- children: [],
915
- groups: [],
916
- relationships: [],
917
- lineNumber,
918
- };
919
- elementMetadata.set(element, metadata);
920
- if (prefixDescription !== undefined)
921
- elementDescription.set(element, prefixDescription);
922
-
923
- // Check for duplicate name (forgiving: case + whitespace insensitive)
924
- const key = normalizeName(namePart);
925
- const existingLine = knownNames.get(key);
926
- if (existingLine !== undefined) {
927
- pushError(
928
- lineNumber,
929
- `Duplicate element name "${namePart}" (first defined on line ${existingLine})`
930
- );
931
- } else {
932
- knownNames.set(key, lineNumber);
933
- }
934
-
935
- // Attach to parent or push to top-level
936
- attachElement(element, indent, stack, result);
937
- continue;
938
- }
939
-
940
- // ── Indented metadata (key: value) ──────────────────────
941
- // Only if we have a parent element and line doesn't look like a keyword
942
- const metadataMatch = trimmed.match(METADATA_RE);
943
- if (metadataMatch && !ELEMENT_RE.test(trimmed)) {
944
- const parentEntry = findParentElement(indent, stack);
945
- if (parentEntry) {
946
- // Capture groups [1] and [2] guaranteed by regex match.
947
- const rawKey = metadataMatch[1]!.trim().toLowerCase();
948
-
949
- // Special case: `import: file.dgmo`
950
- if (rawKey === 'import') {
951
- parentEntry.element.importPath = metadataMatch[2]!.trim();
952
- continue;
953
- }
954
-
955
- const key = metaAliasMap.get(rawKey) ?? rawKey;
956
- const value = metadataMatch[2]!.trim();
957
-
958
- // Extract description into dedicated field
959
- if (key === 'description') {
960
- let desc = elementDescription.get(parentEntry.element);
961
- if (!desc) {
962
- desc = [];
963
- elementDescription.set(parentEntry.element, desc);
964
- parentEntry.element.description = desc;
965
- }
966
- desc.push(value);
967
- continue;
968
- }
969
-
970
- const meta = elementMetadata.get(parentEntry.element);
971
- if (meta) meta[key] = value;
972
- continue;
973
- }
974
- }
975
-
976
- // ── Unknown line ────────────────────────────────────────
977
- // Check if it looks like a misspelled element keyword
978
- // split() always produces at least one segment.
979
- const firstWord = trimmed.split(/\s+/)[0]!.toLowerCase();
980
- if (firstWord.length > 3) {
981
- const hint = suggest(firstWord, [...VALID_ELEMENT_TYPES]);
982
- if (hint) {
983
- pushError(lineNumber, `Unknown keyword "${firstWord}". ${hint}`);
984
- continue;
985
- }
986
- }
987
-
988
- // If inside a parent, only the `description` keyword attaches as a
989
- // description; a bare prose line is no longer auto-promoted.
990
- const parent = findParentElement(indent, stack);
991
- const descResult = tryStripDescriptionKeyword(trimmed);
992
- if (parent && descResult.isKeyword && !descResult.needsColon) {
993
- let desc = elementDescription.get(parent.element);
994
- if (!desc) {
995
- desc = [];
996
- elementDescription.set(parent.element, desc);
997
- parent.element.description = desc;
998
- }
999
- desc.push(descResult.text);
1000
- } else {
1001
- pushError(
1002
- lineNumber,
1003
- `Unexpected content: "${trimmed}". Expected an element ('Web App is a container'), a relationship ('-Uses-> Database'), indented metadata ('tech: React'), or a group ('[Backend]'). (§8)`
1004
- );
1005
- }
1006
- }
1007
-
1008
- // Assign palette colors to bare (colorless) tag values.
1009
- finalizeAutoTagColors(result.tagGroups as Writable<TagGroup>[], palette);
1010
-
1011
- // ── Post-parse validation ───────────────────────────────
1012
- validateRelationshipTargets(result, knownNames, pushError);
1013
- validateDeploymentRefs(result, knownNames, pushError);
1014
- validateTagGroupNames(result.tagGroups, (line, msg) =>
1015
- pushError(line, msg, 'warning')
1016
- );
1017
-
1018
- return result;
1019
- }
1020
-
1021
- // ============================================================
1022
- // Attachment helpers
1023
- // ============================================================
1024
-
1025
- /** Find the nearest parent element entry on the stack at shallower indent. */
1026
- function findParentElement(
1027
- indent: number,
1028
- stack: StackEntry[]
1029
- ): ElementStackEntry | null {
1030
- for (let i = stack.length - 1; i >= 0; i--) {
1031
- // In-bounds by loop guard.
1032
- const entry = stack[i]!;
1033
- if (entry.indent >= indent) continue;
1034
- if (entry.kind === 'element') return entry;
1035
- if (entry.kind === 'group') {
1036
- // Walk further up to find the element that owns this group
1037
- continue;
1038
- }
1039
- if (entry.kind === 'section') {
1040
- // The section's parent element is the attachment target
1041
- return {
1042
- kind: 'element',
1043
- element: entry.parentElement,
1044
- indent: entry.indent,
1045
- };
1046
- }
1047
- }
1048
- return null;
1049
- }
1050
-
1051
- function attachElement(
1052
- element: Writable<C4Element>,
1053
- indent: number,
1054
- stack: StackEntry[],
1055
- result: Writable<ParsedC4>
1056
- ): void {
1057
- // Find the immediate context: group, section, or parent element
1058
- let attached = false;
1059
-
1060
- for (let i = stack.length - 1; i >= 0; i--) {
1061
- // In-bounds by loop guard.
1062
- const entry = stack[i]!;
1063
- if (entry.indent >= indent) continue;
1064
-
1065
- if (entry.kind === 'group') {
1066
- // Attach to the group
1067
- entry.group.children.push(element);
1068
- attached = true;
1069
- break;
1070
- }
1071
- if (entry.kind === 'section') {
1072
- // Attach as child of the section's parent element
1073
- entry.parentElement.children.push(element);
1074
- attached = true;
1075
- break;
1076
- }
1077
- if (entry.kind === 'element') {
1078
- entry.element.children.push(element);
1079
- attached = true;
1080
- break;
1081
- }
1082
- }
1083
-
1084
- if (!attached) {
1085
- result.elements.push(element);
1086
- }
1087
-
1088
- stack.push({ kind: 'element', element, indent });
1089
- }
1090
-
1091
- // ============================================================
1092
- // Post-parse validation
1093
- // ============================================================
1094
-
1095
- function validateRelationshipTargets(
1096
- result: ParsedC4,
1097
- knownNames: Map<string, number>,
1098
- pushWarning: (
1099
- line: number,
1100
- message: string,
1101
- severity?: 'error' | 'warning'
1102
- ) => void
1103
- ): void {
1104
- const allNames = Array.from(knownNames.keys());
1105
- function walkRels(elements: readonly C4Element[]) {
1106
- for (const el of elements) {
1107
- for (const rel of el.relationships) {
1108
- if (!knownNames.has(normalizeName(rel.target))) {
1109
- let msg = `Relationship target "${rel.target}" not found — declare it first (e.g. '${rel.target} is a container'), then reference it in the arrow`;
1110
- const hint = suggest(normalizeName(rel.target), allNames);
1111
- if (hint) msg += `. ${hint}`;
1112
- pushWarning(rel.lineNumber, msg, 'warning');
1113
- }
1114
- }
1115
- walkRels(el.children);
1116
- for (const g of el.groups) {
1117
- walkRels(g.children);
1118
- }
1119
- }
1120
- }
1121
- walkRels(result.elements);
1122
-
1123
- // Also check top-level relationships
1124
- for (const rel of result.relationships) {
1125
- if (!knownNames.has(normalizeName(rel.target))) {
1126
- let msg = `Relationship target "${rel.target}" not found`;
1127
- const hint = suggest(normalizeName(rel.target), allNames);
1128
- if (hint) msg += `. ${hint}`;
1129
- pushWarning(rel.lineNumber, msg, 'warning');
1130
- }
1131
- }
1132
- }
1133
-
1134
- function validateDeploymentRefs(
1135
- result: ParsedC4,
1136
- knownNames: Map<string, number>,
1137
- pushWarning: (
1138
- line: number,
1139
- message: string,
1140
- severity?: 'error' | 'warning'
1141
- ) => void
1142
- ): void {
1143
- const allNames = Array.from(knownNames.keys());
1144
- function walkDeploy(nodes: readonly C4DeploymentNode[]) {
1145
- for (const node of nodes) {
1146
- for (const ref of node.containerRefs) {
1147
- if (!knownNames.has(normalizeName(ref))) {
1148
- let msg = `Deployment reference "container ${ref}" not found — declare the container element first, then reference it under the deployment node`;
1149
- const hint = suggest(normalizeName(ref), allNames);
1150
- if (hint) msg += `. ${hint}`;
1151
- pushWarning(node.lineNumber, msg, 'warning');
1152
- }
1153
- }
1154
- walkDeploy(node.children);
1155
- }
1156
- }
1157
- walkDeploy(result.deployment);
1158
- }