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.
- package/CHANGELOG.md +149 -4
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +25 -32
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/assets/fonts/FONT-LICENSES.md +9 -5
- package/assets/fonts/Inter-Bold.ttf +0 -0
- package/assets/fonts/Inter-Medium.ttf +0 -0
- package/assets/fonts/Inter-Regular.ttf +0 -0
- package/assets/fonts/Inter-SemiBold.ttf +0 -0
- package/dist/agent-core.d.ts +463 -0
- package/dist/agent-core.js +397 -0
- package/dist/agent.d.ts +81 -1822
- package/dist/agent.js +336 -36839
- package/dist/agentic-mermaid-mcp.js +7 -37908
- package/dist/am.js +33638 -36261
- package/dist/chunk-4QECACIA.js +4702 -0
- package/dist/chunk-CDPKJ2BQ.js +37 -0
- package/dist/chunk-QKDLMOJJ.js +3487 -0
- package/dist/chunk-WI35L7IR.js +615 -0
- package/dist/chunk-WN3YDOBG.js +149 -0
- package/dist/chunk-ZYMMKQ3I.js +62637 -0
- package/dist/index-D54dqSdC.d.ts +6777 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +310 -32836
- package/dist/mcp-cli-SBQSTJFC.js +17 -0
- package/docs/README.md +14 -6
- package/docs/schemas/style-spec.schema.json +1221 -30
- package/examples/styles/catalog.json +54 -0
- package/examples/styles/cloudflare-workers-inspired-prototype.mmd +9 -0
- package/examples/styles/cloudflare-workers-inspired-prototype.style.json +110 -0
- package/examples/styles/cupertino-prototype.mmd +10 -0
- package/examples/styles/cupertino-prototype.style.json +63 -0
- package/examples/styles/mid-century-report.style.json +2 -2
- package/examples/styles/star-chart-atlas.style.json +2 -1
- package/examples/styles/transit-route-map.style.json +2 -2
- package/examples/styles/vercel-inspired-prototype.mmd +6 -0
- package/examples/styles/vercel-inspired-prototype.style.json +87 -0
- package/package.json +70 -43
- package/server.json +41 -0
- package/skills/agentic-mermaid-diagram-workflow/SKILL.md +17 -36
- package/skills/agentic-mermaid-diagram-workflow/references/cli.md +4 -5
- package/skills/agentic-mermaid-diagram-workflow/references/code-mode.md +15 -31
- package/skills/agentic-mermaid-diagram-workflow/references/sequence.md +10 -14
- package/skills/agentic-mermaid-live-editor/SKILL.md +100 -206
- package/AGENT_NATIVE.md +0 -602
- package/Instructions_for_agents.md +0 -83
- package/TODO.md +0 -662
- package/assets/hero.png +0 -0
- package/dist/index-x2Cj1dVv.d.ts +0 -1089
- package/docs/agent-api-cookbook.md +0 -308
- package/docs/agent-mutation-policy.md +0 -104
- package/docs/agent-workflow-examples.md +0 -46
- package/docs/api.md +0 -203
- package/docs/ascii.md +0 -103
- package/docs/assets/improvements/box-start-connector.png +0 -0
- package/docs/assets/improvements/comment-dropped-lint.png +0 -0
- package/docs/assets/improvements/er-content-loss.png +0 -0
- package/docs/assets/improvements/fan-in-trunks.png +0 -0
- package/docs/assets/improvements/gantt-family.png +0 -0
- package/docs/assets/improvements/gantt-milestone-clamp.png +0 -0
- package/docs/assets/improvements/gantt-verify-seam.png +0 -0
- package/docs/assets/improvements/subgraph-container-edges.png +0 -0
- package/docs/assets/improvements/wrapper-frontmatter.png +0 -0
- package/docs/assets/improvements/wrapper-init-directive.png +0 -0
- package/docs/assets/improvements/wrapper-leading-comment.png +0 -0
- package/docs/assets/style-cookbook/mid-century-report.png +0 -0
- package/docs/assets/style-cookbook/star-chart-atlas.png +0 -0
- package/docs/assets/style-cookbook/transit-route-map.png +0 -0
- package/docs/comparison.md +0 -94
- package/docs/config.md +0 -105
- package/docs/contributing/adding-diagram-types.md +0 -144
- package/docs/contributing/diagram-family-citizenship.matrix.json +0 -2127
- package/docs/contributing/diagram-family-citizenship.md +0 -82
- package/docs/contributing/harvesting-upstream-tests.md +0 -126
- package/docs/contributing/lessons-learned.md +0 -276
- package/docs/contributing/releasing.md +0 -71
- package/docs/contributing/visual-review-evidence.md +0 -53
- package/docs/custom-style-cookbook.md +0 -154
- package/docs/design/families/README.md +0 -26
- package/docs/design/families/architecture-beta.md +0 -48
- package/docs/design/families/flowchart-parser-conformance.md +0 -22
- package/docs/design/families/gantt-research.md +0 -137
- package/docs/design/families/gantt.md +0 -329
- package/docs/design/families/journey-agentic-cards-before.png +0 -0
- package/docs/design/families/journey-mermaid-classic-implementation.png +0 -0
- package/docs/design/families/journey-mermaid-classic-prototype.png +0 -0
- package/docs/design/families/journey-mermaid-classic-prototype.svg +0 -163
- package/docs/design/families/journey-mermaid-classic-prototype.svg.png +0 -0
- package/docs/design/families/journey-migration-parity.md +0 -433
- package/docs/design/families/journey-usage-research.md +0 -423
- package/docs/design/families/journey.md +0 -125
- package/docs/design/families/xychart.md +0 -85
- package/docs/design/system/README.md +0 -45
- package/docs/design/system/abstraction-audit.md +0 -224
- package/docs/design/system/abstraction-recommendations.md +0 -655
- package/docs/design/system/architecture.mmd +0 -29
- package/docs/design/system/architecture.svg +0 -137
- package/docs/design/system/layout-guarantees-and-robustness.md +0 -351
- package/docs/design/system/layout-rubric.md +0 -138
- package/docs/design/system/route-contracts.md +0 -848
- package/docs/design/system/source-preservation-ladder.md +0 -52
- package/docs/design/system/styles-rollout.md +0 -216
- package/docs/design/system/ugly-layouts.md +0 -119
- package/docs/diagram-families.md +0 -193
- package/docs/features.md +0 -147
- package/docs/fork-differences.md +0 -130
- package/docs/getting-started.md +0 -175
- package/docs/issue-derived-test-cases.md +0 -156
- package/docs/layout-characterization/README.md +0 -245
- package/docs/layout-characterization/contact-sheet-families.md +0 -496
- package/docs/layout-characterization/contact-sheet.md +0 -638
- package/docs/layout-characterization/properties.md +0 -281
- package/docs/layout-characterization/visual-quality.md +0 -160
- package/docs/layout-characterization/visual-snapshots/architecture.svg +0 -79
- package/docs/layout-characterization/visual-snapshots/class.svg +0 -60
- package/docs/layout-characterization/visual-snapshots/er.svg +0 -61
- package/docs/layout-characterization/visual-snapshots/flowchart.svg +0 -58
- package/docs/layout-characterization/visual-snapshots/gantt.svg +0 -52
- package/docs/layout-characterization/visual-snapshots/journey.svg +0 -118
- package/docs/layout-characterization/visual-snapshots/pie.svg +0 -36
- package/docs/layout-characterization/visual-snapshots/quadrant.svg +0 -47
- package/docs/layout-characterization/visual-snapshots/sequence.svg +0 -59
- package/docs/layout-characterization/visual-snapshots/state.svg +0 -52
- package/docs/layout-characterization/visual-snapshots/timeline.svg +0 -64
- package/docs/layout-characterization/visual-snapshots/xychart.svg +0 -97
- package/docs/mcp-code-mode-rationale.md +0 -101
- package/docs/mcp-http-transport.md +0 -223
- package/docs/mermaid-layout-complaints.md +0 -837
- package/docs/mutation-testing.md +0 -206
- package/docs/project/divergences.md +0 -98
- package/docs/project/dogfooding-docs-strategy.md +0 -66
- package/docs/project/hosted-mcp-cloudflare-plan.md +0 -338
- package/docs/project/lessons-learned.md +0 -1019
- package/docs/project/remove-role-styling-plan.md +0 -176
- package/docs/quality.md +0 -212
- package/docs/react.md +0 -97
- package/docs/style-authoring.md +0 -133
- package/docs/testing-strategy.md +0 -335
- package/docs/theming.md +0 -162
- package/examples/agent-improve-auth-flow.ts +0 -206
- package/examples/agent-loop.ts +0 -53
- package/examples/mcp-vs-cli-complex-diagrams.ts +0 -133
- package/llms.txt +0 -104
- package/skill-evals/README.md +0 -77
- package/skill-evals/fixtures/architecture-cache/input.mmd +0 -5
- package/skill-evals/fixtures/class-payment/input.mmd +0 -4
- package/skill-evals/fixtures/er-orders/input.mmd +0 -10
- package/skill-evals/fixtures/flowchart-cache/input.mmd +0 -3
- package/skill-evals/fixtures/gantt-release/input.mmd +0 -7
- package/skill-evals/fixtures/journey-checkout/input.mmd +0 -6
- package/skill-evals/fixtures/pie-exports/input.mmd +0 -5
- package/skill-evals/fixtures/quadrant-priorities/input.mmd +0 -11
- package/skill-evals/fixtures/sequence-retry/input.mmd +0 -8
- package/skill-evals/fixtures/state-lifecycle/input.mmd +0 -5
- package/skill-evals/fixtures/timeline-release/input.mmd +0 -6
- package/skill-evals/fixtures/xychart-growth/input.mmd +0 -5
- package/skill-evals/shared-benchmark.json +0 -510
- package/src/agent/analyze.ts +0 -157
- package/src/agent/apply.ts +0 -146
- package/src/agent/architecture-body.ts +0 -458
- package/src/agent/certificates.ts +0 -110
- package/src/agent/class-body.ts +0 -319
- package/src/agent/core.ts +0 -207
- package/src/agent/create.ts +0 -106
- package/src/agent/describe.ts +0 -364
- package/src/agent/er-body.ts +0 -224
- package/src/agent/facts.ts +0 -337
- package/src/agent/families-builtin.ts +0 -934
- package/src/agent/families.ts +0 -232
- package/src/agent/family-layouts.ts +0 -826
- package/src/agent/flowchart-body.ts +0 -372
- package/src/agent/flowchart-unsupported.ts +0 -243
- package/src/agent/gantt-body.ts +0 -495
- package/src/agent/index.ts +0 -10
- package/src/agent/journey-body.ts +0 -414
- package/src/agent/label-metrics.ts +0 -28
- package/src/agent/layout-to-rendered.ts +0 -63
- package/src/agent/mutate.ts +0 -97
- package/src/agent/mutation-ops.ts +0 -51
- package/src/agent/op-schema.ts +0 -439
- package/src/agent/parse.ts +0 -223
- package/src/agent/pie-body.ts +0 -270
- package/src/agent/png.ts +0 -117
- package/src/agent/quadrant-body.ts +0 -336
- package/src/agent/quality.ts +0 -363
- package/src/agent/readability-audit.ts +0 -92
- package/src/agent/rendered-route-audit.ts +0 -61
- package/src/agent/sequence-body.ts +0 -345
- package/src/agent/serialize.ts +0 -214
- package/src/agent/state-body.ts +0 -589
- package/src/agent/structural-count.ts +0 -150
- package/src/agent/timeline-body.ts +0 -280
- package/src/agent/trace-log.ts +0 -43
- package/src/agent/types.ts +0 -979
- package/src/agent/verify.ts +0 -571
- package/src/agent/xychart-body.ts +0 -472
- package/src/architecture/config.ts +0 -282
- package/src/architecture/layout.ts +0 -496
- package/src/architecture/parser.ts +0 -375
- package/src/architecture/renderer.ts +0 -611
- package/src/architecture/types.ts +0 -113
- package/src/ascii/ansi.ts +0 -423
- package/src/ascii/architecture.ts +0 -210
- package/src/ascii/canvas.ts +0 -443
- package/src/ascii/class-diagram.ts +0 -697
- package/src/ascii/converter.ts +0 -475
- package/src/ascii/draw.ts +0 -1693
- package/src/ascii/edge-bundling.ts +0 -328
- package/src/ascii/edge-routing.ts +0 -330
- package/src/ascii/er-diagram.ts +0 -439
- package/src/ascii/gantt.ts +0 -276
- package/src/ascii/grid.ts +0 -747
- package/src/ascii/index.ts +0 -196
- package/src/ascii/journey.ts +0 -189
- package/src/ascii/meta.ts +0 -244
- package/src/ascii/multiline-utils.ts +0 -80
- package/src/ascii/pathfinder.ts +0 -301
- package/src/ascii/pie.ts +0 -67
- package/src/ascii/quadrant.ts +0 -158
- package/src/ascii/reverse.ts +0 -216
- package/src/ascii/sequence.ts +0 -499
- package/src/ascii/shapes/circle.ts +0 -27
- package/src/ascii/shapes/corners.ts +0 -143
- package/src/ascii/shapes/diamond.ts +0 -27
- package/src/ascii/shapes/hexagon.ts +0 -27
- package/src/ascii/shapes/index.ts +0 -106
- package/src/ascii/shapes/rectangle.ts +0 -168
- package/src/ascii/shapes/rounded.ts +0 -27
- package/src/ascii/shapes/special.ts +0 -332
- package/src/ascii/shapes/stadium.ts +0 -110
- package/src/ascii/shapes/state.ts +0 -192
- package/src/ascii/shapes/types.ts +0 -73
- package/src/ascii/timeline.ts +0 -128
- package/src/ascii/types.ts +0 -312
- package/src/ascii/validate.ts +0 -120
- package/src/ascii/width.ts +0 -81
- package/src/ascii/xychart.ts +0 -919
- package/src/browser.ts +0 -29
- package/src/class/layout.ts +0 -240
- package/src/class/parser.ts +0 -332
- package/src/class/renderer.ts +0 -638
- package/src/class/types.ts +0 -129
- package/src/cli/agent-instructions.ts +0 -1
- package/src/cli/am-bin.ts +0 -6
- package/src/cli/exit-codes.ts +0 -18
- package/src/cli/index.ts +0 -1117
- package/src/cli/init-agent.ts +0 -147
- package/src/color-resolver.ts +0 -152
- package/src/elk-instance.ts +0 -114
- package/src/er/layout.ts +0 -187
- package/src/er/parser.ts +0 -226
- package/src/er/renderer.ts +0 -792
- package/src/er/types.ts +0 -99
- package/src/font-manifest.ts +0 -27
- package/src/gantt/layout.ts +0 -365
- package/src/gantt/parser.ts +0 -282
- package/src/gantt/pipeline.ts +0 -27
- package/src/gantt/renderer.ts +0 -414
- package/src/gantt/schedule.ts +0 -513
- package/src/gantt/types.ts +0 -255
- package/src/index.ts +0 -368
- package/src/journey/layout.ts +0 -520
- package/src/journey/parser.ts +0 -218
- package/src/journey/renderer.ts +0 -811
- package/src/journey/types.ts +0 -144
- package/src/layout/geometry.ts +0 -182
- package/src/layout/pass.ts +0 -142
- package/src/layout/passes/index.ts +0 -3916
- package/src/layout-engine.ts +0 -1565
- package/src/layout-rubric.ts +0 -659
- package/src/mcp/artifacts.ts +0 -137
- package/src/mcp/dynamic-harness.ts +0 -31
- package/src/mcp/dynamic-modules.d.ts +0 -7
- package/src/mcp/facade.ts +0 -608
- package/src/mcp/harness-runtime.ts +0 -203
- package/src/mcp/hosted-server.ts +0 -468
- package/src/mcp/mcp-bin.ts +0 -3
- package/src/mcp/mcp-cli.ts +0 -82
- package/src/mcp/protocol.ts +0 -13
- package/src/mcp/sandbox.ts +0 -195
- package/src/mcp/sdk-decl.ts +0 -387
- package/src/mcp/server.ts +0 -362
- package/src/mcp/tool-surface.ts +0 -146
- package/src/mermaid-source.ts +0 -655
- package/src/multiline-utils.ts +0 -228
- package/src/parser.ts +0 -1031
- package/src/pie/layout.ts +0 -168
- package/src/pie/parser.ts +0 -136
- package/src/pie/renderer.ts +0 -234
- package/src/pie/types.ts +0 -82
- package/src/quadrant/layout.ts +0 -191
- package/src/quadrant/parser.ts +0 -183
- package/src/quadrant/renderer.ts +0 -322
- package/src/quadrant/types.ts +0 -113
- package/src/render-family-hooks.ts +0 -244
- package/src/renderer.ts +0 -1099
- package/src/route-contracts.ts +0 -2723
- package/src/scene/backend.ts +0 -76
- package/src/scene/fidelity.ts +0 -183
- package/src/scene/hybrid-backend.ts +0 -174
- package/src/scene/ir.ts +0 -181
- package/src/scene/marks.ts +0 -127
- package/src/scene/rough-backend.ts +0 -378
- package/src/scene/seed.ts +0 -46
- package/src/scene/style-registry.ts +0 -556
- package/src/sequence/layout.ts +0 -456
- package/src/sequence/parser.ts +0 -255
- package/src/sequence/renderer.ts +0 -604
- package/src/sequence/types.ts +0 -154
- package/src/shape-clipping.ts +0 -351
- package/src/shared/accessibility-directives.ts +0 -22
- package/src/shared/batched.ts +0 -61
- package/src/shared/color-math.ts +0 -72
- package/src/shared/svg-a11y.ts +0 -29
- package/src/shared/syntax-error.ts +0 -17
- package/src/shared/unicode-ranges.ts +0 -135
- package/src/styles.ts +0 -318
- package/src/svg-paths.ts +0 -28
- package/src/text-metrics.ts +0 -248
- package/src/theme.ts +0 -639
- package/src/timeline/layout.ts +0 -381
- package/src/timeline/parser.ts +0 -215
- package/src/timeline/renderer.ts +0 -538
- package/src/timeline/types.ts +0 -105
- package/src/types.ts +0 -454
- package/src/website-worker-modules.d.ts +0 -18
- package/src/xychart/axis-utils.ts +0 -109
- package/src/xychart/colors.ts +0 -124
- package/src/xychart/config.ts +0 -88
- package/src/xychart/layout.ts +0 -656
- package/src/xychart/parser.ts +0 -504
- package/src/xychart/renderer.ts +0 -640
- package/src/xychart/types.ts +0 -282
package/src/parser.ts
DELETED
|
@@ -1,1031 +0,0 @@
|
|
|
1
|
-
import type { MermaidGraph, MermaidNode, MermaidEdge, MermaidSubgraph, Direction, NodeShape, EdgeStyle, EdgeMarker } from './types.ts'
|
|
2
|
-
import { normalizeBrTags } from './multiline-utils.ts'
|
|
3
|
-
|
|
4
|
-
// ============================================================================
|
|
5
|
-
// Mermaid parser — flowcharts and state diagrams
|
|
6
|
-
//
|
|
7
|
-
// Supports:
|
|
8
|
-
// Flowcharts: graph TD / flowchart LR
|
|
9
|
-
// State diagrams: stateDiagram-v2
|
|
10
|
-
//
|
|
11
|
-
// Line-by-line regex approach — the grammar is regular enough
|
|
12
|
-
// that we don't need a grammar generator or full parser combinator.
|
|
13
|
-
// ============================================================================
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Parse Mermaid text into a logical graph structure.
|
|
17
|
-
* Auto-detects diagram type (flowchart or state diagram).
|
|
18
|
-
* Throws on invalid/unsupported input.
|
|
19
|
-
*/
|
|
20
|
-
export function parseMermaid(text: string): MermaidGraph {
|
|
21
|
-
const lines = expandInlineHeaderStatements(coalesceMetadataLines(text.split('\n')).map(l => l.trim()).filter(l => l.length > 0 && !l.startsWith('%%')))
|
|
22
|
-
|
|
23
|
-
if (lines.length === 0) {
|
|
24
|
-
throw new Error('Empty mermaid diagram')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Detect diagram type from header
|
|
28
|
-
const header = lines[0]!
|
|
29
|
-
|
|
30
|
-
// State diagram: "stateDiagram-v2" or "stateDiagram"
|
|
31
|
-
if (/^stateDiagram(-v2)?\s*$/i.test(header)) {
|
|
32
|
-
return parseStateDiagram(lines)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Flowchart: "graph TD" or "flowchart LR"
|
|
36
|
-
return parseFlowchart(lines)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function expandInlineHeaderStatements(lines: string[]): string[] {
|
|
40
|
-
if (lines.length === 0 || !lines[0]!.includes(';')) return lines
|
|
41
|
-
if (!/^(?:graph|flowchart|swimlane|stateDiagram(?:-v2)?)(?:\b|\s)/i.test(lines[0]!)) return lines
|
|
42
|
-
return [...splitFlowchartStatements(lines[0]!), ...lines.slice(1)]
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Mermaid v11 uses `id@{ ... }` metadata blocks for flowchart nodes. The
|
|
47
|
-
* legacy parser is line-oriented; without this coalescing pass, multiline
|
|
48
|
-
* metadata keys such as `shape:` and `label:` are treated as standalone node
|
|
49
|
-
* statements. Keep the whole metadata object attached to its node token so the
|
|
50
|
-
* node consumer can handle it as one unit.
|
|
51
|
-
*/
|
|
52
|
-
function coalesceMetadataLines(lines: string[]): string[] {
|
|
53
|
-
const out: string[] = []
|
|
54
|
-
let current: string[] | null = null
|
|
55
|
-
let balance = 0
|
|
56
|
-
|
|
57
|
-
for (const line of lines) {
|
|
58
|
-
if (current) {
|
|
59
|
-
current.push(line.trim())
|
|
60
|
-
balance += metadataBraceDelta(line)
|
|
61
|
-
if (balance <= 0) {
|
|
62
|
-
out.push(current.join(' '))
|
|
63
|
-
current = null
|
|
64
|
-
balance = 0
|
|
65
|
-
}
|
|
66
|
-
continue
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (/(?:^|[\s;])[\w-]+@\s*\{/.test(line)) {
|
|
70
|
-
balance = metadataBraceDelta(line)
|
|
71
|
-
if (balance > 0) current = [line.trim()]
|
|
72
|
-
else out.push(line)
|
|
73
|
-
continue
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
out.push(line)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (current) out.push(current.join(' '))
|
|
80
|
-
return out
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function metadataBraceDelta(text: string): number {
|
|
84
|
-
let delta = 0
|
|
85
|
-
let quote: '"' | "'" | null = null
|
|
86
|
-
let escaped = false
|
|
87
|
-
for (const ch of text) {
|
|
88
|
-
if (escaped) { escaped = false; continue }
|
|
89
|
-
if (ch === '\\') { escaped = true; continue }
|
|
90
|
-
if (quote) {
|
|
91
|
-
if (ch === quote) quote = null
|
|
92
|
-
continue
|
|
93
|
-
}
|
|
94
|
-
if (ch === '"' || ch === "'") { quote = ch; continue }
|
|
95
|
-
if (ch === '{') delta++
|
|
96
|
-
else if (ch === '}') delta--
|
|
97
|
-
}
|
|
98
|
-
return delta
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function splitFlowchartStatements(line: string): string[] {
|
|
102
|
-
const out: string[] = []
|
|
103
|
-
let start = 0
|
|
104
|
-
let depth = 0
|
|
105
|
-
let quote: '"' | "'" | '`' | null = null
|
|
106
|
-
let escaped = false
|
|
107
|
-
let inPipeLabel = false
|
|
108
|
-
|
|
109
|
-
for (let i = 0; i < line.length; i++) {
|
|
110
|
-
const ch = line[i]!
|
|
111
|
-
if (escaped) { escaped = false; continue }
|
|
112
|
-
if (ch === '\\') { escaped = true; continue }
|
|
113
|
-
if (quote) {
|
|
114
|
-
if (ch === quote) quote = null
|
|
115
|
-
continue
|
|
116
|
-
}
|
|
117
|
-
if (ch === '"' || ch === "'" || ch === '`') { quote = ch; continue }
|
|
118
|
-
if (ch === '|' && depth === 0) { inPipeLabel = !inPipeLabel; continue }
|
|
119
|
-
if (inPipeLabel) continue
|
|
120
|
-
if (ch === '[' || ch === '(' || ch === '{') depth++
|
|
121
|
-
else if (ch === ']' || ch === ')' || ch === '}') depth = Math.max(0, depth - 1)
|
|
122
|
-
else if (ch === ';' && depth === 0 && !semicolonInsideTextArrowLabel(line, i, start)) {
|
|
123
|
-
const part = line.slice(start, i).trim()
|
|
124
|
-
if (part) out.push(part)
|
|
125
|
-
start = i + 1
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const tail = line.slice(start).trim()
|
|
130
|
-
if (tail) out.push(tail)
|
|
131
|
-
return out
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function semicolonInsideTextArrowLabel(line: string, index: number, start: number): boolean {
|
|
135
|
-
const before = line.slice(start, index)
|
|
136
|
-
const after = line.slice(index + 1)
|
|
137
|
-
const openerRe = /(?:^|\s)(?:[\w-]+@\s*)?(?:<)?(?:-{2,}|-\.+|={2,})\s+/g
|
|
138
|
-
const closerRe = /(?:^|\s)(?:-{2,}>|-{3,}|\.+->|-\.+-|={2,}>|={3,})/
|
|
139
|
-
let activeTextLabel = false
|
|
140
|
-
for (const match of before.matchAll(openerRe)) {
|
|
141
|
-
const tail = before.slice((match.index ?? 0) + match[0].length)
|
|
142
|
-
if (!closerRe.test(tail)) activeTextLabel = true
|
|
143
|
-
}
|
|
144
|
-
return activeTextLabel && closerRe.test(after)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function isFlowchartInteractionDirective(line: string): boolean {
|
|
148
|
-
return /^(?:click|href)\s+/i.test(line.trim())
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function isUnsupportedEdgeMetadataLine(line: string): boolean {
|
|
152
|
-
const match = line.trim().match(/^[\w-]+@\s*\{([\s\S]*)\}\s*$/)
|
|
153
|
-
if (!match) return false
|
|
154
|
-
const metadata = match[1]!
|
|
155
|
-
// Node metadata is modeled by the safety fallback; edge metadata currently
|
|
156
|
-
// has animate/curve semantics only Mermaid itself understands.
|
|
157
|
-
return !/(?:^|,)\s*(?:shape|label|icon|img)\s*:/i.test(metadata)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ============================================================================
|
|
161
|
-
// Flowchart parser
|
|
162
|
-
// ============================================================================
|
|
163
|
-
|
|
164
|
-
function parseFlowchart(lines: string[]): MermaidGraph {
|
|
165
|
-
const headerMatch = lines[0]!.match(/^(?:graph|flowchart|swimlane)\s+(TD|TB|LR|BT|RL|[<>^v])\s*$/i)
|
|
166
|
-
if (!headerMatch) {
|
|
167
|
-
throw new Error(`Invalid mermaid header: "${lines[0]}". Expected "graph TD", "flowchart LR", "stateDiagram-v2", etc.`)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const direction = normalizeFlowchartDirection(headerMatch[1]!)
|
|
171
|
-
|
|
172
|
-
const graph: MermaidGraph = {
|
|
173
|
-
direction,
|
|
174
|
-
nodes: new Map(),
|
|
175
|
-
edges: [],
|
|
176
|
-
subgraphs: [],
|
|
177
|
-
classDefs: new Map(),
|
|
178
|
-
classAssignments: new Map(),
|
|
179
|
-
nodeStyles: new Map(),
|
|
180
|
-
linkStyles: new Map(),
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Subgraph stack for nested subgraphs.
|
|
184
|
-
const subgraphStack: MermaidSubgraph[] = []
|
|
185
|
-
const declaredSubgraphIds = collectDeclaredFlowchartSubgraphIds(lines.slice(1))
|
|
186
|
-
|
|
187
|
-
for (let i = 1; i < lines.length; i++) {
|
|
188
|
-
for (const line of splitFlowchartStatements(lines[i]!)) {
|
|
189
|
-
// --- source-level directives that do not affect local layout ---
|
|
190
|
-
if (isFlowchartInteractionDirective(line) || isUnsupportedEdgeMetadataLine(line)) continue
|
|
191
|
-
|
|
192
|
-
// --- classDef: `classDef name prop:val,prop:val` ---
|
|
193
|
-
const classDefMatch = line.match(/^classDef\s+([\w,-]+)\s+(.+)$/)
|
|
194
|
-
if (classDefMatch) {
|
|
195
|
-
const names = classDefMatch[1]!.split(',').map(s => s.trim()).filter(Boolean)
|
|
196
|
-
const props = parseStyleProps(classDefMatch[2]!)
|
|
197
|
-
if (Object.keys(props).length === 0) continue
|
|
198
|
-
for (const name of names) graph.classDefs.set(name, props)
|
|
199
|
-
continue
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// --- class assignment: `class A,B className` ---
|
|
203
|
-
const classAssignMatch = line.match(/^class\s+([\w,-]+)\s+([\w-]+)\s*;?$/)
|
|
204
|
-
if (classAssignMatch) {
|
|
205
|
-
const nodeIds = classAssignMatch[1]!.split(',').map(s => s.trim())
|
|
206
|
-
const className = classAssignMatch[2]!
|
|
207
|
-
for (const id of nodeIds) {
|
|
208
|
-
graph.classAssignments.set(id, className)
|
|
209
|
-
}
|
|
210
|
-
continue
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// --- style statement: `style A,B fill:#f00,stroke:#333` ---
|
|
214
|
-
const styleMatch = line.match(/^style\s+([\w,-]+)\s+(.+)$/)
|
|
215
|
-
if (styleMatch) {
|
|
216
|
-
const nodeIds = styleMatch[1]!.split(',').map(s => s.trim())
|
|
217
|
-
const props = parseStyleProps(styleMatch[2]!)
|
|
218
|
-
if (Object.keys(props).length === 0) continue
|
|
219
|
-
for (const id of nodeIds) {
|
|
220
|
-
graph.nodeStyles.set(id, { ...graph.nodeStyles.get(id), ...props })
|
|
221
|
-
}
|
|
222
|
-
continue
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// --- linkStyle: `linkStyle 0 stroke:#f00` or `linkStyle default stroke:#f00` ---
|
|
226
|
-
const linkStyleMatch = line.match(/^linkStyle\s+(default|[\d,\s]+)\s+(.+)$/)
|
|
227
|
-
if (linkStyleMatch) {
|
|
228
|
-
const target = linkStyleMatch[1]!.trim()
|
|
229
|
-
const props = parseStyleProps(linkStyleMatch[2]!)
|
|
230
|
-
if (Object.keys(props).length === 0) continue
|
|
231
|
-
if (target === 'default') {
|
|
232
|
-
graph.linkStyles.set('default', { ...graph.linkStyles.get('default'), ...props })
|
|
233
|
-
} else {
|
|
234
|
-
const indices = target.split(',').map(s => parseInt(s.trim(), 10))
|
|
235
|
-
for (const idx of indices) {
|
|
236
|
-
if (!isNaN(idx)) {
|
|
237
|
-
graph.linkStyles.set(idx, { ...graph.linkStyles.get(idx), ...props })
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
continue
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// --- direction override inside subgraph: `direction LR` ---
|
|
245
|
-
const dirMatch = line.match(/^direction\s+(TD|TB|LR|BT|RL)\s*$/i)
|
|
246
|
-
if (dirMatch && subgraphStack.length > 0) {
|
|
247
|
-
subgraphStack[subgraphStack.length - 1]!.direction = dirMatch[1]!.toUpperCase() as Direction
|
|
248
|
-
continue
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// --- subgraph start: `subgraph Label` or `subgraph id [Label]` ---
|
|
252
|
-
const subgraphMatch = line.match(/^subgraph\s+(.+)$/)
|
|
253
|
-
if (subgraphMatch) {
|
|
254
|
-
const rest = subgraphMatch[1]!.trim()
|
|
255
|
-
// Check for "subgraph id [Label]" form
|
|
256
|
-
// ID can contain hyphens (e.g. "us-east"), so use [\w-]+ not \w+
|
|
257
|
-
const bracketMatch = rest.match(/^([\w-]+)\s*\[(.+)\]$/)
|
|
258
|
-
let id: string
|
|
259
|
-
let label: string
|
|
260
|
-
if (bracketMatch) {
|
|
261
|
-
id = bracketMatch[1]!
|
|
262
|
-
label = normalizeBrTags(bracketMatch[2]!)
|
|
263
|
-
} else {
|
|
264
|
-
// Use the label text as id (slugified)
|
|
265
|
-
label = normalizeBrTags(rest)
|
|
266
|
-
id = rest.replace(/\s+/g, '_').replace(/[^\w]/g, '')
|
|
267
|
-
}
|
|
268
|
-
const sg: MermaidSubgraph = { id, label, nodeIds: [], children: [] }
|
|
269
|
-
subgraphStack.push(sg)
|
|
270
|
-
continue
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// --- subgraph end ---
|
|
274
|
-
if (line === 'end') {
|
|
275
|
-
const completed = subgraphStack.pop()
|
|
276
|
-
if (completed) {
|
|
277
|
-
if (subgraphStack.length > 0) {
|
|
278
|
-
subgraphStack[subgraphStack.length - 1]!.children.push(completed)
|
|
279
|
-
} else {
|
|
280
|
-
graph.subgraphs.push(completed)
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
continue
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// --- Edge/node definitions ---
|
|
287
|
-
parseEdgeLine(line, graph, subgraphStack, declaredSubgraphIds)
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return graph
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function collectDeclaredFlowchartSubgraphIds(lines: string[]): Set<string> {
|
|
295
|
-
const ids = new Set<string>()
|
|
296
|
-
for (const raw of lines) {
|
|
297
|
-
for (const line of splitFlowchartStatements(raw)) {
|
|
298
|
-
const subgraphMatch = line.match(/^subgraph\s+(.+)$/)
|
|
299
|
-
if (!subgraphMatch) continue
|
|
300
|
-
const rest = subgraphMatch[1]!.trim()
|
|
301
|
-
const bracketMatch = rest.match(/^([\w-]+)\s*\[(.+)\]$/)
|
|
302
|
-
ids.add(bracketMatch ? bracketMatch[1]! : rest.replace(/\s+/g, '_').replace(/[^\w]/g, ''))
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return ids
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function normalizeFlowchartDirection(raw: string): Direction {
|
|
309
|
-
const direction = raw.toUpperCase()
|
|
310
|
-
if (direction === '>') return 'LR'
|
|
311
|
-
if (direction === '<') return 'RL'
|
|
312
|
-
if (direction === '^') return 'BT'
|
|
313
|
-
if (direction === 'V') return 'TB'
|
|
314
|
-
return direction as Direction
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// ============================================================================
|
|
318
|
-
// State diagram parser
|
|
319
|
-
//
|
|
320
|
-
// Supported syntax:
|
|
321
|
-
// stateDiagram-v2
|
|
322
|
-
// s1 : Description
|
|
323
|
-
// state "Description" as s1
|
|
324
|
-
// s1 --> s2 : label
|
|
325
|
-
// [*] --> s1 (start pseudostate)
|
|
326
|
-
// s1 --> [*] (end pseudostate)
|
|
327
|
-
// state CompositeState {
|
|
328
|
-
// inner1 --> inner2
|
|
329
|
-
// }
|
|
330
|
-
// ============================================================================
|
|
331
|
-
|
|
332
|
-
function parseStateDiagram(lines: string[]): MermaidGraph {
|
|
333
|
-
const graph: MermaidGraph = {
|
|
334
|
-
direction: 'TD',
|
|
335
|
-
nodes: new Map(),
|
|
336
|
-
edges: [],
|
|
337
|
-
subgraphs: [],
|
|
338
|
-
classDefs: new Map(),
|
|
339
|
-
classAssignments: new Map(),
|
|
340
|
-
nodeStyles: new Map(),
|
|
341
|
-
linkStyles: new Map(),
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Track composite state nesting (like subgraphs)
|
|
345
|
-
const compositeStack: MermaidSubgraph[] = []
|
|
346
|
-
// Track all composite state IDs to avoid creating duplicate nodes
|
|
347
|
-
const compositeStateIds = new Set<string>()
|
|
348
|
-
// Counter for unique [*] pseudostate IDs
|
|
349
|
-
let startCount = 0
|
|
350
|
-
let endCount = 0
|
|
351
|
-
|
|
352
|
-
for (let i = 1; i < lines.length; i++) {
|
|
353
|
-
const line = lines[i]!
|
|
354
|
-
|
|
355
|
-
// --- direction override ---
|
|
356
|
-
const dirMatch = line.match(/^direction\s+(TD|TB|LR|BT|RL)\s*$/i)
|
|
357
|
-
if (dirMatch) {
|
|
358
|
-
if (compositeStack.length > 0) {
|
|
359
|
-
compositeStack[compositeStack.length - 1]!.direction = dirMatch[1]!.toUpperCase() as Direction
|
|
360
|
-
} else {
|
|
361
|
-
graph.direction = dirMatch[1]!.toUpperCase() as Direction
|
|
362
|
-
}
|
|
363
|
-
continue
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// --- linkStyle: `linkStyle 0 stroke:#f00` or `linkStyle default stroke:#f00` ---
|
|
367
|
-
const linkStyleMatch = line.match(/^linkStyle\s+(default|[\d,\s]+)\s+(.+)$/)
|
|
368
|
-
if (linkStyleMatch) {
|
|
369
|
-
const target = linkStyleMatch[1]!.trim()
|
|
370
|
-
const props = parseStyleProps(linkStyleMatch[2]!)
|
|
371
|
-
if (Object.keys(props).length === 0) continue
|
|
372
|
-
if (target === 'default') {
|
|
373
|
-
graph.linkStyles.set('default', { ...graph.linkStyles.get('default'), ...props })
|
|
374
|
-
} else {
|
|
375
|
-
const indices = target.split(',').map(s => parseInt(s.trim(), 10))
|
|
376
|
-
for (const idx of indices) {
|
|
377
|
-
if (!isNaN(idx)) {
|
|
378
|
-
graph.linkStyles.set(idx, { ...graph.linkStyles.get(idx), ...props })
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
continue
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// --- composite state start: `state CompositeState {` ---
|
|
386
|
-
const compositeMatch = line.match(/^state\s+(?:"([^"]+)"\s+as\s+)?([\w\p{L}]+)\s*\{$/u)
|
|
387
|
-
if (compositeMatch) {
|
|
388
|
-
const label = compositeMatch[1] ?? compositeMatch[2]!
|
|
389
|
-
const id = compositeMatch[2]!
|
|
390
|
-
const sg: MermaidSubgraph = { id, label, nodeIds: [], children: [] }
|
|
391
|
-
compositeStack.push(sg)
|
|
392
|
-
// Track this ID to avoid creating a duplicate node for the composite state
|
|
393
|
-
compositeStateIds.add(id)
|
|
394
|
-
// Remove any existing node that was created when parsing transitions before
|
|
395
|
-
// this composite state definition (e.g., "A --> Processing" before "state Processing {")
|
|
396
|
-
graph.nodes.delete(id)
|
|
397
|
-
continue
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// --- composite state end ---
|
|
401
|
-
if (line === '}') {
|
|
402
|
-
const completed = compositeStack.pop()
|
|
403
|
-
if (completed) {
|
|
404
|
-
if (compositeStack.length > 0) {
|
|
405
|
-
compositeStack[compositeStack.length - 1]!.children.push(completed)
|
|
406
|
-
} else {
|
|
407
|
-
graph.subgraphs.push(completed)
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
continue
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// --- state alias: `state "Description" as s1` (without brace) ---
|
|
414
|
-
const stateAliasMatch = line.match(/^state\s+"([^"]+)"\s+as\s+([\w\p{L}]+)\s*$/u)
|
|
415
|
-
if (stateAliasMatch) {
|
|
416
|
-
const label = normalizeBrTags(stateAliasMatch[1]!)
|
|
417
|
-
const id = stateAliasMatch[2]!
|
|
418
|
-
registerStateNode(graph, compositeStack, { id, label, shape: 'rounded' })
|
|
419
|
-
continue
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// --- transition: `s1 --> s2` or `s1 --> s2 : label` or `[*] --> s1` ---
|
|
423
|
-
const transitionMatch = line.match(/^(\[\*\]|[\w\p{L}-]+)\s*(-->)\s*(\[\*\]|[\w\p{L}-]+)(?:\s*:\s*(.+))?$/u)
|
|
424
|
-
if (transitionMatch) {
|
|
425
|
-
let sourceId = transitionMatch[1]!
|
|
426
|
-
let targetId = transitionMatch[3]!
|
|
427
|
-
const rawTransitionLabel = transitionMatch[4]?.trim()
|
|
428
|
-
const edgeLabel = rawTransitionLabel ? normalizeBrTags(rawTransitionLabel) : undefined
|
|
429
|
-
|
|
430
|
-
// Handle [*] pseudostates — each occurrence gets a unique ID
|
|
431
|
-
if (sourceId === '[*]') {
|
|
432
|
-
startCount++
|
|
433
|
-
sourceId = `_start${startCount > 1 ? startCount : ''}`
|
|
434
|
-
registerStateNode(graph, compositeStack, { id: sourceId, label: '', shape: 'state-start' })
|
|
435
|
-
} else if (!compositeStateIds.has(sourceId)) {
|
|
436
|
-
// Only create a node if this isn't a composite state
|
|
437
|
-
ensureStateNode(graph, compositeStack, sourceId)
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
if (targetId === '[*]') {
|
|
441
|
-
endCount++
|
|
442
|
-
targetId = `_end${endCount > 1 ? endCount : ''}`
|
|
443
|
-
registerStateNode(graph, compositeStack, { id: targetId, label: '', shape: 'state-end' })
|
|
444
|
-
} else if (!compositeStateIds.has(targetId)) {
|
|
445
|
-
// Only create a node if this isn't a composite state
|
|
446
|
-
ensureStateNode(graph, compositeStack, targetId)
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
graph.edges.push({
|
|
450
|
-
source: sourceId,
|
|
451
|
-
target: targetId,
|
|
452
|
-
label: edgeLabel,
|
|
453
|
-
style: 'solid',
|
|
454
|
-
hasArrowStart: false,
|
|
455
|
-
hasArrowEnd: true,
|
|
456
|
-
})
|
|
457
|
-
continue
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// --- state description: `s1 : Description` ---
|
|
461
|
-
const stateDescMatch = line.match(/^([\w\p{L}-]+)\s*:\s*(.+)$/u)
|
|
462
|
-
if (stateDescMatch) {
|
|
463
|
-
const id = stateDescMatch[1]!
|
|
464
|
-
const label = normalizeBrTags(stateDescMatch[2]!.trim())
|
|
465
|
-
registerStateNode(graph, compositeStack, { id, label, shape: 'rounded' })
|
|
466
|
-
continue
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
return graph
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/** Register a state node and track in composite state if applicable */
|
|
474
|
-
function registerStateNode(
|
|
475
|
-
graph: MermaidGraph,
|
|
476
|
-
compositeStack: MermaidSubgraph[],
|
|
477
|
-
node: MermaidNode
|
|
478
|
-
): void {
|
|
479
|
-
const isNew = !graph.nodes.has(node.id)
|
|
480
|
-
if (isNew) {
|
|
481
|
-
graph.nodes.set(node.id, node)
|
|
482
|
-
}
|
|
483
|
-
if (compositeStack.length > 0) {
|
|
484
|
-
const current = compositeStack[compositeStack.length - 1]!
|
|
485
|
-
if (!current.nodeIds.includes(node.id)) {
|
|
486
|
-
current.nodeIds.push(node.id)
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/** Ensure a state node exists with default rounded shape */
|
|
492
|
-
function ensureStateNode(
|
|
493
|
-
graph: MermaidGraph,
|
|
494
|
-
compositeStack: MermaidSubgraph[],
|
|
495
|
-
id: string
|
|
496
|
-
): void {
|
|
497
|
-
if (!graph.nodes.has(id)) {
|
|
498
|
-
registerStateNode(graph, compositeStack, { id, label: id, shape: 'rounded' })
|
|
499
|
-
} else {
|
|
500
|
-
// Track in composite if applicable
|
|
501
|
-
if (compositeStack.length > 0) {
|
|
502
|
-
const current = compositeStack[compositeStack.length - 1]!
|
|
503
|
-
if (!current.nodeIds.includes(id)) {
|
|
504
|
-
current.nodeIds.push(id)
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// ============================================================================
|
|
511
|
-
// Shared utilities
|
|
512
|
-
// ============================================================================
|
|
513
|
-
|
|
514
|
-
/** Parse "fill:#f00,stroke:#333" style property strings into a Record */
|
|
515
|
-
/**
|
|
516
|
-
* Split on top-level commas only — commas inside parentheses (e.g.
|
|
517
|
-
* `rgb(10,10,10)`, `rgba(0,0,0,.5)`, `hsl(120,50%,50%)`) are NOT separators.
|
|
518
|
-
* Fixes the bug where `fill:rgb(10,10,10)` was split into `fill:rgb(10`.
|
|
519
|
-
*/
|
|
520
|
-
function splitTopLevelCommas(s: string): string[] {
|
|
521
|
-
const out: string[] = []
|
|
522
|
-
let depth = 0
|
|
523
|
-
let start = 0
|
|
524
|
-
let escaped = false
|
|
525
|
-
for (let i = 0; i < s.length; i++) {
|
|
526
|
-
const c = s[i]
|
|
527
|
-
if (escaped) { escaped = false; continue }
|
|
528
|
-
if (c === '\\') { escaped = true; continue }
|
|
529
|
-
if (c === '(') depth++
|
|
530
|
-
else if (c === ')') depth = Math.max(0, depth - 1)
|
|
531
|
-
else if (c === ',' && depth === 0) { out.push(s.slice(start, i)); start = i + 1 }
|
|
532
|
-
}
|
|
533
|
-
out.push(s.slice(start))
|
|
534
|
-
return out.map(part => part.replace(/\\,/g, ','))
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function parseStyleProps(propsStr: string): Record<string, string> {
|
|
538
|
-
// Strip trailing semicolons — Mermaid tolerates them (e.g. `stroke:#f00;`)
|
|
539
|
-
const cleaned = propsStr.replace(/;\s*$/, '')
|
|
540
|
-
const props: Record<string, string> = {}
|
|
541
|
-
for (const pair of splitTopLevelCommas(cleaned)) {
|
|
542
|
-
const colonIdx = pair.indexOf(':')
|
|
543
|
-
if (colonIdx > 0) {
|
|
544
|
-
const key = pair.slice(0, colonIdx).trim()
|
|
545
|
-
const val = pair.slice(colonIdx + 1).trim()
|
|
546
|
-
if (key && val) {
|
|
547
|
-
props[key] = val
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
return props
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// ============================================================================
|
|
555
|
-
// Flowchart edge line parser
|
|
556
|
-
//
|
|
557
|
-
// Handles chained edges like: A[Label] --> B(Label) -.-> C{Label}
|
|
558
|
-
// Also handles & parallel links: A & B --> C & D
|
|
559
|
-
// ============================================================================
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* Arrow regex — matches all arrow operators with optional labels. Operators
|
|
563
|
-
* are VARIABLE LENGTH (Mermaid lets you lengthen a link to push rank): every
|
|
564
|
-
* shaft accepts extra units —
|
|
565
|
-
* --> ---> ----> solid arrow --- ---- ----- solid line
|
|
566
|
-
* -.-> -..-> dotted arrow -.- -..- dotted line
|
|
567
|
-
* ==> ===> thick arrow === ==== thick line
|
|
568
|
-
* ~~~ ~~~~ invisible link (participates in layout, draws nothing)
|
|
569
|
-
* --o --x o--o … circle / cross endpoint markers (also length-variable)
|
|
570
|
-
* <--> <-.-> <==> bidirectional variants (leading `<`)
|
|
571
|
-
*
|
|
572
|
-
* Alternation order matters (leftmost wins): the dotted/thick/marker forms
|
|
573
|
-
* with explicit terminators MUST precede the bare solid-line `-{3,}`, or a
|
|
574
|
-
* greedy dash run would swallow a marker/arrow prefix and mangle the line.
|
|
575
|
-
*
|
|
576
|
-
* Optional label: -->|label text|
|
|
577
|
-
*/
|
|
578
|
-
const ARROW_REGEX = /^(<)?(~{3,}|-\.+->|-\.+-|={2,}>|={3,}|o-{2,}o|o-{2,}x|x-{2,}o|x-{2,}x|-{2,}[ox]|-{2,}>|-{3,})(?:\|([^|]*)\|)?/
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
* Text-embedded label regex — matches "-- label -->", "-. label .->", "== label ==>"
|
|
582
|
-
* syntax, with variable-length shafts on both the opener and the closer.
|
|
583
|
-
* Tried as fallback when ARROW_REGEX doesn't match.
|
|
584
|
-
*
|
|
585
|
-
* Based on PR #36 by @liuxiaopai-ai (https://github.com/lukilabs/beautiful-mermaid/pull/36)
|
|
586
|
-
*/
|
|
587
|
-
const TEXT_ARROW_REGEX = /^(<)?(-{2,}|-\.+|={2,})\s+(.+?)\s+(-{2,}>|-{3,}|\.+->|-\.+-|={2,}>|={3,})/
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Node shape patterns — ordered from most specific delimiters to least.
|
|
591
|
-
* Multi-char delimiters must be tried before single-char to avoid false matches.
|
|
592
|
-
*/
|
|
593
|
-
const NODE_PATTERNS: Array<{ regex: RegExp; shape: NodeShape }> = [
|
|
594
|
-
// Triple delimiters (must be first)
|
|
595
|
-
{ regex: /^([\w-]+)\(\(\((.+?)\)\)\)/, shape: 'doublecircle' }, // A(((text)))
|
|
596
|
-
|
|
597
|
-
// Double delimiters with mixed brackets
|
|
598
|
-
{ regex: /^([\w-]+)\(\[(.+?)\]\)/, shape: 'stadium' }, // A([text])
|
|
599
|
-
{ regex: /^([\w-]+)\(\((.+?)\)\)/, shape: 'circle' }, // A((text))
|
|
600
|
-
{ regex: /^([\w-]+)\[\[(.+?)\]\]/, shape: 'subroutine' }, // A[[text]]
|
|
601
|
-
{ regex: /^([\w-]+)\[\((.+?)\)\]/, shape: 'cylinder' }, // A[(text)]
|
|
602
|
-
|
|
603
|
-
// Trapezoid + parallelogram variants — must come before plain [text].
|
|
604
|
-
// All four share the [/ or [\ opener and differ only in the closer (\] vs
|
|
605
|
-
// /] vs \]), so labels exclude ']' — a non-greedy (.+?) could otherwise
|
|
606
|
-
// skip past the true closer to a later node's on the same line.
|
|
607
|
-
{ regex: /^([\w-]+)\[\/([^\]]+?)\\\]/, shape: 'trapezoid' }, // A[/text\]
|
|
608
|
-
{ regex: /^([\w-]+)\[\\([^\]]+?)\/\]/, shape: 'trapezoid-alt' }, // A[\text/]
|
|
609
|
-
{ regex: /^([\w-]+)\[\/([^\]]+?)\/\]/, shape: 'lean-r' }, // A[/text/]
|
|
610
|
-
{ regex: /^([\w-]+)\[\\([^\]]+?)\\\]/, shape: 'lean-l' }, // A[\text\]
|
|
611
|
-
|
|
612
|
-
// Asymmetric flag shape
|
|
613
|
-
{ regex: /^([\w-]+)>(.+?)\]/, shape: 'asymmetric' }, // A>text]
|
|
614
|
-
|
|
615
|
-
// Double curly braces (hexagon) — must come before single {text}
|
|
616
|
-
{ regex: /^([\w-]+)\{\{(.+?)\}\}/, shape: 'hexagon' }, // A{{text}}
|
|
617
|
-
|
|
618
|
-
// Single-char delimiters (last — most common, least specific)
|
|
619
|
-
{ regex: /^([\w-]+)\[(.+?)\]/, shape: 'rectangle' }, // A[text]
|
|
620
|
-
{ regex: /^([\w-]+)\((.+?)\)/, shape: 'rounded' }, // A(text)
|
|
621
|
-
{ regex: /^([\w-]+)\{(.+?)\}/, shape: 'diamond' }, // A{text}
|
|
622
|
-
]
|
|
623
|
-
|
|
624
|
-
/** Regex for a bare node reference (just an ID, no shape brackets) */
|
|
625
|
-
const BARE_NODE_REGEX = /^([\w-]+)/
|
|
626
|
-
|
|
627
|
-
function consumeBareNodeId(text: string): { id: string; length: number } | null {
|
|
628
|
-
const whole = text.match(BARE_NODE_REGEX)
|
|
629
|
-
if (!whole) return null
|
|
630
|
-
const max = whole[1]!.length
|
|
631
|
-
let end = 0
|
|
632
|
-
for (let i = 0; i < max; i++) {
|
|
633
|
-
if (i > 0 && startsFlowchartArrow(text.slice(i))) break
|
|
634
|
-
end = i + 1
|
|
635
|
-
}
|
|
636
|
-
return end > 0 ? { id: text.slice(0, end), length: end } : null
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function startsFlowchartArrow(text: string): boolean {
|
|
640
|
-
return ARROW_REGEX.test(text) || TEXT_ARROW_REGEX.test(text)
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function nodePatternSwallowedArrow(text: string, idLength: number): boolean {
|
|
644
|
-
for (let i = 1; i < idLength; i++) {
|
|
645
|
-
if (startsFlowchartArrow(text.slice(i))) return true
|
|
646
|
-
}
|
|
647
|
-
return false
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
const EDGE_ID_PREFIX_REGEX = /^([\w-]+)@\s*(?=(?:<)?(?:~{3,}|-\.+->|-\.+-|={2,}>|={3,}|o-{2,}[ox]|x-{2,}[ox]|-{2,}[ox]|-{2,}>|-{3,}|(?:-{2,}|-\.+|={2,})\s+))/
|
|
651
|
-
|
|
652
|
-
function consumeClassShorthand(text: string): { className: string; length: number } | null {
|
|
653
|
-
if (!text.startsWith(':::')) return null
|
|
654
|
-
const rest = text.slice(3)
|
|
655
|
-
if (!/^[\w]/.test(rest)) return null
|
|
656
|
-
const allowed = rest.match(/^[\w-]+/)
|
|
657
|
-
if (!allowed) return null
|
|
658
|
-
const max = allowed[0].length
|
|
659
|
-
let end = max
|
|
660
|
-
for (let i = 1; i < max; i++) {
|
|
661
|
-
if (startsFlowchartArrow(rest.slice(i))) { end = i; break }
|
|
662
|
-
}
|
|
663
|
-
return { className: rest.slice(0, end), length: 3 + end }
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
/**
|
|
667
|
-
* Parse a line that contains node definitions and edges.
|
|
668
|
-
* Handles chaining: A --> B --> C produces edges A→B and B→C.
|
|
669
|
-
* Handles parallel links: A & B --> C & D produces 4 edges.
|
|
670
|
-
*/
|
|
671
|
-
function parseEdgeLine(
|
|
672
|
-
line: string,
|
|
673
|
-
graph: MermaidGraph,
|
|
674
|
-
subgraphStack: MermaidSubgraph[],
|
|
675
|
-
declaredSubgraphIds: Set<string> = new Set(),
|
|
676
|
-
): void {
|
|
677
|
-
let remaining = line.trim()
|
|
678
|
-
|
|
679
|
-
// Parse the first node group (possibly with & separators)
|
|
680
|
-
const firstGroup = consumeNodeGroup(remaining, graph, subgraphStack, declaredSubgraphIds)
|
|
681
|
-
if (!firstGroup || firstGroup.ids.length === 0) return
|
|
682
|
-
|
|
683
|
-
remaining = firstGroup.remaining.trim()
|
|
684
|
-
let prevGroupIds = firstGroup.ids
|
|
685
|
-
|
|
686
|
-
// Parse arrow + node-group pairs until the line is exhausted
|
|
687
|
-
while (remaining.length > 0) {
|
|
688
|
-
let hasArrowStart: boolean
|
|
689
|
-
let style: EdgeStyle
|
|
690
|
-
let hasArrowEnd: boolean
|
|
691
|
-
let startMarker: EdgeMarker | undefined
|
|
692
|
-
let endMarker: EdgeMarker | undefined
|
|
693
|
-
let edgeLabel: string | undefined
|
|
694
|
-
let length: number | undefined
|
|
695
|
-
|
|
696
|
-
const edgeIdMatch = remaining.match(EDGE_ID_PREFIX_REGEX)
|
|
697
|
-
if (edgeIdMatch) remaining = remaining.slice(edgeIdMatch[0].length).trim()
|
|
698
|
-
|
|
699
|
-
const arrowMatch = remaining.match(ARROW_REGEX)
|
|
700
|
-
if (arrowMatch) {
|
|
701
|
-
const arrowOp = arrowMatch[2]!
|
|
702
|
-
const rawEdgeLabel = arrowMatch[3]?.trim()
|
|
703
|
-
edgeLabel = rawEdgeLabel ? normalizeBrTags(rawEdgeLabel) : undefined
|
|
704
|
-
remaining = remaining.slice(arrowMatch[0].length).trim()
|
|
705
|
-
style = arrowStyleFromOp(arrowOp)
|
|
706
|
-
length = arrowLengthFromOp(arrowOp)
|
|
707
|
-
startMarker = startMarkerForOp(arrowOp, Boolean(arrowMatch[1]))
|
|
708
|
-
endMarker = endMarkerForOp(arrowOp)
|
|
709
|
-
hasArrowStart = startMarker !== undefined
|
|
710
|
-
hasArrowEnd = endMarker !== undefined
|
|
711
|
-
} else {
|
|
712
|
-
// Fallback: text-embedded label syntax (-- Yes -->, -. Maybe .->, == Sure ==>)
|
|
713
|
-
const textMatch = remaining.match(TEXT_ARROW_REGEX)
|
|
714
|
-
if (!textMatch) break
|
|
715
|
-
hasArrowStart = Boolean(textMatch[1])
|
|
716
|
-
const rawLabel = textMatch[3]!.trim()
|
|
717
|
-
edgeLabel = rawLabel ? normalizeBrTags(rawLabel) : undefined
|
|
718
|
-
const openOp = textMatch[2]!
|
|
719
|
-
const closeOp = textMatch[4]!
|
|
720
|
-
remaining = remaining.slice(textMatch[0].length).trim()
|
|
721
|
-
style = textArrowStyleFromOps(openOp, closeOp)
|
|
722
|
-
length = textArrowLengthFromOps(openOp, closeOp)
|
|
723
|
-
hasArrowEnd = closeOp.endsWith('>')
|
|
724
|
-
startMarker = hasArrowStart ? 'arrow' : undefined
|
|
725
|
-
endMarker = hasArrowEnd ? 'arrow' : undefined
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// Parse the next node group
|
|
729
|
-
const nextGroup = consumeNodeGroup(remaining, graph, subgraphStack, declaredSubgraphIds)
|
|
730
|
-
if (!nextGroup || nextGroup.ids.length === 0) break
|
|
731
|
-
|
|
732
|
-
remaining = nextGroup.remaining.trim()
|
|
733
|
-
|
|
734
|
-
// Emit Cartesian product of edges: every source × every target
|
|
735
|
-
for (const sourceId of prevGroupIds) {
|
|
736
|
-
for (const targetId of nextGroup.ids) {
|
|
737
|
-
graph.edges.push({
|
|
738
|
-
source: sourceId,
|
|
739
|
-
target: targetId,
|
|
740
|
-
label: edgeLabel,
|
|
741
|
-
style,
|
|
742
|
-
hasArrowStart,
|
|
743
|
-
hasArrowEnd,
|
|
744
|
-
startMarker,
|
|
745
|
-
endMarker,
|
|
746
|
-
...(length !== undefined ? { length } : {}),
|
|
747
|
-
})
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
prevGroupIds = nextGroup.ids
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
interface ConsumedNodeGroup {
|
|
756
|
-
ids: string[]
|
|
757
|
-
remaining: string
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
* Consume one or more nodes separated by `&`.
|
|
762
|
-
* E.g. "A & B & C --> ..." returns ids: ['A', 'B', 'C']
|
|
763
|
-
*/
|
|
764
|
-
function consumeNodeGroup(
|
|
765
|
-
text: string,
|
|
766
|
-
graph: MermaidGraph,
|
|
767
|
-
subgraphStack: MermaidSubgraph[],
|
|
768
|
-
declaredSubgraphIds: Set<string>,
|
|
769
|
-
): ConsumedNodeGroup | null {
|
|
770
|
-
const first = consumeNode(text, graph, subgraphStack, declaredSubgraphIds)
|
|
771
|
-
if (!first) return null
|
|
772
|
-
|
|
773
|
-
const ids = [first.id]
|
|
774
|
-
let remaining = first.remaining.trim()
|
|
775
|
-
|
|
776
|
-
// Check for & separators
|
|
777
|
-
while (remaining.startsWith('&')) {
|
|
778
|
-
remaining = remaining.slice(1).trim()
|
|
779
|
-
const next = consumeNode(remaining, graph, subgraphStack, declaredSubgraphIds)
|
|
780
|
-
if (!next) break
|
|
781
|
-
ids.push(next.id)
|
|
782
|
-
remaining = next.remaining.trim()
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
return { ids, remaining }
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
interface ConsumedNode {
|
|
789
|
-
id: string
|
|
790
|
-
remaining: string
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function consumeMetadataNode(
|
|
794
|
-
text: string,
|
|
795
|
-
graph: MermaidGraph,
|
|
796
|
-
subgraphStack: MermaidSubgraph[]
|
|
797
|
-
): ConsumedNode | null {
|
|
798
|
-
const start = text.match(/^([\w-]+)@\s*\{/)
|
|
799
|
-
if (!start) return null
|
|
800
|
-
const id = start[1]!
|
|
801
|
-
const objectStart = text.indexOf('{', start[0].indexOf('@'))
|
|
802
|
-
const objectEnd = findMetadataObjectEnd(text, objectStart)
|
|
803
|
-
if (objectEnd < 0) return null
|
|
804
|
-
|
|
805
|
-
const metadata = text.slice(objectStart + 1, objectEnd)
|
|
806
|
-
const label = parseMetadataLabel(metadata)
|
|
807
|
-
// #29 safety floor: we preserve labels/edges for rendering and reserve the
|
|
808
|
-
// expanded Mermaid v11 shape vocabulary for the follow-up modeled support.
|
|
809
|
-
const existing = graph.nodes.get(id)
|
|
810
|
-
if (existing) {
|
|
811
|
-
if (label !== undefined) graph.nodes.set(id, { ...existing, label: normalizeBrTags(label) })
|
|
812
|
-
trackInSubgraph(subgraphStack, id)
|
|
813
|
-
} else {
|
|
814
|
-
registerNode(graph, subgraphStack, { id, label: normalizeBrTags(label ?? id), shape: 'rectangle' })
|
|
815
|
-
}
|
|
816
|
-
return { id, remaining: text.slice(objectEnd + 1) }
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
function findMetadataObjectEnd(text: string, start: number): number {
|
|
820
|
-
if (start < 0 || text[start] !== '{') return -1
|
|
821
|
-
let depth = 0
|
|
822
|
-
let quote: '"' | "'" | null = null
|
|
823
|
-
let escaped = false
|
|
824
|
-
for (let i = start; i < text.length; i++) {
|
|
825
|
-
const ch = text[i]!
|
|
826
|
-
if (escaped) { escaped = false; continue }
|
|
827
|
-
if (ch === '\\') { escaped = true; continue }
|
|
828
|
-
if (quote) {
|
|
829
|
-
if (ch === quote) quote = null
|
|
830
|
-
continue
|
|
831
|
-
}
|
|
832
|
-
if (ch === '"' || ch === "'") { quote = ch; continue }
|
|
833
|
-
if (ch === '{') depth++
|
|
834
|
-
else if (ch === '}') {
|
|
835
|
-
depth--
|
|
836
|
-
if (depth === 0) return i
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
return -1
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function parseMetadataLabel(metadata: string): string | undefined {
|
|
843
|
-
const match = metadata.match(/(?:^|,)\s*label\s*:\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^,}]+)/)
|
|
844
|
-
if (!match) return undefined
|
|
845
|
-
const raw = match[1]!.trim()
|
|
846
|
-
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
|
|
847
|
-
return raw.slice(1, -1).replace(/\\([\\"'])/g, '$1')
|
|
848
|
-
}
|
|
849
|
-
return raw.trim()
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
function consumeQuotedNode(
|
|
853
|
-
text: string,
|
|
854
|
-
graph: MermaidGraph,
|
|
855
|
-
subgraphStack: MermaidSubgraph[]
|
|
856
|
-
): ConsumedNode | null {
|
|
857
|
-
const match = text.match(/^([\w-]+)\["((?:\\.|[^"\\])*)"\]/)
|
|
858
|
-
if (!match) return null
|
|
859
|
-
const id = match[1]!
|
|
860
|
-
const label = normalizeBrTags(match[2]!.replace(/\\(["\\])/g, '$1'))
|
|
861
|
-
registerNode(graph, subgraphStack, { id, label, shape: 'rectangle' })
|
|
862
|
-
return { id, remaining: text.slice(match[0].length) }
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
/**
|
|
866
|
-
* Try to consume a node definition from the start of `text`.
|
|
867
|
-
* If the node has a shape+label (e.g. A[Text]), it's registered in the graph.
|
|
868
|
-
* If it's a bare reference (e.g. A), we look it up or create a default.
|
|
869
|
-
* Also handles ::: class shorthand suffix.
|
|
870
|
-
*/
|
|
871
|
-
function consumeNode(
|
|
872
|
-
text: string,
|
|
873
|
-
graph: MermaidGraph,
|
|
874
|
-
subgraphStack: MermaidSubgraph[],
|
|
875
|
-
declaredSubgraphIds: Set<string>,
|
|
876
|
-
): ConsumedNode | null {
|
|
877
|
-
const metadataNode = consumeMetadataNode(text, graph, subgraphStack)
|
|
878
|
-
if (metadataNode) {
|
|
879
|
-
let remaining = metadataNode.remaining
|
|
880
|
-
const classMatch = consumeClassShorthand(remaining)
|
|
881
|
-
if (classMatch) {
|
|
882
|
-
graph.classAssignments.set(metadataNode.id, classMatch.className)
|
|
883
|
-
remaining = remaining.slice(classMatch.length)
|
|
884
|
-
}
|
|
885
|
-
return { id: metadataNode.id, remaining }
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
const quotedNode = consumeQuotedNode(text, graph, subgraphStack)
|
|
889
|
-
if (quotedNode) return quotedNode
|
|
890
|
-
|
|
891
|
-
let id: string | null = null
|
|
892
|
-
let remaining: string = text
|
|
893
|
-
|
|
894
|
-
// Try each node pattern (shape-qualified)
|
|
895
|
-
for (const { regex, shape } of NODE_PATTERNS) {
|
|
896
|
-
const match = text.match(regex)
|
|
897
|
-
if (match) {
|
|
898
|
-
if (nodePatternSwallowedArrow(text, match[1]!.length)) continue
|
|
899
|
-
id = match[1]!
|
|
900
|
-
const label = normalizeBrTags(match[2]!)
|
|
901
|
-
registerNode(graph, subgraphStack, { id, label, shape })
|
|
902
|
-
remaining = text.slice(match[0].length)
|
|
903
|
-
break
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
// Bare node reference — only register if node doesn't exist yet.
|
|
908
|
-
// If it already exists, do NOT track it in the current subgraph;
|
|
909
|
-
// nodes belong to the subgraph where they're first defined.
|
|
910
|
-
if (id === null) {
|
|
911
|
-
const bare = consumeBareNodeId(text)
|
|
912
|
-
if (bare) {
|
|
913
|
-
id = bare.id
|
|
914
|
-
if (!graph.nodes.has(id) && !declaredSubgraphIds.has(id)) {
|
|
915
|
-
registerNode(graph, subgraphStack, { id, label: id, shape: 'rectangle' })
|
|
916
|
-
}
|
|
917
|
-
remaining = text.slice(bare.length)
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
if (id === null) return null
|
|
922
|
-
|
|
923
|
-
// Check for ::: class shorthand suffix immediately after the node
|
|
924
|
-
const classMatch = consumeClassShorthand(remaining)
|
|
925
|
-
if (classMatch) {
|
|
926
|
-
graph.classAssignments.set(id, classMatch.className)
|
|
927
|
-
remaining = remaining.slice(classMatch.length)
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
return { id, remaining }
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/** Register a node in the graph and track it in the current subgraph */
|
|
934
|
-
function registerNode(
|
|
935
|
-
graph: MermaidGraph,
|
|
936
|
-
subgraphStack: MermaidSubgraph[],
|
|
937
|
-
node: MermaidNode
|
|
938
|
-
): void {
|
|
939
|
-
const isNew = !graph.nodes.has(node.id)
|
|
940
|
-
if (isNew) {
|
|
941
|
-
graph.nodes.set(node.id, node)
|
|
942
|
-
}
|
|
943
|
-
trackInSubgraph(subgraphStack, node.id)
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
/** Add node ID to the innermost subgraph if we're inside one */
|
|
947
|
-
function trackInSubgraph(subgraphStack: MermaidSubgraph[], nodeId: string): void {
|
|
948
|
-
if (subgraphStack.length > 0) {
|
|
949
|
-
const current = subgraphStack[subgraphStack.length - 1]!
|
|
950
|
-
if (!current.nodeIds.includes(nodeId)) {
|
|
951
|
-
current.nodeIds.push(nodeId)
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
/** Map arrow operator string to edge style (ignoring direction/length) */
|
|
957
|
-
function arrowStyleFromOp(op: string): EdgeStyle {
|
|
958
|
-
if (op[0] === '~') return 'invisible'
|
|
959
|
-
if (op.includes('.')) return 'dotted'
|
|
960
|
-
if (op.includes('=')) return 'thick'
|
|
961
|
-
return 'solid'
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
/**
|
|
965
|
-
* Mermaid link length (rank distance): 1 for a base operator, +1 per extra
|
|
966
|
-
* shaft unit. Returns undefined for the base length so base-form edges carry
|
|
967
|
-
* no `length` field and serialize byte-identically.
|
|
968
|
-
*/
|
|
969
|
-
function arrowLengthFromOp(op: string): number | undefined {
|
|
970
|
-
let extra: number
|
|
971
|
-
if (op[0] === '~') {
|
|
972
|
-
extra = op.length - 3 // ~~~ base
|
|
973
|
-
} else if (op.includes('.')) {
|
|
974
|
-
extra = (op.match(/\./g)?.length ?? 1) - 1 // -.-> / -.- base = 1 dot
|
|
975
|
-
} else if (op.includes('=')) {
|
|
976
|
-
const eq = op.match(/=/g)?.length ?? 2
|
|
977
|
-
extra = op.endsWith('>') ? eq - 2 : eq - 3 // ==> base 2, === base 3
|
|
978
|
-
} else {
|
|
979
|
-
const dashes = op.match(/-/g)?.length ?? 2
|
|
980
|
-
const terminated = /[>ox]$/.test(op) || /^[ox]/.test(op)
|
|
981
|
-
extra = terminated ? dashes - 2 : dashes - 3 // --> base 2, --- base 3
|
|
982
|
-
}
|
|
983
|
-
return extra > 0 ? extra + 1 : undefined
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
/** Map text-embedded arrow open/close operators to edge style */
|
|
987
|
-
function textArrowStyleFromOps(openOp: string, closeOp: string): EdgeStyle {
|
|
988
|
-
if (openOp.includes('.') || closeOp.includes('.')) return 'dotted'
|
|
989
|
-
if (openOp.includes('=') || closeOp.includes('=')) return 'thick'
|
|
990
|
-
return 'solid'
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
/**
|
|
994
|
-
* Mermaid's text-embedded label syntax splits the operator around the label
|
|
995
|
-
* (`-- label -->`, `-. label .->`, `== label ==>`). Extra shaft units may be
|
|
996
|
-
* written on either side; serialize them through the single MermaidEdge length
|
|
997
|
-
* field so round-tripping does not collapse rank distance.
|
|
998
|
-
*/
|
|
999
|
-
function textArrowLengthFromOps(openOp: string, closeOp: string): number | undefined {
|
|
1000
|
-
const style = textArrowStyleFromOps(openOp, closeOp)
|
|
1001
|
-
const count = (s: string, ch: string) => (s.match(new RegExp(`\\${ch}`, 'g')) ?? []).length
|
|
1002
|
-
let extraOpen = 0
|
|
1003
|
-
let extraClose = 0
|
|
1004
|
-
if (style === 'dotted') {
|
|
1005
|
-
extraOpen = Math.max(0, count(openOp, '.') - 1)
|
|
1006
|
-
extraClose = Math.max(0, count(closeOp, '.') - 1)
|
|
1007
|
-
} else if (style === 'thick') {
|
|
1008
|
-
extraOpen = Math.max(0, count(openOp, '=') - 2)
|
|
1009
|
-
extraClose = Math.max(0, count(closeOp, '=') - (closeOp.endsWith('>') ? 2 : 3))
|
|
1010
|
-
} else {
|
|
1011
|
-
extraOpen = Math.max(0, count(openOp, '-') - 2)
|
|
1012
|
-
extraClose = Math.max(0, count(closeOp, '-') - (closeOp.endsWith('>') ? 2 : 3))
|
|
1013
|
-
}
|
|
1014
|
-
const extra = Math.max(extraOpen, extraClose)
|
|
1015
|
-
return extra > 0 ? extra + 1 : undefined
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
function startMarkerForOp(op: string, hasLeftAngle: boolean): EdgeMarker | undefined {
|
|
1019
|
-
if (hasLeftAngle) return 'arrow'
|
|
1020
|
-
if (op.startsWith('o')) return 'circle'
|
|
1021
|
-
if (op.startsWith('x')) return 'cross'
|
|
1022
|
-
return undefined
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
function endMarkerForOp(op: string): EdgeMarker | undefined {
|
|
1026
|
-
const last = op[op.length - 1]
|
|
1027
|
-
if (last === '>') return 'arrow'
|
|
1028
|
-
if (last === 'o') return 'circle'
|
|
1029
|
-
if (last === 'x') return 'cross'
|
|
1030
|
-
return undefined
|
|
1031
|
-
}
|