@hncode/hncode 0.5.6 → 0.6.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/package.json +2 -3
- package/src/agent.js +67 -2
- package/src/clipboard.js +221 -33
- package/src/image-row.js +111 -0
- package/src/latex.js +1557 -0
- package/src/llm.js +19 -4
- package/src/mermaid/ansi.js +55 -0
- package/src/mermaid/canvas.js +449 -0
- package/src/mermaid/class-style.js +57 -0
- package/src/mermaid/css-colors.js +153 -0
- package/src/mermaid/diagrams/class.js +289 -0
- package/src/mermaid/diagrams/er.js +201 -0
- package/src/mermaid/diagrams/flowchart.js +488 -0
- package/src/mermaid/diagrams/gitgraph.js +205 -0
- package/src/mermaid/diagrams/mindmap.js +101 -0
- package/src/mermaid/diagrams/pie.js +104 -0
- package/src/mermaid/diagrams/sequence.js +282 -0
- package/src/mermaid/diagrams/state.js +292 -0
- package/src/mermaid/diagrams/timeline.js +99 -0
- package/src/mermaid/graph.js +138 -0
- package/src/mermaid/index.js +59 -0
- package/src/mermaid/labels.js +339 -0
- package/src/mermaid/layout-seq.js +225 -0
- package/src/mermaid/layout.js +1454 -0
- package/src/mermaid/registry.js +54 -0
- package/src/mermaid/source-box.js +102 -0
- package/src/mermaid/statements.js +235 -0
- package/src/mermaid/types.js +1 -0
- package/src/mermaid/width.js +29 -0
- package/src/screen-diff.js +512 -0
- package/src/screen-paint.js +128 -0
- package/src/screen.js +277 -0
- package/src/subagent-types.js +136 -147
- package/src/term-caps.js +10 -4
- package/src/tex/font.js +292 -0
- package/src/tex/fonts/LICENSE.txt +11 -0
- package/src/tex/fonts/STIX2Math.ttf +0 -0
- package/src/tex/index.js +293 -0
- package/src/tex/kitty.js +186 -0
- package/src/tex/layout.js +446 -0
- package/src/tex/parse.js +331 -0
- package/src/tex/raster.js +216 -0
- package/src/tex/render.js +167 -0
- package/src/tex/sixel.js +326 -0
- package/src/tex/svg.js +237 -0
- package/src/tool-result.js +2 -2
- package/src/tools/agent-swarm.js +3 -15
- package/src/tools/agent.js +454 -464
- package/src/tools/ask-user-question.js +195 -206
- package/src/tools/bash.js +39 -3
- package/src/tools/edit.js +10 -112
- package/src/tools/fetch-url.js +130 -120
- package/src/tools/goal.js +93 -0
- package/src/tools/index.js +2 -0
- package/src/tools/memory.js +80 -88
- package/src/tools/read.js +27 -23
- package/src/tools/todo.js +8 -13
- package/src/tools/validate-args.js +78 -0
- package/src/tui.js +1718 -585
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mindmap`: an indentation tree, drawn the way every TUI draws trees
|
|
3
|
+
* (`├──`/`└──` guides). Parses raw lines rather than statements — the
|
|
4
|
+
* indentation IS the grammar, and `statementsOf` trims it away.
|
|
5
|
+
*/
|
|
6
|
+
import { Canvas, drawText } from '../canvas.js';
|
|
7
|
+
import { MAX_NODES } from '../graph.js';
|
|
8
|
+
import { cleanLabel, fitLabel, srcLines, WRAP_WIDTH } from '../labels.js';
|
|
9
|
+
import { frontmatterEnd } from '../statements.js';
|
|
10
|
+
import { stringWidth } from '../width.js';
|
|
11
|
+
export const mindmap = {
|
|
12
|
+
kind: 'mindmap',
|
|
13
|
+
headers: ['mindmap'],
|
|
14
|
+
render(src) {
|
|
15
|
+
const parsed = parseMindmap(src);
|
|
16
|
+
if (parsed === null)
|
|
17
|
+
return null;
|
|
18
|
+
const { roots, warnings } = parsed;
|
|
19
|
+
/** [prefix, text] per row; prefixes carry the `│ ├ └` guides. */
|
|
20
|
+
const rows = [];
|
|
21
|
+
const walk = (node, prefix, childPrefix) => {
|
|
22
|
+
rows.push([prefix, node.text]);
|
|
23
|
+
node.children.forEach((child, i) => {
|
|
24
|
+
const last = i === node.children.length - 1;
|
|
25
|
+
walk(child, childPrefix + (last ? '└── ' : '├── '), childPrefix + (last ? ' ' : '│ '));
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
for (const root of roots)
|
|
29
|
+
walk(root, '', '');
|
|
30
|
+
const width = Math.max(...rows.map(([p, t]) => stringWidth(p) + stringWidth(t)));
|
|
31
|
+
const canvas = new Canvas(width, rows.length);
|
|
32
|
+
rows.forEach(([prefix, text], y) => {
|
|
33
|
+
drawText(canvas, prefix, 0, y, 'edge');
|
|
34
|
+
drawText(canvas, text, stringWidth(prefix), y, 'text');
|
|
35
|
+
});
|
|
36
|
+
return { canvas, warnings, classDefs: {} };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
function parseMindmap(src) {
|
|
40
|
+
const lines = srcLines(src).slice(frontmatterEnd(srcLines(src)));
|
|
41
|
+
const headerAt = lines.findIndex((l) => l.trim() !== '');
|
|
42
|
+
if (headerAt === -1 || lines[headerAt].trim().toLowerCase() !== 'mindmap')
|
|
43
|
+
return null;
|
|
44
|
+
const roots = [];
|
|
45
|
+
const warnings = [];
|
|
46
|
+
/** Ancestors of the next node: the node at each indent level seen so far. */
|
|
47
|
+
const stack = [];
|
|
48
|
+
let count = 0;
|
|
49
|
+
let truncated = false;
|
|
50
|
+
for (const raw of lines.slice(headerAt + 1)) {
|
|
51
|
+
const noComment = raw.split('%%')[0];
|
|
52
|
+
if (noComment.trim() === '')
|
|
53
|
+
continue;
|
|
54
|
+
const indent = noComment.length - noComment.trimStart().length;
|
|
55
|
+
const body = noComment.trim();
|
|
56
|
+
// Decoration lines attach to the previous node and draw nothing.
|
|
57
|
+
if (body.startsWith('::icon') || body.startsWith(':::'))
|
|
58
|
+
continue;
|
|
59
|
+
const text = nodeText(body);
|
|
60
|
+
if (text === '') {
|
|
61
|
+
warnings.push(`dropped, unreadable statement: "${body}"`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (count >= MAX_NODES) {
|
|
65
|
+
truncated = true;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
count++;
|
|
69
|
+
const node = { text: fitLabel(text, WRAP_WIDTH), children: [] };
|
|
70
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent)
|
|
71
|
+
stack.pop();
|
|
72
|
+
if (stack.length === 0)
|
|
73
|
+
roots.push(node);
|
|
74
|
+
else
|
|
75
|
+
stack[stack.length - 1].node.children.push(node);
|
|
76
|
+
stack.push({ indent, node });
|
|
77
|
+
}
|
|
78
|
+
if (truncated)
|
|
79
|
+
warnings.push(`diagram truncated: node cap (${MAX_NODES}) reached`);
|
|
80
|
+
return roots.length === 0 ? null : { roots, warnings };
|
|
81
|
+
}
|
|
82
|
+
/** Shape brackets around a mindmap node all mean "text" in a terminal. */
|
|
83
|
+
const SHAPES = [
|
|
84
|
+
['((', '))'],
|
|
85
|
+
['))', '(('],
|
|
86
|
+
['(-', '-)'],
|
|
87
|
+
['{{', '}}'],
|
|
88
|
+
['[', ']'],
|
|
89
|
+
['(', ')'],
|
|
90
|
+
];
|
|
91
|
+
function nodeText(body) {
|
|
92
|
+
// `id((text))` — an optional id may precede the bracket.
|
|
93
|
+
for (const [open, close] of SHAPES) {
|
|
94
|
+
const at = body.indexOf(open);
|
|
95
|
+
if (at !== -1 && body.endsWith(close) && body.length > at + open.length + close.length - 1) {
|
|
96
|
+
return cleanLabel(body.slice(at + open.length, body.length - close.length));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return cleanLabel(body);
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=mindmap.js.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pie`: proportions as a labelled bar list. A terminal has no circle worth
|
|
3
|
+
* drawing; bars carry the same information in less space and align with how
|
|
4
|
+
* every TUI shows usage. Lenient: an unreadable statement is dropped and
|
|
5
|
+
* recorded in `warnings`.
|
|
6
|
+
*/
|
|
7
|
+
import { Canvas, drawText } from '../canvas.js';
|
|
8
|
+
import { MAX_NODES } from '../graph.js';
|
|
9
|
+
import { cleanLabel, fitLabel } from '../labels.js';
|
|
10
|
+
import { headerKind, nonEmpty, quoteMask, statementsOf, words } from '../statements.js';
|
|
11
|
+
import { stringWidth } from '../width.js';
|
|
12
|
+
/** Columns of the full-scale bar; eighth blocks refine below one cell. */
|
|
13
|
+
const BAR_W = 20;
|
|
14
|
+
const EIGHTHS = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉'];
|
|
15
|
+
export const pie = {
|
|
16
|
+
kind: 'pie',
|
|
17
|
+
headers: ['pie'],
|
|
18
|
+
render(src) {
|
|
19
|
+
const parsed = parsePie(src);
|
|
20
|
+
if (parsed === null)
|
|
21
|
+
return null;
|
|
22
|
+
const { title, slices, showData, warnings } = parsed;
|
|
23
|
+
const labelW = Math.max(...slices.map((s) => stringWidth(s.label)));
|
|
24
|
+
const total = slices.reduce((sum, s) => sum + s.value, 0);
|
|
25
|
+
const rows = slices.map((s) => {
|
|
26
|
+
const share = total === 0 ? 0 : s.value / total;
|
|
27
|
+
const pct = `${Math.round(share * 100)}%`.padStart(4);
|
|
28
|
+
const data = showData ? ` (${s.value})` : '';
|
|
29
|
+
return { ...s, share, suffix: pct + data };
|
|
30
|
+
});
|
|
31
|
+
const barX = labelW + 2;
|
|
32
|
+
const suffixX = barX + BAR_W + 1;
|
|
33
|
+
const width = suffixX + Math.max(...rows.map((r) => stringWidth(r.suffix)));
|
|
34
|
+
const top = title === null ? 0 : 1;
|
|
35
|
+
const canvas = new Canvas(width, top + rows.length);
|
|
36
|
+
if (title !== null) {
|
|
37
|
+
drawText(canvas, title, Math.max(0, Math.floor((width - stringWidth(title)) / 2)), 0, 'title');
|
|
38
|
+
}
|
|
39
|
+
rows.forEach((r, i) => {
|
|
40
|
+
const y = top + i;
|
|
41
|
+
drawText(canvas, r.label, 0, y, 'text');
|
|
42
|
+
const eighths = Math.round(r.share * BAR_W * 8);
|
|
43
|
+
let bar = '█'.repeat(Math.floor(eighths / 8)) + EIGHTHS[eighths % 8];
|
|
44
|
+
// A nonzero slice always shows at least a sliver.
|
|
45
|
+
if (bar === '' && r.value > 0)
|
|
46
|
+
bar = '▏';
|
|
47
|
+
drawText(canvas, bar, barX, y, 'edge');
|
|
48
|
+
// The unfilled remainder is a track, so every bar shows its full scale.
|
|
49
|
+
drawText(canvas, '░'.repeat(BAR_W - stringWidth(bar)), barX + stringWidth(bar), y, 'border');
|
|
50
|
+
drawText(canvas, r.suffix, suffixX, y, 'edgeLabel');
|
|
51
|
+
});
|
|
52
|
+
return { canvas, warnings, classDefs: {} };
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
function parsePie(src) {
|
|
56
|
+
const statements = statementsOf(src);
|
|
57
|
+
if (headerKind(statements) !== 'pie')
|
|
58
|
+
return null;
|
|
59
|
+
// The header line may carry `showData` and an inline `title ...`.
|
|
60
|
+
const head = words(statements[0]);
|
|
61
|
+
const showData = head.some((w) => w.toLowerCase() === 'showdata');
|
|
62
|
+
const inlineTitle = head.findIndex((w) => w.toLowerCase() === 'title');
|
|
63
|
+
let title = inlineTitle === -1 ? null : nonEmpty(head.slice(inlineTitle + 1).join(' '));
|
|
64
|
+
const slices = [];
|
|
65
|
+
const warnings = [];
|
|
66
|
+
let truncated = false;
|
|
67
|
+
for (const st of statements.slice(1)) {
|
|
68
|
+
const first = words(st)[0]?.toLowerCase();
|
|
69
|
+
if (first === 'title') {
|
|
70
|
+
title = nonEmpty(st.slice(st.toLowerCase().indexOf('title') + 5).trim());
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const slice = parseSlice(st);
|
|
74
|
+
if (slice === null) {
|
|
75
|
+
warnings.push(`dropped, unreadable statement: "${st}"`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (slices.length >= MAX_NODES) {
|
|
79
|
+
truncated = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
slices.push(slice);
|
|
83
|
+
}
|
|
84
|
+
if (truncated)
|
|
85
|
+
warnings.push(`diagram truncated: slice cap (${MAX_NODES}) reached`);
|
|
86
|
+
return slices.length === 0 ? null : { title, slices, showData, warnings };
|
|
87
|
+
}
|
|
88
|
+
/** `"Label" : 42.5` — the label may be unquoted as long as it has no colon. */
|
|
89
|
+
function parseSlice(st) {
|
|
90
|
+
const chars = [...st];
|
|
91
|
+
const quoted = quoteMask(chars);
|
|
92
|
+
const colon = chars.findIndex((c, i) => c === ':' && !quoted[i]);
|
|
93
|
+
if (colon === -1)
|
|
94
|
+
return null;
|
|
95
|
+
const label = fitLabel(cleanLabel(chars.slice(0, colon).join('')), 24);
|
|
96
|
+
const value = Number(chars
|
|
97
|
+
.slice(colon + 1)
|
|
98
|
+
.join('')
|
|
99
|
+
.trim());
|
|
100
|
+
if (label === '' || !Number.isFinite(value) || value < 0)
|
|
101
|
+
return null;
|
|
102
|
+
return { label, value };
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=pie.js.map
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sequenceDiagram`: participants, messages, notes and block dividers.
|
|
3
|
+
* Lenient: an unreadable statement is dropped and recorded in `warnings`.
|
|
4
|
+
*
|
|
5
|
+
* Sequence diagrams have their own model — participants in declaration order
|
|
6
|
+
* plus a flat list of items — and their own geometry in `layout-seq.ts`.
|
|
7
|
+
*/
|
|
8
|
+
import { MAX_EDGES, MAX_NODES } from '../graph.js';
|
|
9
|
+
import { asciiLower, cleanLabel, decodeHtmlEntities } from '../labels.js';
|
|
10
|
+
import { layoutSequence } from '../layout-seq.js';
|
|
11
|
+
import { firstWord, headerKind, nonEmpty, splitOnce, statementsOf } from '../statements.js';
|
|
12
|
+
export const sequence = {
|
|
13
|
+
kind: 'sequence',
|
|
14
|
+
headers: ['sequencediagram'],
|
|
15
|
+
render(src) {
|
|
16
|
+
const seq = parseSequence(src);
|
|
17
|
+
if (seq === null)
|
|
18
|
+
return null;
|
|
19
|
+
const canvas = layoutSequence(seq);
|
|
20
|
+
if (canvas === null)
|
|
21
|
+
return null;
|
|
22
|
+
return { canvas, warnings: seq.warnings, classDefs: {} };
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
/** Message operators, longest-first so `-->>` wins over `-->`. */
|
|
26
|
+
const SEQ_OPS = [
|
|
27
|
+
['-->>', true, 'arrow'],
|
|
28
|
+
['->>', false, 'arrow'],
|
|
29
|
+
['--x', true, 'cross'],
|
|
30
|
+
['-x', false, 'cross'],
|
|
31
|
+
['--)', true, 'arrow'],
|
|
32
|
+
['-)', false, 'arrow'],
|
|
33
|
+
['-->', true, 'arrow'],
|
|
34
|
+
['->', false, 'arrow'],
|
|
35
|
+
];
|
|
36
|
+
const MAX_SEQ_OP = 4;
|
|
37
|
+
export class Sequence {
|
|
38
|
+
labels = [];
|
|
39
|
+
index = new Map();
|
|
40
|
+
items = [];
|
|
41
|
+
activations = [];
|
|
42
|
+
/** Set when a size cap was hit; the parser stops and renders the prefix. */
|
|
43
|
+
truncated = null;
|
|
44
|
+
/** Statements the grammar could not read and dropped. */
|
|
45
|
+
warnings = [];
|
|
46
|
+
participant(id, label) {
|
|
47
|
+
const existing = this.index.get(id);
|
|
48
|
+
if (existing !== undefined) {
|
|
49
|
+
if (label !== null)
|
|
50
|
+
this.labels[existing] = label;
|
|
51
|
+
return existing;
|
|
52
|
+
}
|
|
53
|
+
if (this.labels.length >= MAX_NODES) {
|
|
54
|
+
this.truncated ??= `participant cap (${MAX_NODES}) reached`;
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
this.index.set(id, this.labels.length);
|
|
58
|
+
this.labels.push(label ?? id);
|
|
59
|
+
return this.labels.length - 1;
|
|
60
|
+
}
|
|
61
|
+
/** Append an item, or flag `truncated` when the item cap is reached. */
|
|
62
|
+
pushItem(item) {
|
|
63
|
+
if (this.items.length >= MAX_EDGES)
|
|
64
|
+
this.truncated ??= `item cap (${MAX_EDGES}) reached`;
|
|
65
|
+
else
|
|
66
|
+
this.items.push(item);
|
|
67
|
+
}
|
|
68
|
+
/** Record an unreadable statement; skipped once truncated. */
|
|
69
|
+
drop(st) {
|
|
70
|
+
if (this.truncated === null)
|
|
71
|
+
this.warnings.push(`dropped, unreadable statement: "${st}"`);
|
|
72
|
+
}
|
|
73
|
+
/** Open an activation at item `from`. */
|
|
74
|
+
activate(at, from) {
|
|
75
|
+
this.activations.push({ at, from, to: null });
|
|
76
|
+
}
|
|
77
|
+
/** Close the innermost open activation of `at` at item `to`. */
|
|
78
|
+
deactivate(at, to) {
|
|
79
|
+
for (let i = this.activations.length - 1; i >= 0; i--) {
|
|
80
|
+
const a = this.activations[i];
|
|
81
|
+
if (a.at === at && a.to === null) {
|
|
82
|
+
a.to = to;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function parseSequence(src) {
|
|
89
|
+
const statements = statementsOf(src);
|
|
90
|
+
const kind = headerKind(statements);
|
|
91
|
+
if (kind === null || !sequence.headers.includes(kind))
|
|
92
|
+
return null;
|
|
93
|
+
const seq = new Sequence();
|
|
94
|
+
let autonumber = false;
|
|
95
|
+
let msgCount = 0;
|
|
96
|
+
/** One entry per open block; `true` when it draws a divider on `end`. */
|
|
97
|
+
const blocks = [];
|
|
98
|
+
for (const st of statements.slice(1)) {
|
|
99
|
+
const first = firstWord(st);
|
|
100
|
+
const lower = asciiLower(first);
|
|
101
|
+
if (lower === 'participant' || lower === 'actor') {
|
|
102
|
+
const rest = st.slice(first.length).trim();
|
|
103
|
+
if (rest === '') {
|
|
104
|
+
seq.drop(st);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const as = splitOnce(rest, ' as ');
|
|
108
|
+
seq.participant(as ? as[0].trim() : rest, as ? cleanLabel(as[1]) : null);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (lower === 'autonumber') {
|
|
112
|
+
autonumber = true;
|
|
113
|
+
}
|
|
114
|
+
else if (lower === 'activate' || lower === 'deactivate') {
|
|
115
|
+
// Applies to the preceding message's row.
|
|
116
|
+
const who = seq.index.get(st.slice(first.length).trim());
|
|
117
|
+
if (who !== undefined && seq.items.length > 0) {
|
|
118
|
+
if (lower === 'activate')
|
|
119
|
+
seq.activate(who, seq.items.length - 1);
|
|
120
|
+
else
|
|
121
|
+
seq.deactivate(who, seq.items.length - 1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if ([
|
|
125
|
+
'create',
|
|
126
|
+
'destroy',
|
|
127
|
+
'title',
|
|
128
|
+
'acctitle',
|
|
129
|
+
'accdescr',
|
|
130
|
+
'links',
|
|
131
|
+
'link',
|
|
132
|
+
'properties',
|
|
133
|
+
].includes(lower)) {
|
|
134
|
+
// No layout meaning.
|
|
135
|
+
}
|
|
136
|
+
else if (lower === 'note') {
|
|
137
|
+
const note = parseNoteAnchor(st.slice(first.length).trim(), seq);
|
|
138
|
+
if (!note)
|
|
139
|
+
seq.drop(st);
|
|
140
|
+
else
|
|
141
|
+
seq.pushItem({ kind: 'note', anchor: note.anchor, text: note.text });
|
|
142
|
+
}
|
|
143
|
+
else if (['loop', 'alt', 'opt', 'par', 'critical', 'break', 'else', 'and', 'option'].includes(lower)) {
|
|
144
|
+
// A continuation only divides a block that opened one.
|
|
145
|
+
const continues = ['else', 'and', 'option'].includes(lower);
|
|
146
|
+
if (!continues)
|
|
147
|
+
blocks.push(true);
|
|
148
|
+
if (!continues || blocks.at(-1) === true) {
|
|
149
|
+
seq.pushItem({ kind: 'divider', text: decodeHtmlEntities(st) });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (lower === 'rect' || lower === 'box') {
|
|
153
|
+
blocks.push(false);
|
|
154
|
+
}
|
|
155
|
+
else if (lower === 'end') {
|
|
156
|
+
if (blocks.pop() === true)
|
|
157
|
+
seq.pushItem({ kind: 'divider', text: 'end' });
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
const msg = parseSeqMessage(st, seq);
|
|
161
|
+
if (!msg) {
|
|
162
|
+
seq.drop(st);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
let text = msg.text;
|
|
166
|
+
if (autonumber) {
|
|
167
|
+
msgCount++;
|
|
168
|
+
text = text === null ? `${msgCount}.` : `${msgCount}. ${text}`;
|
|
169
|
+
}
|
|
170
|
+
const item = seq.items.length;
|
|
171
|
+
seq.pushItem({
|
|
172
|
+
kind: 'message',
|
|
173
|
+
from: msg.from,
|
|
174
|
+
to: msg.to,
|
|
175
|
+
text,
|
|
176
|
+
dashed: msg.dashed,
|
|
177
|
+
head: msg.head,
|
|
178
|
+
});
|
|
179
|
+
// `+` activates the receiver on this row; `-` deactivates the sender.
|
|
180
|
+
// Only once the item was accepted — an activation pointing past the
|
|
181
|
+
// item cap would dereference a message that does not exist.
|
|
182
|
+
if (seq.items.length === item + 1) {
|
|
183
|
+
if (msg.marks.includes('+'))
|
|
184
|
+
seq.activate(msg.to, item);
|
|
185
|
+
if (msg.marks.includes('-'))
|
|
186
|
+
seq.deactivate(msg.from, item);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (seq.truncated !== null) {
|
|
191
|
+
seq.warnings.push(`diagram truncated: ${seq.truncated}`);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return seq.labels.length === 0 ? null : seq;
|
|
196
|
+
}
|
|
197
|
+
function parseNoteAnchor(rest, seq) {
|
|
198
|
+
const lower = asciiLower(rest);
|
|
199
|
+
let kind;
|
|
200
|
+
let idsAndText;
|
|
201
|
+
if (lower.startsWith('over ')) {
|
|
202
|
+
kind = 'over';
|
|
203
|
+
idsAndText = rest.slice('over '.length);
|
|
204
|
+
}
|
|
205
|
+
else if (lower.startsWith('left of ')) {
|
|
206
|
+
kind = 'left';
|
|
207
|
+
idsAndText = rest.slice('left of '.length);
|
|
208
|
+
}
|
|
209
|
+
else if (lower.startsWith('right of ')) {
|
|
210
|
+
kind = 'right';
|
|
211
|
+
idsAndText = rest.slice('right of '.length);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const split = splitOnce(idsAndText, ':');
|
|
217
|
+
if (!split)
|
|
218
|
+
return null;
|
|
219
|
+
const text = decodeHtmlEntities(split[1].trim());
|
|
220
|
+
const parts = split[0]
|
|
221
|
+
.split(',')
|
|
222
|
+
.map((s) => s.trim())
|
|
223
|
+
.filter((s) => s !== '');
|
|
224
|
+
if (parts.length === 0)
|
|
225
|
+
return null;
|
|
226
|
+
const a = seq.participant(parts[0], null);
|
|
227
|
+
if (a === null)
|
|
228
|
+
return null;
|
|
229
|
+
if (kind !== 'over')
|
|
230
|
+
return { text, anchor: { kind, at: a } };
|
|
231
|
+
let b = a;
|
|
232
|
+
if (parts[1] !== undefined) {
|
|
233
|
+
const second = seq.participant(parts[1], null);
|
|
234
|
+
if (second === null)
|
|
235
|
+
return null;
|
|
236
|
+
b = second;
|
|
237
|
+
}
|
|
238
|
+
return { text, anchor: { kind: 'over', from: Math.min(a, b), to: Math.max(a, b) } };
|
|
239
|
+
}
|
|
240
|
+
function parseSeqMessage(st, seq) {
|
|
241
|
+
const chars = [...st];
|
|
242
|
+
let found = null;
|
|
243
|
+
outer: for (let pos = 0; pos < chars.length; pos++) {
|
|
244
|
+
const tail = chars.slice(pos, pos + MAX_SEQ_OP).join('');
|
|
245
|
+
for (const [op, dashed, head] of SEQ_OPS) {
|
|
246
|
+
if (tail.startsWith(op)) {
|
|
247
|
+
// `-x` / `-)` embedded in a hyphenated token (`pre-x->>B`) is not an
|
|
248
|
+
// operator — the real one follows. Require a non-link char after.
|
|
249
|
+
const after = chars[pos + op.length];
|
|
250
|
+
if ((op.endsWith('x') || op.endsWith(')')) && (after === '-' || after === '>'))
|
|
251
|
+
continue;
|
|
252
|
+
found = { pos, op, dashed, head };
|
|
253
|
+
break outer;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!found)
|
|
258
|
+
return null;
|
|
259
|
+
const fromId = chars.slice(0, found.pos).join('').trim();
|
|
260
|
+
if (fromId === '')
|
|
261
|
+
return null;
|
|
262
|
+
// `+` activates the receiver, `-` deactivates the sender.
|
|
263
|
+
const afterOp = chars
|
|
264
|
+
.slice(found.pos + [...found.op].length)
|
|
265
|
+
.join('')
|
|
266
|
+
.trimStart();
|
|
267
|
+
const marks = afterOp.match(/^[+-]+/)?.[0] ?? '';
|
|
268
|
+
const rest = afterOp.slice(marks.length);
|
|
269
|
+
const split = splitOnce(rest, ':');
|
|
270
|
+
const toId = (split ? split[0] : rest).trim();
|
|
271
|
+
const text = split ? nonEmpty(decodeHtmlEntities(split[1].trim())) : null;
|
|
272
|
+
if (toId === '')
|
|
273
|
+
return null;
|
|
274
|
+
const from = seq.participant(fromId, null);
|
|
275
|
+
if (from === null)
|
|
276
|
+
return null;
|
|
277
|
+
const to = seq.participant(toId, null);
|
|
278
|
+
if (to === null)
|
|
279
|
+
return null;
|
|
280
|
+
return { from, to, text, dashed: found.dashed, head: found.head, marks };
|
|
281
|
+
}
|
|
282
|
+
//# sourceMappingURL=sequence.js.map
|