ntk 3.10.2 → 4.1.0

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.
@@ -1,720 +0,0 @@
1
- // Mermaid diagram rendering for the markdown widget (```mermaid fences).
2
- //
3
- // Parsing uses the mermaid package's own grammar (lazily imported, run
4
- // headless — see loadMermaid()); mermaid's *renderer* is the only part
5
- // that needs a browser, so the two most-used diagram types are drawn
6
- // natively instead: **flowcharts** laid out by @dagrejs/dagre and
7
- // **sequence diagrams** with a classic column/row layout. Text is measured
8
- // with the ntk font pipeline and everything draws through the 2d context —
9
- // no SVG round-trip, no browser. Diagram types without a native renderer
10
- // here throw from parseMermaid(), and MarkdownView falls back to a plain
11
- // code block (same contract as invalid math).
12
- //
13
- // const model = await parseMermaid('flowchart LR\n A[Start] --> B{OK?}');
14
- // const box = layoutMermaid(model, { fonts });
15
- // box.draw(ctx, x, y); // box.width / box.height
16
- import dagre from '@dagrejs/dagre';
17
-
18
- const DEFAULT_THEME = {
19
- nodeFill: '#ececff',
20
- nodeBorder: '#9370db',
21
- line: '#555555',
22
- text: '#333333',
23
- labelBg: '#ffffff', // behind edge labels; set to the page background
24
- actorFill: '#ececff',
25
- actorBorder: '#9370db',
26
- noteFill: '#fff5ad',
27
- noteBorder: '#b8b855',
28
- frameBorder: '#999999',
29
- frameLabel: '#666666'
30
- };
31
-
32
- // ---------------------------------------------------------------------------
33
- // parsing: the mermaid package's own grammar, run headless
34
- //
35
- // mermaid's renderer needs a browser (getBBox and friends), but its parsers
36
- // and diagram DBs work in Node once DOMPurify — which mermaid only uses to
37
- // sanitize label HTML for browser injection — is shimmed to the identity.
38
- // Labels here never reach a DOM: they are drawn as plain text.
39
-
40
- let mermaidLoad = null;
41
-
42
- function loadMermaid() {
43
- if (!mermaidLoad) {
44
- mermaidLoad = (async () => {
45
- const DOMPurify = (await import('dompurify')).default;
46
- if (typeof DOMPurify.sanitize !== 'function') {
47
- DOMPurify.sanitize = (s) => String(s);
48
- DOMPurify.addHook = () => {};
49
- }
50
- const mermaid = (await import('mermaid')).default;
51
- mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' });
52
- return mermaid;
53
- })();
54
- }
55
- return mermaidLoad;
56
- }
57
-
58
- function cleanLabel(text) {
59
- return String(text ?? '')
60
- .replace(/<br\s*\/?>/gi, '\n')
61
- .trim();
62
- }
63
-
64
- const SHAPE_MAP = {
65
- square: 'rect',
66
- rect: 'rect',
67
- round: 'round',
68
- rounded: 'round',
69
- stadium: 'stadium',
70
- subroutine: 'subroutine',
71
- cylinder: 'cylinder',
72
- cyl: 'cylinder',
73
- circle: 'circle',
74
- doublecircle: 'circle',
75
- dbl_circ: 'circle',
76
- diamond: 'diamond',
77
- question: 'diamond',
78
- decision: 'diamond',
79
- hexagon: 'hexagon',
80
- hex: 'hexagon'
81
- };
82
-
83
- function normalizeFlowchart(db) {
84
- const nodes = [];
85
- for (const [id, v] of db.getVertices()) {
86
- const kind = v.type ?? v.shape;
87
- nodes.push({ id, shape: SHAPE_MAP[kind] ?? 'rect', label: cleanLabel(v.text ?? id) || id });
88
- }
89
- const edges = db.getEdges().map((e) => ({
90
- from: e.start,
91
- to: e.end,
92
- label: cleanLabel(e.text) || null,
93
- style: e.stroke === 'dotted' ? 'dotted' : e.stroke === 'thick' ? 'thick' : 'solid',
94
- arrow: e.type !== 'arrow_open'
95
- }));
96
- if (nodes.length === 0) throw new Error('flowchart: no nodes');
97
- const dir = db.getDirection?.() ?? 'TB';
98
- const rankdir = { TD: 'TB' }[dir] ?? (['TB', 'BT', 'LR', 'RL'].includes(dir) ? dir : 'TB');
99
- return { type: 'flowchart', rankdir, nodes, edges, warnings: [] };
100
- }
101
-
102
- // sequenceDb LINETYPE values (stable across mermaid 8-11)
103
- const LT = {
104
- SOLID: 0, DOTTED: 1, NOTE: 2, SOLID_CROSS: 3, DOTTED_CROSS: 4,
105
- SOLID_OPEN: 5, DOTTED_OPEN: 6,
106
- LOOP_START: 10, LOOP_END: 11, ALT_START: 12, ALT_ELSE: 13, ALT_END: 14,
107
- OPT_START: 15, OPT_END: 16, ACTIVE_START: 17, ACTIVE_END: 18,
108
- PAR_START: 19, PAR_AND: 20, PAR_END: 21, RECT_START: 22, RECT_END: 23,
109
- SOLID_POINT: 24, DOTTED_POINT: 25, AUTONUMBER: 26,
110
- CRITICAL_START: 27, CRITICAL_OPTION: 28, CRITICAL_END: 29,
111
- BREAK_START: 30, BREAK_END: 31, PAR_OVER_START: 32
112
- };
113
-
114
- const SEQ_LINE = {
115
- [LT.SOLID]: { dotted: false, head: 'arrow' },
116
- [LT.DOTTED]: { dotted: true, head: 'arrow' },
117
- [LT.SOLID_CROSS]: { dotted: false, head: 'cross' },
118
- [LT.DOTTED_CROSS]: { dotted: true, head: 'cross' },
119
- [LT.SOLID_OPEN]: { dotted: false, head: 'none' },
120
- [LT.DOTTED_OPEN]: { dotted: true, head: 'none' },
121
- [LT.SOLID_POINT]: { dotted: false, head: 'open' },
122
- [LT.DOTTED_POINT]: { dotted: true, head: 'open' }
123
- };
124
-
125
- const FRAME_START = {
126
- [LT.LOOP_START]: 'loop', [LT.ALT_START]: 'alt', [LT.OPT_START]: 'opt',
127
- [LT.PAR_START]: 'par', [LT.PAR_OVER_START]: 'par',
128
- [LT.CRITICAL_START]: 'critical', [LT.BREAK_START]: 'break'
129
- };
130
- const FRAME_END = new Set([LT.LOOP_END, LT.ALT_END, LT.OPT_END, LT.PAR_END, LT.CRITICAL_END, LT.BREAK_END]);
131
- const FRAME_DIVIDER = new Set([LT.ALT_ELSE, LT.PAR_AND, LT.CRITICAL_OPTION]);
132
-
133
- function normalizeSequence(db) {
134
- const participants = [];
135
- for (const [id, a] of db.getActors()) {
136
- participants.push({ id, label: cleanLabel(a.description) || id, actor: a.type === 'actor' });
137
- }
138
- if (participants.length === 0) throw new Error('sequence: no participants');
139
-
140
- const items = [];
141
- const warnings = [];
142
- let numbering = db.showSequenceNumbers?.() ? 1 : 0;
143
- const actorId = (v) => (typeof v === 'object' && v !== null ? v.actor : v);
144
-
145
- for (const m of db.getMessages()) {
146
- const t = m.type;
147
- if (t === LT.AUTONUMBER) {
148
- numbering = m.message?.visible === false ? 0 : 1;
149
- continue;
150
- }
151
- if (t === LT.NOTE) {
152
- const pos = m.placement === 0 ? 'left of' : m.placement === 1 ? 'right of' : 'over';
153
- const ids = [actorId(m.from)];
154
- const to = actorId(m.to);
155
- if (to && to !== ids[0]) ids.push(to);
156
- items.push({ kind: 'note', pos, ids, text: cleanLabel(m.message) });
157
- continue;
158
- }
159
- if (t in FRAME_START) {
160
- items.push({ kind: 'frame', frame: FRAME_START[t], text: cleanLabel(m.message ?? '') });
161
- continue;
162
- }
163
- if (FRAME_END.has(t)) {
164
- items.push({ kind: 'frameEnd' });
165
- continue;
166
- }
167
- if (FRAME_DIVIDER.has(t)) {
168
- items.push({ kind: 'divider', text: cleanLabel(m.message ?? '') });
169
- continue;
170
- }
171
- const line = SEQ_LINE[t];
172
- if (line) {
173
- let text = cleanLabel(m.message);
174
- if (numbering) text = `${numbering++}. ${text}`;
175
- items.push({ kind: 'message', from: actorId(m.from), to: actorId(m.to), text, ...line });
176
- continue;
177
- }
178
- if (t !== LT.ACTIVE_START && t !== LT.ACTIVE_END && t !== LT.RECT_START && t !== LT.RECT_END) {
179
- warnings.push(`sequence: ignored message type ${t}`);
180
- }
181
- }
182
- return { type: 'sequence', participants, items, warnings };
183
- }
184
-
185
- /**
186
- * Parse mermaid source with the mermaid package's own parser (loaded
187
- * lazily, headless) and normalize the diagram DB into the model consumed
188
- * by layoutMermaid(). Throws for diagram types without a native renderer
189
- * here (anything but flowchart / sequence).
190
- */
191
- export async function parseMermaid(text) {
192
- const mermaid = await loadMermaid();
193
- const diagram = await mermaid.mermaidAPI.getDiagramFromText(String(text));
194
- const type = diagram.type;
195
- if (/^flowchart/.test(type) || type === 'graph') return normalizeFlowchart(diagram.db);
196
- if (/^sequence/.test(type)) return normalizeSequence(diagram.db);
197
- throw new Error(`mermaid: no native renderer for diagram type "${type}"`);
198
- }
199
-
200
- // ---------------------------------------------------------------------------
201
- // text measurement helpers
202
-
203
- function makeMeasurer(fonts, family, size) {
204
- const style = { family, size, weight: 'normal', style: 'normal' };
205
- style.font = fonts.match(family, style);
206
- const metrics = style.font.metrics(size);
207
- const lineH = Math.ceil((metrics.ascent + metrics.descent) * 1.25);
208
- return {
209
- style,
210
- lineH,
211
- ascent: metrics.ascent,
212
- width: (s) => (s ? fonts.shape(s, style).width : 0),
213
- linesOf: (s) => String(s ?? '').split('\n'),
214
- block(s) {
215
- const lines = this.linesOf(s);
216
- let w = 0;
217
- for (const l of lines) w = Math.max(w, this.width(l));
218
- return { lines, w, h: lines.length * lineH };
219
- }
220
- };
221
- }
222
-
223
- function drawTextBlock(ctx, meas, family, size, color, lines, cx, topY) {
224
- ctx.fillStyle = color;
225
- ctx.font = `${size}px ${family}`;
226
- ctx.textAlign = 'left';
227
- ctx.textBaseline = 'alphabetic';
228
- let y = topY + meas.ascent;
229
- for (const line of lines) {
230
- const w = meas.width(line);
231
- ctx.fillText(line, cx - w / 2, y);
232
- y += meas.lineH;
233
- }
234
- }
235
-
236
- // manual dashes (the 2d context has no setLineDash)
237
- function dashedLine(ctx, x0, y0, x1, y1, dash = 4, gap = 4) {
238
- const dx = x1 - x0;
239
- const dy = y1 - y0;
240
- const len = Math.hypot(dx, dy);
241
- if (!len) return;
242
- const ux = dx / len;
243
- const uy = dy / len;
244
- let t = 0;
245
- ctx.beginPath();
246
- while (t < len) {
247
- const e = Math.min(t + dash, len);
248
- ctx.moveTo(x0 + ux * t, y0 + uy * t);
249
- ctx.lineTo(x0 + ux * e, y0 + uy * e);
250
- t = e + gap;
251
- }
252
- ctx.stroke();
253
- }
254
-
255
- function dashedPolyline(ctx, pts, dash = 4, gap = 4) {
256
- for (let i = 0; i + 1 < pts.length; i++) dashedLine(ctx, pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y, dash, gap);
257
- }
258
-
259
- function arrowHead(ctx, x, y, fromX, fromY, size, color) {
260
- const a = Math.atan2(y - fromY, x - fromX);
261
- const s = size;
262
- ctx.fillStyle = color;
263
- ctx.beginPath();
264
- ctx.moveTo(x, y);
265
- ctx.lineTo(x - s * Math.cos(a - 0.45), y - s * Math.sin(a - 0.45));
266
- ctx.lineTo(x - s * Math.cos(a + 0.45), y - s * Math.sin(a + 0.45));
267
- ctx.closePath();
268
- ctx.fill();
269
- }
270
-
271
- // ---------------------------------------------------------------------------
272
- // flowchart layout + drawing
273
-
274
- function nodeSize(node, meas) {
275
- const { w, h } = meas.block(node.label);
276
- const padX = 14;
277
- const padY = 9;
278
- switch (node.shape) {
279
- case 'diamond':
280
- return { w: w * 1.5 + 28, h: h * 2 + 18 };
281
- case 'circle': {
282
- const d = Math.max(w, h) + 24;
283
- return { w: d, h: d };
284
- }
285
- case 'hexagon':
286
- return { w: w + padX * 2 + h + 12, h: h + padY * 2 };
287
- case 'cylinder':
288
- return { w: w + padX * 2, h: h + padY * 2 + 14 };
289
- case 'subroutine':
290
- return { w: w + padX * 2 + 12, h: h + padY * 2 };
291
- case 'stadium':
292
- return { w: w + padX * 2 + h, h: h + padY * 2 };
293
- default:
294
- return { w: w + padX * 2, h: h + padY * 2 };
295
- }
296
- }
297
-
298
- function nodePath(ctx, node, x, y, w, h) {
299
- // (x, y) is the node center
300
- ctx.beginPath();
301
- switch (node.shape) {
302
- case 'round':
303
- ctx.roundRect(x - w / 2, y - h / 2, w, h, 6);
304
- break;
305
- case 'stadium':
306
- ctx.roundRect(x - w / 2, y - h / 2, w, h, h / 2);
307
- break;
308
- case 'circle':
309
- ctx.arc(x, y, w / 2, 0, Math.PI * 2);
310
- break;
311
- case 'diamond':
312
- ctx.moveTo(x, y - h / 2);
313
- ctx.lineTo(x + w / 2, y);
314
- ctx.lineTo(x, y + h / 2);
315
- ctx.lineTo(x - w / 2, y);
316
- ctx.closePath();
317
- break;
318
- case 'hexagon': {
319
- const c = h / 2;
320
- ctx.moveTo(x - w / 2 + c, y - h / 2);
321
- ctx.lineTo(x + w / 2 - c, y - h / 2);
322
- ctx.lineTo(x + w / 2, y);
323
- ctx.lineTo(x + w / 2 - c, y + h / 2);
324
- ctx.lineTo(x - w / 2 + c, y + h / 2);
325
- ctx.lineTo(x - w / 2, y);
326
- ctx.closePath();
327
- break;
328
- }
329
- default:
330
- ctx.rect(x - w / 2, y - h / 2, w, h);
331
- }
332
- }
333
-
334
- function layoutFlowchart(model, meas, family, size, theme) {
335
- const g = new dagre.graphlib.Graph({ multigraph: true });
336
- // separations follow the font size so shrink-to-fit scales ~linearly
337
- g.setGraph({
338
- rankdir: model.rankdir,
339
- nodesep: Math.round(size * 2.8),
340
- ranksep: Math.round(size * 3),
341
- marginx: 6,
342
- marginy: 6
343
- });
344
- g.setDefaultEdgeLabel(() => ({}));
345
-
346
- for (const node of model.nodes) {
347
- const { w, h } = nodeSize(node, meas);
348
- g.setNode(node.id, { width: w, height: h, node });
349
- }
350
- model.edges.forEach((edge, i) => {
351
- const attrs = { edge };
352
- if (edge.label) {
353
- const { w, h } = meas.block(edge.label);
354
- attrs.width = w + 8;
355
- attrs.height = h + 4;
356
- attrs.labelpos = 'c';
357
- }
358
- g.setEdge(edge.from, edge.to, attrs, `e${i}`);
359
- });
360
-
361
- dagre.layout(g);
362
- const gw = Math.ceil(g.graph().width ?? 0);
363
- const gh = Math.ceil(g.graph().height ?? 0);
364
-
365
- return {
366
- type: 'flowchart',
367
- width: gw,
368
- height: gh,
369
- warnings: model.warnings,
370
- draw(ctx, ox = 0, oy = 0) {
371
- // edges under nodes
372
- for (const e of g.edges()) {
373
- const info = g.edge(e);
374
- const edge = info.edge;
375
- const pts = info.points.map((p) => ({ x: p.x + ox, y: p.y + oy }));
376
- ctx.strokeStyle = theme.line;
377
- ctx.lineWidth = edge.style === 'thick' ? 2.6 : 1.4;
378
- if (edge.style === 'dotted') {
379
- dashedPolyline(ctx, pts, 3, 3.5);
380
- } else {
381
- ctx.beginPath();
382
- ctx.moveTo(pts[0].x, pts[0].y);
383
- if (pts.length === 2) {
384
- ctx.lineTo(pts[1].x, pts[1].y);
385
- } else {
386
- for (let i = 1; i < pts.length - 1; i++) {
387
- const mx = (pts[i].x + pts[i + 1].x) / 2;
388
- const my = (pts[i].y + pts[i + 1].y) / 2;
389
- if (i === pts.length - 2) ctx.quadraticCurveTo(pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y);
390
- else ctx.quadraticCurveTo(pts[i].x, pts[i].y, mx, my);
391
- }
392
- }
393
- ctx.stroke();
394
- }
395
- if (edge.arrow) {
396
- const tip = pts[pts.length - 1];
397
- const before = pts[pts.length - 2] ?? pts[0];
398
- arrowHead(ctx, tip.x, tip.y, before.x, before.y, 7 + (edge.style === 'thick' ? 2 : 0), theme.line);
399
- }
400
- if (edge.label) {
401
- const { lines, w, h } = meas.block(edge.label);
402
- const lx = info.x + ox;
403
- const ly = info.y + oy;
404
- ctx.fillStyle = theme.labelBg;
405
- ctx.fillRect(Math.round(lx - w / 2 - 3), Math.round(ly - h / 2 - 1), Math.ceil(w + 6), Math.ceil(h + 2));
406
- drawTextBlock(ctx, meas, family, size, theme.text, lines, lx, ly - h / 2);
407
- }
408
- }
409
- // nodes
410
- for (const id of g.nodes()) {
411
- const info = g.node(id);
412
- const node = info.node;
413
- const x = info.x + ox;
414
- const y = info.y + oy;
415
- ctx.fillStyle = theme.nodeFill;
416
- ctx.strokeStyle = theme.nodeBorder;
417
- ctx.lineWidth = 1.3;
418
- if (node.shape === 'cylinder') {
419
- const capH = 10;
420
- const w = info.width;
421
- const h = info.height;
422
- const top = y - h / 2;
423
- ctx.beginPath();
424
- ctx.rect(x - w / 2, top + capH / 2, w, h - capH);
425
- ctx.fill();
426
- ctx.beginPath();
427
- ctx.moveTo(x - w / 2, top + capH / 2);
428
- ctx.lineTo(x - w / 2, top + h - capH / 2);
429
- ctx.moveTo(x + w / 2, top + capH / 2);
430
- ctx.lineTo(x + w / 2, top + h - capH / 2);
431
- ctx.stroke();
432
- ctx.beginPath();
433
- ctx.ellipse(x, top + h - capH / 2, w / 2, capH / 2, 0, 0, Math.PI * 2);
434
- ctx.fill();
435
- ctx.stroke();
436
- ctx.beginPath();
437
- ctx.ellipse(x, top + capH / 2, w / 2, capH / 2, 0, 0, Math.PI * 2);
438
- ctx.fill();
439
- ctx.stroke();
440
- } else {
441
- nodePath(ctx, node, x, y, info.width, info.height);
442
- ctx.fill();
443
- nodePath(ctx, node, x, y, info.width, info.height);
444
- ctx.stroke();
445
- if (node.shape === 'subroutine') {
446
- ctx.beginPath();
447
- ctx.moveTo(x - info.width / 2 + 5, y - info.height / 2);
448
- ctx.lineTo(x - info.width / 2 + 5, y + info.height / 2);
449
- ctx.moveTo(x + info.width / 2 - 5, y - info.height / 2);
450
- ctx.lineTo(x + info.width / 2 - 5, y + info.height / 2);
451
- ctx.stroke();
452
- }
453
- }
454
- const { lines, h } = meas.block(node.label);
455
- drawTextBlock(ctx, meas, family, size, theme.text, lines, x, y - h / 2);
456
- }
457
- }
458
- };
459
- }
460
-
461
- // ---------------------------------------------------------------------------
462
- // sequence layout + drawing
463
-
464
- function layoutSequence(model, meas, family, size, theme) {
465
- const P = model.participants;
466
- const idx = new Map(P.map((p, i) => [p.id, i]));
467
- const actorPadX = 12;
468
- const actorH = meas.lineH + 16;
469
- const boxW = P.map((p) => Math.max(60, meas.width(p.label) + actorPadX * 2));
470
-
471
- // gaps between adjacent lifelines, widened until every message/note fits
472
- const gaps = new Array(Math.max(0, P.length - 1)).fill(110);
473
- const need = (a, b, w) => {
474
- if (a === b) return;
475
- const [lo, hi] = a < b ? [a, b] : [b, a];
476
- let sum = 0;
477
- for (let i = lo; i < hi; i++) sum += gaps[i];
478
- if (sum < w) {
479
- const add = (w - sum) / (hi - lo);
480
- for (let i = lo; i < hi; i++) gaps[i] += add;
481
- }
482
- };
483
- for (const item of model.items) {
484
- if (item.kind === 'message' && item.from !== item.to) {
485
- need(idx.get(item.from), idx.get(item.to), meas.width(item.text) + 36);
486
- } else if (item.kind === 'note' && item.pos === 'over' && item.ids.length > 1) {
487
- need(idx.get(item.ids[0]), idx.get(item.ids[item.ids.length - 1]), meas.block(item.text).w + 30);
488
- }
489
- }
490
- // lifeline x positions
491
- const cx = [];
492
- let x = Math.max(20, boxW[0] / 2 + 8);
493
- for (let i = 0; i < P.length; i++) {
494
- if (i > 0) x += Math.max(gaps[i - 1], boxW[i - 1] / 2 + boxW[i] / 2 + 20);
495
- cx.push(x);
496
- }
497
-
498
- // vertical layout pass: assign a y to every item
499
- const rows = [];
500
- const frames = [];
501
- const openFrames = [];
502
- let y = actorH + 14;
503
- const frameInset = () => openFrames.length * 8;
504
- for (const item of model.items) {
505
- if (item.kind === 'frame') {
506
- openFrames.push({ item, top: y, dividers: [] });
507
- y += meas.lineH + 12;
508
- continue;
509
- }
510
- if (item.kind === 'divider') {
511
- const f = openFrames[openFrames.length - 1];
512
- y += 8;
513
- if (f) f.dividers.push({ y, text: item.text });
514
- y += meas.lineH + 6;
515
- continue;
516
- }
517
- if (item.kind === 'frameEnd') {
518
- const f = openFrames.pop();
519
- if (f) {
520
- y += 8;
521
- frames.push({ ...f, bottom: y });
522
- y += 10;
523
- }
524
- continue;
525
- }
526
- if (item.kind === 'note') {
527
- const { w, h } = { w: meas.block(item.text).w, h: meas.block(item.text).h };
528
- rows.push({ item, y, noteW: w + 20, noteH: h + 10 });
529
- y += h + 24;
530
- continue;
531
- }
532
- // message
533
- const self = item.from === item.to;
534
- rows.push({ item, y });
535
- y += meas.lineH + (self ? 26 : 16);
536
- }
537
- while (openFrames.length) {
538
- const f = openFrames.pop();
539
- frames.push({ ...f, bottom: y });
540
- y += 10;
541
- }
542
- const bodyBottom = y + 6;
543
- const height = Math.ceil(bodyBottom + actorH);
544
- const width = Math.ceil(cx[cx.length - 1] + Math.max(24, boxW[P.length - 1] / 2 + 10));
545
-
546
- const drawActor = (ctx, i, ox, top) => {
547
- const w = boxW[i];
548
- const h = actorH;
549
- const x0 = ox + cx[i] - w / 2;
550
- ctx.fillStyle = theme.actorFill;
551
- ctx.strokeStyle = theme.actorBorder;
552
- ctx.lineWidth = 1.3;
553
- ctx.beginPath();
554
- ctx.roundRect(x0, top, w, h, 4);
555
- ctx.fill();
556
- ctx.beginPath();
557
- ctx.roundRect(x0, top, w, h, 4);
558
- ctx.stroke();
559
- drawTextBlock(ctx, meas, family, size, theme.text, [P[i].label], ox + cx[i], top + (h - meas.lineH) / 2);
560
- };
561
-
562
- return {
563
- type: 'sequence',
564
- width,
565
- height,
566
- warnings: model.warnings,
567
- draw(ctx, ox = 0, oy = 0) {
568
- // lifelines
569
- ctx.strokeStyle = theme.frameBorder;
570
- ctx.lineWidth = 1;
571
- for (let i = 0; i < P.length; i++) {
572
- dashedLine(ctx, cx[i] + ox, oy + actorH, cx[i] + ox, oy + bodyBottom, 4, 4);
573
- }
574
- // frames (behind messages, above lifelines)
575
- for (const f of frames) {
576
- const x0 = ox + 8;
577
- const x1 = ox + width - 8;
578
- ctx.strokeStyle = theme.frameBorder;
579
- ctx.lineWidth = 1;
580
- ctx.beginPath();
581
- ctx.rect(x0, oy + f.top, x1 - x0, f.bottom - f.top);
582
- ctx.stroke();
583
- const tag = f.item.frame;
584
- const tagW = meas.width(tag) + 14;
585
- ctx.fillStyle = theme.actorFill;
586
- ctx.fillRect(x0, oy + f.top, tagW, meas.lineH + 6);
587
- ctx.strokeRect(x0, oy + f.top, tagW, meas.lineH + 6);
588
- drawTextBlock(ctx, meas, family, size, theme.text, [tag], x0 + tagW / 2, oy + f.top + 3);
589
- if (f.item.text) {
590
- drawTextBlock(ctx, meas, family, size, theme.frameLabel, [`[${f.item.text}]`], x0 + tagW + meas.width(`[${f.item.text}]`) / 2 + 10, oy + f.top + 3);
591
- }
592
- for (const d of f.dividers) {
593
- dashedLine(ctx, x0, oy + d.y, x1, oy + d.y, 5, 4);
594
- if (d.text) {
595
- drawTextBlock(ctx, meas, family, size, theme.frameLabel, [`[${d.text}]`], x0 + 30 + meas.width(`[${d.text}]`) / 2, oy + d.y + 3);
596
- }
597
- }
598
- }
599
- // messages and notes
600
- for (const row of rows) {
601
- const item = row.item;
602
- if (item.kind === 'note') {
603
- const ids = item.ids.map((id) => idx.get(id));
604
- let nx;
605
- if (item.pos === 'over') {
606
- const lo = Math.min(...ids);
607
- const hi = Math.max(...ids);
608
- nx = (cx[lo] + cx[hi]) / 2;
609
- } else if (item.pos === 'right of') {
610
- nx = cx[ids[0]] + row.noteW / 2 + 12;
611
- } else {
612
- nx = cx[ids[0]] - row.noteW / 2 - 12;
613
- }
614
- const { lines } = meas.block(item.text);
615
- ctx.fillStyle = theme.noteFill;
616
- ctx.strokeStyle = theme.noteBorder;
617
- ctx.lineWidth = 1;
618
- ctx.fillRect(ox + nx - row.noteW / 2, oy + row.y, row.noteW, row.noteH);
619
- ctx.strokeRect(ox + nx - row.noteW / 2, oy + row.y, row.noteW, row.noteH);
620
- drawTextBlock(ctx, meas, family, size, theme.text, lines, ox + nx, oy + row.y + 5);
621
- continue;
622
- }
623
- // message
624
- const a = idx.get(item.from);
625
- const b = idx.get(item.to);
626
- const lineY = oy + row.y + meas.lineH + 4;
627
- ctx.strokeStyle = theme.line;
628
- ctx.lineWidth = 1.3;
629
- if (a === b) {
630
- // self message: out, down, back
631
- const x0 = ox + cx[a];
632
- const loopW = 28;
633
- const pts = [
634
- { x: x0, y: lineY },
635
- { x: x0 + loopW, y: lineY },
636
- { x: x0 + loopW, y: lineY + 14 },
637
- { x: x0, y: lineY + 14 }
638
- ];
639
- if (item.dotted) dashedPolyline(ctx, pts, 4, 3);
640
- else {
641
- ctx.beginPath();
642
- ctx.moveTo(pts[0].x, pts[0].y);
643
- for (const p of pts.slice(1)) ctx.lineTo(p.x, p.y);
644
- ctx.stroke();
645
- }
646
- arrowHead(ctx, x0 + 2, lineY + 14, x0 + loopW, lineY + 14, 7, theme.line);
647
- drawTextBlock(ctx, meas, family, size, theme.text, meas.linesOf(item.text), x0 + loopW + 8 + meas.width(item.text) / 2, oy + row.y + 6);
648
- continue;
649
- }
650
- const x0 = ox + cx[a];
651
- const x1 = ox + cx[b];
652
- if (item.dotted) dashedLine(ctx, x0, lineY, x1, lineY, 5, 4);
653
- else {
654
- ctx.beginPath();
655
- ctx.moveTo(x0, lineY);
656
- ctx.lineTo(x1, lineY);
657
- ctx.stroke();
658
- }
659
- const dir = x1 > x0 ? 1 : -1;
660
- if (item.head === 'cross') {
661
- const cxx = x1 - dir * 6;
662
- ctx.beginPath();
663
- ctx.moveTo(cxx - 5, lineY - 5);
664
- ctx.lineTo(cxx + 5, lineY + 5);
665
- ctx.moveTo(cxx - 5, lineY + 5);
666
- ctx.lineTo(cxx + 5, lineY - 5);
667
- ctx.stroke();
668
- } else if (item.head !== 'none') {
669
- arrowHead(ctx, x1, lineY, x0, lineY, 8, theme.line);
670
- }
671
- if (item.text) {
672
- drawTextBlock(ctx, meas, family, size, theme.text, [item.text], (x0 + x1) / 2, oy + row.y);
673
- }
674
- }
675
- // actors, top and bottom
676
- for (let i = 0; i < P.length; i++) {
677
- drawActor(ctx, i, ox, oy);
678
- drawActor(ctx, i, ox, oy + bodyBottom);
679
- }
680
- }
681
- };
682
- }
683
-
684
- // ---------------------------------------------------------------------------
685
-
686
- /**
687
- * Lay out a mermaid diagram without drawing it. Returns a box:
688
- * `{ width, height, type, warnings, draw(ctx, x, y) }`.
689
- *
690
- * Supported: `flowchart`/`graph` (TB/TD/BT/LR/RL, all common node shapes,
691
- * solid/dotted/thick edges with labels, chains and `&` fans) and
692
- * `sequenceDiagram` (participants/actors, all arrow kinds, notes,
693
- * loop/opt/alt/par frames with else/and dividers, autonumber).
694
- * Anything else throws — callers fall back to a code block.
695
- *
696
- * @param {object} model parsed diagram from `await parseMermaid(text)`
697
- * @param {object} options { fonts, size = 13, family = 'sans-serif', theme,
698
- * maxWidth } — when maxWidth is given and the diagram comes out wider,
699
- * it is laid out again at a proportionally smaller font size (floor 8px)
700
- */
701
- export function layoutMermaid(model, { fonts, size = 13, family = 'sans-serif', theme = {}, maxWidth } = {}) {
702
- if (!fonts) throw new Error('layoutMermaid: fonts (FontManager) is required');
703
- if (!model || (model.type !== 'flowchart' && model.type !== 'sequence')) {
704
- throw new Error('layoutMermaid: expected a model from parseMermaid()');
705
- }
706
- const th = { ...DEFAULT_THEME, ...theme };
707
- const attempt = (px) => {
708
- const meas = makeMeasurer(fonts, family, px);
709
- return model.type === 'flowchart'
710
- ? layoutFlowchart(model, meas, family, px, th)
711
- : layoutSequence(model, meas, family, px, th);
712
- };
713
- let box = attempt(size);
714
- let px = size;
715
- for (let i = 0; maxWidth && box.width > maxWidth && px > 8 && i < 4; i++) {
716
- px = Math.max(8, Math.floor((px * maxWidth) / box.width));
717
- box = attempt(px);
718
- }
719
- return box;
720
- }