@mermaid-js/diagram-sequence 13.0.0-alpha.0 → 13.0.0-alpha.1

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 (85) hide show
  1. package/dist/ascii/draw.d.ts +16 -0
  2. package/dist/ascii/draw.js +391 -0
  3. package/dist/ascii/symbols.d.ts +4 -0
  4. package/dist/ascii/symbols.js +9 -0
  5. package/dist/ascii/warnings.d.ts +28 -0
  6. package/dist/ascii/warnings.js +141 -0
  7. package/dist/ascii.d.ts +22 -0
  8. package/dist/ascii.js +34 -0
  9. package/dist/facet/completions.d.ts +3 -0
  10. package/dist/facet/completions.js +282 -0
  11. package/dist/facet/hover.d.ts +8 -0
  12. package/dist/facet/hover.js +107 -0
  13. package/dist/facet/tables.d.ts +38 -0
  14. package/dist/facet/tables.js +153 -0
  15. package/dist/generated/categories.d.ts +6 -0
  16. package/dist/generated/categories.js +92 -0
  17. package/dist/generated/docs.d.ts +5 -0
  18. package/dist/generated/docs.js +73 -0
  19. package/dist/index.d.ts +18 -0
  20. package/dist/index.js +4 -0
  21. package/dist/labels.d.ts +6 -0
  22. package/dist/labels.js +20 -0
  23. package/dist/language.d.ts +16 -0
  24. package/dist/language.js +79 -0
  25. package/dist/layout/bounds.d.ts +31 -0
  26. package/dist/layout/bounds.js +46 -0
  27. package/dist/layout/columns.d.ts +44 -0
  28. package/dist/layout/columns.js +207 -0
  29. package/dist/layout/env.d.ts +95 -0
  30. package/dist/layout/env.js +173 -0
  31. package/dist/layout/fixtures.d.ts +15 -0
  32. package/dist/layout/fixtures.js +197 -0
  33. package/dist/layout/geometry.d.ts +38 -0
  34. package/dist/layout/geometry.js +200 -0
  35. package/dist/layout/index.d.ts +10 -0
  36. package/dist/layout/index.js +8 -0
  37. package/dist/layout/invariants.d.ts +15 -0
  38. package/dist/layout/invariants.js +212 -0
  39. package/dist/layout/layout.d.ts +26 -0
  40. package/dist/layout/layout.js +314 -0
  41. package/dist/layout/metrics.d.ts +196 -0
  42. package/dist/layout/metrics.js +250 -0
  43. package/dist/layout/rows.d.ts +64 -0
  44. package/dist/layout/rows.js +487 -0
  45. package/dist/layout/types.d.ts +154 -0
  46. package/dist/layout/types.js +1 -0
  47. package/dist/model.d.ts +165 -0
  48. package/dist/model.js +47 -0
  49. package/dist/parser/arrows.d.ts +6 -0
  50. package/dist/parser/arrows.js +32 -0
  51. package/dist/parser/ast.d.ts +130 -0
  52. package/dist/parser/ast.js +1 -0
  53. package/dist/parser/builder.d.ts +104 -0
  54. package/dist/parser/builder.js +238 -0
  55. package/dist/parser/colors.d.ts +4 -0
  56. package/dist/parser/colors.js +28 -0
  57. package/dist/parser/detect.d.ts +2 -0
  58. package/dist/parser/detect.js +8 -0
  59. package/dist/parser/index.d.ts +18 -0
  60. package/dist/parser/index.js +29 -0
  61. package/dist/parser/lexer.d.ts +6 -0
  62. package/dist/parser/lexer.js +701 -0
  63. package/dist/parser/normalise.d.ts +54 -0
  64. package/dist/parser/normalise.js +217 -0
  65. package/dist/parser/parser.d.ts +14 -0
  66. package/dist/parser/parser.js +738 -0
  67. package/dist/parser/rules.d.ts +47 -0
  68. package/dist/parser/rules.js +126 -0
  69. package/dist/parser/tables.d.ts +72 -0
  70. package/dist/parser/tables.js +179 -0
  71. package/dist/parser/tokens.d.ts +104 -0
  72. package/dist/parser/tokens.js +193 -0
  73. package/dist/plugin.d.ts +22 -0
  74. package/dist/plugin.js +24 -0
  75. package/dist/render/arrows.d.ts +24 -0
  76. package/dist/render/arrows.js +283 -0
  77. package/dist/render/index.d.ts +2 -0
  78. package/dist/render/index.js +2 -0
  79. package/dist/render/paint.d.ts +73 -0
  80. package/dist/render/paint.js +92 -0
  81. package/dist/render/render.d.ts +8 -0
  82. package/dist/render/render.js +345 -0
  83. package/dist/render/symbols.d.ts +22 -0
  84. package/dist/render/symbols.js +237 -0
  85. package/package.json +2 -2
@@ -0,0 +1,16 @@
1
+ import { type GlyphSet } from '@mermaid-js/core/ascii';
2
+ import type { SequenceLayout } from '../layout/index.ts';
3
+ /** How the lines of a label sit in its box: `messageAlign` / `noteAlign`, else centred. */
4
+ export type Align = 'left' | 'center' | 'right';
5
+ export interface DrawOptions {
6
+ /** The front-matter title, for a diagram with no `title` statement of its own. */
7
+ readonly title?: string;
8
+ readonly messageAlign?: Align;
9
+ readonly noteAlign?: Align;
10
+ }
11
+ /**
12
+ * Draws a sequence layout in cells. Returns the text: `\n` line ends, no trailing spaces, the
13
+ * empty margin around the drawing trimmed, and `title` (the front-matter title, when the
14
+ * diagram has no `title` statement of its own) centred on the first line.
15
+ */
16
+ export declare function drawSequence(layout: SequenceLayout, glyphs: GlyphSet, options?: DrawOptions): string;
@@ -0,0 +1,391 @@
1
+ // cspell:words Grooff
2
+ // Draws a sequence layout computed in cells onto the text grid (D100, D108, D82): participant
3
+ // boxes with their lifelines, activation bars as heavy lifelines, fragment frames with the
4
+ // keyword tab on the top border and dashed section dividers, message lines with their heads,
5
+ // self loops, notes as boxes, and every label last so lines never cut through text.
6
+ // Two forms follow mermaid-ascii's sequence renderer (Alexander Grooff, pkg/sequence/charset.go
7
+ // at 5a9df15): an activation drawn as the lifeline made heavy, and the async heads `(` and `)`.
8
+ // No code is taken from it: this draws v13's own cell layout with core's text-grid kit, whose
9
+ // mermaid-ascii port core's THIRD_PARTY_NOTICES lists.
10
+ import { armOf, Canvas, cellWidth, DOWN, drawBox, drawText, glyphFor, LEFT, RIGHT, sideOf, UP, } from '@mermaid-js/core/ascii';
11
+ import { ARROW_HEADS } from "../model.js";
12
+ import { symbolForm } from "./symbols.js";
13
+ /** The cell each head (`ARROW_HEADS`) is drawn as: a half head is a harpoon, barb up or down. */
14
+ const CELL = {
15
+ arrow: 'arrow',
16
+ open: 'open',
17
+ cross: 'cross',
18
+ solidTop: 'top',
19
+ stickTop: 'top',
20
+ solidBottom: 'bottom',
21
+ stickBottom: 'bottom',
22
+ };
23
+ const headOf = (h) => (h ? CELL[h] : undefined);
24
+ // Heads the glyph sets have no entry for: the async head (as mermaid-ascii draws it) and the
25
+ // half heads as harpoons, barb up (`top`) or down (`bottom`); ASCII has only `<` and `>`.
26
+ const OPEN = { left: '(', right: ')' };
27
+ const HARPOON = {
28
+ top: { left: '↼', right: '⇀' },
29
+ bottom: { left: '↽', right: '⇁' },
30
+ };
31
+ function headGlyph(glyphs, head, d) {
32
+ const side = d === LEFT ? 'left' : 'right';
33
+ switch (head) {
34
+ case 'arrow':
35
+ return sideOf(glyphs.head, d);
36
+ case 'cross':
37
+ return glyphs.cross;
38
+ case 'open':
39
+ return OPEN[side];
40
+ case 'top':
41
+ case 'bottom':
42
+ return glyphs.name === 'unicode' ? HARPOON[head][side] : sideOf(glyphs.head, d);
43
+ }
44
+ }
45
+ const dirOf = (a, b) => {
46
+ if (b.x !== a.x)
47
+ return b.x > a.x ? RIGHT : LEFT;
48
+ return b.y > a.y ? DOWN : UP;
49
+ };
50
+ /**
51
+ * Draws a run of cells as one line: each cell gets arms to its neighbours, the ends also the
52
+ * arm that carries the line on, so a straight run is `─` to the end and a turn is a corner.
53
+ */
54
+ function drawRun(canvas, glyphs, cells, dotted) {
55
+ const last = cells.length - 1;
56
+ cells.forEach((c, i) => {
57
+ let arms = 0;
58
+ if (i > 0)
59
+ arms |= armOf(dirOf(c, cells[i - 1]), 1);
60
+ if (i < last)
61
+ arms |= armOf(dirOf(c, cells[i + 1]), 1);
62
+ if (i === 0 && last > 0)
63
+ arms |= armOf(dirOf(cells[1], c), 1);
64
+ if (i === last && last > 0)
65
+ arms |= armOf(dirOf(cells[last - 1], c), 1);
66
+ if (last === 0)
67
+ arms = armOf(LEFT, 1) | armOf(RIGHT, 1);
68
+ let glyph = glyphFor(arms, glyphs);
69
+ if (dotted && arms === (armOf(LEFT, 1) | armOf(RIGHT, 1)))
70
+ glyph = glyphs.dotted.h;
71
+ if (dotted && arms === (armOf(UP, 1) | armOf(DOWN, 1)))
72
+ glyph = glyphs.dotted.v;
73
+ if (glyph)
74
+ canvas.draw(c.x, c.y, glyph);
75
+ });
76
+ }
77
+ /** A one-armed stub merged into the line glyph at a cell: a lifeline becomes a tee. */
78
+ function stub(canvas, glyphs, x, y, d) {
79
+ if (canvas.get(x, y) === ' ')
80
+ return;
81
+ canvas.draw(x, y, glyphFor(armOf(d, 1), glyphs));
82
+ }
83
+ const row = (x1, x2, y) => {
84
+ const out = [];
85
+ const step = x2 >= x1 ? 1 : -1;
86
+ for (let x = x1; x !== x2 + step; x += step)
87
+ out.push({ x, y });
88
+ return out;
89
+ };
90
+ const column = (x, y1, y2) => {
91
+ const out = [];
92
+ for (let y = y1; y <= y2; y++)
93
+ out.push({ x, y });
94
+ return out;
95
+ };
96
+ const textOf = (label, i) => (label.lines[i] ?? []).map((r) => r.text).join('');
97
+ /** Writes each line of a placed label in the label's width, aligned as `align` says. */
98
+ function writeLabel(canvas, p, align = 'center') {
99
+ const { text } = p;
100
+ text.lines.forEach((_, i) => {
101
+ const line = textOf(text, i);
102
+ const room = p.width - cellWidth(line);
103
+ let x = p.x + Math.floor(room / 2);
104
+ if (align === 'left')
105
+ x = p.x;
106
+ else if (align === 'right')
107
+ x = p.x + room;
108
+ drawText(canvas, x, p.y + i, line);
109
+ });
110
+ }
111
+ /** Writes a label's lines centred in a row band `x … x + width − 1`, from row `y`. */
112
+ function writeCentred(canvas, label, x, width, y) {
113
+ label.lines.forEach((_, i) => {
114
+ const line = textOf(label, i);
115
+ drawText(canvas, x + Math.floor((width - cellWidth(line)) / 2), y + i, line);
116
+ });
117
+ }
118
+ /** The first column from which `length` cells of row `y` up to `to` are plain border. */
119
+ function plainRun(canvas, glyphs, from, to, y, length) {
120
+ let run = 0;
121
+ for (let x = from; x <= to; x++) {
122
+ run = canvas.get(x, y) === glyphs.solid.h ? run + 1 : 0;
123
+ if (run === length)
124
+ return x - length + 1;
125
+ }
126
+ return undefined;
127
+ }
128
+ /** Clears a rectangle's cells to spaces (what a filled SVG shape hides). */
129
+ function clear(canvas, r) {
130
+ for (let y = r.y; y < r.y + r.height; y++)
131
+ for (let x = r.x; x < r.x + r.width; x++)
132
+ canvas.set(x, y, ' ');
133
+ }
134
+ /**
135
+ * Writes `text` on a frame's top border from column `x`, with a space either side
136
+ * (`┌─ text ──┐`) where that leaves the corners `left` and `right` standing.
137
+ */
138
+ function onBorder(canvas, x, y, text, left, right) {
139
+ if (x - 1 > left)
140
+ canvas.set(x - 1, y, ' ');
141
+ const end = drawText(canvas, x, y, text);
142
+ if (end < right)
143
+ canvas.set(end, y, ' ');
144
+ }
145
+ class Drawing {
146
+ layout;
147
+ glyphs;
148
+ align;
149
+ canvas = new Canvas();
150
+ labels = [];
151
+ columns;
152
+ // Layout coordinates may be negative (a box's margin left of the first column).
153
+ dx;
154
+ dy;
155
+ constructor(layout, glyphs, align) {
156
+ this.layout = layout;
157
+ this.glyphs = glyphs;
158
+ this.align = align;
159
+ this.columns = new Map(layout.participants.map((p) => [p.id, p]));
160
+ this.dx = -layout.bounds.x;
161
+ this.dy = -layout.bounds.y;
162
+ }
163
+ rect(r) {
164
+ return { x: r.x + this.dx, y: r.y + this.dy, width: r.width, height: r.height };
165
+ }
166
+ at(p) {
167
+ return { ...p, x: p.x + this.dx, y: p.y + this.dy };
168
+ }
169
+ draw() {
170
+ this.boxes();
171
+ this.lifelines();
172
+ this.fragments();
173
+ this.activations();
174
+ for (const m of this.layout.messages)
175
+ this.message(m);
176
+ this.notes();
177
+ this.participants();
178
+ const { title } = this.layout;
179
+ if (title)
180
+ this.labels.push(() => writeLabel(this.canvas, this.at(title)));
181
+ for (const label of this.labels)
182
+ label();
183
+ return this.canvas.toString();
184
+ }
185
+ boxes() {
186
+ for (const b of this.layout.boxes) {
187
+ const r = this.rect(b);
188
+ drawBox(this.canvas, this.glyphs, r);
189
+ if (b.label) {
190
+ const label = this.at(b.label);
191
+ const right = r.x + r.width - 1;
192
+ // Centred in the box as drawn, which may be narrower than the label's own room.
193
+ const first = textOf(label.text, 0);
194
+ const x = Math.max(r.x + 1, r.x + Math.floor((r.width - cellWidth(first)) / 2));
195
+ this.labels.push(() => onBorder(this.canvas, x, r.y, first, r.x, right));
196
+ for (let i = 1; i < label.text.lines.length; i++)
197
+ this.labels.push(() => drawText(this.canvas, label.x, label.y + i, textOf(label.text, i)));
198
+ }
199
+ }
200
+ }
201
+ lifelines() {
202
+ const { canvas, glyphs } = this;
203
+ for (const p of this.layout.participants) {
204
+ const x = p.lifeline.x + this.dx;
205
+ for (let y = p.lifeline.y1 + this.dy; y < p.lifeline.y2 + this.dy; y++)
206
+ canvas.draw(x, y, glyphs.dotted.v);
207
+ }
208
+ }
209
+ // A bar is the lifeline drawn heavy from its first row to its last: a bar on a bar is one
210
+ // column to the right, as v12 offsets it.
211
+ activations() {
212
+ const heavy = glyphFor(armOf(UP, 2) | armOf(DOWN, 2), this.glyphs);
213
+ for (const a of this.layout.activations) {
214
+ const x = a.x + a.width - 1 + this.dx;
215
+ // Only the lifeline turns heavy: a frame border the bar starts or ends on stays.
216
+ for (let y = a.y + this.dy; y <= a.y + a.height + this.dy; y++) {
217
+ const here = this.canvas.get(x, y);
218
+ if (here === ' ' || here === this.glyphs.dotted.v || here === heavy)
219
+ this.canvas.set(x, y, heavy);
220
+ }
221
+ }
222
+ }
223
+ fragments() {
224
+ const { canvas, glyphs } = this;
225
+ for (const f of this.layout.fragments) {
226
+ // A frame's edges are lines between cells: its border takes the cells on them, so the
227
+ // last row and column it holds (a message line, a lifeline) stay inside.
228
+ const r = { ...this.rect(f), width: f.width + 1, height: f.height + 1 };
229
+ drawBox(canvas, glyphs, r);
230
+ const right = r.x + r.width - 1;
231
+ for (const s of f.sections) {
232
+ const y = s.y + this.dy;
233
+ if (r.width > 2)
234
+ drawRun(canvas, glyphs, row(r.x + 1, right - 1, y), true);
235
+ stub(canvas, glyphs, r.x, y, RIGHT);
236
+ stub(canvas, glyphs, right, y, LEFT);
237
+ if (s.label) {
238
+ const label = this.at(s.label);
239
+ this.labels.push(() => writeLabel(canvas, label));
240
+ }
241
+ }
242
+ const tab = this.at(f.tab);
243
+ // The keyword goes on the first stretch of plain border it fits, so the lifeline
244
+ // crossings (`┼`) stay; on none, right after the corner.
245
+ const keyword = textOf(tab.text, 0);
246
+ this.labels.push(() => {
247
+ const at = plainRun(canvas, glyphs, r.x + 1, right - 1, r.y, cellWidth(keyword) + 2);
248
+ onBorder(canvas, (at ?? r.x + 1) + 1, r.y, keyword, r.x, right);
249
+ });
250
+ if (f.title) {
251
+ const title = this.at(f.title);
252
+ this.labels.push(() => writeLabel(canvas, title));
253
+ }
254
+ }
255
+ }
256
+ message(m) {
257
+ if (m.self)
258
+ this.selfLoop(m, m.self);
259
+ else
260
+ this.line(m);
261
+ // The layout put the sequence number in front of the label (`1. Hello`).
262
+ if (m.label) {
263
+ const label = this.at(m.label);
264
+ this.labels.push(() => writeLabel(this.canvas, label, this.align.message));
265
+ }
266
+ }
267
+ /** A self message: out along its row, down, and back to the lifeline. */
268
+ selfLoop(m, self) {
269
+ const { canvas, glyphs } = this;
270
+ const y = m.y + this.dy;
271
+ const startX = m.startX + this.dx;
272
+ const end = headOf(ARROW_HEADS[m.arrow].end);
273
+ const start = headOf(ARROW_HEADS[m.arrow].start);
274
+ const right = self.right + this.dx;
275
+ const bottom = self.bottom + this.dy;
276
+ const lifeline = (this.columns.get(m.from)?.lifeline.x ?? m.startX - 1) + this.dx;
277
+ // A central circle sits on the lifeline; the loop leaves from the cell next to it.
278
+ const first = m.central ? lifeline + 1 : startX;
279
+ const cells = [...row(first, right, y)];
280
+ for (let j = y + 1; j <= bottom; j++)
281
+ cells.push({ x: right, y: j });
282
+ cells.push(...row(right - 1, first, bottom));
283
+ drawRun(canvas, glyphs, cells, m.arrow.includes('dotted'));
284
+ // `A()->>A` leaves from a circle, `A->>()A` comes back to one (mermaid-ascii: both rows).
285
+ const fromCircle = m.central === 'from' || m.central === 'both';
286
+ const toCircle = m.central === 'to' || m.central === 'both';
287
+ if (fromCircle)
288
+ canvas.set(lifeline, y, glyphs.circle);
289
+ else
290
+ stub(canvas, glyphs, first - 1, y, RIGHT);
291
+ if (toCircle)
292
+ canvas.set(lifeline, bottom, glyphs.circle);
293
+ if (end)
294
+ canvas.set(first, bottom, headGlyph(glyphs, end, LEFT));
295
+ if (start)
296
+ canvas.set(first, y, headGlyph(glyphs, start, LEFT));
297
+ }
298
+ /** A message between two lifelines, with its heads and central circles. */
299
+ line(m) {
300
+ const { canvas, glyphs } = this;
301
+ const y = m.y + this.dy;
302
+ const startX = m.startX + this.dx;
303
+ const stopX = m.stopX + this.dx;
304
+ const dotted = m.arrow.includes('dotted');
305
+ const end = headOf(ARROW_HEADS[m.arrow].end);
306
+ const start = headOf(ARROW_HEADS[m.arrow].start);
307
+ const d = stopX >= startX ? RIGHT : LEFT;
308
+ const step = d === RIGHT ? 1 : -1;
309
+ const from = this.columns.get(m.from);
310
+ const to = this.columns.get(m.to);
311
+ const fromCircle = m.central === 'from' || m.central === 'both';
312
+ const toCircle = m.central === 'to' || m.central === 'both';
313
+ // A circle sits on the lifeline, so the line runs from the cell next to it.
314
+ const first = fromCircle && from ? from.lifeline.x + this.dx + step : startX;
315
+ drawRun(canvas, glyphs, row(first, stopX, y), dotted);
316
+ if (!fromCircle)
317
+ stub(canvas, glyphs, first - step, y, d);
318
+ if (end)
319
+ canvas.set(stopX, y, headGlyph(glyphs, end, d));
320
+ if (start)
321
+ canvas.set(first, y, headGlyph(glyphs, start, d === RIGHT ? LEFT : RIGHT));
322
+ if (fromCircle && from)
323
+ canvas.set(from.lifeline.x + this.dx, y, glyphs.circle);
324
+ if (toCircle && to)
325
+ canvas.set(to.lifeline.x + this.dx, y, glyphs.circle);
326
+ }
327
+ notes() {
328
+ const { canvas, glyphs } = this;
329
+ for (const n of this.layout.notes) {
330
+ const r = this.rect(n);
331
+ clear(canvas, r);
332
+ drawBox(canvas, glyphs, r);
333
+ // A box side the note spans is drawn through it (the layout keeps the text clear).
334
+ for (const b of this.layout.boxes) {
335
+ const box = this.rect(b);
336
+ for (const x of [box.x, box.x + box.width - 1])
337
+ if (x > r.x && x < r.x + r.width - 1)
338
+ drawRun(canvas, glyphs, column(x, r.y, r.y + r.height - 1), false);
339
+ }
340
+ const label = this.at(n.label);
341
+ this.labels.push(() => writeLabel(canvas, label, this.align.note));
342
+ }
343
+ }
344
+ participants() {
345
+ const { canvas, glyphs } = this;
346
+ for (const p of this.layout.participants) {
347
+ const options = symbolForm(p.kind) ?? {};
348
+ const x = p.x + this.dx;
349
+ const lifeline = p.lifeline.x + this.dx;
350
+ const box = (top, joint) => {
351
+ const r = { x, y: top, width: p.width, height: p.height };
352
+ clear(canvas, r);
353
+ drawBox(canvas, glyphs, r, options);
354
+ const border = joint === DOWN ? top + p.height - 1 : top;
355
+ const outside = joint === DOWN ? border + 1 : border - 1;
356
+ if (canvas.get(lifeline, outside) !== ' ')
357
+ canvas.draw(lifeline, border, glyphFor(armOf(joint, 1), glyphs));
358
+ const rows = p.label.lines.length;
359
+ writeCentred(canvas, p.label, x, p.width, top + p.labelY - Math.floor(rows / 2));
360
+ };
361
+ box(p.top + this.dy, DOWN);
362
+ if (p.bottom !== undefined)
363
+ box(p.bottom + this.dy, UP);
364
+ }
365
+ }
366
+ }
367
+ /**
368
+ * Draws a sequence layout in cells. Returns the text: `\n` line ends, no trailing spaces, the
369
+ * empty margin around the drawing trimmed, and `title` (the front-matter title, when the
370
+ * diagram has no `title` statement of its own) centred on the first line.
371
+ */
372
+ export function drawSequence(layout, glyphs, options = {}) {
373
+ const align = { message: options.messageAlign ?? 'center', note: options.noteAlign ?? 'center' };
374
+ let lines = new Drawing(layout, glyphs, align).draw().split('\n');
375
+ const title = options.title;
376
+ while (lines.length > 0 && lines[0] === '')
377
+ lines.shift();
378
+ while (lines.length > 0 && lines[lines.length - 1] === '')
379
+ lines.pop();
380
+ const margin = Math.min(...lines.filter((l) => l !== '').map((l) => l.search(/\S/)));
381
+ if (margin > 0 && Number.isFinite(margin))
382
+ lines = lines.map((l) => l.slice(margin));
383
+ const text = title?.trim();
384
+ if (text) {
385
+ const width = Math.max(0, ...lines.map(cellWidth));
386
+ const out = new Canvas();
387
+ drawText(out, Math.max(0, Math.floor((width - cellWidth(text)) / 2)), 0, text);
388
+ lines.unshift(out.toString(), '');
389
+ }
390
+ return lines.join('\n');
391
+ }
@@ -0,0 +1,4 @@
1
+ import type { BoxOptions } from '@mermaid-js/core/ascii';
2
+ import type { ParticipantKind } from '../model.ts';
3
+ /** The box options a participant kind is drawn with, or undefined when it has no text form. */
4
+ export declare function symbolForm(kind: ParticipantKind): BoxOptions | undefined;
@@ -0,0 +1,9 @@
1
+ const FORMS = {
2
+ participant: {},
3
+ actor: { form: 'rounded' },
4
+ database: { form: 'caps' },
5
+ };
6
+ /** The box options a participant kind is drawn with, or undefined when it has no text form. */
7
+ export function symbolForm(kind) {
8
+ return FORMS[kind];
9
+ }
@@ -0,0 +1,28 @@
1
+ import type { Warning } from '@mermaid-js/core';
2
+ import type { AstNode } from '@mermaid-js/core/parser-kit';
3
+ import type { SequenceLayout } from '../layout/index.ts';
4
+ import type { SequenceModel } from '../model.ts';
5
+ /** The feature kinds an `ASCII_DROPPED` warning names, in the order they are reported. */
6
+ export type AsciiDroppedFeature = 'style' | 'emphasis' | 'link' | 'accessibility';
7
+ /** One feature kind text output dropped (D82). */
8
+ export interface AsciiDroppedWarning extends Warning {
9
+ readonly code: 'ASCII_DROPPED';
10
+ readonly feature: AsciiDroppedFeature;
11
+ readonly astIds: readonly string[];
12
+ }
13
+ /** The participant symbols drawn as plain boxes because they have no text form (D82). */
14
+ export interface AsciiShapeWarning extends Warning {
15
+ readonly code: 'ASCII_SHAPE';
16
+ readonly shapes: readonly string[];
17
+ readonly astIds: readonly string[];
18
+ }
19
+ export type AsciiWarning = AsciiDroppedWarning | AsciiShapeWarning;
20
+ /**
21
+ * The warnings for one drawing of `model`; `shown` holds the ids of the participants drawn.
22
+ * `ast` adds the statements' ids (`st:3` for a `link` line, `accTitle`, …).
23
+ */
24
+ export declare function asciiWarnings(model: SequenceModel, layout: SequenceLayout, options: {
25
+ readonly ast?: AstNode;
26
+ readonly charset: 'unicode' | 'ascii';
27
+ readonly config: Readonly<Record<string, unknown>>;
28
+ }): AsciiWarning[];
@@ -0,0 +1,141 @@
1
+ import { Style } from '@mermaid-js/core/text';
2
+ import { symbolForm } from "./symbols.js";
3
+ const WHAT = {
4
+ style: 'colours (box colours, rect highlighting)',
5
+ emphasis: 'bold and italic text',
6
+ link: 'actor menus (links, properties, details)',
7
+ accessibility: 'accTitle and accDescr',
8
+ };
9
+ function rects(statements, out) {
10
+ for (const s of statements) {
11
+ if (s.kind === 'rect') {
12
+ out.push(s.ast);
13
+ rects(s.statements, out);
14
+ }
15
+ else if (s.kind === 'fragment')
16
+ for (const sec of s.sections)
17
+ rects(sec.statements, out);
18
+ }
19
+ }
20
+ // The statements behind each feature kind, by AST node type.
21
+ const STATEMENTS = {
22
+ style: [],
23
+ emphasis: [],
24
+ link: ['Links', 'Link', 'Properties', 'Details'],
25
+ accessibility: ['AccTitle', 'AccDescr'],
26
+ };
27
+ /**
28
+ * The AST ids by node type. A menu statement counts only while it still carries its `menu`:
29
+ * a later redeclaration drops it, as v12 dropped the merged menu, and the plugin draws none.
30
+ */
31
+ function idsByType(ast, out) {
32
+ if (!ast)
33
+ return;
34
+ const dropped = STATEMENTS.link.includes(ast.type) && !('menu' in ast && ast.menu);
35
+ if (!dropped) {
36
+ const list = out.get(ast.type);
37
+ if (list)
38
+ list.push(ast.id);
39
+ else
40
+ out.set(ast.type, [ast.id]);
41
+ }
42
+ for (const child of ast.children ?? [])
43
+ idsByType(child, out);
44
+ }
45
+ const emphasised = (label) => label?.lines.some((line) => line.some((run) => (run.style & (Style.Bold | Style.Italic)) !== 0)) ?? false;
46
+ // `sequence.*FontWeight` set bold, which text cannot show either.
47
+ const WEIGHTS = ['actorFontWeight', 'messageFontWeight', 'noteFontWeight'];
48
+ const bold = (v) => v === 'bold' || v === 'bolder' || Number(v) >= 600;
49
+ /** The AST ids of the drawn elements whose text is bold or italic. */
50
+ function emphasisIds(layout) {
51
+ const out = [];
52
+ for (const p of layout.participants)
53
+ if (emphasised(p.label))
54
+ out.push(p.ast);
55
+ for (const m of layout.messages)
56
+ if (emphasised(m.label?.text))
57
+ out.push(m.ast);
58
+ for (const n of layout.notes)
59
+ if (emphasised(n.label.text))
60
+ out.push(n.ast);
61
+ for (const f of layout.fragments)
62
+ if (emphasised(f.title?.text) || f.sections.some((s) => emphasised(s.label?.text)))
63
+ out.push(f.ast);
64
+ for (const b of layout.boxes)
65
+ if (emphasised(b.label?.text))
66
+ out.push(b.ast);
67
+ return out;
68
+ }
69
+ const hasKeys = (o) => o !== undefined && Object.keys(o).length > 0;
70
+ /**
71
+ * The warnings for one drawing of `model`; `shown` holds the ids of the participants drawn.
72
+ * `ast` adds the statements' ids (`st:3` for a `link` line, `accTitle`, …).
73
+ */
74
+ export function asciiWarnings(model, layout, options) {
75
+ const shown = new Set(layout.participants.map((p) => p.id));
76
+ const { ast } = options;
77
+ const found = {
78
+ style: undefined,
79
+ emphasis: undefined,
80
+ link: undefined,
81
+ accessibility: undefined,
82
+ };
83
+ const add = (feature, ids) => {
84
+ (found[feature] ??= []).push(...ids);
85
+ };
86
+ const coloured = model.boxes.filter((b) => b.color !== undefined).map((b) => b.ast);
87
+ const rectIds = [];
88
+ rects(model.statements, rectIds);
89
+ if (coloured.length > 0 || rectIds.length > 0)
90
+ add('style', [...coloured, ...rectIds]);
91
+ const menus = model.participants.filter((p) => shown.has(p.id) && (hasKeys(p.links) || hasKeys(p.properties)));
92
+ if (menus.length > 0)
93
+ add('link', menus.map((p) => p.ast));
94
+ const emphasis = emphasisIds(layout);
95
+ if (emphasis.length > 0 || WEIGHTS.some((k) => bold(options.config[k])))
96
+ add('emphasis', emphasis);
97
+ if (model.accTitle.trim() !== '' || model.accDescr.trim() !== '')
98
+ add('accessibility', []);
99
+ const stmts = new Map();
100
+ idsByType(ast, stmts);
101
+ for (const feature of Object.keys(STATEMENTS)) {
102
+ const ids = STATEMENTS[feature].flatMap((type) => stmts.get(type) ?? []);
103
+ if (ids.length > 0)
104
+ add(feature, ids);
105
+ }
106
+ const out = [];
107
+ for (const feature of Object.keys(WHAT)) {
108
+ const ids = found[feature];
109
+ if (!ids)
110
+ continue;
111
+ out.push({
112
+ code: 'ASCII_DROPPED',
113
+ feature,
114
+ astIds: [...new Set(ids)],
115
+ message: `Text output has no ${WHAT[feature]}; dropped.`,
116
+ });
117
+ }
118
+ const shapes = [];
119
+ const astIds = [];
120
+ for (const p of model.participants) {
121
+ if (!shown.has(p.id) || symbolForm(p.kind))
122
+ continue;
123
+ if (!shapes.includes(p.kind))
124
+ shapes.push(p.kind);
125
+ astIds.push(p.ast);
126
+ }
127
+ // ASCII has no half arrowheads: `-|\` and friends are drawn as `>` and `<`.
128
+ const half = layout.messages.filter((m) => /_(?:top|bottom)/u.test(m.arrow)).map((m) => m.ast);
129
+ if (options.charset === 'ascii' && half.length > 0) {
130
+ shapes.push('half-arrowhead');
131
+ astIds.push(...half);
132
+ }
133
+ if (shapes.length > 0)
134
+ out.push({
135
+ code: 'ASCII_SHAPE',
136
+ shapes,
137
+ astIds,
138
+ message: `Text output draws these as plain boxes or full heads: ${shapes.join(', ')}.`,
139
+ });
140
+ return out;
141
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The sequence diagram's text output (D100, D108, DESIGN §6.3), a separate lazily loaded entry
3
+ * (`@mermaid-js/diagram-sequence/ascii`, manifest key `ascii`) so SVG renders never load it.
4
+ *
5
+ * It draws the same `SequenceLayout` the SVG renderer draws, computed with the cell metrics
6
+ * (the cell measurer and the cell profile, D108), with core's text-grid kit.
7
+ */
8
+ import type { TextRenderContext } from '@mermaid-js/core';
9
+ import type { SequenceModel } from './model.ts';
10
+ import { type SequenceLayout } from './layout/index.ts';
11
+ export type { AsciiDroppedFeature, AsciiDroppedWarning, AsciiShapeWarning, AsciiWarning, } from './ascii/warnings.ts';
12
+ /**
13
+ * Lays a sequence diagram out in cells: the cell measurer and the cell profile
14
+ * (`ascii.padding`), whatever `ctx` carries (D108).
15
+ */
16
+ export declare function layoutAscii(model: SequenceModel, ctx: TextRenderContext): SequenceLayout;
17
+ /**
18
+ * The sequence diagram's `ascii` hook (DESIGN §12.1): the drawing as text, `\n` line ends, no
19
+ * trailing spaces. Warns once per feature kind it drops and once for the participant symbols
20
+ * drawn as plain boxes (D82). A layout not in cells is redone with `layoutAscii`.
21
+ */
22
+ export declare function ascii(model: SequenceModel, layout: SequenceLayout, ctx: TextRenderContext): string;
package/dist/ascii.js ADDED
@@ -0,0 +1,34 @@
1
+ import { ascii as asciiGlyphs, asciiOptions, cellMeasurer, unicode } from '@mermaid-js/core/ascii';
2
+ import { cellProfile } from '@mermaid-js/core/layout';
3
+ import { layoutSequence } from "./layout/index.js";
4
+ import { drawSequence } from "./ascii/draw.js";
5
+ import { asciiWarnings } from "./ascii/warnings.js";
6
+ /**
7
+ * Lays a sequence diagram out in cells: the cell measurer and the cell profile
8
+ * (`ascii.padding`), whatever `ctx` carries (D108).
9
+ */
10
+ export function layoutAscii(model, ctx) {
11
+ const { padding } = asciiOptions(ctx.config);
12
+ return layoutSequence(model, { ...ctx, measurer: cellMeasurer, units: cellProfile(padding) });
13
+ }
14
+ /**
15
+ * The sequence diagram's `ascii` hook (DESIGN §12.1): the drawing as text, `\n` line ends, no
16
+ * trailing spaces. Warns once per feature kind it drops and once for the participant symbols
17
+ * drawn as plain boxes (D82). A layout not in cells is redone with `layoutAscii`.
18
+ */
19
+ export function ascii(model, layout, ctx) {
20
+ const laid = layout.units === 'cell' ? layout : layoutAscii(model, ctx);
21
+ const glyphs = asciiOptions(ctx.config).charset === 'ascii' ? asciiGlyphs : unicode;
22
+ const config = (ctx.config.sequence ?? {});
23
+ const warnings = asciiWarnings(model, laid, { ast: ctx.ast, charset: glyphs.name, config });
24
+ for (const w of warnings)
25
+ ctx.warn(w);
26
+ // The diagram's own `title` statement is laid out; else the front-matter title goes on top.
27
+ const seq = ctx.config.sequence ?? {};
28
+ const align = (v) => v === 'left' || v === 'center' || v === 'right' ? v : undefined;
29
+ return drawSequence(laid, glyphs, {
30
+ ...(laid.title || ctx.title === undefined ? {} : { title: ctx.title }),
31
+ ...(align(seq.messageAlign) ? { messageAlign: align(seq.messageAlign) } : {}),
32
+ ...(align(seq.noteAlign) ? { noteAlign: align(seq.noteAlign) } : {}),
33
+ });
34
+ }
@@ -0,0 +1,3 @@
1
+ import type { CompletionItem, DocumentView, ProbeResult } from '@mermaid-js/core/parser-kit';
2
+ import type { SequenceDocument } from '../parser/ast.ts';
3
+ export declare function sequenceCompletions(probe: ProbeResult, doc: DocumentView<SequenceDocument>): CompletionItem[];