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
|
@@ -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
|
-
}
|