@mermaid-js/diagram-sequence 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +14 -2
- package/dist/ascii/draw.d.ts +16 -0
- package/dist/ascii/draw.js +391 -0
- package/dist/ascii/symbols.d.ts +4 -0
- package/dist/ascii/symbols.js +9 -0
- package/dist/ascii/warnings.d.ts +28 -0
- package/dist/ascii/warnings.js +141 -0
- package/dist/ascii.d.ts +22 -0
- package/dist/ascii.js +34 -0
- package/dist/facet/completions.d.ts +3 -0
- package/dist/facet/completions.js +282 -0
- package/dist/facet/hover.d.ts +8 -0
- package/dist/facet/hover.js +107 -0
- package/dist/facet/tables.d.ts +38 -0
- package/dist/facet/tables.js +153 -0
- package/dist/generated/categories.d.ts +6 -0
- package/dist/generated/categories.js +92 -0
- package/dist/generated/docs.d.ts +5 -0
- package/dist/generated/docs.js +73 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +4 -0
- package/dist/labels.d.ts +6 -0
- package/dist/labels.js +20 -0
- package/dist/language.d.ts +16 -0
- package/dist/language.js +79 -0
- package/dist/layout/bounds.d.ts +31 -0
- package/dist/layout/bounds.js +46 -0
- package/dist/layout/columns.d.ts +44 -0
- package/dist/layout/columns.js +207 -0
- package/dist/layout/env.d.ts +95 -0
- package/dist/layout/env.js +173 -0
- package/dist/layout/fixtures.d.ts +15 -0
- package/dist/layout/fixtures.js +197 -0
- package/dist/layout/geometry.d.ts +38 -0
- package/dist/layout/geometry.js +200 -0
- package/dist/layout/index.d.ts +10 -0
- package/dist/layout/index.js +8 -0
- package/dist/layout/invariants.d.ts +15 -0
- package/dist/layout/invariants.js +212 -0
- package/dist/layout/layout.d.ts +26 -0
- package/dist/layout/layout.js +314 -0
- package/dist/layout/metrics.d.ts +196 -0
- package/dist/layout/metrics.js +250 -0
- package/dist/layout/rows.d.ts +64 -0
- package/dist/layout/rows.js +487 -0
- package/dist/layout/types.d.ts +154 -0
- package/dist/layout/types.js +1 -0
- package/dist/model.d.ts +165 -0
- package/dist/model.js +47 -0
- package/dist/parser/arrows.d.ts +6 -0
- package/dist/parser/arrows.js +32 -0
- package/dist/parser/ast.d.ts +130 -0
- package/dist/parser/ast.js +1 -0
- package/dist/parser/builder.d.ts +104 -0
- package/dist/parser/builder.js +238 -0
- package/dist/parser/colors.d.ts +4 -0
- package/dist/parser/colors.js +28 -0
- package/dist/parser/detect.d.ts +2 -0
- package/dist/parser/detect.js +8 -0
- package/dist/parser/index.d.ts +18 -0
- package/dist/parser/index.js +29 -0
- package/dist/parser/lexer.d.ts +6 -0
- package/dist/parser/lexer.js +701 -0
- package/dist/parser/normalise.d.ts +54 -0
- package/dist/parser/normalise.js +217 -0
- package/dist/parser/parser.d.ts +14 -0
- package/dist/parser/parser.js +738 -0
- package/dist/parser/rules.d.ts +47 -0
- package/dist/parser/rules.js +126 -0
- package/dist/parser/tables.d.ts +72 -0
- package/dist/parser/tables.js +179 -0
- package/dist/parser/tokens.d.ts +104 -0
- package/dist/parser/tokens.js +193 -0
- package/dist/plugin.d.ts +22 -0
- package/dist/plugin.js +24 -0
- package/dist/render/arrows.d.ts +24 -0
- package/dist/render/arrows.js +283 -0
- package/dist/render/index.d.ts +2 -0
- package/dist/render/index.js +2 -0
- package/dist/render/paint.d.ts +73 -0
- package/dist/render/paint.js +92 -0
- package/dist/render/render.d.ts +8 -0
- package/dist/render/render.js +345 -0
- package/dist/render/symbols.d.ts +22 -0
- package/dist/render/symbols.js +237 -0
- package/package.json +53 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014 - 2022 Knut Sveidqvist
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mermaid-js/diagram-sequence (v13 alpha)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The sequence diagram plugin for Mermaid v13: parser, model, layout and SVG renderer, plus text output from `@mermaid-js/diagram-sequence/ascii` and the language facet at `@mermaid-js/diagram-sequence/language`.
|
|
4
|
+
|
|
5
|
+
`mermaid` already includes it. Install it yourself only when you use `mermaid/slim`. `@mermaid-js/diagram-sequence@v13-alpha` is the version the newest `mermaid@v13-alpha` depends on:
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { createMermaid } from 'mermaid/slim';
|
|
9
|
+
import { sequence } from '@mermaid-js/diagram-sequence';
|
|
10
|
+
|
|
11
|
+
const mermaid = createMermaid({ diagrams: [sequence] });
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
> [!WARNING]
|
|
15
|
+
> Alpha: APIs can change between builds. Pin an exact version.
|
|
@@ -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,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
|
+
}
|
package/dist/ascii.d.ts
ADDED
|
@@ -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;
|