@tldraw/mermaid 4.6.0-internal.c7df3c92455a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist-cjs/blueprint.js +17 -0
  2. package/dist-cjs/blueprint.js.map +7 -0
  3. package/dist-cjs/colors.js +173 -0
  4. package/dist-cjs/colors.js.map +7 -0
  5. package/dist-cjs/createMermaidDiagram.js +144 -0
  6. package/dist-cjs/createMermaidDiagram.js.map +7 -0
  7. package/dist-cjs/flowchartDiagram.js +202 -0
  8. package/dist-cjs/flowchartDiagram.js.map +7 -0
  9. package/dist-cjs/index.d.ts +114 -0
  10. package/dist-cjs/index.js +34 -0
  11. package/dist-cjs/index.js.map +7 -0
  12. package/dist-cjs/renderBlueprint.js +314 -0
  13. package/dist-cjs/renderBlueprint.js.map +7 -0
  14. package/dist-cjs/sequenceDiagram.js +686 -0
  15. package/dist-cjs/sequenceDiagram.js.map +7 -0
  16. package/dist-cjs/stateDiagram.js +373 -0
  17. package/dist-cjs/stateDiagram.js.map +7 -0
  18. package/dist-cjs/svgParsing.js +187 -0
  19. package/dist-cjs/svgParsing.js.map +7 -0
  20. package/dist-cjs/utils.js +75 -0
  21. package/dist-cjs/utils.js.map +7 -0
  22. package/dist-esm/blueprint.mjs +1 -0
  23. package/dist-esm/blueprint.mjs.map +7 -0
  24. package/dist-esm/colors.mjs +153 -0
  25. package/dist-esm/colors.mjs.map +7 -0
  26. package/dist-esm/createMermaidDiagram.mjs +114 -0
  27. package/dist-esm/createMermaidDiagram.mjs.map +7 -0
  28. package/dist-esm/flowchartDiagram.mjs +188 -0
  29. package/dist-esm/flowchartDiagram.mjs.map +7 -0
  30. package/dist-esm/index.d.mts +114 -0
  31. package/dist-esm/index.mjs +14 -0
  32. package/dist-esm/index.mjs.map +7 -0
  33. package/dist-esm/renderBlueprint.mjs +298 -0
  34. package/dist-esm/renderBlueprint.mjs.map +7 -0
  35. package/dist-esm/sequenceDiagram.mjs +666 -0
  36. package/dist-esm/sequenceDiagram.mjs.map +7 -0
  37. package/dist-esm/stateDiagram.mjs +359 -0
  38. package/dist-esm/stateDiagram.mjs.map +7 -0
  39. package/dist-esm/svgParsing.mjs +167 -0
  40. package/dist-esm/svgParsing.mjs.map +7 -0
  41. package/dist-esm/utils.mjs +55 -0
  42. package/dist-esm/utils.mjs.map +7 -0
  43. package/package.json +62 -0
  44. package/src/blueprint.ts +75 -0
  45. package/src/colors.ts +215 -0
  46. package/src/createMermaidDiagram.test.ts +31 -0
  47. package/src/createMermaidDiagram.ts +155 -0
  48. package/src/flowchartDiagram.ts +232 -0
  49. package/src/index.ts +18 -0
  50. package/src/mermaidDiagrams.test.ts +880 -0
  51. package/src/renderBlueprint.ts +373 -0
  52. package/src/sequenceDiagram.ts +851 -0
  53. package/src/stateDiagram.ts +477 -0
  54. package/src/svgParsing.ts +240 -0
  55. package/src/utils.ts +73 -0
@@ -0,0 +1,155 @@
1
+ let nextMermaidId = 0
2
+
3
+ import mermaid from 'mermaid'
4
+ import type { FlowDB } from 'mermaid/dist/diagrams/flowchart/flowDb.d.ts'
5
+ import type { FlowEdge, FlowSubGraph, FlowVertex } from 'mermaid/dist/diagrams/flowchart/types.js'
6
+ import type { SequenceDB } from 'mermaid/dist/diagrams/sequence/sequenceDb.d.ts'
7
+ import type { StateDB } from 'mermaid/dist/diagrams/state/stateDb.d.ts'
8
+ import { Editor } from 'tldraw'
9
+ import { flowchartToBlueprint, parseFlowchartLayout } from './flowchartDiagram'
10
+ import { BlueprintRenderingOptions, renderBlueprint } from './renderBlueprint'
11
+ import { countSequenceEvents, parseSequenceLayout, sequenceToBlueprint } from './sequenceDiagram'
12
+ import { parseStateDiagramLayout, stateToBlueprint } from './stateDiagram'
13
+
14
+ /** @public */
15
+ export class MermaidDiagramError extends Error {
16
+ constructor(
17
+ public diagramType: string,
18
+ public type: 'parse' | 'unsupported'
19
+ ) {
20
+ super(`mermaid diagram error: ${diagramType}`)
21
+ this.name = 'MermaidDiagramError'
22
+ }
23
+ }
24
+
25
+ // Inflate the font size so Mermaid's layout engine allocates larger nodes,
26
+ // compensating for tldraw's hand-drawn font being wider than Mermaid's default.
27
+ const FONT_INFLATE = 1.4
28
+
29
+ const MERMAID_CONFIG = {
30
+ startOnLoad: false,
31
+ flowchart: { nodeSpacing: 80, rankSpacing: 80, padding: 20 },
32
+ state: { nodeSpacing: 80, rankSpacing: 80, padding: 20 },
33
+ sequence: { actorMargin: 50, noteMargin: 20 },
34
+ themeVariables: { fontSize: `${18 * FONT_INFLATE}px` },
35
+ }
36
+
37
+ /** @public */
38
+ export interface MermaidDiagramOptions {
39
+ mermaidConfig?: Record<string, any>
40
+ blueprintRender?: BlueprintRenderingOptions
41
+ onUnsupportedDiagram?(svg: string): Promise<void>
42
+ }
43
+
44
+ /**
45
+ * Parse mermaid text and create tldraw shapes for supported diagram types.
46
+ * Returns the SVG string for supported diagrams, or `null` when the diagram type
47
+ * is unsupported (after calling `onUnsupportedDiagram` if provided).
48
+ * Throws {@link MermaidDiagramError} if parsing fails.
49
+ * @public
50
+ */
51
+ export async function createMermaidDiagram(
52
+ editor: Editor,
53
+ text: string,
54
+ options: MermaidDiagramOptions = {}
55
+ ): Promise<void> {
56
+ mermaid.initialize({
57
+ ...MERMAID_CONFIG,
58
+ ...(options.mermaidConfig ?? {}),
59
+ flowchart: { ...MERMAID_CONFIG.flowchart, ...options.mermaidConfig?.flowchart },
60
+ state: { ...MERMAID_CONFIG.state, ...options.mermaidConfig?.state },
61
+ sequence: { ...MERMAID_CONFIG.sequence, ...options.mermaidConfig?.sequence },
62
+ themeVariables: { ...MERMAID_CONFIG.themeVariables, ...options.mermaidConfig?.themeVariables },
63
+ })
64
+
65
+ const parsedResult = await mermaid.parse(text, { suppressErrors: true })
66
+
67
+ if (!parsedResult) {
68
+ throw new MermaidDiagramError('not a mermaid diagram', 'parse')
69
+ }
70
+
71
+ const offscreen = document.createElement('div')
72
+ offscreen.style.position = 'absolute'
73
+ offscreen.style.left = '-9999px'
74
+ offscreen.style.top = '-9999px'
75
+ offscreen.style.overflow = 'hidden'
76
+ document.body.appendChild(offscreen)
77
+
78
+ try {
79
+ const parsedSvg = (await mermaid.render(`mermaid-${nextMermaidId++}`, text, offscreen)).svg
80
+
81
+ // Reuse the live SVG that mermaid.render() already mounted into the
82
+ // offscreen container. This avoids a second DOM mount and ensures
83
+ // getBBox() works for every diagram type (state diagrams in particular
84
+ // lack explicit dimension attributes and rely on live layout).
85
+ let liveSvg = offscreen.querySelector('svg')
86
+
87
+ if (!liveSvg) {
88
+ offscreen.innerHTML = parsedSvg
89
+ liveSvg = offscreen.querySelector('svg')
90
+ if (!liveSvg) {
91
+ throw new MermaidDiagramError(parsedResult.diagramType, 'parse')
92
+ }
93
+ }
94
+
95
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
96
+ const diagramResult = await mermaid.mermaidAPI.getDiagramFromText(text)
97
+
98
+ let blueprint
99
+ switch (parsedResult.diagramType) {
100
+ case 'flowchart-v2': {
101
+ const db = diagramResult.db as FlowDB
102
+ const vertices = db.getVertices() as Map<string, FlowVertex>
103
+ const edges = db.getEdges() as FlowEdge[]
104
+ const subGraphs = db.getSubGraphs() as FlowSubGraph[]
105
+ const classes = db.getClasses()
106
+ const layout = parseFlowchartLayout(liveSvg)
107
+ blueprint = flowchartToBlueprint(layout, vertices, edges, subGraphs, classes)
108
+ break
109
+ }
110
+ case 'sequence': {
111
+ const db = diagramResult.db as SequenceDB
112
+ const actors = db.getActors()
113
+ const actorKeys = db.getActorKeys()
114
+ const messages = db.getMessages()
115
+ const layout = parseSequenceLayout(liveSvg, actorKeys.length, countSequenceEvents(messages))
116
+ blueprint = sequenceToBlueprint(
117
+ layout,
118
+ actors,
119
+ actorKeys,
120
+ messages,
121
+ db.getCreatedActors(),
122
+ db.getDestroyedActors()
123
+ )
124
+ break
125
+ }
126
+ case 'state':
127
+ case 'stateDiagram': {
128
+ const db = diagramResult.db as StateDB
129
+ const states = db.getStates()
130
+ const relations = db.getRelations()
131
+ const classes = db.getClasses()
132
+ const layout = parseStateDiagramLayout(liveSvg)
133
+ blueprint = stateToBlueprint(layout, states, relations, classes)
134
+ break
135
+ }
136
+ default:
137
+ if (options.onUnsupportedDiagram) {
138
+ await options.onUnsupportedDiagram(parsedSvg)
139
+ } else {
140
+ throw new MermaidDiagramError(parsedResult.diagramType, 'unsupported')
141
+ }
142
+ break
143
+ }
144
+
145
+ if (blueprint) {
146
+ renderBlueprint(editor, blueprint, options.blueprintRender)
147
+ }
148
+ } catch (e) {
149
+ if (e instanceof MermaidDiagramError) throw e
150
+ console.error(e)
151
+ throw new MermaidDiagramError(parsedResult.diagramType, 'parse')
152
+ } finally {
153
+ offscreen.remove()
154
+ }
155
+ }
@@ -0,0 +1,232 @@
1
+ import type {
2
+ FlowClass,
3
+ FlowEdge,
4
+ FlowSubGraph,
5
+ FlowVertex,
6
+ } from 'mermaid/dist/diagrams/flowchart/types.js'
7
+ import { TLArrowShapeArrowheadStyle, TLDefaultDashStyle, TLGeoShape } from 'tldraw'
8
+ import type {
9
+ DiagramMermaidBlueprint,
10
+ MermaidBlueprintEdge,
11
+ MermaidBlueprintGeoNode,
12
+ } from './blueprint'
13
+ import { buildClassDefColorMap, parseCssStyles, parseNodeInlineColor } from './colors'
14
+ import {
15
+ buildNodeCentersFromSvg,
16
+ parseAllEdgePointsFromSvg,
17
+ parseClustersFromSvg,
18
+ type ParsedDiagramLayout,
19
+ parseNodesFromSvg,
20
+ scaleLayout,
21
+ } from './svgParsing'
22
+ import { getArrowBend, LAYOUT_SCALE, orderTopDown } from './utils'
23
+
24
+ function mapEdgeTypeToArrowhead(type: string | undefined): TLArrowShapeArrowheadStyle {
25
+ if (!type) return 'arrow'
26
+
27
+ if (type.includes('point')) return 'arrow'
28
+ if (type.includes('circle')) return 'dot'
29
+ if (type.includes('cross')) return 'bar'
30
+ if (type.includes('open')) return 'none'
31
+
32
+ return 'arrow'
33
+ }
34
+
35
+ function mapFlowShapeTypeToGeo(type: string | undefined): TLGeoShape['props']['geo'] {
36
+ switch (type) {
37
+ case 'diamond':
38
+ return 'diamond'
39
+ case 'ellipse':
40
+ case 'circle':
41
+ case 'doublecircle':
42
+ case 'stadium':
43
+ case 'cylinder':
44
+ return 'ellipse'
45
+ case 'hexagon':
46
+ return 'hexagon'
47
+ case 'trapezoid':
48
+ // TODO: implement inv_trapezoid in SDK
49
+ case 'inv_trapezoid':
50
+ return 'trapezoid'
51
+ case 'lean_right':
52
+ return 'rhombus'
53
+ case 'lean_left':
54
+ return 'rhombus-2'
55
+ case 'square':
56
+ case 'rect':
57
+ case 'round':
58
+ case 'subroutine':
59
+ default:
60
+ return 'rectangle'
61
+ }
62
+ }
63
+
64
+ function mapEdgeStrokeToDash(stroke: string | undefined): TLDefaultDashStyle {
65
+ if (!stroke) return 'solid'
66
+ if (stroke === 'dotted') return 'dotted'
67
+ return 'solid'
68
+ }
69
+
70
+ const FRAME_TOP_PAD = 14
71
+
72
+ function buildHierarchy(subGraphs: FlowSubGraph[]) {
73
+ const subGraphIds = new Set(subGraphs.map((subGraph) => subGraph.id))
74
+ const nodeToSubGraph = new Map<string, string>()
75
+ const subGraphParent = new Map<string, string>()
76
+ for (const subGraph of subGraphs) {
77
+ for (const nodeId of subGraph.nodes) {
78
+ if (subGraphIds.has(nodeId)) {
79
+ subGraphParent.set(nodeId, subGraph.id)
80
+ } else if (!nodeToSubGraph.has(nodeId)) {
81
+ nodeToSubGraph.set(nodeId, subGraph.id)
82
+ }
83
+ }
84
+ }
85
+ return { nodeToSubGraph, subGraphParent }
86
+ }
87
+
88
+ /** Parse flowchart-specific SVG layout data for use by {@link flowchartToBlueprint}. */
89
+ export function parseFlowchartLayout(root: Element): ParsedDiagramLayout {
90
+ const nodes = parseNodesFromSvg(root, '.node', (domId) => {
91
+ const match = domId.match(/^flowchart-(.+)-\d+$/)
92
+ return match ? match[1] : domId
93
+ })
94
+ const clusters = parseClustersFromSvg(root, '.cluster')
95
+ const edges = parseAllEdgePointsFromSvg(root, (dataId) => {
96
+ const match = dataId.match(/^L_(.+)_([^_]+)_\d+$/)
97
+ return match ? { start: match[1], end: match[2] } : null
98
+ })
99
+ scaleLayout(nodes, clusters, edges, LAYOUT_SCALE)
100
+ return { nodes, clusters, edges }
101
+ }
102
+
103
+ /** Convert a parsed Mermaid flowchart into a tldraw blueprint of nodes and edges. */
104
+ export function flowchartToBlueprint(
105
+ layout: ParsedDiagramLayout,
106
+ vertices: Map<string, FlowVertex>,
107
+ edges: FlowEdge[],
108
+ subGraphs?: FlowSubGraph[],
109
+ classDefs?: Map<string, FlowClass>
110
+ ): DiagramMermaidBlueprint {
111
+ const nodeColorMap = classDefs ? buildClassDefColorMap(classDefs, vertices) : new Map()
112
+ const { nodes: svgNodes, clusters: svgClusters, edges: svgEdges } = layout
113
+ const nodeCenters = buildNodeCentersFromSvg(svgNodes, svgClusters)
114
+
115
+ const allSubGraphs = subGraphs || []
116
+ const { nodeToSubGraph, subGraphParent } = buildHierarchy(allSubGraphs)
117
+
118
+ const nodes: MermaidBlueprintGeoNode[] = []
119
+ const blueprintEdges: MermaidBlueprintEdge[] = []
120
+
121
+ // Frames for subgraphs
122
+ for (const subGraph of orderTopDown(
123
+ allSubGraphs,
124
+ (subGraph) => subGraph.id,
125
+ (subGraph) => subGraphParent.get(subGraph.id)
126
+ )) {
127
+ const cluster = svgClusters.get(subGraph.id)
128
+ if (!cluster) continue
129
+
130
+ nodes.push({
131
+ id: subGraph.id,
132
+ x: cluster.topLeft.x,
133
+ y: cluster.topLeft.y - FRAME_TOP_PAD,
134
+ w: cluster.width,
135
+ h: cluster.height + FRAME_TOP_PAD,
136
+ geo: 'rectangle',
137
+ parentId: subGraphParent.get(subGraph.id),
138
+ label: subGraph.title || subGraph.id,
139
+ fill: 'semi',
140
+ color: 'black',
141
+ dash: 'draw',
142
+ size: 's',
143
+ align: 'middle',
144
+ verticalAlign: 'start',
145
+ })
146
+ }
147
+
148
+ // Node shapes
149
+ for (const [id, vertex] of vertices) {
150
+ const svgNode = svgNodes.get(id)
151
+ if (!svgNode) continue
152
+
153
+ const geo = mapFlowShapeTypeToGeo(vertex.type)
154
+ const colors = nodeColorMap.get(id) ?? parseNodeInlineColor(vertex.styles)
155
+
156
+ let { width: w, height: h } = svgNode
157
+ if (vertex.type === 'circle' || vertex.type === 'doublecircle') {
158
+ w = h = Math.max(w, h)
159
+ }
160
+
161
+ nodes.push({
162
+ id,
163
+ x: svgNode.center.x - w / 2,
164
+ y: svgNode.center.y - h / 2,
165
+ w,
166
+ h,
167
+ geo,
168
+ parentId: nodeToSubGraph.get(id),
169
+ label: vertex.text || undefined,
170
+ ...(colors?.fillColor && { fill: 'solid' as const }),
171
+ ...(colors && { color: colors.strokeColor ?? colors.fillColor }),
172
+ align: 'middle',
173
+ verticalAlign: 'middle',
174
+ size: 'm',
175
+ } satisfies MermaidBlueprintGeoNode)
176
+ }
177
+
178
+ // Edges: match DB edges to SVG edges by proximity, compute bends
179
+ const claimed = new Set<number>()
180
+ for (const edge of edges) {
181
+ const startCenter = nodeCenters.get(edge.start)
182
+ const endCenter = nodeCenters.get(edge.end)
183
+
184
+ let bend = 0
185
+ if (startCenter && endCenter) {
186
+ let bestIndex = -1
187
+ let bestDist = Infinity
188
+ for (let i = 0; i < svgEdges.length; i++) {
189
+ if (claimed.has(i) || svgEdges[i].points.length < 2) continue
190
+
191
+ const points = svgEdges[i].points
192
+ const distance =
193
+ Math.hypot(points[0].x - startCenter.x, points[0].y - startCenter.y) +
194
+ Math.hypot(
195
+ points[points.length - 1].x - endCenter.x,
196
+ points[points.length - 1].y - endCenter.y
197
+ )
198
+ if (distance < bestDist) {
199
+ bestDist = distance
200
+ bestIndex = i
201
+ }
202
+ }
203
+ if (bestIndex >= 0) {
204
+ claimed.add(bestIndex)
205
+ bend = getArrowBend(svgEdges[bestIndex])
206
+ }
207
+ }
208
+
209
+ const cssOverrides = edge.style ? parseCssStyles(edge.style) : undefined
210
+ const arrowheadEnd = mapEdgeTypeToArrowhead(edge.type)
211
+ const dash = cssOverrides?.dashOverride ?? mapEdgeStrokeToDash(edge.stroke)
212
+ const size = cssOverrides?.sizeOverride ?? (edge.stroke === 'thick' ? 'l' : 's')
213
+
214
+ blueprintEdges.push({
215
+ startNodeId: edge.start,
216
+ endNodeId: edge.end,
217
+ label: edge.text,
218
+ bend,
219
+ arrowheadEnd,
220
+ arrowheadStart: edge.type?.includes('double_arrow') ? arrowheadEnd : undefined,
221
+ dash,
222
+ size,
223
+ color: cssOverrides?.color,
224
+ })
225
+ }
226
+
227
+ const nodeIds = new Set(nodes.map((n) => n.id))
228
+ const validEdges = blueprintEdges.filter(
229
+ (e) => nodeIds.has(e.startNodeId) && nodeIds.has(e.endNodeId)
230
+ )
231
+ return { nodes, edges: validEdges }
232
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { registerTldrawLibraryVersion } from '@tldraw/utils'
2
+
3
+ export type {
4
+ DiagramMermaidBlueprint,
5
+ MermaidBlueprintEdge,
6
+ MermaidBlueprintGeoNode,
7
+ MermaidBlueprintLineNode,
8
+ } from './blueprint'
9
+ export { createMermaidDiagram, MermaidDiagramError } from './createMermaidDiagram'
10
+ export type { MermaidDiagramOptions } from './createMermaidDiagram'
11
+ export { renderBlueprint } from './renderBlueprint'
12
+ export type { BlueprintRenderingOptions } from './renderBlueprint'
13
+
14
+ registerTldrawLibraryVersion(
15
+ (globalThis as any).TLDRAW_LIBRARY_NAME,
16
+ (globalThis as any).TLDRAW_LIBRARY_VERSION,
17
+ (globalThis as any).TLDRAW_LIBRARY_MODULES
18
+ )