agentic-mermaid 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/CHANGELOG.md +149 -4
  2. package/LICENSES/Apache-2.0.txt +202 -0
  3. package/README.md +25 -32
  4. package/THIRD_PARTY_NOTICES.md +29 -0
  5. package/assets/fonts/FONT-LICENSES.md +9 -5
  6. package/assets/fonts/Inter-Bold.ttf +0 -0
  7. package/assets/fonts/Inter-Medium.ttf +0 -0
  8. package/assets/fonts/Inter-Regular.ttf +0 -0
  9. package/assets/fonts/Inter-SemiBold.ttf +0 -0
  10. package/dist/agent-core.d.ts +463 -0
  11. package/dist/agent-core.js +397 -0
  12. package/dist/agent.d.ts +81 -1822
  13. package/dist/agent.js +336 -36839
  14. package/dist/agentic-mermaid-mcp.js +7 -37908
  15. package/dist/am.js +33638 -36261
  16. package/dist/chunk-4QECACIA.js +4702 -0
  17. package/dist/chunk-CDPKJ2BQ.js +37 -0
  18. package/dist/chunk-QKDLMOJJ.js +3487 -0
  19. package/dist/chunk-WI35L7IR.js +615 -0
  20. package/dist/chunk-WN3YDOBG.js +149 -0
  21. package/dist/chunk-ZYMMKQ3I.js +62637 -0
  22. package/dist/index-D54dqSdC.d.ts +6777 -0
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +310 -32836
  25. package/dist/mcp-cli-SBQSTJFC.js +17 -0
  26. package/docs/README.md +14 -6
  27. package/docs/schemas/style-spec.schema.json +1221 -30
  28. package/examples/styles/catalog.json +54 -0
  29. package/examples/styles/cloudflare-workers-inspired-prototype.mmd +9 -0
  30. package/examples/styles/cloudflare-workers-inspired-prototype.style.json +110 -0
  31. package/examples/styles/cupertino-prototype.mmd +10 -0
  32. package/examples/styles/cupertino-prototype.style.json +63 -0
  33. package/examples/styles/mid-century-report.style.json +2 -2
  34. package/examples/styles/star-chart-atlas.style.json +2 -1
  35. package/examples/styles/transit-route-map.style.json +2 -2
  36. package/examples/styles/vercel-inspired-prototype.mmd +6 -0
  37. package/examples/styles/vercel-inspired-prototype.style.json +87 -0
  38. package/package.json +70 -43
  39. package/server.json +41 -0
  40. package/skills/agentic-mermaid-diagram-workflow/SKILL.md +17 -36
  41. package/skills/agentic-mermaid-diagram-workflow/references/cli.md +4 -5
  42. package/skills/agentic-mermaid-diagram-workflow/references/code-mode.md +15 -31
  43. package/skills/agentic-mermaid-diagram-workflow/references/sequence.md +10 -14
  44. package/skills/agentic-mermaid-live-editor/SKILL.md +100 -206
  45. package/AGENT_NATIVE.md +0 -602
  46. package/Instructions_for_agents.md +0 -83
  47. package/TODO.md +0 -662
  48. package/assets/hero.png +0 -0
  49. package/dist/index-x2Cj1dVv.d.ts +0 -1089
  50. package/docs/agent-api-cookbook.md +0 -308
  51. package/docs/agent-mutation-policy.md +0 -104
  52. package/docs/agent-workflow-examples.md +0 -46
  53. package/docs/api.md +0 -203
  54. package/docs/ascii.md +0 -103
  55. package/docs/assets/improvements/box-start-connector.png +0 -0
  56. package/docs/assets/improvements/comment-dropped-lint.png +0 -0
  57. package/docs/assets/improvements/er-content-loss.png +0 -0
  58. package/docs/assets/improvements/fan-in-trunks.png +0 -0
  59. package/docs/assets/improvements/gantt-family.png +0 -0
  60. package/docs/assets/improvements/gantt-milestone-clamp.png +0 -0
  61. package/docs/assets/improvements/gantt-verify-seam.png +0 -0
  62. package/docs/assets/improvements/subgraph-container-edges.png +0 -0
  63. package/docs/assets/improvements/wrapper-frontmatter.png +0 -0
  64. package/docs/assets/improvements/wrapper-init-directive.png +0 -0
  65. package/docs/assets/improvements/wrapper-leading-comment.png +0 -0
  66. package/docs/assets/style-cookbook/mid-century-report.png +0 -0
  67. package/docs/assets/style-cookbook/star-chart-atlas.png +0 -0
  68. package/docs/assets/style-cookbook/transit-route-map.png +0 -0
  69. package/docs/comparison.md +0 -94
  70. package/docs/config.md +0 -105
  71. package/docs/contributing/adding-diagram-types.md +0 -144
  72. package/docs/contributing/diagram-family-citizenship.matrix.json +0 -2127
  73. package/docs/contributing/diagram-family-citizenship.md +0 -82
  74. package/docs/contributing/harvesting-upstream-tests.md +0 -126
  75. package/docs/contributing/lessons-learned.md +0 -276
  76. package/docs/contributing/releasing.md +0 -71
  77. package/docs/contributing/visual-review-evidence.md +0 -53
  78. package/docs/custom-style-cookbook.md +0 -154
  79. package/docs/design/families/README.md +0 -26
  80. package/docs/design/families/architecture-beta.md +0 -48
  81. package/docs/design/families/flowchart-parser-conformance.md +0 -22
  82. package/docs/design/families/gantt-research.md +0 -137
  83. package/docs/design/families/gantt.md +0 -329
  84. package/docs/design/families/journey-agentic-cards-before.png +0 -0
  85. package/docs/design/families/journey-mermaid-classic-implementation.png +0 -0
  86. package/docs/design/families/journey-mermaid-classic-prototype.png +0 -0
  87. package/docs/design/families/journey-mermaid-classic-prototype.svg +0 -163
  88. package/docs/design/families/journey-mermaid-classic-prototype.svg.png +0 -0
  89. package/docs/design/families/journey-migration-parity.md +0 -433
  90. package/docs/design/families/journey-usage-research.md +0 -423
  91. package/docs/design/families/journey.md +0 -125
  92. package/docs/design/families/xychart.md +0 -85
  93. package/docs/design/system/README.md +0 -45
  94. package/docs/design/system/abstraction-audit.md +0 -224
  95. package/docs/design/system/abstraction-recommendations.md +0 -655
  96. package/docs/design/system/architecture.mmd +0 -29
  97. package/docs/design/system/architecture.svg +0 -137
  98. package/docs/design/system/layout-guarantees-and-robustness.md +0 -351
  99. package/docs/design/system/layout-rubric.md +0 -138
  100. package/docs/design/system/route-contracts.md +0 -848
  101. package/docs/design/system/source-preservation-ladder.md +0 -52
  102. package/docs/design/system/styles-rollout.md +0 -216
  103. package/docs/design/system/ugly-layouts.md +0 -119
  104. package/docs/diagram-families.md +0 -193
  105. package/docs/features.md +0 -147
  106. package/docs/fork-differences.md +0 -130
  107. package/docs/getting-started.md +0 -175
  108. package/docs/issue-derived-test-cases.md +0 -156
  109. package/docs/layout-characterization/README.md +0 -245
  110. package/docs/layout-characterization/contact-sheet-families.md +0 -496
  111. package/docs/layout-characterization/contact-sheet.md +0 -638
  112. package/docs/layout-characterization/properties.md +0 -281
  113. package/docs/layout-characterization/visual-quality.md +0 -160
  114. package/docs/layout-characterization/visual-snapshots/architecture.svg +0 -79
  115. package/docs/layout-characterization/visual-snapshots/class.svg +0 -60
  116. package/docs/layout-characterization/visual-snapshots/er.svg +0 -61
  117. package/docs/layout-characterization/visual-snapshots/flowchart.svg +0 -58
  118. package/docs/layout-characterization/visual-snapshots/gantt.svg +0 -52
  119. package/docs/layout-characterization/visual-snapshots/journey.svg +0 -118
  120. package/docs/layout-characterization/visual-snapshots/pie.svg +0 -36
  121. package/docs/layout-characterization/visual-snapshots/quadrant.svg +0 -47
  122. package/docs/layout-characterization/visual-snapshots/sequence.svg +0 -59
  123. package/docs/layout-characterization/visual-snapshots/state.svg +0 -52
  124. package/docs/layout-characterization/visual-snapshots/timeline.svg +0 -64
  125. package/docs/layout-characterization/visual-snapshots/xychart.svg +0 -97
  126. package/docs/mcp-code-mode-rationale.md +0 -101
  127. package/docs/mcp-http-transport.md +0 -223
  128. package/docs/mermaid-layout-complaints.md +0 -837
  129. package/docs/mutation-testing.md +0 -206
  130. package/docs/project/divergences.md +0 -98
  131. package/docs/project/dogfooding-docs-strategy.md +0 -66
  132. package/docs/project/hosted-mcp-cloudflare-plan.md +0 -338
  133. package/docs/project/lessons-learned.md +0 -1019
  134. package/docs/project/remove-role-styling-plan.md +0 -176
  135. package/docs/quality.md +0 -212
  136. package/docs/react.md +0 -97
  137. package/docs/style-authoring.md +0 -133
  138. package/docs/testing-strategy.md +0 -335
  139. package/docs/theming.md +0 -162
  140. package/examples/agent-improve-auth-flow.ts +0 -206
  141. package/examples/agent-loop.ts +0 -53
  142. package/examples/mcp-vs-cli-complex-diagrams.ts +0 -133
  143. package/llms.txt +0 -104
  144. package/skill-evals/README.md +0 -77
  145. package/skill-evals/fixtures/architecture-cache/input.mmd +0 -5
  146. package/skill-evals/fixtures/class-payment/input.mmd +0 -4
  147. package/skill-evals/fixtures/er-orders/input.mmd +0 -10
  148. package/skill-evals/fixtures/flowchart-cache/input.mmd +0 -3
  149. package/skill-evals/fixtures/gantt-release/input.mmd +0 -7
  150. package/skill-evals/fixtures/journey-checkout/input.mmd +0 -6
  151. package/skill-evals/fixtures/pie-exports/input.mmd +0 -5
  152. package/skill-evals/fixtures/quadrant-priorities/input.mmd +0 -11
  153. package/skill-evals/fixtures/sequence-retry/input.mmd +0 -8
  154. package/skill-evals/fixtures/state-lifecycle/input.mmd +0 -5
  155. package/skill-evals/fixtures/timeline-release/input.mmd +0 -6
  156. package/skill-evals/fixtures/xychart-growth/input.mmd +0 -5
  157. package/skill-evals/shared-benchmark.json +0 -510
  158. package/src/agent/analyze.ts +0 -157
  159. package/src/agent/apply.ts +0 -146
  160. package/src/agent/architecture-body.ts +0 -458
  161. package/src/agent/certificates.ts +0 -110
  162. package/src/agent/class-body.ts +0 -319
  163. package/src/agent/core.ts +0 -207
  164. package/src/agent/create.ts +0 -106
  165. package/src/agent/describe.ts +0 -364
  166. package/src/agent/er-body.ts +0 -224
  167. package/src/agent/facts.ts +0 -337
  168. package/src/agent/families-builtin.ts +0 -934
  169. package/src/agent/families.ts +0 -232
  170. package/src/agent/family-layouts.ts +0 -826
  171. package/src/agent/flowchart-body.ts +0 -372
  172. package/src/agent/flowchart-unsupported.ts +0 -243
  173. package/src/agent/gantt-body.ts +0 -495
  174. package/src/agent/index.ts +0 -10
  175. package/src/agent/journey-body.ts +0 -414
  176. package/src/agent/label-metrics.ts +0 -28
  177. package/src/agent/layout-to-rendered.ts +0 -63
  178. package/src/agent/mutate.ts +0 -97
  179. package/src/agent/mutation-ops.ts +0 -51
  180. package/src/agent/op-schema.ts +0 -439
  181. package/src/agent/parse.ts +0 -223
  182. package/src/agent/pie-body.ts +0 -270
  183. package/src/agent/png.ts +0 -117
  184. package/src/agent/quadrant-body.ts +0 -336
  185. package/src/agent/quality.ts +0 -363
  186. package/src/agent/readability-audit.ts +0 -92
  187. package/src/agent/rendered-route-audit.ts +0 -61
  188. package/src/agent/sequence-body.ts +0 -345
  189. package/src/agent/serialize.ts +0 -214
  190. package/src/agent/state-body.ts +0 -589
  191. package/src/agent/structural-count.ts +0 -150
  192. package/src/agent/timeline-body.ts +0 -280
  193. package/src/agent/trace-log.ts +0 -43
  194. package/src/agent/types.ts +0 -979
  195. package/src/agent/verify.ts +0 -571
  196. package/src/agent/xychart-body.ts +0 -472
  197. package/src/architecture/config.ts +0 -282
  198. package/src/architecture/layout.ts +0 -496
  199. package/src/architecture/parser.ts +0 -375
  200. package/src/architecture/renderer.ts +0 -611
  201. package/src/architecture/types.ts +0 -113
  202. package/src/ascii/ansi.ts +0 -423
  203. package/src/ascii/architecture.ts +0 -210
  204. package/src/ascii/canvas.ts +0 -443
  205. package/src/ascii/class-diagram.ts +0 -697
  206. package/src/ascii/converter.ts +0 -475
  207. package/src/ascii/draw.ts +0 -1693
  208. package/src/ascii/edge-bundling.ts +0 -328
  209. package/src/ascii/edge-routing.ts +0 -330
  210. package/src/ascii/er-diagram.ts +0 -439
  211. package/src/ascii/gantt.ts +0 -276
  212. package/src/ascii/grid.ts +0 -747
  213. package/src/ascii/index.ts +0 -196
  214. package/src/ascii/journey.ts +0 -189
  215. package/src/ascii/meta.ts +0 -244
  216. package/src/ascii/multiline-utils.ts +0 -80
  217. package/src/ascii/pathfinder.ts +0 -301
  218. package/src/ascii/pie.ts +0 -67
  219. package/src/ascii/quadrant.ts +0 -158
  220. package/src/ascii/reverse.ts +0 -216
  221. package/src/ascii/sequence.ts +0 -499
  222. package/src/ascii/shapes/circle.ts +0 -27
  223. package/src/ascii/shapes/corners.ts +0 -143
  224. package/src/ascii/shapes/diamond.ts +0 -27
  225. package/src/ascii/shapes/hexagon.ts +0 -27
  226. package/src/ascii/shapes/index.ts +0 -106
  227. package/src/ascii/shapes/rectangle.ts +0 -168
  228. package/src/ascii/shapes/rounded.ts +0 -27
  229. package/src/ascii/shapes/special.ts +0 -332
  230. package/src/ascii/shapes/stadium.ts +0 -110
  231. package/src/ascii/shapes/state.ts +0 -192
  232. package/src/ascii/shapes/types.ts +0 -73
  233. package/src/ascii/timeline.ts +0 -128
  234. package/src/ascii/types.ts +0 -312
  235. package/src/ascii/validate.ts +0 -120
  236. package/src/ascii/width.ts +0 -81
  237. package/src/ascii/xychart.ts +0 -919
  238. package/src/browser.ts +0 -29
  239. package/src/class/layout.ts +0 -240
  240. package/src/class/parser.ts +0 -332
  241. package/src/class/renderer.ts +0 -638
  242. package/src/class/types.ts +0 -129
  243. package/src/cli/agent-instructions.ts +0 -1
  244. package/src/cli/am-bin.ts +0 -6
  245. package/src/cli/exit-codes.ts +0 -18
  246. package/src/cli/index.ts +0 -1117
  247. package/src/cli/init-agent.ts +0 -147
  248. package/src/color-resolver.ts +0 -152
  249. package/src/elk-instance.ts +0 -114
  250. package/src/er/layout.ts +0 -187
  251. package/src/er/parser.ts +0 -226
  252. package/src/er/renderer.ts +0 -792
  253. package/src/er/types.ts +0 -99
  254. package/src/font-manifest.ts +0 -27
  255. package/src/gantt/layout.ts +0 -365
  256. package/src/gantt/parser.ts +0 -282
  257. package/src/gantt/pipeline.ts +0 -27
  258. package/src/gantt/renderer.ts +0 -414
  259. package/src/gantt/schedule.ts +0 -513
  260. package/src/gantt/types.ts +0 -255
  261. package/src/index.ts +0 -368
  262. package/src/journey/layout.ts +0 -520
  263. package/src/journey/parser.ts +0 -218
  264. package/src/journey/renderer.ts +0 -811
  265. package/src/journey/types.ts +0 -144
  266. package/src/layout/geometry.ts +0 -182
  267. package/src/layout/pass.ts +0 -142
  268. package/src/layout/passes/index.ts +0 -3916
  269. package/src/layout-engine.ts +0 -1565
  270. package/src/layout-rubric.ts +0 -659
  271. package/src/mcp/artifacts.ts +0 -137
  272. package/src/mcp/dynamic-harness.ts +0 -31
  273. package/src/mcp/dynamic-modules.d.ts +0 -7
  274. package/src/mcp/facade.ts +0 -608
  275. package/src/mcp/harness-runtime.ts +0 -203
  276. package/src/mcp/hosted-server.ts +0 -468
  277. package/src/mcp/mcp-bin.ts +0 -3
  278. package/src/mcp/mcp-cli.ts +0 -82
  279. package/src/mcp/protocol.ts +0 -13
  280. package/src/mcp/sandbox.ts +0 -195
  281. package/src/mcp/sdk-decl.ts +0 -387
  282. package/src/mcp/server.ts +0 -362
  283. package/src/mcp/tool-surface.ts +0 -146
  284. package/src/mermaid-source.ts +0 -655
  285. package/src/multiline-utils.ts +0 -228
  286. package/src/parser.ts +0 -1031
  287. package/src/pie/layout.ts +0 -168
  288. package/src/pie/parser.ts +0 -136
  289. package/src/pie/renderer.ts +0 -234
  290. package/src/pie/types.ts +0 -82
  291. package/src/quadrant/layout.ts +0 -191
  292. package/src/quadrant/parser.ts +0 -183
  293. package/src/quadrant/renderer.ts +0 -322
  294. package/src/quadrant/types.ts +0 -113
  295. package/src/render-family-hooks.ts +0 -244
  296. package/src/renderer.ts +0 -1099
  297. package/src/route-contracts.ts +0 -2723
  298. package/src/scene/backend.ts +0 -76
  299. package/src/scene/fidelity.ts +0 -183
  300. package/src/scene/hybrid-backend.ts +0 -174
  301. package/src/scene/ir.ts +0 -181
  302. package/src/scene/marks.ts +0 -127
  303. package/src/scene/rough-backend.ts +0 -378
  304. package/src/scene/seed.ts +0 -46
  305. package/src/scene/style-registry.ts +0 -556
  306. package/src/sequence/layout.ts +0 -456
  307. package/src/sequence/parser.ts +0 -255
  308. package/src/sequence/renderer.ts +0 -604
  309. package/src/sequence/types.ts +0 -154
  310. package/src/shape-clipping.ts +0 -351
  311. package/src/shared/accessibility-directives.ts +0 -22
  312. package/src/shared/batched.ts +0 -61
  313. package/src/shared/color-math.ts +0 -72
  314. package/src/shared/svg-a11y.ts +0 -29
  315. package/src/shared/syntax-error.ts +0 -17
  316. package/src/shared/unicode-ranges.ts +0 -135
  317. package/src/styles.ts +0 -318
  318. package/src/svg-paths.ts +0 -28
  319. package/src/text-metrics.ts +0 -248
  320. package/src/theme.ts +0 -639
  321. package/src/timeline/layout.ts +0 -381
  322. package/src/timeline/parser.ts +0 -215
  323. package/src/timeline/renderer.ts +0 -538
  324. package/src/timeline/types.ts +0 -105
  325. package/src/types.ts +0 -454
  326. package/src/website-worker-modules.d.ts +0 -18
  327. package/src/xychart/axis-utils.ts +0 -109
  328. package/src/xychart/colors.ts +0 -124
  329. package/src/xychart/config.ts +0 -88
  330. package/src/xychart/layout.ts +0 -656
  331. package/src/xychart/parser.ts +0 -504
  332. package/src/xychart/renderer.ts +0 -640
  333. package/src/xychart/types.ts +0 -282
@@ -1,826 +0,0 @@
1
- // ============================================================================
2
- // QUAL-1 — RenderedLayout adapters for the non-graph diagram families.
3
- //
4
- // The perceptual-quality metrics (measureQuality / checkQuality) operate on a
5
- // RenderedLayout. Flowchart/state reuse the ELK geometric path; sequence and
6
- // timeline have bespoke adapters in index.ts. Everything else previously fell
7
- // through to emptyRenderedLayout, so those families showed nodeCount 0 — the
8
- // metrics were blind to them (TODO QUAL-1, docs/quality.md honest-gap).
9
- //
10
- // This module closes that gap by projecting each family's REAL positioned
11
- // layout (the same geometry the SVG renderer draws) into a RenderedLayout:
12
- //
13
- // family nodes edges groups
14
- // ------------ -------------------------- -------------------- ------------------
15
- // class class boxes relations —
16
- // er entity boxes relations —
17
- // journey task boxes + section labels — section frames
18
- // architecture services + junctions edges groups (flattened)
19
- // xychart bars + line points + axis — plot area
20
- // ticks
21
- // pie slice label boxes (legend — —
22
- // anchor + approx bbox)
23
- // quadrant points — quadrant regions
24
- // gantt tasks + milestones — section bands
25
- //
26
- // Opaque-safe: every adapter parses d.canonicalSource via the legacy parser +
27
- // layouter (the exact path renderMermaidSVG uses) wrapped in try/catch. An
28
- // invalid opaque diagram of a renderable family must NOT make layoutMermaid
29
- // throw — it degrades to emptyRenderedLayout. All coordinates pass through
30
- // toFinite (throws on NaN/Infinity), so a garbage coordinate fails loudly
31
- // rather than silently poisoning the metrics.
32
- //
33
- // Determinism: every layouter here is deterministic (no RNG / clock), so
34
- // layoutMermaid(d) called twice is deep-equal.
35
- // ============================================================================
36
-
37
- import type {
38
- ValidDiagram,
39
- RenderedLayout,
40
- RenderedLayoutNode,
41
- RenderedLayoutEdge,
42
- RenderedLayoutGroup,
43
- LayoutWarning,
44
- Finite,
45
- XyChartBody,
46
- PieBody,
47
- QuadrantBody,
48
- GanttBody,
49
- } from './types.ts'
50
- import type { FamilyEdgeRouteCertificate, Point, RegionContainmentCertificate } from '../types.ts'
51
- import { toFinite } from './types.ts'
52
- import { emptyRenderedLayout } from './layout-to-rendered.ts'
53
-
54
- import { toMermaidLines, normalizeMermaidSource } from '../mermaid-source.ts'
55
- import { parseClassDiagram } from '../class/parser.ts'
56
- import { layoutClassDiagram } from '../class/layout.ts'
57
- import { parseErDiagram } from '../er/parser.ts'
58
- import { layoutErDiagram } from '../er/layout.ts'
59
- import { layoutSequenceDiagram } from '../sequence/layout.ts'
60
- import { parseSequenceDiagram } from '../sequence/parser.ts'
61
- import { parseTimelineDiagram } from '../timeline/parser.ts'
62
- import { layoutTimelineDiagram } from '../timeline/layout.ts'
63
- import { parseJourneyDiagram } from '../journey/parser.ts'
64
- import { layoutJourneyDiagram } from '../journey/layout.ts'
65
- import { parseArchitectureDiagram } from '../architecture/parser.ts'
66
- import { layoutArchitectureDiagram } from '../architecture/layout.ts'
67
- import { applyXYChartFrontmatterConfig, parseXYChart, resolveXYChartConfig, resolveXYChartTheme } from '../xychart/parser.ts'
68
- import { layoutXYChart } from '../xychart/layout.ts'
69
- import type { XYAxis, XYChart } from '../xychart/types.ts'
70
- import type { PositionedArchitectureEdge, PositionedArchitectureGroup, PositionedArchitectureJunction, PositionedArchitectureService } from '../architecture/types.ts'
71
- import { parsePieChart } from '../pie/parser.ts'
72
- import { layoutPieChart } from '../pie/layout.ts'
73
- import type { PieChart } from '../pie/types.ts'
74
- import { parseQuadrantChart } from '../quadrant/parser.ts'
75
- import { layoutQuadrantChart } from '../quadrant/layout.ts'
76
- import type { QuadrantChart } from '../quadrant/types.ts'
77
- import { parseGanttModel, applyGanttFrontmatterConfig, GANTT_DURATION_RE } from '../gantt/parser.ts'
78
- import { resolveGanttSchedule } from '../gantt/schedule.ts'
79
- import { buildGanttRenderPipeline } from '../gantt/pipeline.ts'
80
- import { layoutGantt } from '../gantt/layout.ts'
81
- import type { GanttEndExpr, GanttLayoutResult, GanttModel, GanttModelSection, GanttModelTask, GanttStartExpr, GanttTaskTag } from '../gantt/types.ts'
82
-
83
- function f(n: number): Finite { return toFinite(Math.round(n)) }
84
-
85
- /** Round a span against its rounded start. Rounding x and width
86
- * independently can shift the far edge ±1px away from rounded edge
87
- * endpoints, which fires the class/ER anchor tripwires (TOL 0.5) on
88
- * geometry that sits exactly on-boundary pre-rounding — observed live on
89
- * an onboarding-probe ER diagram. Used where edgeAnchors checks run. */
90
- function fSpan(start: number, length: number): Finite { return toFinite(Math.round(start + length) - Math.round(start)) }
91
-
92
- /**
93
- * Family-aware RenderedLayout adapter. Dispatches on body kind; for the
94
- * renderable non-graph families it layouts from d.canonicalSource (which is
95
- * always populated for both structured and opaque bodies). Returns null for
96
- * families this module doesn't own (flowchart/state graph layouts and opaque
97
- * flowchart compatibility are handled in index.ts) so the caller can keep its
98
- * existing dispatch.
99
- */
100
- export function layoutFamilyToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout | null {
101
- // Each adapter is self-contained: it wraps the legacy parse+layout in
102
- // try/catch and degrades to emptyRenderedLayout(d.kind) on render-error, so
103
- // an invalid opaque body of a renderable family never makes this throw.
104
- switch (d.kind) {
105
- case 'class': return classToRendered(d, opts)
106
- case 'er': return erToRendered(d, opts)
107
- case 'sequence': return sequenceToRendered(d, opts)
108
- case 'timeline': return timelineToRendered(d, opts)
109
- case 'journey': return journeyToRendered(d)
110
- case 'architecture': return architectureToRendered(d, opts)
111
- case 'xychart': return xychartToRendered(d, opts)
112
- case 'pie': return pieToRendered(d, opts)
113
- case 'quadrant': return quadrantToRendered(d, opts)
114
- case 'gantt': return ganttToRendered(d, opts)
115
- default: return null
116
- }
117
- }
118
-
119
- // ---- class ----------------------------------------------------------------
120
-
121
- function classToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
122
- try {
123
- const positioned = layoutClassDiagram(parseClassDiagram(toMermaidLines(d.canonicalSource)))
124
- const nodes: RenderedLayoutNode[] = positioned.classes.map(c => ({
125
- id: c.id, x: f(c.x), y: f(c.y), w: fSpan(c.x, c.width), h: fSpan(c.y, c.height), shape: 'rectangle', label: c.label,
126
- }))
127
- const boxById = new Map(positioned.classes.map(c => [c.id, { x: c.x, y: c.y, width: c.width, height: c.height }]))
128
- const edges: RenderedLayoutEdge[] = positioned.relationships.map((r, i) => ({
129
- id: `rel#${i}:${r.from}->${r.to}`, from: r.from, to: r.to,
130
- path: r.points.map(p => [f(p.x), f(p.y)] as [Finite, Finite]),
131
- label: r.label && r.labelPosition ? { x: f(r.labelPosition.x), y: f(r.labelPosition.y), text: r.label } : undefined,
132
- route: opts.debug ? boxRouteCertificate('class', i, r.points, boxById.get(r.from), boxById.get(r.to)) : undefined,
133
- }))
134
- return { version: 1, kind: d.kind, nodes, edges, groups: [], bounds: { w: f(positioned.width), h: f(positioned.height) } }
135
- } catch { return emptyRenderedLayout(d.kind) }
136
- }
137
-
138
- // ---- er -------------------------------------------------------------------
139
-
140
- function erToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
141
- try {
142
- const positioned = layoutErDiagram(parseErDiagram(toMermaidLines(d.canonicalSource)))
143
- const nodes: RenderedLayoutNode[] = positioned.entities.map(e => ({
144
- id: e.id, x: f(e.x), y: f(e.y), w: fSpan(e.x, e.width), h: fSpan(e.y, e.height), shape: 'rectangle', label: e.label,
145
- }))
146
- const boxById = new Map(positioned.entities.map(e => [e.id, { x: e.x, y: e.y, width: e.width, height: e.height }]))
147
- const edges: RenderedLayoutEdge[] = positioned.relationships.map((r, i) => ({
148
- id: `rel#${i}:${r.entity1}->${r.entity2}`, from: r.entity1, to: r.entity2,
149
- path: r.points.map(p => [f(p.x), f(p.y)] as [Finite, Finite]),
150
- label: r.label ? labelMidpoint(r.points, r.label) : undefined,
151
- route: opts.debug ? boxRouteCertificate('er', i, r.points, boxById.get(r.entity1), boxById.get(r.entity2)) : undefined,
152
- }))
153
- return { version: 1, kind: d.kind, nodes, edges, groups: [], bounds: { w: f(positioned.width), h: f(positioned.height) } }
154
- } catch { return emptyRenderedLayout(d.kind) }
155
- }
156
-
157
- function boxRouteCertificate(
158
- family: 'class' | 'er',
159
- edgeIndex: number,
160
- points: Point[],
161
- source: { x: number; y: number; width: number; height: number } | undefined,
162
- target: { x: number; y: number; width: number; height: number } | undefined,
163
- ): FamilyEdgeRouteCertificate {
164
- const sourceBoundary = !!(source && points[0] && pointOnRawRectBoundary(points[0].x, points[0].y, source, 1))
165
- const targetBoundary = !!(target && points[points.length - 1] && pointOnRawRectBoundary(points[points.length - 1]!.x, points[points.length - 1]!.y, target, 1))
166
- const orthogonal = routeOrthogonal(points)
167
- return {
168
- family,
169
- edgeIndex,
170
- routeClass: 'family-layout',
171
- invariant: sourceBoundary && targetBoundary && orthogonal ? 'orthogonal-box' : 'unverified-family-route',
172
- bendCount: bendCount(points),
173
- orthogonal,
174
- sourceBoundary,
175
- targetBoundary,
176
- }
177
- }
178
-
179
- function routeOrthogonal(points: Point[]): boolean {
180
- for (let i = 1; i < points.length; i++) {
181
- const a = points[i - 1]!, b = points[i]!
182
- if (Math.abs(a.x - b.x) > 0.5 && Math.abs(a.y - b.y) > 0.5) return false
183
- }
184
- return true
185
- }
186
-
187
- function bendCount(points: Point[]): number {
188
- if (points.length < 3) return 0
189
- let bends = 0
190
- for (let i = 1; i < points.length - 1; i++) {
191
- const a = points[i - 1]!, b = points[i]!, c = points[i + 1]!
192
- const sameX = Math.abs(a.x - b.x) <= 0.5 && Math.abs(b.x - c.x) <= 0.5
193
- const sameY = Math.abs(a.y - b.y) <= 0.5 && Math.abs(b.y - c.y) <= 0.5
194
- if (!sameX && !sameY) bends++
195
- }
196
- return bends
197
- }
198
-
199
- function pointOnRawRectBoundary(x: number, y: number, n: { x: number; y: number; width: number; height: number }, tol: number): boolean {
200
- const onVertical = (Math.abs(x - n.x) <= tol || Math.abs(x - (n.x + n.width)) <= tol) && y >= n.y - tol && y <= n.y + n.height + tol
201
- const onHorizontal = (Math.abs(y - n.y) <= tol || Math.abs(y - (n.y + n.height)) <= tol) && x >= n.x - tol && x <= n.x + n.width + tol
202
- return onVertical || onHorizontal
203
- }
204
-
205
- function labelMidpoint(points: Array<{ x: number; y: number }>, text: string): { x: Finite; y: Finite; text: string } | undefined {
206
- if (points.length === 0) return undefined
207
- const mid = points[Math.floor(points.length / 2)]!
208
- return { x: f(mid.x), y: f(mid.y), text }
209
- }
210
-
211
- type ElementCertificateFamily = 'timeline' | 'xychart' | 'pie' | 'quadrant' | 'gantt'
212
- type ElementCertificateInvariant = RegionContainmentCertificate['invariant']
213
-
214
- function elementCertificates(
215
- family: ElementCertificateFamily,
216
- layout: RenderedLayout,
217
- invariant: ElementCertificateInvariant,
218
- referenceGroup?: RenderedLayoutGroup,
219
- containment: 'bounds' | 'center' = 'bounds',
220
- ): RegionContainmentCertificate[] {
221
- const groupsByMember = new Map<string, RenderedLayoutGroup>()
222
- for (const group of layout.groups) {
223
- for (const member of group.members) groupsByMember.set(member, group)
224
- }
225
- return layout.nodes.map(n => {
226
- const withinBounds = nodeWithinBounds(n, layout.bounds)
227
- const group = groupsByMember.get(n.id) ?? referenceGroup
228
- const withinGroup = group ? (containment === 'center' ? nodeCenterWithinGroup(n, group) : nodeWithinGroup(n, group)) : undefined
229
- return {
230
- family,
231
- elementId: n.id,
232
- routeClass: 'family-layout',
233
- invariant: withinBounds && (withinGroup ?? true) ? invariant : 'unverified-family-layout',
234
- bounds: { x: n.x, y: n.y, w: n.w, h: n.h },
235
- center: { x: f(n.x + n.w / 2), y: f(n.y + n.h / 2) },
236
- containment,
237
- withinBounds,
238
- ...(group ? { groupId: group.id, withinGroup } : {}),
239
- }
240
- })
241
- }
242
-
243
- function nodeWithinBounds(n: RenderedLayoutNode, bounds: RenderedLayout['bounds'], tol = 0.5): boolean {
244
- return n.x >= -tol && n.y >= -tol && n.x + n.w <= bounds.w + tol && n.y + n.h <= bounds.h + tol
245
- }
246
-
247
- function nodeWithinGroup(n: RenderedLayoutNode, g: RenderedLayoutGroup, tol = 0.5): boolean {
248
- return n.x >= g.x - tol && n.y >= g.y - tol && n.x + n.w <= g.x + g.w + tol && n.y + n.h <= g.y + g.h + tol
249
- }
250
-
251
- function nodeCenterWithinGroup(n: RenderedLayoutNode, g: RenderedLayoutGroup, tol = 0.5): boolean {
252
- const cx = n.x + n.w / 2
253
- const cy = n.y + n.h / 2
254
- return cx >= g.x - tol && cy >= g.y - tol && cx <= g.x + g.w + tol && cy <= g.y + g.h + tol
255
- }
256
-
257
- // ---- sequence -------------------------------------------------------------
258
-
259
- function sequenceToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
260
- try {
261
- if (d.kind !== 'sequence') return emptyRenderedLayout(d.kind)
262
- const positioned = layoutSequenceDiagram(parseSequenceDiagram(toMermaidLines(d.canonicalSource)))
263
- const lifelineByActor = new Map(positioned.lifelines.map(l => [l.actorId, l.x]))
264
- const nodes: RenderedLayoutNode[] = [
265
- ...positioned.actors.map(a => ({
266
- id: a.id, x: f(a.x - a.width / 2), y: f(a.y), w: f(a.width), h: f(a.height),
267
- shape: 'rectangle', label: a.label,
268
- })),
269
- ...positioned.notes.map((n, i) => ({
270
- id: `note#${i}`, x: f(n.x), y: f(n.y), w: f(n.width), h: f(n.height),
271
- shape: 'note', label: n.text,
272
- })),
273
- ]
274
- return {
275
- version: 1, kind: d.kind,
276
- nodes,
277
- edges: positioned.messages.map((m, i) => {
278
- const path = sequenceMessagePath(m)
279
- return {
280
- id: `msg#${i}:${m.from}->${m.to}`, from: m.from, to: m.to,
281
- path: path.map(p => [f(p.x), f(p.y)] as [Finite, Finite]),
282
- label: m.label ? sequenceMessageLabel(m) : undefined,
283
- route: opts.debug ? sequenceRouteCertificate(i, m, path, lifelineByActor) : undefined,
284
- }
285
- }),
286
- groups: positioned.blocks.map((b, i) => ({
287
- id: `block#${i}:${b.type}`, x: f(b.x), y: f(b.y), w: f(b.width), h: f(b.height), members: [], label: b.label,
288
- })),
289
- bounds: { w: f(positioned.width), h: f(positioned.height) },
290
- }
291
- } catch { return emptyRenderedLayout(d.kind) }
292
- }
293
-
294
- function sequenceMessagePath(message: { x1: number; x2: number; y: number; isSelf: boolean }): Point[] {
295
- if (!message.isSelf) return [{ x: message.x1, y: message.y }, { x: message.x2, y: message.y }]
296
- const loopW = 30
297
- const loopH = 20
298
- return [
299
- { x: message.x1, y: message.y },
300
- { x: message.x1 + loopW, y: message.y },
301
- { x: message.x1 + loopW, y: message.y + loopH },
302
- { x: message.x2, y: message.y + loopH },
303
- ]
304
- }
305
-
306
- function sequenceMessageLabel(message: { x1: number; x2: number; y: number; isSelf: boolean; label: string }): { x: Finite; y: Finite; text: string } {
307
- if (message.isSelf) return { x: f(message.x1 + 38), y: f(message.y + 10), text: message.label }
308
- return { x: f((message.x1 + message.x2) / 2), y: f(message.y - 10), text: message.label }
309
- }
310
-
311
- function sequenceRouteCertificate(
312
- edgeIndex: number,
313
- message: { from: string; to: string; x1: number; x2: number; y: number; isSelf: boolean },
314
- points: Point[],
315
- lifelineByActor: Map<string, number>,
316
- ): FamilyEdgeRouteCertificate {
317
- const sourceX = lifelineByActor.get(message.from)
318
- const targetX = lifelineByActor.get(message.to)
319
- const first = points[0]
320
- const last = points[points.length - 1]
321
- const sourceLifeline = sourceX !== undefined && first !== undefined && Math.abs(first.x - sourceX) <= 1
322
- const targetLifeline = targetX !== undefined && last !== undefined && Math.abs(last.x - targetX) <= 1
323
- const horizontal = !message.isSelf && points.every(p => Math.abs(p.y - message.y) <= 1)
324
- const orthogonal = routeOrthogonal(points)
325
- return {
326
- family: 'sequence',
327
- edgeIndex,
328
- routeClass: 'family-layout',
329
- invariant: sourceLifeline && targetLifeline && orthogonal ? (message.isSelf ? 'self-message' : 'lifeline-message') : 'unverified-family-route',
330
- bendCount: bendCount(points),
331
- horizontal,
332
- sourceLifeline,
333
- targetLifeline,
334
- selfMessage: message.isSelf,
335
- }
336
- }
337
-
338
- // ---- timeline -------------------------------------------------------------
339
-
340
- function timelineToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
341
- try {
342
- const positioned = layoutTimelineDiagram(parseTimelineDiagram(toMermaidLines(d.canonicalSource)))
343
- const nodes: RenderedLayoutNode[] = []
344
- const groups: RenderedLayoutGroup[] = []
345
- for (const section of positioned.sections) {
346
- const members: string[] = []
347
- for (const period of section.periods) {
348
- const periodId = `${period.id}:period`
349
- nodes.push({
350
- id: periodId, x: f(period.pillX), y: f(period.pillY), w: f(period.pillWidth), h: f(period.pillHeight),
351
- shape: 'rounded', label: period.label,
352
- })
353
- members.push(periodId)
354
- for (const event of period.events) {
355
- nodes.push({
356
- id: event.id, x: f(event.x), y: f(event.y), w: f(event.width), h: f(event.height),
357
- shape: 'rectangle', label: event.text,
358
- })
359
- members.push(event.id)
360
- }
361
- }
362
- if (section.framed) {
363
- groups.push({ id: section.id, x: f(section.x), y: f(section.y), w: f(section.width), h: f(section.height), members, label: section.label })
364
- }
365
- }
366
- const layout: RenderedLayout = { version: 1, kind: d.kind, nodes, edges: [], groups, bounds: { w: f(positioned.width), h: f(positioned.height) } }
367
- if (opts.debug) layout.certificates = elementCertificates('timeline', layout, 'timeline-interval')
368
- return layout
369
- } catch { return emptyRenderedLayout(d.kind) }
370
- }
371
-
372
- // ---- journey --------------------------------------------------------------
373
-
374
- function journeyToRendered(d: ValidDiagram): RenderedLayout {
375
- try {
376
- const positioned = layoutJourneyDiagram(parseJourneyDiagram(toMermaidLines(d.canonicalSource)))
377
- const nodes: RenderedLayoutNode[] = []
378
- const groups: RenderedLayoutGroup[] = []
379
- for (const s of positioned.sections) {
380
- const memberIds: string[] = []
381
- for (const t of s.tasks) {
382
- nodes.push({ id: t.id, x: f(t.x), y: f(t.y), w: f(t.width), h: f(t.height), shape: 'rectangle', label: t.text })
383
- memberIds.push(t.id)
384
- }
385
- groups.push({ id: s.id, x: f(s.x), y: f(s.y), w: f(s.width), h: f(s.height), members: memberIds, label: s.label })
386
- }
387
- return { version: 1, kind: d.kind, nodes, edges: [], groups, bounds: { w: f(positioned.width), h: f(positioned.height) } }
388
- } catch { return emptyRenderedLayout(d.kind) }
389
- }
390
-
391
- // ---- architecture ---------------------------------------------------------
392
-
393
- function architectureToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
394
- try {
395
- const positioned = layoutArchitectureDiagram(parseArchitectureDiagram(toMermaidLines(d.canonicalSource)))
396
- const nodes: RenderedLayoutNode[] = [
397
- ...positioned.services.map(s => ({
398
- id: s.id, x: f(s.x), y: f(s.y), w: f(s.width), h: f(s.height), shape: 'service', label: s.label,
399
- })),
400
- ...positioned.junctions.map(j => ({
401
- id: j.id, x: f(j.x), y: f(j.y), w: f(j.width), h: f(j.height), shape: 'circle' as const, label: undefined,
402
- })),
403
- ]
404
- const flatGroups = new Map<string, PositionedArchitectureGroup>()
405
- const groups: RenderedLayoutGroup[] = []
406
- const flatten = (g: PositionedArchitectureGroup): void => {
407
- flatGroups.set(g.id, g)
408
- groups.push({ id: g.id, x: f(g.x), y: f(g.y), w: f(g.width), h: f(g.height), members: [], label: g.label, parentId: g.parentId })
409
- for (const c of g.children) flatten(c)
410
- }
411
- for (const g of positioned.groups) flatten(g)
412
- const serviceById = new Map(positioned.services.map(s => [s.id, s]))
413
- const junctionById = new Map(positioned.junctions.map(j => [j.id, j]))
414
- const edges: RenderedLayoutEdge[] = positioned.edges.map((e, i) => ({
415
- id: `edge#${i}:${e.source.id}->${e.target.id}`, from: e.source.id, to: e.target.id,
416
- path: e.points.map(p => [f(p.x), f(p.y)] as [Finite, Finite]),
417
- label: e.label && e.labelPosition ? { x: f(e.labelPosition.x), y: f(e.labelPosition.y), text: e.label } : undefined,
418
- route: opts.debug ? architectureRouteCertificate(i, e, serviceById, junctionById, flatGroups) : undefined,
419
- }))
420
- return { version: 1, kind: d.kind, nodes, edges, groups, bounds: { w: f(positioned.width), h: f(positioned.height) } }
421
- } catch { return emptyRenderedLayout(d.kind) }
422
- }
423
-
424
- function architectureRouteCertificate(
425
- edgeIndex: number,
426
- edge: PositionedArchitectureEdge,
427
- services: Map<string, PositionedArchitectureService>,
428
- junctions: Map<string, PositionedArchitectureJunction>,
429
- groups: Map<string, PositionedArchitectureGroup>,
430
- ): FamilyEdgeRouteCertificate {
431
- const boundsFor = (endpoint: PositionedArchitectureEdge['source']) => {
432
- if (endpoint.boundary === 'group') {
433
- const service = services.get(endpoint.id)
434
- return service?.parentId ? groups.get(service.parentId) : undefined
435
- }
436
- return services.get(endpoint.id) ?? junctions.get(endpoint.id)
437
- }
438
- const sourceBounds = boundsFor(edge.source)
439
- const targetBounds = boundsFor(edge.target)
440
- const first = edge.points[0]
441
- const last = edge.points[edge.points.length - 1]
442
- const sourceAnchored = !!(first && sourceBounds && pointOnSide(first, sourceBounds, edge.source.side, 1))
443
- const targetAnchored = !!(last && targetBounds && pointOnSide(last, targetBounds, edge.target.side, 1))
444
- const orthogonal = routeOrthogonal(edge.points)
445
- return {
446
- family: 'architecture',
447
- edgeIndex,
448
- routeClass: 'family-layout',
449
- invariant: sourceAnchored && targetAnchored && orthogonal ? 'side-anchored' : 'unverified-family-route',
450
- bendCount: bendCount(edge.points),
451
- orthogonal,
452
- sourceSide: edge.source.side,
453
- targetSide: edge.target.side,
454
- sourceBoundary: edge.source.boundary,
455
- targetBoundary: edge.target.boundary,
456
- sourceAnchored,
457
- targetAnchored,
458
- }
459
- }
460
-
461
- function pointOnSide(
462
- point: Point,
463
- bounds: { x: number; y: number; width: number; height: number },
464
- side: 'L' | 'R' | 'T' | 'B',
465
- tol: number,
466
- ): boolean {
467
- switch (side) {
468
- case 'L': return Math.abs(point.x - bounds.x) <= tol && point.y >= bounds.y - tol && point.y <= bounds.y + bounds.height + tol
469
- case 'R': return Math.abs(point.x - (bounds.x + bounds.width)) <= tol && point.y >= bounds.y - tol && point.y <= bounds.y + bounds.height + tol
470
- case 'T': return Math.abs(point.y - bounds.y) <= tol && point.x >= bounds.x - tol && point.x <= bounds.x + bounds.width + tol
471
- case 'B': return Math.abs(point.y - (bounds.y + bounds.height)) <= tol && point.x >= bounds.x - tol && point.x <= bounds.x + bounds.width + tol
472
- }
473
- }
474
-
475
- // ---- xychart --------------------------------------------------------------
476
-
477
- function xychartToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
478
- try {
479
- const positioned = layoutXYChart(xychartForRenderedLayout(d))
480
- const nodes: RenderedLayoutNode[] = []
481
- // Bars are the primary boxes.
482
- positioned.bars.forEach((b, i) => {
483
- nodes.push({ id: `bar#${i}`, x: f(b.x), y: f(b.y), w: f(b.width), h: f(b.height), shape: 'rectangle', label: b.label })
484
- })
485
- // Line series points become small marker boxes so line-only charts are
486
- // still measured (whitespace/legibility care about node area).
487
- positioned.lines.forEach((ln, li) => {
488
- ln.points.forEach((p, pi) => {
489
- nodes.push({ id: `line#${li}:pt#${pi}`, x: f(p.x - 3), y: f(p.y - 3), w: f(6), h: f(6), shape: 'circle', label: p.label })
490
- })
491
- })
492
- // Plot area is the single group (the chart's content frame).
493
- const groups: RenderedLayoutGroup[] = [{
494
- id: 'plot', x: f(positioned.plotArea.x), y: f(positioned.plotArea.y),
495
- w: f(positioned.plotArea.width), h: f(positioned.plotArea.height), members: [],
496
- }]
497
- const layout: RenderedLayout = { version: 1, kind: d.kind, nodes, edges: [], groups, bounds: { w: f(positioned.width), h: f(positioned.height) } }
498
- if (opts.debug) layout.certificates = elementCertificates('xychart', layout, 'plot-contained', groups[0], 'center')
499
- return layout
500
- } catch { return emptyRenderedLayout(d.kind) }
501
- }
502
-
503
- function xychartForRenderedLayout(d: ValidDiagram): XYChart {
504
- const normalized = normalizeMermaidSource(d.canonicalSource)
505
- if (d.body.kind === 'xychart') return xychartFromBody(d.body, normalized.frontmatter)
506
- return applyXYChartFrontmatterConfig(parseXYChart(normalized.lines), normalized.frontmatter)
507
- }
508
-
509
- function xychartFromBody(body: XyChartBody, frontmatter: ReturnType<typeof normalizeMermaidSource>['frontmatter']): XYChart {
510
- const series = body.series.map((s): XYChart['series'][number] => ({
511
- type: s.kind,
512
- label: s.name,
513
- data: [...s.values],
514
- }))
515
- const yAxis = xyAxisFromBody(body.yAxis)
516
- if (!yAxis.range && series.length > 0) {
517
- const allValues = series.flatMap(s => s.data)
518
- let min = Math.min(...allValues)
519
- let max = Math.max(...allValues)
520
- const span = max - min || 1
521
- min = min - span * 0.1
522
- max = max + span * 0.1
523
- if (min > 0 && min < span * 0.5) min = 0
524
- yAxis.range = { min, max }
525
- }
526
- if (!yAxis.range) yAxis.range = { min: 0, max: 100 }
527
-
528
- const chart: XYChart = {
529
- horizontal: body.horizontal ?? false,
530
- xAxis: xyAxisFromBody(body.xAxis),
531
- yAxis,
532
- series,
533
- config: resolveXYChartConfig({}),
534
- theme: resolveXYChartTheme({}),
535
- }
536
- if (body.title !== undefined) chart.title = body.title
537
- if (body.horizontal !== undefined) chart.headerOrientation = body.horizontal ? 'horizontal' : 'vertical'
538
- return applyXYChartFrontmatterConfig(chart, frontmatter)
539
- }
540
-
541
- function xyAxisFromBody(axis: XyChartBody['xAxis']): XYAxis {
542
- const out: XYAxis = {}
543
- if (!axis) return out
544
- if (axis.name !== undefined) out.title = axis.name
545
- if (axis.categories) out.categories = [...axis.categories]
546
- if (axis.range) out.range = { ...axis.range }
547
- return out
548
- }
549
-
550
- // ---- pie ------------------------------------------------------------------
551
-
552
- function pieToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
553
- try {
554
- const positioned = layoutPieChart(pieChartForRenderedLayout(d))
555
- // Pie has no structural nodes/edges — the slices are angular wedges. Use
556
- // each slice's legend row as a label-anchored box (legend swatch top-left,
557
- // approximate width from label length at the legend font baseline). This
558
- // gives the metrics a positive node area + legible labels to measure.
559
- const CHAR_PX = 7
560
- const nodes: RenderedLayoutNode[] = positioned.legend.map((l, i) => {
561
- const labelText = `${l.label} (${(l.fraction * 100).toFixed(1)}%)`
562
- const w = Math.max(l.swatchSize, labelText.length * CHAR_PX + l.swatchSize)
563
- return { id: `slice#${i}:${l.label}`, x: f(l.x), y: f(l.y), w: f(w), h: f(l.swatchSize), shape: 'rectangle', label: labelText }
564
- })
565
- const layout: RenderedLayout = { version: 1, kind: d.kind, nodes, edges: [], groups: [], bounds: { w: f(positioned.width), h: f(positioned.height) } }
566
- if (opts.debug) layout.certificates = elementCertificates('pie', layout, 'legend-contained')
567
- return layout
568
- } catch { return emptyRenderedLayout(d.kind) }
569
- }
570
-
571
- function pieChartForRenderedLayout(d: ValidDiagram): PieChart {
572
- if (d.body.kind === 'pie') return pieChartFromBody(d.body)
573
- return parsePieChart(toMermaidLines(d.canonicalSource))
574
- }
575
-
576
- function pieChartFromBody(body: PieBody): PieChart {
577
- const chart: PieChart = {
578
- showData: body.showData,
579
- entries: body.slices.map(s => ({ label: s.label, value: s.value })),
580
- }
581
- if (body.title !== undefined) chart.title = body.title
582
- return chart
583
- }
584
-
585
- // ---- gantt ------------------------------------------------------------------
586
-
587
- function ganttToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
588
- try {
589
- const positioned = ganttPositionedForRenderedLayout(d)
590
- // Bars and milestones are the nodes; section bands are the groups. Verts
591
- // and ticks are markers, not boxes — they carry no node area. Milestones
592
- // report the diamond's true bounding box; the zero-width floor on bars is
593
- // pulled back inside the plot so a range-edge task never fakes a breach.
594
- const plotRight = positioned.plot.x + positioned.plot.w
595
- const nodes: RenderedLayoutNode[] = positioned.bars.map(b => {
596
- const id = b.id ?? `task#${b.taskIndex}`
597
- if (b.milestoneX !== undefined) {
598
- const r = b.h / 2
599
- return { id, x: f(b.milestoneX - r), y: f(b.y), w: f(b.h), h: f(b.h), shape: 'diamond', label: b.label }
600
- }
601
- const w = Math.max(2, b.w)
602
- const x = Math.min(b.x, plotRight - w)
603
- return { id, x: f(x), y: f(b.y), w: f(w), h: f(b.h), shape: 'rectangle', label: b.label }
604
- })
605
- const groups: RenderedLayoutGroup[] = positioned.sections.map((s, i) => ({
606
- id: `section#${i}`, x: f(positioned.plot.x), y: f(s.y), w: f(positioned.plot.w), h: f(s.h),
607
- members: positioned.bars.filter(b => b.sectionIndex === i).map(b => b.id ?? `task#${b.taskIndex}`),
608
- label: s.label,
609
- }))
610
- const layout: RenderedLayout = { version: 1, kind: d.kind, nodes, edges: [], groups, bounds: { w: f(positioned.width), h: f(positioned.height) } }
611
- if (opts.debug) layout.certificates = elementCertificates('gantt', layout, 'section-contained')
612
- return layout
613
- } catch { return emptyRenderedLayout(d.kind) }
614
- }
615
-
616
- function ganttPositionedForRenderedLayout(d: ValidDiagram): GanttLayoutResult {
617
- const normalized = normalizeMermaidSource(d.canonicalSource)
618
- if (d.body.kind === 'gantt') {
619
- const model = ganttModelFromBody(d.body)
620
- if (model) {
621
- const configured = applyGanttFrontmatterConfig(model, normalized.frontmatter)
622
- const schedule = resolveGanttSchedule(configured)
623
- return layoutGantt(configured, schedule, { today: schedule.today })
624
- }
625
- }
626
- return buildGanttRenderPipeline(normalized.lines, normalized.frontmatter).positioned
627
- }
628
-
629
- function ganttModelFromBody(body: GanttBody): GanttModel | null {
630
- if (body.statements?.some(st => st.kind === 'opaque-block')) return null
631
- const sections: GanttModelSection[] = body.sections.map((s, index) => {
632
- const section: GanttModelSection = { taskIndexes: [], line: index + 1 }
633
- if (s.label !== undefined) section.label = s.label
634
- return section
635
- })
636
- const model: GanttModel = {
637
- dateFormat: 'YYYY-MM-DD',
638
- inclusiveEndDates: false,
639
- topAxis: false,
640
- excludes: [],
641
- includes: [],
642
- weekendStart: 'saturday',
643
- weekStart: 'sunday',
644
- sections: sections.length > 0 ? sections : [{ taskIndexes: [] }],
645
- tasks: [],
646
- clicks: [],
647
- }
648
- if (body.title !== undefined) model.title = body.title
649
-
650
- for (let sectionIndex = 0; sectionIndex < body.sections.length; sectionIndex++) {
651
- const section = body.sections[sectionIndex]!
652
- const modelSection = model.sections[sectionIndex]!
653
- for (const task of section.tasks) {
654
- const taskIndex = model.tasks.length
655
- const modelTask: GanttModelTask = {
656
- index: taskIndex,
657
- label: task.label,
658
- tags: [...task.tags] as GanttTaskTag[],
659
- end: ganttEndExpr(task.end),
660
- sectionIndex,
661
- line: taskIndex + 1,
662
- }
663
- if (task.taskId !== undefined) modelTask.id = task.taskId
664
- if (task.start !== undefined) modelTask.start = ganttStartExpr(task.start)
665
- model.tasks.push(modelTask)
666
- modelSection.taskIndexes.push(taskIndex)
667
- }
668
- }
669
- return model
670
- }
671
-
672
- function ganttStartExpr(raw: string): GanttStartExpr {
673
- const after = raw.match(/^after\s+(.+)$/)
674
- if (after) return { kind: 'after', refs: after[1]!.split(/\s+/).filter(Boolean) }
675
- return { kind: 'date', raw }
676
- }
677
-
678
- function ganttEndExpr(raw: string): GanttEndExpr {
679
- const until = raw.match(/^until\s+(.+)$/)
680
- if (until) return { kind: 'until', refs: until[1]!.split(/\s+/).filter(Boolean) }
681
- if (GANTT_DURATION_RE.test(raw)) return { kind: 'duration', raw }
682
- return { kind: 'date', raw }
683
- }
684
-
685
- /**
686
- * Closes the "verify ok but render throws" seam for structured gantt bodies
687
- * (found by harvesting upstream's `excludes weekdays …` parser case): when the
688
- * schedule cannot resolve, verify surfaces UNRESOLVABLE_SCHEDULE (error
689
- * severity) carrying the named GANTT_* reason, instead of silently degrading
690
- * to an empty layout. GANTT_EMPTY maps to the existing EMPTY_DIAGRAM code.
691
- */
692
- export function ganttScheduleWarning(d: ValidDiagram): LayoutWarning | null {
693
- try {
694
- const normalized = normalizeMermaidSource(d.canonicalSource)
695
- const model = applyGanttFrontmatterConfig(parseGanttModel(normalized.lines), normalized.frontmatter)
696
- resolveGanttSchedule(model)
697
- return null
698
- } catch (e) {
699
- const message = e instanceof Error ? e.message : String(e)
700
- if (message.startsWith('GANTT_EMPTY')) return { code: 'EMPTY_DIAGRAM' }
701
- return { code: 'UNRESOLVABLE_SCHEDULE', reason: message }
702
- }
703
- }
704
-
705
- /**
706
- * Geometric tripwires for the gantt layout (docs/design/families/gantt.md
707
- * §Verification; issue #26 WS11): OFF_CANVAS when a resolved bar/milestone
708
- * leaves the canvas, GROUP_BREACH when a section-owned bar leaves its section
709
- * band. The layout produces contained geometry by construction (property-
710
- * tested), so these fire only if a later pass mutates geometry — the same
711
- * zero-noise contract as the route-contract tripwires.
712
- */
713
- export function layoutGeometryWarnings(
714
- layout: RenderedLayout,
715
- opts: { edgeAnchors?: boolean; nodeOverlaps?: boolean; groupContainment?: boolean } = {},
716
- ): LayoutWarning[] {
717
- const warnings: LayoutWarning[] = []
718
- const TOL = 0.5
719
- for (const n of layout.nodes) {
720
- if (n.x < -TOL || n.x + n.w > layout.bounds.w + TOL) warnings.push({ code: 'OFF_CANVAS', target: n.id, axis: 'x' })
721
- if (n.y < -TOL || n.y + n.h > layout.bounds.h + TOL) warnings.push({ code: 'OFF_CANVAS', target: n.id, axis: 'y' })
722
- }
723
- for (const e of layout.edges) {
724
- for (const [x, y] of e.path) {
725
- if (x < -TOL || x > layout.bounds.w + TOL) { warnings.push({ code: 'OFF_CANVAS', target: e.id, axis: 'x' }); break }
726
- }
727
- for (const [x, y] of e.path) {
728
- if (y < -TOL || y > layout.bounds.h + TOL) { warnings.push({ code: 'OFF_CANVAS', target: e.id, axis: 'y' }); break }
729
- }
730
- }
731
- if (opts.nodeOverlaps) {
732
- for (let i = 0; i < layout.nodes.length; i++) {
733
- for (let j = i + 1; j < layout.nodes.length; j++) {
734
- const a = layout.nodes[i]!, b = layout.nodes[j]!
735
- const ox = Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x))
736
- const oy = Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y))
737
- const area = ox * oy
738
- if (area > TOL) warnings.push({ code: 'NODE_OVERLAP', a: a.id, b: b.id, areaPx: Math.round(area) })
739
- }
740
- }
741
- }
742
- if (opts.groupContainment) {
743
- const nodeById = new Map(layout.nodes.map(n => [n.id, n]))
744
- for (const g of layout.groups) {
745
- for (const memberId of g.members) {
746
- const n = nodeById.get(memberId)
747
- if (!n) continue
748
- const inside = n.x >= g.x - TOL && n.y >= g.y - TOL &&
749
- n.x + n.w <= g.x + g.w + TOL && n.y + n.h <= g.y + g.h + TOL
750
- if (!inside) warnings.push({ code: 'GROUP_BREACH', group: g.id, member: memberId })
751
- }
752
- }
753
- }
754
- if (opts.edgeAnchors) {
755
- const nodeById = new Map(layout.nodes.map(n => [n.id, n]))
756
- for (const e of layout.edges) {
757
- const first = e.path[0]
758
- const last = e.path[e.path.length - 1]
759
- const source = nodeById.get(e.from)
760
- const target = nodeById.get(e.to)
761
- if (first && source && !pointOnRectBoundary(first[0], first[1], source, TOL)) warnings.push({ code: 'ROUTE_SHAPE_MISANCHOR', edge: e.id, node: source.id })
762
- if (last && target && !pointOnRectBoundary(last[0], last[1], target, TOL)) warnings.push({ code: 'ROUTE_SHAPE_MISANCHOR', edge: e.id, node: target.id })
763
- }
764
- }
765
- return warnings
766
- }
767
-
768
- function pointOnRectBoundary(x: number, y: number, n: RenderedLayoutNode, tol: number): boolean {
769
- const onVertical = (Math.abs(x - n.x) <= tol || Math.abs(x - (n.x + n.w)) <= tol) && y >= n.y - tol && y <= n.y + n.h + tol
770
- const onHorizontal = (Math.abs(y - n.y) <= tol || Math.abs(y - (n.y + n.h)) <= tol) && x >= n.x - tol && x <= n.x + n.w + tol
771
- return onVertical || onHorizontal
772
- }
773
-
774
- export function ganttGeometryWarnings(layout: RenderedLayout): LayoutWarning[] {
775
- const warnings: LayoutWarning[] = []
776
- const TOL = 0.5 // coordinates round through f(); allow rounding slack
777
- for (const n of layout.nodes) {
778
- if (n.x < -TOL || n.x + n.w > layout.bounds.w + TOL) warnings.push({ code: 'OFF_CANVAS', target: n.id, axis: 'x' })
779
- if (n.y < -TOL || n.y + n.h > layout.bounds.h + TOL) warnings.push({ code: 'OFF_CANVAS', target: n.id, axis: 'y' })
780
- }
781
- const nodeById = new Map(layout.nodes.map(n => [n.id, n]))
782
- for (const g of layout.groups) {
783
- for (const memberId of g.members) {
784
- const n = nodeById.get(memberId)
785
- if (!n) continue
786
- const inside = n.x >= g.x - TOL && n.y >= g.y - TOL &&
787
- n.x + n.w <= g.x + g.w + TOL && n.y + n.h <= g.y + g.h + TOL
788
- if (!inside) warnings.push({ code: 'GROUP_BREACH', group: g.id, member: memberId })
789
- }
790
- }
791
- return warnings
792
- }
793
-
794
- // ---- quadrant -------------------------------------------------------------
795
-
796
- function quadrantToRendered(d: ValidDiagram, opts: { debug?: boolean } = {}): RenderedLayout {
797
- try {
798
- const positioned = layoutQuadrantChart(quadrantChartForRenderedLayout(d))
799
- const nodes: RenderedLayoutNode[] = positioned.points.map((p, i) => ({
800
- id: `point#${i}:${p.label}`, x: f(p.cx - p.radius), y: f(p.cy - p.radius),
801
- w: f(p.radius * 2), h: f(p.radius * 2), shape: 'circle', label: p.label,
802
- }))
803
- const groups: RenderedLayoutGroup[] = positioned.regions.map(r => ({
804
- id: `quadrant#${r.number}`, x: f(r.x), y: f(r.y), w: f(r.width), h: f(r.height), members: [], label: r.label,
805
- }))
806
- const layout: RenderedLayout = { version: 1, kind: d.kind, nodes, edges: [], groups, bounds: { w: f(positioned.width), h: f(positioned.height) } }
807
- if (opts.debug) layout.certificates = elementCertificates('quadrant', layout, 'plot-contained')
808
- return layout
809
- } catch { return emptyRenderedLayout(d.kind) }
810
- }
811
-
812
- function quadrantChartForRenderedLayout(d: ValidDiagram): QuadrantChart {
813
- if (d.body.kind === 'quadrant') return quadrantChartFromBody(d.body)
814
- return parseQuadrantChart(toMermaidLines(d.canonicalSource))
815
- }
816
-
817
- function quadrantChartFromBody(body: QuadrantBody): QuadrantChart {
818
- const chart: QuadrantChart = {
819
- quadrants: [...body.quadrants] as QuadrantChart['quadrants'],
820
- points: body.points.map(p => ({ label: p.label, x: p.x, y: p.y })),
821
- }
822
- if (body.title !== undefined) chart.title = body.title
823
- if (body.xAxis) chart.xAxis = { ...body.xAxis }
824
- if (body.yAxis) chart.yAxis = { ...body.yAxis }
825
- return chart
826
- }