agentic-mermaid 0.1.1 → 0.2.0

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