@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.
- package/dist-cjs/blueprint.js +17 -0
- package/dist-cjs/blueprint.js.map +7 -0
- package/dist-cjs/colors.js +173 -0
- package/dist-cjs/colors.js.map +7 -0
- package/dist-cjs/createMermaidDiagram.js +144 -0
- package/dist-cjs/createMermaidDiagram.js.map +7 -0
- package/dist-cjs/flowchartDiagram.js +202 -0
- package/dist-cjs/flowchartDiagram.js.map +7 -0
- package/dist-cjs/index.d.ts +114 -0
- package/dist-cjs/index.js +34 -0
- package/dist-cjs/index.js.map +7 -0
- package/dist-cjs/renderBlueprint.js +314 -0
- package/dist-cjs/renderBlueprint.js.map +7 -0
- package/dist-cjs/sequenceDiagram.js +686 -0
- package/dist-cjs/sequenceDiagram.js.map +7 -0
- package/dist-cjs/stateDiagram.js +373 -0
- package/dist-cjs/stateDiagram.js.map +7 -0
- package/dist-cjs/svgParsing.js +187 -0
- package/dist-cjs/svgParsing.js.map +7 -0
- package/dist-cjs/utils.js +75 -0
- package/dist-cjs/utils.js.map +7 -0
- package/dist-esm/blueprint.mjs +1 -0
- package/dist-esm/blueprint.mjs.map +7 -0
- package/dist-esm/colors.mjs +153 -0
- package/dist-esm/colors.mjs.map +7 -0
- package/dist-esm/createMermaidDiagram.mjs +114 -0
- package/dist-esm/createMermaidDiagram.mjs.map +7 -0
- package/dist-esm/flowchartDiagram.mjs +188 -0
- package/dist-esm/flowchartDiagram.mjs.map +7 -0
- package/dist-esm/index.d.mts +114 -0
- package/dist-esm/index.mjs +14 -0
- package/dist-esm/index.mjs.map +7 -0
- package/dist-esm/renderBlueprint.mjs +298 -0
- package/dist-esm/renderBlueprint.mjs.map +7 -0
- package/dist-esm/sequenceDiagram.mjs +666 -0
- package/dist-esm/sequenceDiagram.mjs.map +7 -0
- package/dist-esm/stateDiagram.mjs +359 -0
- package/dist-esm/stateDiagram.mjs.map +7 -0
- package/dist-esm/svgParsing.mjs +167 -0
- package/dist-esm/svgParsing.mjs.map +7 -0
- package/dist-esm/utils.mjs +55 -0
- package/dist-esm/utils.mjs.map +7 -0
- package/package.json +62 -0
- package/src/blueprint.ts +75 -0
- package/src/colors.ts +215 -0
- package/src/createMermaidDiagram.test.ts +31 -0
- package/src/createMermaidDiagram.ts +155 -0
- package/src/flowchartDiagram.ts +232 -0
- package/src/index.ts +18 -0
- package/src/mermaidDiagrams.test.ts +880 -0
- package/src/renderBlueprint.ts +373 -0
- package/src/sequenceDiagram.ts +851 -0
- package/src/stateDiagram.ts +477 -0
- package/src/svgParsing.ts +240 -0
- package/src/utils.ts +73 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createShapeId,
|
|
3
|
+
Editor,
|
|
4
|
+
IndexKey,
|
|
5
|
+
TLGeoShape,
|
|
6
|
+
TLLineShape,
|
|
7
|
+
TLShapeId,
|
|
8
|
+
toRichText,
|
|
9
|
+
Vec,
|
|
10
|
+
} from 'tldraw'
|
|
11
|
+
import type {
|
|
12
|
+
DiagramMermaidBlueprint,
|
|
13
|
+
MermaidBlueprintEdge,
|
|
14
|
+
MermaidBlueprintGeoNode,
|
|
15
|
+
MermaidBlueprintLineNode,
|
|
16
|
+
} from './blueprint'
|
|
17
|
+
import { orderTopDown, sanitizeDiagramText } from './utils'
|
|
18
|
+
|
|
19
|
+
/** @public */
|
|
20
|
+
export interface BlueprintRenderingOptions {
|
|
21
|
+
centerOnPosition?: boolean
|
|
22
|
+
position?: { x: number; y: number }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const defaultBlueprintRenderingOptions = {
|
|
26
|
+
centerOnPosition: true,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @public */
|
|
30
|
+
export function renderBlueprint(
|
|
31
|
+
editor: Editor,
|
|
32
|
+
blueprint: DiagramMermaidBlueprint,
|
|
33
|
+
opts?: BlueprintRenderingOptions
|
|
34
|
+
) {
|
|
35
|
+
const options = { ...defaultBlueprintRenderingOptions, ...(opts || {}) }
|
|
36
|
+
const { nodes, edges, lines } = blueprint
|
|
37
|
+
|
|
38
|
+
const bounds = computeBlueprintBounds(nodes, lines)
|
|
39
|
+
const center = options.position
|
|
40
|
+
? options.position
|
|
41
|
+
: editor.user.getIsPasteAtCursorMode()
|
|
42
|
+
? editor.inputs.getCurrentPagePoint()
|
|
43
|
+
: editor.getViewportPageBounds().center
|
|
44
|
+
const offsetX = options.centerOnPosition
|
|
45
|
+
? center.x - (bounds.maxX + bounds.minX) / 2
|
|
46
|
+
: center.x - bounds.minX
|
|
47
|
+
const offsetY = options.centerOnPosition
|
|
48
|
+
? center.y - (bounds.maxY + bounds.minY) / 2
|
|
49
|
+
: center.y - bounds.minY
|
|
50
|
+
|
|
51
|
+
const ordered = orderTopDown(
|
|
52
|
+
nodes,
|
|
53
|
+
(n) => n.id,
|
|
54
|
+
(n) => n.parentId
|
|
55
|
+
)
|
|
56
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]))
|
|
57
|
+
|
|
58
|
+
const shapeIds = new Map<string, TLShapeId>()
|
|
59
|
+
|
|
60
|
+
// Lines first so nodes render on top (z-order = creation order in tldraw)
|
|
61
|
+
if (lines) {
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
const lineId = createShapeId()
|
|
64
|
+
shapeIds.set(line.id, lineId)
|
|
65
|
+
editor.createShape<TLLineShape>({
|
|
66
|
+
id: lineId,
|
|
67
|
+
type: 'line',
|
|
68
|
+
x: offsetX + line.x,
|
|
69
|
+
y: offsetY + line.y,
|
|
70
|
+
props: {
|
|
71
|
+
dash: line.dash ?? 'solid',
|
|
72
|
+
size: line.size ?? 's',
|
|
73
|
+
color: line.color ?? 'black',
|
|
74
|
+
spline: 'line',
|
|
75
|
+
points: {
|
|
76
|
+
a1: { id: 'a1', index: 'a1' as IndexKey, x: 0, y: 0 },
|
|
77
|
+
a2: { id: 'a2', index: 'a2' as IndexKey, x: line.endX ?? 0, y: line.endY },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const node of ordered) {
|
|
85
|
+
const shapeId = createShapeId()
|
|
86
|
+
shapeIds.set(node.id, shapeId)
|
|
87
|
+
|
|
88
|
+
const parent = node.parentId ? nodeById.get(node.parentId) : undefined
|
|
89
|
+
const parentShapeId = node.parentId ? shapeIds.get(node.parentId) : undefined
|
|
90
|
+
|
|
91
|
+
const absoluteX = offsetX + node.x
|
|
92
|
+
const absoluteY = offsetY + node.y
|
|
93
|
+
const x = parent ? absoluteX - (offsetX + parent.x) : absoluteX
|
|
94
|
+
const y = parent ? absoluteY - (offsetY + parent.y) : absoluteY
|
|
95
|
+
|
|
96
|
+
editor.createShape<TLGeoShape>({
|
|
97
|
+
id: shapeId,
|
|
98
|
+
type: 'geo',
|
|
99
|
+
x,
|
|
100
|
+
y,
|
|
101
|
+
parentId: parentShapeId,
|
|
102
|
+
props: {
|
|
103
|
+
geo: node.geo,
|
|
104
|
+
w: node.w,
|
|
105
|
+
h: node.h,
|
|
106
|
+
fill: node.fill ?? 'none',
|
|
107
|
+
color: node.color ?? 'black',
|
|
108
|
+
dash: node.dash ?? 'draw',
|
|
109
|
+
size: node.size ?? 'm',
|
|
110
|
+
...(node.label && { richText: toRichText(sanitizeDiagramText(node.label)) }),
|
|
111
|
+
...(node.align && { align: node.align }),
|
|
112
|
+
...(node.verticalAlign && { verticalAlign: node.verticalAlign }),
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const arrowIds: TLShapeId[] = []
|
|
118
|
+
for (const edge of edges) {
|
|
119
|
+
const arrowId = createArrowFromEdge(editor, edge, shapeIds)
|
|
120
|
+
if (arrowId) arrowIds.push(arrowId)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Create sub-groups and track which shape IDs are consumed by a group
|
|
124
|
+
const groupedIds = new Set<TLShapeId>()
|
|
125
|
+
const subGroupIds: TLShapeId[] = []
|
|
126
|
+
if (blueprint.groups) {
|
|
127
|
+
for (const group of blueprint.groups) {
|
|
128
|
+
const members: TLShapeId[] = []
|
|
129
|
+
for (const blueprintId of group) {
|
|
130
|
+
const memberShapeId = shapeIds.get(blueprintId)
|
|
131
|
+
if (memberShapeId) {
|
|
132
|
+
members.push(memberShapeId)
|
|
133
|
+
groupedIds.add(memberShapeId)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (members.length > 1) {
|
|
137
|
+
editor.groupShapes(members)
|
|
138
|
+
const first = editor.getShape(members[0])
|
|
139
|
+
if (first && first.parentId !== editor.getCurrentPageId()) {
|
|
140
|
+
subGroupIds.push(first.parentId as TLShapeId)
|
|
141
|
+
} else {
|
|
142
|
+
subGroupIds.push(members[0])
|
|
143
|
+
}
|
|
144
|
+
} else if (members.length === 1) {
|
|
145
|
+
subGroupIds.push(members[0])
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Collect ungrouped top-level IDs
|
|
151
|
+
const topLevelIds: TLShapeId[] = [...subGroupIds]
|
|
152
|
+
for (const node of nodes) {
|
|
153
|
+
if (!node.parentId) {
|
|
154
|
+
const nodeShapeId = shapeIds.get(node.id)
|
|
155
|
+
if (nodeShapeId && !groupedIds.has(nodeShapeId)) topLevelIds.push(nodeShapeId)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (lines) {
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
const lineShapeId = shapeIds.get(line.id)
|
|
161
|
+
if (lineShapeId && !groupedIds.has(lineShapeId)) topLevelIds.push(lineShapeId)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
topLevelIds.push(...arrowIds)
|
|
165
|
+
|
|
166
|
+
let rootShapeId: TLShapeId | undefined
|
|
167
|
+
if (topLevelIds.length > 1) {
|
|
168
|
+
editor.groupShapes(topLevelIds)
|
|
169
|
+
const first = editor.getShape(topLevelIds[0])
|
|
170
|
+
if (first && first.parentId !== editor.getCurrentPageId()) {
|
|
171
|
+
rootShapeId = first.parentId as TLShapeId
|
|
172
|
+
}
|
|
173
|
+
} else if (topLevelIds.length === 1) {
|
|
174
|
+
rootShapeId = topLevelIds[0]
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (rootShapeId) {
|
|
178
|
+
const actualBounds = editor.getShapePageBounds(rootShapeId)
|
|
179
|
+
if (actualBounds) {
|
|
180
|
+
const desiredX = options.centerOnPosition ? center.x - actualBounds.w / 2 : center.x
|
|
181
|
+
const desiredY = options.centerOnPosition ? center.y - actualBounds.h / 2 : center.y
|
|
182
|
+
const dx = desiredX - actualBounds.x
|
|
183
|
+
const dy = desiredY - actualBounds.y
|
|
184
|
+
if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
|
|
185
|
+
const shape = editor.getShape(rootShapeId)!
|
|
186
|
+
editor.updateShape({
|
|
187
|
+
id: rootShapeId,
|
|
188
|
+
type: shape.type,
|
|
189
|
+
x: shape.x + dx,
|
|
190
|
+
y: shape.y + dy,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function makeArrowBinding(
|
|
198
|
+
arrowId: TLShapeId,
|
|
199
|
+
targetId: TLShapeId,
|
|
200
|
+
terminal: 'start' | 'end',
|
|
201
|
+
anchor: { x: number; y: number },
|
|
202
|
+
isExact: boolean,
|
|
203
|
+
isPrecise: boolean
|
|
204
|
+
) {
|
|
205
|
+
return {
|
|
206
|
+
fromId: arrowId,
|
|
207
|
+
toId: targetId,
|
|
208
|
+
type: 'arrow' as const,
|
|
209
|
+
props: { terminal, normalizedAnchor: anchor, isExact, isPrecise },
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function createArrowFromEdge(
|
|
214
|
+
editor: Editor,
|
|
215
|
+
edge: MermaidBlueprintEdge,
|
|
216
|
+
shapeIds: Map<string, TLShapeId>
|
|
217
|
+
): TLShapeId | undefined {
|
|
218
|
+
const startShapeId = shapeIds.get(edge.startNodeId)
|
|
219
|
+
const endShapeId = shapeIds.get(edge.endNodeId)
|
|
220
|
+
if (!startShapeId || !endShapeId) return undefined
|
|
221
|
+
|
|
222
|
+
const startBounds = editor.getShapePageBounds(startShapeId)
|
|
223
|
+
const endBounds = editor.getShapePageBounds(endShapeId)
|
|
224
|
+
if (!startBounds || !endBounds) return undefined
|
|
225
|
+
|
|
226
|
+
const arrowId = createShapeId()
|
|
227
|
+
const isSelfLoop = startShapeId === endShapeId
|
|
228
|
+
const hasPreciseAnchors = edge.anchorStartY !== undefined || edge.anchorEndY !== undefined
|
|
229
|
+
|
|
230
|
+
let labelText = edge.label
|
|
231
|
+
if (edge.decoration?.type === 'autonumber') {
|
|
232
|
+
const num = edge.decoration.value
|
|
233
|
+
labelText = labelText ? `${num} ${labelText}` : num
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const baseProps = {
|
|
237
|
+
dash: edge.dash ?? ('solid' as const),
|
|
238
|
+
size: edge.size ?? ('s' as const),
|
|
239
|
+
arrowheadEnd: edge.arrowheadEnd ?? ('arrow' as const),
|
|
240
|
+
...(edge.arrowheadStart && { arrowheadStart: edge.arrowheadStart }),
|
|
241
|
+
color: edge.color ?? ('black' as const),
|
|
242
|
+
...(labelText && { richText: toRichText(sanitizeDiagramText(labelText)) }),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (hasPreciseAnchors) {
|
|
246
|
+
const startAnchorY = edge.anchorStartY ?? 0.5
|
|
247
|
+
const endAnchorY = edge.anchorEndY ?? 0.5
|
|
248
|
+
const exactStart = edge.isExact ?? true
|
|
249
|
+
const preciseStart = edge.isPrecise ?? true
|
|
250
|
+
const exactEnd = edge.isExactEnd ?? exactStart
|
|
251
|
+
const preciseEnd = edge.isPreciseEnd ?? preciseStart
|
|
252
|
+
|
|
253
|
+
const startPoint = {
|
|
254
|
+
x: startBounds.x + startBounds.w * 0.5,
|
|
255
|
+
y: startBounds.y + startBounds.h * startAnchorY,
|
|
256
|
+
}
|
|
257
|
+
const endPoint = {
|
|
258
|
+
x: endBounds.x + endBounds.w * 0.5,
|
|
259
|
+
y: endBounds.y + endBounds.h * endAnchorY,
|
|
260
|
+
}
|
|
261
|
+
const origin = {
|
|
262
|
+
x: Math.min(startPoint.x, endPoint.x),
|
|
263
|
+
y: Math.min(startPoint.y, endPoint.y),
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
editor.run(() => {
|
|
267
|
+
editor.createShape({
|
|
268
|
+
id: arrowId,
|
|
269
|
+
type: 'arrow',
|
|
270
|
+
x: origin.x,
|
|
271
|
+
y: origin.y,
|
|
272
|
+
props: {
|
|
273
|
+
...baseProps,
|
|
274
|
+
start: { x: startPoint.x - origin.x, y: startPoint.y - origin.y },
|
|
275
|
+
end: { x: endPoint.x - origin.x, y: endPoint.y - origin.y },
|
|
276
|
+
bend: edge.bend,
|
|
277
|
+
},
|
|
278
|
+
})
|
|
279
|
+
editor.createBindings([
|
|
280
|
+
makeArrowBinding(
|
|
281
|
+
arrowId,
|
|
282
|
+
startShapeId,
|
|
283
|
+
'start',
|
|
284
|
+
{ x: 0.5, y: startAnchorY },
|
|
285
|
+
exactStart,
|
|
286
|
+
preciseStart
|
|
287
|
+
),
|
|
288
|
+
makeArrowBinding(
|
|
289
|
+
arrowId,
|
|
290
|
+
endShapeId,
|
|
291
|
+
'end',
|
|
292
|
+
{ x: 0.5, y: endAnchorY },
|
|
293
|
+
exactEnd,
|
|
294
|
+
preciseEnd
|
|
295
|
+
),
|
|
296
|
+
])
|
|
297
|
+
})
|
|
298
|
+
return arrowId
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (isSelfLoop) {
|
|
302
|
+
editor.run(() => {
|
|
303
|
+
editor.createShape({
|
|
304
|
+
id: arrowId,
|
|
305
|
+
type: 'arrow',
|
|
306
|
+
x: startBounds.x,
|
|
307
|
+
y: startBounds.y,
|
|
308
|
+
props: {
|
|
309
|
+
...baseProps,
|
|
310
|
+
start: { x: startBounds.w / 2, y: 0 },
|
|
311
|
+
end: { x: startBounds.w, y: startBounds.h / 2 },
|
|
312
|
+
bend: -80,
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
editor.createBindings([
|
|
316
|
+
makeArrowBinding(arrowId, startShapeId, 'start', { x: 0.9, y: 0.5 }, false, false),
|
|
317
|
+
makeArrowBinding(arrowId, endShapeId, 'end', { x: 0.85, y: 0.8 }, false, false),
|
|
318
|
+
])
|
|
319
|
+
})
|
|
320
|
+
return arrowId
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const startCenter = startBounds.center
|
|
324
|
+
const endCenter = endBounds.center
|
|
325
|
+
const arrowOrigin = Vec.Min(startCenter, endCenter)
|
|
326
|
+
|
|
327
|
+
editor.run(() => {
|
|
328
|
+
editor.createShape({
|
|
329
|
+
id: arrowId,
|
|
330
|
+
type: 'arrow',
|
|
331
|
+
x: arrowOrigin.x,
|
|
332
|
+
y: arrowOrigin.y,
|
|
333
|
+
props: {
|
|
334
|
+
...baseProps,
|
|
335
|
+
start: { x: startCenter.x - arrowOrigin.x, y: startCenter.y - arrowOrigin.y },
|
|
336
|
+
end: { x: endCenter.x - arrowOrigin.x, y: endCenter.y - arrowOrigin.y },
|
|
337
|
+
bend: edge.bend,
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
editor.createBindings([
|
|
341
|
+
makeArrowBinding(arrowId, startShapeId, 'start', { x: 0.5, y: 0.5 }, false, false),
|
|
342
|
+
makeArrowBinding(arrowId, endShapeId, 'end', { x: 0.5, y: 0.5 }, false, false),
|
|
343
|
+
])
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
return arrowId
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function computeBlueprintBounds(
|
|
350
|
+
nodes: MermaidBlueprintGeoNode[],
|
|
351
|
+
lines?: MermaidBlueprintLineNode[]
|
|
352
|
+
): { minX: number; minY: number; maxX: number; maxY: number } {
|
|
353
|
+
let minX = Infinity,
|
|
354
|
+
minY = Infinity,
|
|
355
|
+
maxX = -Infinity,
|
|
356
|
+
maxY = -Infinity
|
|
357
|
+
for (const node of nodes) {
|
|
358
|
+
if (node.parentId) continue
|
|
359
|
+
minX = Math.min(minX, node.x)
|
|
360
|
+
minY = Math.min(minY, node.y)
|
|
361
|
+
maxX = Math.max(maxX, node.x + node.w)
|
|
362
|
+
maxY = Math.max(maxY, node.y + node.h)
|
|
363
|
+
}
|
|
364
|
+
if (lines) {
|
|
365
|
+
for (const line of lines) {
|
|
366
|
+
minX = Math.min(minX, line.x)
|
|
367
|
+
minY = Math.min(minY, line.y)
|
|
368
|
+
maxX = Math.max(maxX, line.x)
|
|
369
|
+
maxY = Math.max(maxY, line.y + line.endY)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return { minX, minY, maxX, maxY }
|
|
373
|
+
}
|