agentlas 1.0.41 → 1.0.43
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/CHANGELOG.md +40 -8
- package/engine/agentlas-workforce.cjs +1 -1
- package/engine/commands/index.cjs +5 -5
- package/engine/hephaestus/runtime.cjs +1 -1
- package/engine/storm/storm.cjs +1 -1
- package/engine/storm/swarm.cjs +1 -1
- package/engine/ui/repl.cjs +4 -4
- package/engine/ui/{pitui-screens.cjs → screens.cjs} +143 -6
- package/engine/ui/{pitui-shell.cjs → shell.cjs} +42 -27
- package/engine/ui/width.cjs +1 -1
- package/engine/vendor/mermaid/LICENSE +205 -0
- package/engine/vendor/mermaid/ansi.js +23 -0
- package/engine/vendor/mermaid/canvas.js +366 -0
- package/engine/vendor/mermaid/graph.js +91 -0
- package/engine/vendor/mermaid/index.js +100 -0
- package/engine/vendor/mermaid/labels.js +324 -0
- package/engine/vendor/mermaid/layout-seq.js +194 -0
- package/engine/vendor/mermaid/layout.js +881 -0
- package/engine/vendor/mermaid/package.json +1 -0
- package/engine/vendor/mermaid/parse.js +1108 -0
- package/engine/vendor/mermaid/source-box.js +78 -0
- package/engine/vendor/mermaid/types.js +1 -0
- package/engine/vendor/mermaid/width-data.js +994 -0
- package/engine/vendor/mermaid/width.js +76 -0
- package/engine/vendor/tui/LICENSE +20 -0
- package/engine/vendor/tui/autocomplete.js +632 -0
- package/engine/vendor/tui/components/alt-screen-flash.js +37 -0
- package/engine/vendor/tui/components/box.js +104 -0
- package/engine/vendor/tui/components/cancellable-loader.js +35 -0
- package/engine/vendor/tui/components/editor.js +1961 -0
- package/engine/vendor/tui/components/h-stack.js +43 -0
- package/engine/vendor/tui/components/image.js +90 -0
- package/engine/vendor/tui/components/input.js +378 -0
- package/engine/vendor/tui/components/loader.js +69 -0
- package/engine/vendor/tui/components/markdown.js +806 -0
- package/engine/vendor/tui/components/scroll-view.js +173 -0
- package/engine/vendor/tui/components/select-list.js +159 -0
- package/engine/vendor/tui/components/settings-list.js +182 -0
- package/engine/vendor/tui/components/spacer.js +23 -0
- package/engine/vendor/tui/components/stack.js +111 -0
- package/engine/vendor/tui/components/text.js +89 -0
- package/engine/vendor/tui/components/truncated-text.js +51 -0
- package/engine/vendor/tui/components/v-stack.js +26 -0
- package/engine/vendor/tui/deps/east-asian-width/LICENSE +9 -0
- package/engine/vendor/tui/deps/east-asian-width/index.js +30 -0
- package/engine/vendor/tui/deps/east-asian-width/lookup-data.js +21 -0
- package/engine/vendor/tui/deps/east-asian-width/lookup.js +138 -0
- package/engine/vendor/tui/deps/east-asian-width/utilities.js +24 -0
- package/engine/vendor/tui/deps/marked/LICENSE +44 -0
- package/engine/vendor/tui/deps/marked/index.js +77 -0
- package/engine/vendor/tui/editor-component.js +2 -0
- package/engine/vendor/tui/fuzzy.js +110 -0
- package/engine/vendor/tui/index.js +42 -0
- package/engine/vendor/tui/keybindings.js +209 -0
- package/engine/vendor/tui/keys.js +1174 -0
- package/engine/vendor/tui/kill-ring.js +44 -0
- package/engine/vendor/tui/latex.js +1264 -0
- package/engine/vendor/tui/layout-node.js +6 -0
- package/engine/vendor/tui/layout.js +314 -0
- package/engine/vendor/tui/native-modifiers.js +60 -0
- package/engine/vendor/tui/package.json +1 -0
- package/engine/vendor/tui/stdin-buffer.js +361 -0
- package/engine/vendor/tui/terminal-colors.js +59 -0
- package/engine/vendor/tui/terminal-image.js +518 -0
- package/engine/vendor/tui/terminal.js +436 -0
- package/engine/vendor/tui/tui-alt-screen.js +902 -0
- package/engine/vendor/tui/tui-main-screen.js +533 -0
- package/engine/vendor/tui/tui.js +937 -0
- package/engine/vendor/tui/undo-stack.js +25 -0
- package/engine/vendor/tui/utils.js +1191 -0
- package/engine/vendor/tui/word-navigation.js +96 -0
- package/package.json +2 -6
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { stripControls } from './labels.js';
|
|
2
|
+
import { layoutClass, layoutFlowchart, layoutGrouped } from './layout.js';
|
|
3
|
+
import { layoutSequence } from './layout-seq.js';
|
|
4
|
+
import { diagramKind, parseClass, parseEr, parseGraph, parseSequence, parseState } from './parse.js';
|
|
5
|
+
export { DEFAULT_THEME, toAnsi } from './ansi.js';
|
|
6
|
+
export { diagramKind } from './parse.js';
|
|
7
|
+
export { sourceBox } from './source-box.js';
|
|
8
|
+
/**
|
|
9
|
+
* Render a Mermaid source block as Unicode box-drawing art.
|
|
10
|
+
*
|
|
11
|
+
* Supported: `graph`/`flowchart` (including `subgraph`), `stateDiagram`,
|
|
12
|
+
* `classDiagram`, `erDiagram` and `sequenceDiagram`.
|
|
13
|
+
*
|
|
14
|
+
* The diagram is laid out at whatever size it needs; `art.width` reports the
|
|
15
|
+
* columns that turned out to be. Deciding what to do when that exceeds the
|
|
16
|
+
* space at hand is the caller's — `sourceBox` is the usual answer:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* const art = render(src)
|
|
20
|
+
* show(art && art.width <= cols ? art : sourceBox(src, cols))
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* `null` means there is no art to show: blank input, a syntax error, a diagram
|
|
24
|
+
* type this renderer does not draw, or one large enough that laying it out is
|
|
25
|
+
* refused. `diagramKind` separates the middle two.
|
|
26
|
+
*
|
|
27
|
+
* Rendering is best-effort. A flowchart keeps whatever parsed; the stricter
|
|
28
|
+
* grammars additionally get one retry without their final line, which is what
|
|
29
|
+
* keeps a streaming diagram on screen while its last statement is half-typed.
|
|
30
|
+
* Everything given up on is listed in `art.warnings` — advisory only, never a
|
|
31
|
+
* reason to withhold the art.
|
|
32
|
+
*/
|
|
33
|
+
export function render(src) {
|
|
34
|
+
src = stripControls(src);
|
|
35
|
+
if (src.trim() === '')
|
|
36
|
+
return null;
|
|
37
|
+
const drawn = attempt(src);
|
|
38
|
+
if (drawn === null)
|
|
39
|
+
return null;
|
|
40
|
+
return { ...drawn.canvas.toLines(), warnings: drawn.warnings };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Draw `src`, retrying once without its last line if the grammar rejects it.
|
|
44
|
+
*
|
|
45
|
+
* State, class, ER and sequence fail a whole diagram on one unreadable
|
|
46
|
+
* statement, and while a source is streaming its last line is usually still
|
|
47
|
+
* being typed — so without this a diagram alternates with the source box all
|
|
48
|
+
* the way in. Only the final line is dropped, and doing so is always reported,
|
|
49
|
+
* so a finished document with a bad last line still says what it lost rather
|
|
50
|
+
* than quietly rendering short.
|
|
51
|
+
*/
|
|
52
|
+
function attempt(src) {
|
|
53
|
+
const drawn = draw(src);
|
|
54
|
+
if (drawn !== null)
|
|
55
|
+
return drawn;
|
|
56
|
+
const body = src.replace(/\s+$/, '');
|
|
57
|
+
const cut = body.lastIndexOf('\n');
|
|
58
|
+
if (cut === -1)
|
|
59
|
+
return null;
|
|
60
|
+
const salvaged = draw(body.slice(0, cut));
|
|
61
|
+
if (salvaged === null)
|
|
62
|
+
return null;
|
|
63
|
+
const dropped = body.slice(cut + 1).trim();
|
|
64
|
+
return {
|
|
65
|
+
canvas: salvaged.canvas,
|
|
66
|
+
warnings: [...salvaged.warnings, `dropped, unreadable final line: "${dropped}"`],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Dispatch on the declared diagram type; `null` means nothing was drawn. */
|
|
70
|
+
function draw(src) {
|
|
71
|
+
const plain = (canvas) => (canvas === null ? null : { canvas, warnings: [] });
|
|
72
|
+
switch (diagramKind(src)) {
|
|
73
|
+
case 'flowchart': {
|
|
74
|
+
const graph = parseGraph(src);
|
|
75
|
+
if (graph === null)
|
|
76
|
+
return null;
|
|
77
|
+
const canvas = graph.groups.length === 0 ? layoutFlowchart(graph) : layoutGrouped(graph);
|
|
78
|
+
return canvas === null ? null : { canvas, warnings: graph.warnings };
|
|
79
|
+
}
|
|
80
|
+
case 'state': {
|
|
81
|
+
const state = parseState(src);
|
|
82
|
+
return state === null ? null : plain(layoutFlowchart(state));
|
|
83
|
+
}
|
|
84
|
+
case 'class': {
|
|
85
|
+
const cls = parseClass(src);
|
|
86
|
+
return cls === null ? null : plain(layoutClass(cls.graph, cls.infos));
|
|
87
|
+
}
|
|
88
|
+
case 'er': {
|
|
89
|
+
const er = parseEr(src);
|
|
90
|
+
return er === null ? null : plain(layoutClass(er.graph, er.infos));
|
|
91
|
+
}
|
|
92
|
+
case 'sequence': {
|
|
93
|
+
const seq = parseSequence(src);
|
|
94
|
+
return seq === null ? null : plain(layoutSequence(seq));
|
|
95
|
+
}
|
|
96
|
+
default:
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { measured, stringWidth } from './width.js';
|
|
2
|
+
/** Node labels wrap to at most this many display columns per line ... */
|
|
3
|
+
export const WRAP_WIDTH = 24;
|
|
4
|
+
/** ... and at most this many lines; overflow is truncated with an ellipsis. */
|
|
5
|
+
export const MAX_LINES = 4;
|
|
6
|
+
/** Edge labels are truncated to this many columns. */
|
|
7
|
+
export const MAX_LABEL = 28;
|
|
8
|
+
/**
|
|
9
|
+
* Identifier-boundary characters preferred as break points when a single word
|
|
10
|
+
* is too wide to fit, so it is not sliced mid-segment.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors `TOKEN_BREAK_CHARS` in grok-build's
|
|
13
|
+
* `third_party/mermaid-to-svg/src/text_wrap.rs`; the two renderers are
|
|
14
|
+
* deliberately independent, so keep these in sync.
|
|
15
|
+
*/
|
|
16
|
+
export const LABEL_BREAK_CHARS = ['_', '-', '.', '/'];
|
|
17
|
+
/**
|
|
18
|
+
* ASCII-only case folding, matching Rust's `to_ascii_lowercase`.
|
|
19
|
+
*
|
|
20
|
+
* `String.prototype.toLowerCase` can change a string's length (`İ` becomes two
|
|
21
|
+
* code points), which would desync the byte offsets some parsers slice with.
|
|
22
|
+
*/
|
|
23
|
+
export const asciiLower = (s) => s.replace(/[A-Z]/g, (c) => c.toLowerCase());
|
|
24
|
+
export const asciiUpper = (s) => s.replace(/[a-z]/g, (c) => c.toUpperCase());
|
|
25
|
+
/**
|
|
26
|
+
* C0 and C1 controls, less the `\t\n\r` the parsers and `srcLines` read.
|
|
27
|
+
*
|
|
28
|
+
* They measure one column and paint none, so a box sized around one is drawn a
|
|
29
|
+
* column short of its own border; NUL also collides with the `CONT` sentinel
|
|
30
|
+
* and is dropped after layout has already paid for its cell; ESC would inject
|
|
31
|
+
* ANSI into the caller's scrollback. `decodeEntityBody` refuses to decode an
|
|
32
|
+
* entity into one — this closes the same hole for literals.
|
|
33
|
+
*/
|
|
34
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: the point is to match them
|
|
35
|
+
const CONTROLS = /[\0-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g;
|
|
36
|
+
/** Applied by every public entry point that takes untrusted source. */
|
|
37
|
+
export const stripControls = (src) => src.replace(CONTROLS, '');
|
|
38
|
+
/**
|
|
39
|
+
* Split source into lines the way Rust's `str::lines()` does: on `\n`, with a
|
|
40
|
+
* trailing `\r` stripped, and *without* a final empty line when the input ends
|
|
41
|
+
* in a newline. `String.split` yields that extra element, which would show up
|
|
42
|
+
* as a spurious blank row inside a source box.
|
|
43
|
+
*/
|
|
44
|
+
export function srcLines(src) {
|
|
45
|
+
const out = src.split('\n').map((l) => (l.endsWith('\r') ? l.slice(0, -1) : l));
|
|
46
|
+
if (out.length > 0 && out[out.length - 1] === '')
|
|
47
|
+
out.pop();
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
const ALNUM = /[\p{Alphabetic}\p{N}]/u;
|
|
51
|
+
/** Matches Rust's `char::is_alphanumeric`. */
|
|
52
|
+
export const isAlphanumeric = (c) => ALNUM.test(c);
|
|
53
|
+
/** Characters allowed in a bare node/state/class identifier. */
|
|
54
|
+
export const isIdChar = (c) => isAlphanumeric(c) || c === '_';
|
|
55
|
+
const ENTITY_LOOKAHEAD = 10;
|
|
56
|
+
const NAMED_ENTITIES = {
|
|
57
|
+
lt: '<',
|
|
58
|
+
gt: '>',
|
|
59
|
+
amp: '&',
|
|
60
|
+
quot: '"',
|
|
61
|
+
apos: "'",
|
|
62
|
+
};
|
|
63
|
+
function decodeEntityBody(body) {
|
|
64
|
+
const named = NAMED_ENTITIES[body];
|
|
65
|
+
if (named !== undefined)
|
|
66
|
+
return named;
|
|
67
|
+
if (!body.startsWith('#'))
|
|
68
|
+
return null;
|
|
69
|
+
const num = body.slice(1);
|
|
70
|
+
const hex = /^[xX]/.test(num);
|
|
71
|
+
const digits = hex ? num.slice(1) : num;
|
|
72
|
+
if (!(hex ? /^[0-9a-fA-F]+$/ : /^[0-9]+$/).test(digits))
|
|
73
|
+
return null;
|
|
74
|
+
const code = Number.parseInt(digits, hex ? 16 : 10);
|
|
75
|
+
// Surrogates and out-of-range values are not characters at all.
|
|
76
|
+
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff))
|
|
77
|
+
return null;
|
|
78
|
+
// Reject control chars: NUL collides with the CONT sentinel and ESC would
|
|
79
|
+
// inject ANSI into scrollback.
|
|
80
|
+
if (code < 0x20 || (code >= 0x7f && code <= 0x9f))
|
|
81
|
+
return null;
|
|
82
|
+
return String.fromCodePoint(code);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Decode HTML entities in label text. Called once per label: via `cleanLabel`
|
|
86
|
+
* for bracketed labels, or explicitly at each direct-push sink.
|
|
87
|
+
*/
|
|
88
|
+
export function decodeHtmlEntities(s) {
|
|
89
|
+
if (!s.includes('&'))
|
|
90
|
+
return s;
|
|
91
|
+
const chars = [...s];
|
|
92
|
+
let out = '';
|
|
93
|
+
let i = 0;
|
|
94
|
+
while (i < chars.length) {
|
|
95
|
+
if (chars[i] !== '&') {
|
|
96
|
+
out += chars[i];
|
|
97
|
+
i++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Scan a bounded window including the terminating `;`, so a stray `&` or an
|
|
101
|
+
// over-long run stays literal.
|
|
102
|
+
const hi = Math.min(i + 1 + ENTITY_LOOKAHEAD, chars.length);
|
|
103
|
+
let semi = -1;
|
|
104
|
+
for (let j = i + 1; j < hi; j++) {
|
|
105
|
+
if (chars[j] === ';') {
|
|
106
|
+
semi = j;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const decoded = semi === -1 ? null : decodeEntityBody(chars.slice(i + 1, semi).join(''));
|
|
111
|
+
if (decoded === null) {
|
|
112
|
+
out += '&';
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// Resume past the `;`. The single pass never re-scans emitted text, so
|
|
117
|
+
// `&lt;` decodes to the literal `<` rather than to `<`.
|
|
118
|
+
out += decoded;
|
|
119
|
+
i = semi + 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/** Strip markdown emphasis from a `` `backtick` `` label string. */
|
|
125
|
+
export function stripMarkdown(s) {
|
|
126
|
+
const noCode = [...s].filter((c) => c !== '`').join('');
|
|
127
|
+
const noStrong = noCode.replaceAll('**', '').replaceAll('__', '');
|
|
128
|
+
const chars = [...noStrong];
|
|
129
|
+
let out = '';
|
|
130
|
+
for (let i = 0; i < chars.length; i++) {
|
|
131
|
+
const c = chars[i];
|
|
132
|
+
// Keep `*`/`_` only when they sit inside a word, so snake_case survives.
|
|
133
|
+
const inWord = i > 0 &&
|
|
134
|
+
isAlphanumeric(chars[i - 1]) &&
|
|
135
|
+
chars[i + 1] !== undefined &&
|
|
136
|
+
isAlphanumeric(chars[i + 1]);
|
|
137
|
+
if ((c === '*' || c === '_') && !inWord)
|
|
138
|
+
continue;
|
|
139
|
+
out += c;
|
|
140
|
+
}
|
|
141
|
+
return out.trim();
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Inline formatting tags that carry no meaning in a terminal. Anything else
|
|
145
|
+
* that looks like a tag — `Vec<String>`, `<id>` — is left alone.
|
|
146
|
+
*/
|
|
147
|
+
const HTML_FORMAT_TAGS = new Set([
|
|
148
|
+
'b',
|
|
149
|
+
'strong',
|
|
150
|
+
'i',
|
|
151
|
+
'em',
|
|
152
|
+
'u',
|
|
153
|
+
's',
|
|
154
|
+
'strike',
|
|
155
|
+
'del',
|
|
156
|
+
'ins',
|
|
157
|
+
'mark',
|
|
158
|
+
'small',
|
|
159
|
+
'big',
|
|
160
|
+
'sub',
|
|
161
|
+
'sup',
|
|
162
|
+
'code',
|
|
163
|
+
'kbd',
|
|
164
|
+
'samp',
|
|
165
|
+
'var',
|
|
166
|
+
'tt',
|
|
167
|
+
'span',
|
|
168
|
+
'font',
|
|
169
|
+
'q',
|
|
170
|
+
'abbr',
|
|
171
|
+
'cite',
|
|
172
|
+
'pre',
|
|
173
|
+
]);
|
|
174
|
+
/** Read a tag starting at `start`, returning its name and the index after `>`. */
|
|
175
|
+
function htmlTagAt(chars, start) {
|
|
176
|
+
let i = start + 1;
|
|
177
|
+
if (chars[i] === '/')
|
|
178
|
+
i++;
|
|
179
|
+
const nameStart = i;
|
|
180
|
+
while (i < chars.length && /^[0-9A-Za-z]$/.test(chars[i]))
|
|
181
|
+
i++;
|
|
182
|
+
if (i === nameStart)
|
|
183
|
+
return null;
|
|
184
|
+
const name = chars.slice(nameStart, i).join('');
|
|
185
|
+
while (i < chars.length && chars[i] !== '>') {
|
|
186
|
+
if (chars[i] === '<')
|
|
187
|
+
return null;
|
|
188
|
+
i++;
|
|
189
|
+
}
|
|
190
|
+
return chars[i] === '>' ? { name, end: i + 1 } : null;
|
|
191
|
+
}
|
|
192
|
+
export function stripHtmlTags(s) {
|
|
193
|
+
const chars = [...s];
|
|
194
|
+
let out = '';
|
|
195
|
+
let i = 0;
|
|
196
|
+
while (i < chars.length) {
|
|
197
|
+
if (chars[i] === '<') {
|
|
198
|
+
const tag = htmlTagAt(chars, i);
|
|
199
|
+
if (tag) {
|
|
200
|
+
const lower = tag.name.toLowerCase();
|
|
201
|
+
if (lower === 'br') {
|
|
202
|
+
out += ' ';
|
|
203
|
+
i = tag.end;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (HTML_FORMAT_TAGS.has(lower)) {
|
|
207
|
+
i = tag.end;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
out += chars[i];
|
|
213
|
+
i++;
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
/** Strip one matching pair of wrapping delimiters, if present. */
|
|
218
|
+
function unwrap(s, open, close) {
|
|
219
|
+
return s.length >= open.length + close.length && s.startsWith(open) && s.endsWith(close)
|
|
220
|
+
? s.slice(open.length, s.length - close.length)
|
|
221
|
+
: null;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Normalise raw label text: strip markup, unquote, and decode entities.
|
|
225
|
+
*
|
|
226
|
+
* Decoding happens after tag-stripping so `<b>` is removed as markup while
|
|
227
|
+
* `<b>` survives as the literal text `<b>`.
|
|
228
|
+
*/
|
|
229
|
+
export function cleanLabel(raw) {
|
|
230
|
+
const trimmed = stripHtmlTags(raw.trim()).trim();
|
|
231
|
+
const unquoted = (unwrap(trimmed, '"', '"') ?? unwrap(trimmed, "'", "'") ?? trimmed).trim();
|
|
232
|
+
const md = unwrap(unquoted, '`', '`');
|
|
233
|
+
return decodeHtmlEntities(md === null ? unquoted : stripMarkdown(md.trim()));
|
|
234
|
+
}
|
|
235
|
+
/** Index of the last identifier-boundary character, or -1. */
|
|
236
|
+
function lastBreak(s) {
|
|
237
|
+
let best = -1;
|
|
238
|
+
for (const c of LABEL_BREAK_CHARS)
|
|
239
|
+
best = Math.max(best, s.lastIndexOf(c));
|
|
240
|
+
return best;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Wrap a label to `width` columns over at most `maxLines` lines, truncating the
|
|
244
|
+
* last line with an ellipsis if it overflows.
|
|
245
|
+
*
|
|
246
|
+
* A word too wide to fit is broken after the last identifier boundary
|
|
247
|
+
* (`_-./`) that fits, falling back to a per-character break when it has none.
|
|
248
|
+
*/
|
|
249
|
+
export function wrapLabel(label, width, maxLines) {
|
|
250
|
+
width = Math.max(1, width);
|
|
251
|
+
const lines = [];
|
|
252
|
+
let cur = '';
|
|
253
|
+
let curW = 0;
|
|
254
|
+
for (const word of label.split(/\s+/).filter((w) => w !== '')) {
|
|
255
|
+
const ww = stringWidth(word);
|
|
256
|
+
if (ww > width) {
|
|
257
|
+
if (cur !== '') {
|
|
258
|
+
lines.push(cur);
|
|
259
|
+
cur = '';
|
|
260
|
+
}
|
|
261
|
+
let chunk = '';
|
|
262
|
+
let chunkW = 0;
|
|
263
|
+
for (const [ch, cw] of measured(word)) {
|
|
264
|
+
if (chunkW + cw > width && chunk !== '') {
|
|
265
|
+
const p = lastBreak(chunk);
|
|
266
|
+
const carry = p === -1 ? '' : chunk.slice(p + 1);
|
|
267
|
+
lines.push(p === -1 ? chunk : chunk.slice(0, p + 1));
|
|
268
|
+
chunk = carry;
|
|
269
|
+
chunkW = stringWidth(carry);
|
|
270
|
+
}
|
|
271
|
+
chunk += ch;
|
|
272
|
+
chunkW += cw;
|
|
273
|
+
}
|
|
274
|
+
cur = chunk;
|
|
275
|
+
curW = chunkW;
|
|
276
|
+
}
|
|
277
|
+
else if (cur === '') {
|
|
278
|
+
cur = word;
|
|
279
|
+
curW = ww;
|
|
280
|
+
}
|
|
281
|
+
else if (curW + 1 + ww <= width) {
|
|
282
|
+
cur += ` ${word}`;
|
|
283
|
+
curW += 1 + ww;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
lines.push(cur);
|
|
287
|
+
cur = word;
|
|
288
|
+
curW = ww;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (cur !== '')
|
|
292
|
+
lines.push(cur);
|
|
293
|
+
if (lines.length === 0)
|
|
294
|
+
lines.push('');
|
|
295
|
+
if (lines.length > maxLines) {
|
|
296
|
+
lines.length = maxLines;
|
|
297
|
+
const target = Math.max(1, width - 1);
|
|
298
|
+
let s = '';
|
|
299
|
+
let sw = 0;
|
|
300
|
+
for (const [ch, cw] of measured(lines[lines.length - 1])) {
|
|
301
|
+
if (sw + cw > target)
|
|
302
|
+
break;
|
|
303
|
+
s += ch;
|
|
304
|
+
sw += cw;
|
|
305
|
+
}
|
|
306
|
+
lines[lines.length - 1] = `${s}…`;
|
|
307
|
+
}
|
|
308
|
+
return lines;
|
|
309
|
+
}
|
|
310
|
+
/** Truncate to `inner` columns, leaving room for the ellipsis. */
|
|
311
|
+
export function fitLabel(label, inner) {
|
|
312
|
+
if (stringWidth(label) <= inner)
|
|
313
|
+
return label;
|
|
314
|
+
let out = '';
|
|
315
|
+
let used = 0;
|
|
316
|
+
for (const [c, cw] of measured(label)) {
|
|
317
|
+
if (used + cw + 1 > inner)
|
|
318
|
+
break;
|
|
319
|
+
out += c;
|
|
320
|
+
used += cw;
|
|
321
|
+
}
|
|
322
|
+
return `${out}…`;
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=labels.js.map
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sequence diagram layout.
|
|
3
|
+
*
|
|
4
|
+
* Participants get one column each, with lifelines running the full height and
|
|
5
|
+
* a box repeated at top and bottom. Column gaps are solved from the widest
|
|
6
|
+
* thing that has to fit between any two columns — a message label, a note, a
|
|
7
|
+
* self-message stub — then items stack down the canvas in source order.
|
|
8
|
+
*/
|
|
9
|
+
import { Canvas, D, drawTextOverEdges, L, R, U } from './canvas.js';
|
|
10
|
+
import { fitLabel, WRAP_WIDTH } from './labels.js';
|
|
11
|
+
import { drawBox } from './layout.js';
|
|
12
|
+
import { stringWidth } from './width.js';
|
|
13
|
+
const PAD = 1;
|
|
14
|
+
/** Minimum columns between adjacent lifelines. */
|
|
15
|
+
const SEQ_GAP = 5;
|
|
16
|
+
const MAX_CANVAS_CELLS = 1 << 21;
|
|
17
|
+
const sat = (a, b) => Math.max(0, a - b);
|
|
18
|
+
const half = (n) => Math.floor(n / 2);
|
|
19
|
+
/** Where a note box sits, given the lifeline positions. */
|
|
20
|
+
function noteGeometry(xs, anchor, textW) {
|
|
21
|
+
if (anchor.kind === 'over') {
|
|
22
|
+
const center = half(xs[anchor.from] + xs[anchor.to]);
|
|
23
|
+
const w = Math.max(xs[anchor.to] - xs[anchor.from] + 5, textW + 2 * PAD + 2);
|
|
24
|
+
return { x: sat(center, half(w)), w };
|
|
25
|
+
}
|
|
26
|
+
const w = textW + 2 * PAD + 2;
|
|
27
|
+
if (anchor.kind === 'left')
|
|
28
|
+
return { x: sat(xs[anchor.at], 2 + w - 1), w };
|
|
29
|
+
return { x: xs[anchor.at] + 2, w };
|
|
30
|
+
}
|
|
31
|
+
const itemTextW = (text) => (text === null ? 0 : stringWidth(text));
|
|
32
|
+
export function layoutSequence(seq) {
|
|
33
|
+
const n = seq.labels.length;
|
|
34
|
+
const labels = seq.labels.map((l) => fitLabel(l, WRAP_WIDTH));
|
|
35
|
+
const boxW = labels.map((l) => Math.max(1, stringWidth(l)) + 2 * PAD + 2);
|
|
36
|
+
const boxH = 3;
|
|
37
|
+
const gaps = Array.from({ length: sat(n, 1) }, (_, i) => Math.max(SEQ_GAP, Math.ceil(boxW[i] / 2) + Math.ceil(boxW[i + 1] / 2) + 1));
|
|
38
|
+
// Each requirement is "columns l..r together need at least `need` cells".
|
|
39
|
+
const reqs = [];
|
|
40
|
+
for (const item of seq.items) {
|
|
41
|
+
if (item.kind === 'message') {
|
|
42
|
+
const tw = itemTextW(item.text);
|
|
43
|
+
if (item.from !== item.to) {
|
|
44
|
+
reqs.push([Math.min(item.from, item.to), Math.max(item.from, item.to), Math.max(tw + 2, 4)]);
|
|
45
|
+
}
|
|
46
|
+
else if (item.from + 1 < n) {
|
|
47
|
+
reqs.push([item.from, item.from + 1, 5 + tw + 2]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (item.kind === 'note') {
|
|
51
|
+
const tw = stringWidth(item.text);
|
|
52
|
+
const a = item.anchor;
|
|
53
|
+
if (a.kind === 'over' && a.from < a.to) {
|
|
54
|
+
reqs.push([a.from, a.to, sat(tw, 1)]);
|
|
55
|
+
}
|
|
56
|
+
else if (a.kind === 'over') {
|
|
57
|
+
const need = Math.ceil((tw + 4) / 2) + 2;
|
|
58
|
+
if (a.from > 0)
|
|
59
|
+
reqs.push([a.from - 1, a.from, need]);
|
|
60
|
+
if (a.from + 1 < n)
|
|
61
|
+
reqs.push([a.from, a.from + 1, need]);
|
|
62
|
+
}
|
|
63
|
+
else if (a.kind === 'left' && a.at > 0) {
|
|
64
|
+
reqs.push([a.at - 1, a.at, tw + 7]);
|
|
65
|
+
}
|
|
66
|
+
else if (a.kind === 'right' && a.at + 1 < n) {
|
|
67
|
+
reqs.push([a.at, a.at + 1, tw + 7]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Narrowest spans first, so a wide requirement absorbs what they already gave.
|
|
72
|
+
reqs.sort((a, b) => a[1] - a[0] - (b[1] - b[0]));
|
|
73
|
+
for (const [l, r, need] of reqs) {
|
|
74
|
+
let cur = 0;
|
|
75
|
+
for (let i = l; i < r; i++)
|
|
76
|
+
cur += gaps[i];
|
|
77
|
+
if (cur < need)
|
|
78
|
+
gaps[r - 1] += need - cur;
|
|
79
|
+
}
|
|
80
|
+
const xs = new Array(n);
|
|
81
|
+
xs[0] = half(boxW[0]);
|
|
82
|
+
for (let i = 1; i < n; i++)
|
|
83
|
+
xs[i] = xs[i - 1] + gaps[i - 1];
|
|
84
|
+
let canvasW = xs[n - 1] + Math.ceil(boxW[n - 1] / 2) + 1;
|
|
85
|
+
for (const item of seq.items) {
|
|
86
|
+
if (item.kind === 'message' && item.from === item.to) {
|
|
87
|
+
canvasW = Math.max(canvasW, xs[item.from] + 5 + itemTextW(item.text) + 1);
|
|
88
|
+
}
|
|
89
|
+
else if (item.kind === 'note') {
|
|
90
|
+
const g = noteGeometry(xs, item.anchor, stringWidth(item.text));
|
|
91
|
+
canvasW = Math.max(canvasW, g.x + g.w + 1);
|
|
92
|
+
}
|
|
93
|
+
else if (item.kind === 'divider') {
|
|
94
|
+
canvasW = Math.max(canvasW, stringWidth(item.text) + 4);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const rows = [];
|
|
98
|
+
let y = boxH + 1;
|
|
99
|
+
for (const item of seq.items) {
|
|
100
|
+
rows.push(y);
|
|
101
|
+
y += rowHeight(item);
|
|
102
|
+
}
|
|
103
|
+
const bottomTop = y;
|
|
104
|
+
const canvasH = bottomTop + boxH;
|
|
105
|
+
if (canvasW * canvasH > MAX_CANVAS_CELLS)
|
|
106
|
+
return null;
|
|
107
|
+
const canvas = new Canvas(canvasW, canvasH);
|
|
108
|
+
for (let i = 0; i < n; i++) {
|
|
109
|
+
for (const by of [0, bottomTop]) {
|
|
110
|
+
drawBox(canvas, box(sat(xs[i], half(boxW[i])), by, boxW[i], boxH), [labels[i]], 'rect');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
seq.items.forEach((item, k) => {
|
|
114
|
+
if (item.kind !== 'note')
|
|
115
|
+
return;
|
|
116
|
+
const g = noteGeometry(xs, item.anchor, stringWidth(item.text));
|
|
117
|
+
drawBox(canvas, box(g.x, rows[k], g.w, 3), [item.text], 'rect');
|
|
118
|
+
});
|
|
119
|
+
for (const x of xs) {
|
|
120
|
+
canvas.junction(x, boxH - 1, D);
|
|
121
|
+
canvas.segV(x, boxH, bottomTop - 1);
|
|
122
|
+
canvas.junction(x, bottomTop, U);
|
|
123
|
+
}
|
|
124
|
+
seq.items.forEach((item, k) => {
|
|
125
|
+
const r = rows[k];
|
|
126
|
+
if (item.kind === 'message')
|
|
127
|
+
drawMessage(canvas, item, xs, r);
|
|
128
|
+
else if (item.kind === 'divider')
|
|
129
|
+
drawDivider(canvas, item.text, r, canvasW);
|
|
130
|
+
});
|
|
131
|
+
canvas.finalizeMask();
|
|
132
|
+
return canvas;
|
|
133
|
+
}
|
|
134
|
+
function rowHeight(item) {
|
|
135
|
+
if (item.kind === 'note')
|
|
136
|
+
return 4;
|
|
137
|
+
if (item.kind === 'divider')
|
|
138
|
+
return 2;
|
|
139
|
+
if (item.from === item.to)
|
|
140
|
+
return 4;
|
|
141
|
+
return item.text !== null ? 3 : 2;
|
|
142
|
+
}
|
|
143
|
+
/** Geometry for a box drawn by position and size; ranks are irrelevant here. */
|
|
144
|
+
const box = (x, y, w, h) => ({
|
|
145
|
+
x,
|
|
146
|
+
y,
|
|
147
|
+
w,
|
|
148
|
+
h,
|
|
149
|
+
cx: x + half(w),
|
|
150
|
+
cy: y + 1,
|
|
151
|
+
rank: 0,
|
|
152
|
+
});
|
|
153
|
+
function drawMessage(canvas, item, xs, r) {
|
|
154
|
+
const lineCh = item.dashed ? '╌' : '─';
|
|
155
|
+
if (item.from === item.to) {
|
|
156
|
+
// A stub that leaves the lifeline and returns two rows down.
|
|
157
|
+
const x = xs[item.from];
|
|
158
|
+
canvas.junction(x, r, R);
|
|
159
|
+
canvas.set(x + 1, r, lineCh, 'edge');
|
|
160
|
+
canvas.set(x + 2, r, lineCh, 'edge');
|
|
161
|
+
canvas.set(x + 3, r, '╮', 'edge');
|
|
162
|
+
canvas.set(x + 3, r + 1, '│', 'edge');
|
|
163
|
+
canvas.set(x + 1, r + 2, item.head === 'cross' ? '×' : '◄', 'edge');
|
|
164
|
+
canvas.set(x + 2, r + 2, lineCh, 'edge');
|
|
165
|
+
canvas.set(x + 3, r + 2, '╯', 'edge');
|
|
166
|
+
if (item.text !== null)
|
|
167
|
+
drawTextOverEdges(canvas, item.text, x + 5, r + 1, 'text');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const x0 = xs[item.from];
|
|
171
|
+
const x1 = xs[item.to];
|
|
172
|
+
const rightward = x1 > x0;
|
|
173
|
+
// A labelled message writes its text on `r` and draws the arrow below it.
|
|
174
|
+
const arrowRow = item.text !== null ? r + 1 : r;
|
|
175
|
+
const lo = Math.min(x0, x1);
|
|
176
|
+
const hi = Math.max(x0, x1);
|
|
177
|
+
canvas.junction(x0, arrowRow, rightward ? R : L);
|
|
178
|
+
for (let x = lo + 1; x < hi; x++)
|
|
179
|
+
canvas.set(x, arrowRow, lineCh, 'edge');
|
|
180
|
+
const headCh = item.head === 'cross' ? '×' : rightward ? '▶' : '◄';
|
|
181
|
+
canvas.set(rightward ? x1 - 1 : x1 + 1, arrowRow, headCh, 'edge');
|
|
182
|
+
if (item.text !== null) {
|
|
183
|
+
const span = hi - lo - 1;
|
|
184
|
+
const t = fitLabel(item.text, Math.max(1, span));
|
|
185
|
+
drawTextOverEdges(canvas, t, lo + 1 + half(sat(span, stringWidth(t))), r, 'text');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** A full-width rule labelling a `loop` / `alt` / `opt` block boundary. */
|
|
189
|
+
function drawDivider(canvas, text, r, canvasW) {
|
|
190
|
+
for (let x = 0; x < canvasW; x++)
|
|
191
|
+
canvas.set(x, r, '─', 'edge');
|
|
192
|
+
drawTextOverEdges(canvas, ` ${fitLabel(text, sat(canvasW, 4))} `, 2, r, 'edgeLabel');
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=layout-seq.js.map
|