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,372 +0,0 @@
1
- // ============================================================================
2
- // Flowchart/state structured body: parse / serialize / mutate / source-map
3
- // (FamilyPlugin hooks — BUILD-3 stage 2, removing the in-tree exception).
4
- //
5
- // Flowchart and state are two DiagramKinds sharing one body kind
6
- // ('flowchart', holding the legacy renderer's MermaidGraph). Both register
7
- // a plugin built by `flowchartFamilyHooks(headerKind)`, which binds the
8
- // serialized header (`flowchart <dir>` vs `stateDiagram-v2`).
9
- //
10
- // Contract differences from the narrow structured families, kept on purpose:
11
- // - parse ERRORS on bad syntax instead of falling back to opaque. The
12
- // legacy parser is broad and has no lossless bail-out mode; silent
13
- // opaque fallback would convert crisp parse errors on the flagship
14
- // family into render-time failures.
15
- // - parse consumes the full canonical source (the legacy parser needs the
16
- // header) and contributes a SourceMap via the buildSourceMap hook.
17
- // ============================================================================
18
-
19
- import { parseMermaid as parseFlowchartLegacy } from '../parser.ts'
20
- import { unknownOpMessage } from './mutation-ops.ts'
21
- import type { MermaidGraph, MermaidNode, MermaidEdge, MermaidSubgraph } from '../types.ts'
22
- import type {
23
- DiagramBody, FlowchartMutationOp, MutationError, ParseError, Result, SourceMap,
24
- } from './types.ts'
25
- import { ok, err } from './types.ts'
26
-
27
- export type FlowchartBody = Extract<DiagramBody, { kind: 'flowchart' }>
28
-
29
- // ---- Parser -----------------------------------------------------------------
30
-
31
- export function parseFlowchartBody(canonicalSource: string): Result<FlowchartBody, ParseError[]> {
32
- try {
33
- const graph = parseFlowchartLegacy(canonicalSource)
34
- return ok({ kind: 'flowchart', graph })
35
- } catch (e) {
36
- return err([{ code: 'PARSE_FAILED', message: e instanceof Error ? e.message : String(e) }])
37
- }
38
- }
39
-
40
- // ---- SourceMap --------------------------------------------------------------
41
-
42
- export function buildFlowchartSourceMap(body: FlowchartBody, canonicalSource: string): SourceMap {
43
- const map: SourceMap = { nodes: new Map(), edges: new Map(), groups: new Map(), labels: new Map() }
44
- const lines = canonicalSource.split(/\r?\n/)
45
-
46
- for (const id of Array.from(body.graph.nodes.keys())) {
47
- const re = new RegExp(`\\b${escapeRegex(id)}\\b`)
48
- for (let i = 0; i < lines.length; i++) {
49
- const line = lines[i]!
50
- const idx = line.search(re)
51
- if (idx >= 0) {
52
- map.nodes.set(id, { line: i + 1, col: idx + 1 })
53
- const node = body.graph.nodes.get(id)
54
- const labelCol = node ? labelColumn(line, node.label, idx + id.length) : -1
55
- if (labelCol >= 0) map.labels.set(`node:${id}`, { line: i + 1, col: labelCol + 1 })
56
- break
57
- }
58
- }
59
- }
60
-
61
- for (const sg of body.graph.subgraphs) mapSubgraphSource(sg, lines, map)
62
-
63
- body.graph.edges.forEach((edge, index) => {
64
- const indexedKey = edgeSourceMapKey(index, edge)
65
- const pairKey = `${edge.source}->${edge.target}`
66
- for (let i = 0; i < lines.length; i++) {
67
- const line = lines[i]!
68
- if (!lineMentionsEdge(line, edge.source, edge.target)) continue
69
- const col = Math.max(0, line.search(new RegExp(`\\b${escapeRegex(edge.source)}\\b`)))
70
- const loc = { line: i + 1, col: col + 1 }
71
- map.edges.set(indexedKey, loc)
72
- if (!map.edges.has(pairKey)) map.edges.set(pairKey, loc)
73
- if (edge.label) {
74
- const labelCol = labelColumn(line, edge.label, edgeOperatorSearchStart(line, col, edge.source))
75
- if (labelCol >= 0) map.labels.set(indexedKey, { line: i + 1, col: labelCol + 1 })
76
- }
77
- break
78
- }
79
- })
80
-
81
- return map
82
- }
83
-
84
- function edgeSourceMapKey(index: number, edge: MermaidEdge): string { return `edge#${index}:${edge.source}->${edge.target}` }
85
-
86
- function lineMentionsEdge(line: string, source: string, target: string): boolean {
87
- if (!/(?:[-.=~]+|<[-.=]+|[-.=]+[>ox]|[ox][-.=])/.test(line)) return false
88
- const sourceIdx = line.search(new RegExp(`\\b${escapeRegex(source)}\\b`))
89
- const targetIdx = source === target ? sourceIdx : line.search(new RegExp(`\\b${escapeRegex(target)}\\b`))
90
- // Source maps follow the canonical source direction for ordinary Mermaid
91
- // chains. This rejects the common false positive where the previous forward
92
- // line `A --> B` also mentions a later feedback edge's endpoint pair `B,A`.
93
- return sourceIdx >= 0 && targetIdx >= 0 && (source === target || sourceIdx <= targetIdx)
94
- }
95
-
96
- function edgeOperatorSearchStart(line: string, sourceCol: number, source: string): number {
97
- const afterSource = sourceCol + source.length
98
- const operatorOffset = line.slice(afterSource).search(/[-.=~]/)
99
- return operatorOffset >= 0 ? afterSource + operatorOffset : afterSource
100
- }
101
-
102
- function labelColumn(line: string, label: string, afterCol: number): number {
103
- if (!label || label.trim().length === 0) return -1
104
- const direct = line.indexOf(label, Math.max(0, afterCol))
105
- if (direct >= 0) return direct
106
- const escaped = label.replace(/<br\s*\/?\s*>/gi, '\\n')
107
- if (escaped !== label) return line.indexOf(escaped, Math.max(0, afterCol))
108
- return -1
109
- }
110
-
111
- function mapSubgraphSource(sg: MermaidSubgraph, lines: string[], map: SourceMap): void {
112
- const re = new RegExp(`^\\s*subgraph\\s+${escapeRegex(sg.id)}(?:\\b|\\[|$)`, 'i')
113
- for (let i = 0; i < lines.length; i++) {
114
- const line = lines[i]!
115
- if (!re.test(line)) continue
116
- const col = line.indexOf(sg.id)
117
- map.groups.set(sg.id, { line: i + 1, col: col + 1 })
118
- const labelCol = sg.label && sg.label !== sg.id ? labelColumn(line, sg.label, col + sg.id.length) : -1
119
- if (labelCol >= 0) map.labels.set(`group:${sg.id}`, { line: i + 1, col: labelCol + 1 })
120
- break
121
- }
122
- for (const child of sg.children) mapSubgraphSource(child, lines, map)
123
- }
124
-
125
- function escapeRegex(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') }
126
-
127
- // ---- Serializer -------------------------------------------------------------
128
-
129
- export function renderFlowchart(graph: MermaidGraph, headerKind: 'flowchart' | 'state'): string {
130
- const lines: string[] = [headerKind === 'state' ? 'stateDiagram-v2' : `flowchart ${graph.direction}`]
131
- const declaredInline = new Set<string>()
132
-
133
- // Subgraph blocks MUST come before edges: the legacy parser associates a
134
- // node with the subgraph in whose block it FIRST appears. If an edge at the
135
- // top declared the node first, a later bare reference inside the subgraph is
136
- // ignored and membership is lost on re-parse. Emitting members (with their
137
- // shape declaration) inside the block first makes round-trip stable.
138
- const membersDeclared = new Set<string>()
139
- const renderSubgraph = (sg: MermaidGraph['subgraphs'][number], indent: string) => {
140
- lines.push(`${indent}subgraph ${sg.id}${sg.label !== sg.id ? `[${sg.label}]` : ''}`)
141
- if (sg.direction) lines.push(`${indent} direction ${sg.direction}`)
142
- for (const child of sg.children) renderSubgraph(child, indent + ' ')
143
- for (const nid of sg.nodeIds) {
144
- const node = graph.nodes.get(nid)
145
- if (node && needsExplicitDeclaration(node)) {
146
- lines.push(`${indent} ${node.id}${renderShape(node)}`)
147
- declaredInline.add(nid)
148
- } else {
149
- lines.push(`${indent} ${nid}`)
150
- }
151
- membersDeclared.add(nid)
152
- }
153
- lines.push(`${indent}end`)
154
- }
155
- for (const sg of graph.subgraphs) renderSubgraph(sg, ' ')
156
-
157
- for (const edge of graph.edges) lines.push(' ' + renderEdge(edge, graph.nodes, declaredInline))
158
-
159
- for (const [id, node] of graph.nodes) {
160
- if (declaredInline.has(id) || membersDeclared.has(id)) continue
161
- const orphan = graph.edges.every(e => e.source !== id && e.target !== id)
162
- if (orphan || needsExplicitDeclaration(node)) lines.push(' ' + `${node.id}${renderShape(node)}`)
163
- }
164
-
165
- for (const [name, props] of graph.classDefs) lines.push(` classDef ${name} ${styleProps(props)}`)
166
- for (const [id, cls] of graph.classAssignments) lines.push(` class ${id} ${cls}`)
167
- for (const [id, style] of graph.nodeStyles) lines.push(` style ${id} ${styleProps(style)}`)
168
- for (const [idx, style] of graph.linkStyles) lines.push(` linkStyle ${idx} ${styleProps(style)}`)
169
-
170
- return lines.join('\n') + '\n'
171
- }
172
-
173
- function needsExplicitDeclaration(node: MermaidNode): boolean {
174
- return node.label !== node.id || node.shape !== 'rectangle'
175
- }
176
-
177
- function renderShape(node: MermaidNode): string {
178
- const lbl = escapeLabel(node.label)
179
- switch (node.shape) {
180
- case 'rectangle': return `[${lbl}]`
181
- case 'rounded': return `(${lbl})`
182
- case 'stadium': return `([${lbl}])`
183
- case 'subroutine': return `[[${lbl}]]`
184
- case 'cylinder': return `[(${lbl})]`
185
- case 'circle': return `((${lbl}))`
186
- case 'doublecircle': return `(((${lbl})))`
187
- case 'asymmetric': return `>${lbl}]`
188
- case 'diamond': return `{${lbl}}`
189
- case 'hexagon': return `{{${lbl}}}`
190
- case 'trapezoid': return `[/${lbl}\\]`
191
- case 'trapezoid-alt': return `[\\${lbl}/]`
192
- case 'lean-r': return `[/${lbl}/]`
193
- case 'lean-l': return `[\\${lbl}\\]`
194
- case 'service': return `[${lbl}]`
195
- case 'state-start':
196
- case 'state-end': return ''
197
- }
198
- }
199
-
200
- function escapeLabel(label: string): string {
201
- const normalized = label.replace(/\r?\n/g, '<br>')
202
- if (/[\[\]{}()|]/.test(normalized)) return `"${normalized.replace(/["\\]/g, '\\$&')}"`
203
- return normalized
204
- }
205
-
206
- function renderEdge(edge: MermaidEdge, nodes: Map<string, MermaidNode>, declaredInline: Set<string>): string {
207
- const src = inlineNodeRef(edge.source, nodes, declaredInline)
208
- const dst = inlineNodeRef(edge.target, nodes, declaredInline)
209
- const labelPart = edge.label ? `|${escapeLabel(edge.label)}|` : ''
210
- return `${src} ${renderEdgeArrow(edge)}${labelPart} ${dst}`
211
- }
212
-
213
- function inlineNodeRef(id: string, nodes: Map<string, MermaidNode>, declaredInline: Set<string>): string {
214
- const n = nodes.get(id)
215
- if (!n) return id
216
- if (needsExplicitDeclaration(n) && !declaredInline.has(id)) {
217
- declaredInline.add(id)
218
- return `${id}${renderShape(n)}`
219
- }
220
- return id
221
- }
222
-
223
- function renderEdgeArrow(edge: MermaidEdge): string {
224
- const start = edge.hasArrowStart ? markerChar(edge.startMarker ?? 'arrow', true) : ''
225
- const end = edge.hasArrowEnd ? markerChar(edge.endMarker ?? 'arrow', false) : ''
226
- // Extra shaft units for a lengthened link (Mermaid rank distance). `length`
227
- // is undefined ≡ 1 for base operators, so they serialize byte-identically.
228
- const extra = Math.max(0, (edge.length ?? 1) - 1)
229
- switch (edge.style) {
230
- case 'invisible': return '~'.repeat(3 + extra)
231
- case 'solid': return `${start}${'-'.repeat((!edge.hasArrowStart && !edge.hasArrowEnd ? 3 : 2) + extra)}${end}`
232
- case 'dotted': return `${start}-${'.'.repeat(1 + extra)}-${end}`
233
- case 'thick': return `${start}${'='.repeat((!edge.hasArrowStart && !edge.hasArrowEnd ? 3 : 2) + extra)}${end}`
234
- }
235
- }
236
-
237
- function markerChar(marker: 'arrow' | 'circle' | 'cross', isStart: boolean): string {
238
- if (marker === 'arrow') return isStart ? '<' : '>'
239
- if (marker === 'circle') return 'o'
240
- return 'x'
241
- }
242
-
243
- function styleProps(props: Record<string, string>): string {
244
- return Object.entries(props).map(([k, v]) => `${k}:${v}`).join(',')
245
- }
246
-
247
- // ---- Mutator ----------------------------------------------------------------
248
-
249
- export function mutateFlowchart(body: FlowchartBody, op: FlowchartMutationOp): Result<FlowchartBody, MutationError> {
250
- const graph = cloneGraph(body.graph)
251
- const done = (): Result<FlowchartBody, MutationError> => ok({ kind: 'flowchart', graph })
252
- switch (op.kind) {
253
- case 'add_node': {
254
- if (graph.nodes.has(op.id)) return err({ code: 'DUPLICATE_NODE', message: `Node "${op.id}" already exists` })
255
- graph.nodes.set(op.id, { id: op.id, label: op.label, shape: op.shape ?? 'rectangle' })
256
- if (op.parent) {
257
- const parent = findSubgraph(graph, op.parent)
258
- if (!parent) return err({ code: 'INVALID_OP', message: `Parent group "${op.parent}" not found` })
259
- parent.nodeIds.push(op.id)
260
- }
261
- return done()
262
- }
263
- case 'remove_node': {
264
- if (!graph.nodes.has(op.id)) return err({ code: 'NODE_NOT_FOUND', message: `Node "${op.id}" not found` })
265
- graph.nodes.delete(op.id)
266
- graph.edges = graph.edges.filter(e => e.source !== op.id && e.target !== op.id)
267
- for (const sg of graph.subgraphs) removeFromSubgraph(sg, op.id)
268
- graph.classAssignments.delete(op.id)
269
- graph.nodeStyles.delete(op.id)
270
- return done()
271
- }
272
- case 'rename_node': {
273
- if (!graph.nodes.has(op.from)) return err({ code: 'NODE_NOT_FOUND', message: `Node "${op.from}" not found` })
274
- if (graph.nodes.has(op.to)) return err({ code: 'DUPLICATE_NODE', message: `Cannot rename to "${op.to}" — already exists` })
275
- const node = graph.nodes.get(op.from)!
276
- graph.nodes.delete(op.from)
277
- graph.nodes.set(op.to, { ...node, id: op.to, label: node.label === op.from ? op.to : node.label })
278
- for (const e of graph.edges) {
279
- if (e.source === op.from) e.source = op.to
280
- if (e.target === op.from) e.target = op.to
281
- }
282
- for (const sg of graph.subgraphs) renameInSubgraph(sg, op.from, op.to)
283
- if (graph.classAssignments.has(op.from)) {
284
- graph.classAssignments.set(op.to, graph.classAssignments.get(op.from)!); graph.classAssignments.delete(op.from)
285
- }
286
- if (graph.nodeStyles.has(op.from)) {
287
- graph.nodeStyles.set(op.to, graph.nodeStyles.get(op.from)!); graph.nodeStyles.delete(op.from)
288
- }
289
- return done()
290
- }
291
- case 'set_label': {
292
- if (graph.nodes.has(op.target)) {
293
- const n = graph.nodes.get(op.target)!
294
- graph.nodes.set(op.target, { ...n, label: op.label })
295
- return done()
296
- }
297
- const idx = findEdgeIndexById(graph, op.target)
298
- if (idx >= 0) { graph.edges[idx]!.label = op.label; return done() }
299
- return err({ code: 'NODE_NOT_FOUND', message: `Target "${op.target}" matches no node or edge` })
300
- }
301
- case 'add_edge': {
302
- ensureNode(graph, op.from); ensureNode(graph, op.to)
303
- graph.edges.push({ source: op.from, target: op.to, label: op.label, style: op.style ?? 'solid', hasArrowStart: false, hasArrowEnd: true })
304
- return done()
305
- }
306
- case 'remove_edge': {
307
- const idx = findEdgeIndexById(graph, op.id)
308
- if (idx < 0) return err({ code: 'EDGE_NOT_FOUND', message: `Edge "${op.id}" not found` })
309
- graph.edges.splice(idx, 1)
310
- return done()
311
- }
312
- default: {
313
- const _x: never = op
314
- return err({ code: 'INVALID_OP', message: unknownOpMessage('flowchart', _x) })
315
- }
316
- }
317
- }
318
-
319
- // ---- Graph helpers ----------------------------------------------------------
320
-
321
- export function edgeIdOf(edge: MermaidEdge, idx = 0): string {
322
- return idx === 0 ? `${edge.source}->${edge.target}` : `${edge.source}->${edge.target}#${idx}`
323
- }
324
-
325
- function findEdgeIndexById(graph: MermaidGraph, id: string): number {
326
- const [endpoints, suffix] = id.split('#')
327
- const [from, to] = (endpoints ?? '').split('->')
328
- if (!from || !to) return -1
329
- const occ = suffix ? parseInt(suffix, 10) : 0
330
- let seen = 0
331
- for (let i = 0; i < graph.edges.length; i++) {
332
- const e = graph.edges[i]!
333
- if (e.source === from && e.target === to) { if (seen === occ) return i; seen++ }
334
- }
335
- return -1
336
- }
337
-
338
- function ensureNode(graph: MermaidGraph, id: string): void {
339
- if (!graph.nodes.has(id)) graph.nodes.set(id, { id, label: id, shape: 'rectangle' })
340
- }
341
-
342
- function findSubgraph(graph: MermaidGraph, id: string): MermaidSubgraph | null {
343
- const search = (list: MermaidSubgraph[]): MermaidSubgraph | null => {
344
- for (const sg of list) { if (sg.id === id) return sg; const c = search(sg.children); if (c) return c }
345
- return null
346
- }
347
- return search(graph.subgraphs)
348
- }
349
- function removeFromSubgraph(sg: MermaidSubgraph, id: string): void {
350
- sg.nodeIds = sg.nodeIds.filter(n => n !== id)
351
- for (const c of sg.children) removeFromSubgraph(c, id)
352
- }
353
- function renameInSubgraph(sg: MermaidSubgraph, from: string, to: string): void {
354
- sg.nodeIds = sg.nodeIds.map(n => (n === from ? to : n))
355
- for (const c of sg.children) renameInSubgraph(c, from, to)
356
- }
357
-
358
- function cloneGraph(graph: MermaidGraph): MermaidGraph {
359
- return {
360
- direction: graph.direction,
361
- nodes: new Map(Array.from(graph.nodes, ([k, v]) => [k, { ...v }])),
362
- edges: graph.edges.map(e => ({ ...e })),
363
- subgraphs: graph.subgraphs.map(cloneSubgraph),
364
- classDefs: new Map(Array.from(graph.classDefs, ([k, v]) => [k, { ...v }])),
365
- classAssignments: new Map(graph.classAssignments),
366
- nodeStyles: new Map(Array.from(graph.nodeStyles, ([k, v]) => [k, { ...v }])),
367
- linkStyles: new Map(Array.from(graph.linkStyles, ([k, v]) => [k, { ...v }])),
368
- }
369
- }
370
- function cloneSubgraph(sg: MermaidGraph['subgraphs'][number]): MermaidGraph['subgraphs'][number] {
371
- return { id: sg.id, label: sg.label, nodeIds: [...sg.nodeIds], children: sg.children.map(cloneSubgraph), direction: sg.direction }
372
- }
@@ -1,243 +0,0 @@
1
- import type { LayoutWarning } from './types.ts'
2
-
3
- export interface FlowchartStatement {
4
- text: string
5
- line: number
6
- }
7
-
8
- const EDGE_ID_OPERATOR_LOOKAHEAD = String.raw`(?:<)?(?:~{3,}|-\.+->|-\.+-|={2,}>|={3,}|o-{2,}[ox]|x-{2,}[ox]|-{2,}[ox]|-{2,}>|-{3,}|(?:-{2,}|-\.+|={2,})\s+)`
9
- const EDGE_ID_REGEX = new RegExp(String.raw`(?:^|[\s&])([\w-]+)@\s*(?=${EDGE_ID_OPERATOR_LOOKAHEAD})`)
10
-
11
- export function containsFlowchartOpaqueSyntax(source: string): boolean {
12
- if (/`/.test(source)) return true
13
- return flowchartStatements(source).some(({ text }) =>
14
- /^([\w-]+)@\s*\{/.test(text)
15
- || hasFlowchartEdgeId(text)
16
- || isFlowchartInteractionDirective(text)
17
- )
18
- }
19
-
20
- export function flowchartUnsupportedSyntaxWarnings(source: string): LayoutWarning[] {
21
- const warnings: LayoutWarning[] = []
22
- for (const { text, line } of flowchartStatements(source)) {
23
- if (hasFlowchartEdgeId(text)) {
24
- warnings.push({ code: 'UNSUPPORTED_SYNTAX', line, syntax: 'flowchart_edge_id', message: 'Flowchart edge IDs are preserved as source but not modeled as structured edge identity yet.' })
25
- }
26
- if (isUnsupportedEdgeMetadataStatement(text)) {
27
- warnings.push({ code: 'UNSUPPORTED_SYNTAX', line, syntax: 'flowchart_edge_metadata', message: 'Flowchart edge metadata is preserved as source but ignored by the local renderer/layout.' })
28
- }
29
- if (isNodeMetadataStatement(text)) {
30
- warnings.push({ code: 'UNSUPPORTED_SYNTAX', line, syntax: 'flowchart_node_metadata', message: 'Flowchart node metadata (@{ shape: ... }) is preserved as source and its label is rendered, but the v11 shape vocabulary is not yet modeled by the local renderer/layout.' })
31
- }
32
- if (isFlowchartInteractionDirective(text)) {
33
- warnings.push({ code: 'UNSUPPORTED_SYNTAX', line, syntax: 'flowchart_interaction_directive', message: 'Flowchart click/href directives are preserved as source but ignored by the local renderer for security and layout.' })
34
- }
35
- if (/`/.test(text)) {
36
- warnings.push({ code: 'UNSUPPORTED_SYNTAX', line, syntax: 'flowchart_markdown_string', message: 'Flowchart markdown strings are preserved as source but not fully modeled by the local parser.' })
37
- }
38
- const malformed = malformedFlowchartStatement(text)
39
- if (malformed) {
40
- warnings.push({ code: 'UNSUPPORTED_SYNTAX', line, syntax: malformed.syntax, message: malformed.message })
41
- }
42
- }
43
- return warnings
44
- }
45
-
46
- // Keyword statements where brackets/quotes are free text (subgraph labels) or
47
- // carry no node/edge content that an unclosed delimiter could swallow.
48
- const FLOWCHART_KEYWORD_STATEMENT = /^(?:subgraph\s|end$|direction\s|classDef\s|class\s|style\s|linkStyle\s|click\s|href\s)/i
49
-
50
- /**
51
- * Detect node/edge statements with an unclosed bracket, quote, or |label|
52
- * delimiter. The legacy parser recovers by regex-matching whatever prefix it
53
- * can, silently dropping everything after the unclosed delimiter — including
54
- * arrows, so `A[Start --> B` loses the A→B edge entirely (audit: silent
55
- * content loss passed verify clean). Surfaced as UNSUPPORTED_SYNTAX so the
56
- * loss is announced rather than silent, mirroring the other checks above.
57
- */
58
- function malformedFlowchartStatement(statement: string): { syntax: string; message: string } | null {
59
- if (FLOWCHART_KEYWORD_STATEMENT.test(statement)) return null
60
- // Metadata (`id@{ … }`) may span source lines and markdown strings go
61
- // opaque; both already have dedicated warnings above — skip to avoid noise.
62
- if (/(?:^|[\s;&])[\w-]+@\s*\{/.test(statement) || /`/.test(statement)) return null
63
-
64
- let depth = 0
65
- let inQuote = false
66
- let pipes = 0
67
- let escaped = false
68
- for (const ch of statement) {
69
- if (inQuote) {
70
- // Backslash escapes only exist inside quoted labels (consumeQuotedNode);
71
- // outside quotes `\` is literal — lean/trapezoid shapes spell `A[\x\]`.
72
- if (escaped) { escaped = false; continue }
73
- if (ch === '\\') { escaped = true; continue }
74
- if (ch === '"') inQuote = false
75
- continue
76
- }
77
- if (ch === '"') { inQuote = true; continue }
78
- if (ch === '|' && depth === 0) { pipes++; continue }
79
- if (pipes % 2 === 1) continue // inside a |edge label|
80
- if (ch === '[' || ch === '(' || ch === '{') depth++
81
- // Stray closers are tolerated: `A>text]` (asymmetric) closes without opening.
82
- else if (ch === ']' || ch === ')' || ch === '}') depth = Math.max(0, depth - 1)
83
- }
84
- if (inQuote) {
85
- return { syntax: 'flowchart_unclosed_quote', message: 'Unclosed double quote — the parser drops or mangles everything after it (labels, arrows, edges). Close the quote.' }
86
- }
87
- if (pipes % 2 === 1) {
88
- return { syntax: 'flowchart_unclosed_pipe', message: 'Unclosed |edge label| delimiter — the parser drops the label and the edge target after it. Close the label with a second |.' }
89
- }
90
- if (depth > 0) {
91
- return { syntax: 'flowchart_unclosed_bracket', message: 'Unclosed bracket — the parser drops or mangles everything after it, including arrows and edges (e.g. `A[Start --> B` loses the A→B edge). Close the bracket or quote the label.' }
92
- }
93
- return null
94
- }
95
-
96
- export function flowchartStatements(source: string): FlowchartStatement[] {
97
- const statements: FlowchartStatement[] = []
98
- const lines = source.split(/\r?\n/)
99
- for (let i = 1; i < lines.length; i++) {
100
- let raw = lines[i]!
101
- const startLine = i + 1
102
- // A `id@{ ... }` metadata block may span lines (the form Mermaid's docs
103
- // use). Join it into ONE statement anchored at the opening line, so the
104
- // node-metadata lint sees the same unit the parser tokenizes — otherwise
105
- // the multiline form silently misses the UNSUPPORTED_SYNTAX warning the
106
- // single-line form gets.
107
- while (i + 1 < lines.length && hasUnclosedMetadataBlock(raw)) {
108
- i++
109
- raw += ' ' + lines[i]!
110
- }
111
- for (const text of splitFlowchartStatements(raw)) {
112
- const trimmed = text.trim()
113
- if (!trimmed || trimmed.startsWith('%%')) continue
114
- statements.push({ text: trimmed, line: startLine })
115
- }
116
- }
117
- return statements
118
- }
119
-
120
- function hasUnclosedMetadataBlock(text: string): boolean {
121
- const at = text.search(/[\w-]+@\s*\{/)
122
- if (at < 0) return false
123
- let depth = 0
124
- let quote: '"' | "'" | '`' | null = null
125
- let escaped = false
126
- for (let i = at; i < text.length; i++) {
127
- const ch = text[i]!
128
- if (escaped) { escaped = false; continue }
129
- if (ch === '\\') { escaped = true; continue }
130
- if (quote) {
131
- if (ch === quote) quote = null
132
- continue
133
- }
134
- if (ch === '"' || ch === "'" || ch === '`') { quote = ch; continue }
135
- if (ch === '{') depth++
136
- else if (ch === '}') { depth--; if (depth === 0) return false }
137
- }
138
- return depth > 0
139
- }
140
-
141
- function hasFlowchartEdgeId(statement: string): boolean {
142
- return EDGE_ID_REGEX.test(maskFlowchartLabels(statement))
143
- }
144
-
145
- function isFlowchartInteractionDirective(statement: string): boolean {
146
- return /^(?:click|href)\s+/i.test(statement.trim())
147
- }
148
-
149
- function isUnsupportedEdgeMetadataStatement(statement: string): boolean {
150
- const match = statement.trim().match(/^[\w-]+@\s*\{([\s\S]*)\}\s*$/)
151
- if (!match) return false
152
- const metadata = match[1]!
153
- // Node metadata is source-preserved by the opaque fallback. Edge metadata
154
- // has animate/curve semantics only Mermaid itself understands today.
155
- return !isNodeMetadata(metadata)
156
- }
157
-
158
- // `id@{ shape: ... }` node metadata (Mermaid v11.3+). Preserved losslessly by the
159
- // opaque fallback (issue #29), but still unmodeled — so it is surfaced loudly,
160
- // like every other unsupported flowchart construct (issue #36), rather than kept
161
- // silent. Advisory (Tier-3): it never flips verify.ok and never drops source.
162
- function isNodeMetadataStatement(statement: string): boolean {
163
- const match = statement.trim().match(/^[\w-]+@\s*\{([\s\S]*)\}\s*$/)
164
- return match ? isNodeMetadata(match[1]!) : false
165
- }
166
-
167
- function isNodeMetadata(metadata: string): boolean {
168
- return /(?:^|,)\s*(?:shape|label|icon|img)\s*:/i.test(metadata)
169
- }
170
-
171
- function maskFlowchartLabels(statement: string): string {
172
- let masked = ''
173
- let start = 0
174
- let quote: '"' | "'" | '`' | null = null
175
- let escaped = false
176
- let pipeStart = -1
177
- for (let i = 0; i < statement.length; i++) {
178
- const ch = statement[i]!
179
- if (escaped) { escaped = false; continue }
180
- if (ch === '\\') { escaped = true; continue }
181
- if (quote) {
182
- if (ch === quote) quote = null
183
- continue
184
- }
185
- if (ch === '"' || ch === "'" || ch === '`') { quote = ch; continue }
186
- if (ch !== '|') continue
187
- if (pipeStart < 0) {
188
- pipeStart = i
189
- } else {
190
- masked += statement.slice(start, pipeStart) + '| |'
191
- start = i + 1
192
- pipeStart = -1
193
- }
194
- }
195
- masked += statement.slice(start)
196
- return masked.replace(/((?:<)?(?:-{2,}|-\.+|={2,}))\s+(.+?)\s+(-{2,}>|-{3,}|\.+->|-\.+-|={2,}>|={3,})/g, '$1 $3')
197
- }
198
-
199
- function splitFlowchartStatements(line: string): string[] {
200
- const out: string[] = []
201
- let start = 0
202
- let depth = 0
203
- let quote: '"' | "'" | '`' | null = null
204
- let escaped = false
205
- let inPipeLabel = false
206
-
207
- for (let i = 0; i < line.length; i++) {
208
- const ch = line[i]!
209
- if (escaped) { escaped = false; continue }
210
- if (ch === '\\') { escaped = true; continue }
211
- if (quote) {
212
- if (ch === quote) quote = null
213
- continue
214
- }
215
- if (ch === '"' || ch === "'" || ch === '`') { quote = ch; continue }
216
- if (ch === '|' && depth === 0) { inPipeLabel = !inPipeLabel; continue }
217
- if (inPipeLabel) continue
218
- if (ch === '[' || ch === '(' || ch === '{') depth++
219
- else if (ch === ']' || ch === ')' || ch === '}') depth = Math.max(0, depth - 1)
220
- else if (ch === ';' && depth === 0 && !semicolonInsideTextArrowLabel(line, i, start)) {
221
- const part = line.slice(start, i).trim()
222
- if (part) out.push(part)
223
- start = i + 1
224
- }
225
- }
226
-
227
- const tail = line.slice(start).trim()
228
- if (tail) out.push(tail)
229
- return out
230
- }
231
-
232
- function semicolonInsideTextArrowLabel(line: string, index: number, start: number): boolean {
233
- const before = line.slice(start, index)
234
- const after = line.slice(index + 1)
235
- const openerRe = /(?:^|\s)(?:[\w-]+@\s*)?(?:<)?(?:-{2,}|-\.+|={2,})\s+/g
236
- const closerRe = /(?:^|\s)(?:-{2,}>|-{3,}|\.+->|-\.+-|={2,}>|={3,})/
237
- let activeTextLabel = false
238
- for (const match of before.matchAll(openerRe)) {
239
- const tail = before.slice((match.index ?? 0) + match[0].length)
240
- if (!closerRe.test(tail)) activeTextLabel = true
241
- }
242
- return activeTextLabel && closerRe.test(after)
243
- }