@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.
Files changed (59) hide show
  1. package/package.json +2 -3
  2. package/src/agent.js +67 -2
  3. package/src/clipboard.js +221 -33
  4. package/src/image-row.js +111 -0
  5. package/src/latex.js +1557 -0
  6. package/src/llm.js +19 -4
  7. package/src/mermaid/ansi.js +55 -0
  8. package/src/mermaid/canvas.js +449 -0
  9. package/src/mermaid/class-style.js +57 -0
  10. package/src/mermaid/css-colors.js +153 -0
  11. package/src/mermaid/diagrams/class.js +289 -0
  12. package/src/mermaid/diagrams/er.js +201 -0
  13. package/src/mermaid/diagrams/flowchart.js +488 -0
  14. package/src/mermaid/diagrams/gitgraph.js +205 -0
  15. package/src/mermaid/diagrams/mindmap.js +101 -0
  16. package/src/mermaid/diagrams/pie.js +104 -0
  17. package/src/mermaid/diagrams/sequence.js +282 -0
  18. package/src/mermaid/diagrams/state.js +292 -0
  19. package/src/mermaid/diagrams/timeline.js +99 -0
  20. package/src/mermaid/graph.js +138 -0
  21. package/src/mermaid/index.js +59 -0
  22. package/src/mermaid/labels.js +339 -0
  23. package/src/mermaid/layout-seq.js +225 -0
  24. package/src/mermaid/layout.js +1454 -0
  25. package/src/mermaid/registry.js +54 -0
  26. package/src/mermaid/source-box.js +102 -0
  27. package/src/mermaid/statements.js +235 -0
  28. package/src/mermaid/types.js +1 -0
  29. package/src/mermaid/width.js +29 -0
  30. package/src/screen-diff.js +512 -0
  31. package/src/screen-paint.js +128 -0
  32. package/src/screen.js +277 -0
  33. package/src/subagent-types.js +136 -147
  34. package/src/term-caps.js +10 -4
  35. package/src/tex/font.js +292 -0
  36. package/src/tex/fonts/LICENSE.txt +11 -0
  37. package/src/tex/fonts/STIX2Math.ttf +0 -0
  38. package/src/tex/index.js +293 -0
  39. package/src/tex/kitty.js +186 -0
  40. package/src/tex/layout.js +446 -0
  41. package/src/tex/parse.js +331 -0
  42. package/src/tex/raster.js +216 -0
  43. package/src/tex/render.js +167 -0
  44. package/src/tex/sixel.js +326 -0
  45. package/src/tex/svg.js +237 -0
  46. package/src/tool-result.js +2 -2
  47. package/src/tools/agent-swarm.js +3 -15
  48. package/src/tools/agent.js +454 -464
  49. package/src/tools/ask-user-question.js +195 -206
  50. package/src/tools/bash.js +39 -3
  51. package/src/tools/edit.js +10 -112
  52. package/src/tools/fetch-url.js +130 -120
  53. package/src/tools/goal.js +93 -0
  54. package/src/tools/index.js +2 -0
  55. package/src/tools/memory.js +80 -88
  56. package/src/tools/read.js +27 -23
  57. package/src/tools/todo.js +8 -13
  58. package/src/tools/validate-args.js +78 -0
  59. package/src/tui.js +1718 -585
@@ -0,0 +1,292 @@
1
+ /**
2
+ * `stateDiagram` / `stateDiagram-v2`: states, transitions, descriptions and
3
+ * composite states. Lenient: an unreadable statement is dropped and recorded
4
+ * in `warnings`.
5
+ *
6
+ * A composite (`state X { ... }`) becomes a `Group`, drawn as a titled frame
7
+ * by the same machinery flowchart subgraphs use; `--` splits a composite into
8
+ * unlabelled sibling region groups. `[*]` is scoped per group, so an inner
9
+ * start dot is a different node from the outer one.
10
+ */
11
+ import { Graph, MAX_GROUP_DEPTH, MAX_GROUPS, parseDir } from '../graph.js';
12
+ import { asciiLower, decodeHtmlEntities } from '../labels.js';
13
+ import { layoutFlowchart, layoutGrouped } from '../layout.js';
14
+ import { firstWord, headerKind, nonEmpty, parseClassAssign, parseClassDef, splitColon, splitOnce, statementsOf, takeTags, words, } from '../statements.js';
15
+ export const state = {
16
+ kind: 'state',
17
+ headers: ['statediagram', 'statediagram-v2'],
18
+ render(src) {
19
+ const graph = parseState(src);
20
+ if (graph === null)
21
+ return null;
22
+ const canvas = graph.groups.length === 0 ? layoutFlowchart(graph) : layoutGrouped(graph);
23
+ if (canvas === null)
24
+ return null;
25
+ return { canvas, warnings: graph.warnings, classDefs: graph.classDefs };
26
+ },
27
+ };
28
+ export function parseState(src) {
29
+ const statements = statementsOf(src);
30
+ const kind = headerKind(statements);
31
+ if (kind === null || !state.headers.includes(kind))
32
+ return null;
33
+ const graph = new Graph();
34
+ let inNote = false;
35
+ /** `class A,B name` assignments, applied after the walk. */
36
+ const classAssignments = [];
37
+ /** Open composites: the group itself and its current `--` region, if any. */
38
+ const stack = [];
39
+ /** New group under the current scope, or `null` once a cap is hit. */
40
+ const newGroup = (id, label) => {
41
+ if (graph.groups.length >= MAX_GROUPS || stack.length >= MAX_GROUP_DEPTH) {
42
+ graph.truncated ??= `subgraph cap (${MAX_GROUPS} groups, depth ${MAX_GROUP_DEPTH}) reached`;
43
+ return null;
44
+ }
45
+ graph.groups.push({ id, label, parent: graph.curGroup });
46
+ return graph.groups.length - 1;
47
+ };
48
+ const scopeOf = () => {
49
+ const top = stack.at(-1);
50
+ return top === undefined ? null : (top.region ?? top.base);
51
+ };
52
+ for (const st of statements.slice(1)) {
53
+ if (inNote) {
54
+ if (asciiLower(st) === 'end note')
55
+ inNote = false;
56
+ continue;
57
+ }
58
+ const first = asciiLower(firstWord(st));
59
+ if (first === 'direction') {
60
+ graph.dir = parseDir(words(st)[1] ?? '');
61
+ }
62
+ else if (first === 'note') {
63
+ // A single-line `note ... : text` needs no terminator.
64
+ if (!st.includes(':'))
65
+ inNote = true;
66
+ }
67
+ else if (first === 'state') {
68
+ const rest = st.slice(firstWord(st).length).trim();
69
+ const open = rest.endsWith('{');
70
+ const body = open ? rest.slice(0, -1).trim() : rest;
71
+ if (!open) {
72
+ if (parseStateDecl(body, graph) === null)
73
+ graph.drop(st);
74
+ }
75
+ else {
76
+ // A composite. An unreadable declaration still opens an anonymous
77
+ // frame: the `{` was consumed, so the `}` balance must hold.
78
+ const named = compositeName(body);
79
+ if (named === null)
80
+ graph.drop(st);
81
+ const gi = newGroup(named?.id ?? `anon ${graph.groups.length}`, named?.label ?? '');
82
+ if (gi !== null) {
83
+ stack.push({ base: gi, region: null });
84
+ graph.curGroup = gi;
85
+ }
86
+ }
87
+ }
88
+ else if (first === '}') {
89
+ stack.pop();
90
+ graph.curGroup = scopeOf();
91
+ }
92
+ else if (first === '--') {
93
+ // Region divider: members so far move into region 1 on the first `--`;
94
+ // each divider opens the next unlabelled sibling region.
95
+ const top = stack.at(-1);
96
+ if (top !== undefined) {
97
+ if (top.region === null) {
98
+ const r1 = newGroup(`region ${graph.groups.length}`, '');
99
+ if (r1 !== null) {
100
+ graph.nodeGroup.forEach((g, i) => {
101
+ if (g === top.base)
102
+ graph.nodeGroup[i] = r1;
103
+ });
104
+ graph.groups.forEach((g, i) => {
105
+ if (i !== r1 && g.parent === top.base)
106
+ g.parent = r1;
107
+ });
108
+ }
109
+ }
110
+ graph.curGroup = top.base;
111
+ const next = newGroup(`region ${graph.groups.length}`, '');
112
+ top.region = next;
113
+ graph.curGroup = next ?? top.base;
114
+ }
115
+ }
116
+ else if (first === 'classdef') {
117
+ const def = parseClassDef(st.slice(firstWord(st).length));
118
+ if (def)
119
+ for (const name of def.names)
120
+ graph.classDefs[name] = def.props;
121
+ }
122
+ else if (first === 'class') {
123
+ const assign = parseClassAssign(st.slice(firstWord(st).length));
124
+ if (assign)
125
+ classAssignments.push(assign);
126
+ }
127
+ else if (['hide', 'scale'].includes(first)) {
128
+ // Styling directives carry no layout meaning.
129
+ }
130
+ else if (st.includes('-->')) {
131
+ if (parseTransition(st, graph) === null)
132
+ graph.drop(st);
133
+ }
134
+ else if (parseStateDesc(st, graph) === null) {
135
+ graph.drop(st);
136
+ }
137
+ if (graph.truncated !== null) {
138
+ graph.warnings.push(`diagram truncated: ${graph.truncated}`);
139
+ break;
140
+ }
141
+ }
142
+ graph.applyClasses(classAssignments);
143
+ return graph.nodes.length === 0 ? null : graph;
144
+ }
145
+ /** The id and label of a composite declaration body, or `null`. */
146
+ function compositeName(body) {
147
+ if (body.startsWith('"')) {
148
+ const close = body.indexOf('"', 1);
149
+ if (close === -1)
150
+ return null;
151
+ const label = decodeHtmlEntities(body.slice(1, close));
152
+ const after = body.slice(close + 1).trim();
153
+ // A `:::` tag on a composite is dropped: groups paint no classed cells.
154
+ const id = takeTags(after.startsWith('as') ? after.slice(2).trim() : label).id;
155
+ return id === '' ? null : { id, label };
156
+ }
157
+ // A stereotype on a composite carries no drawing of its own; keep the name.
158
+ const id = takeTags(body.split('<<')[0].trim()).id;
159
+ return id === '' || /\s/.test(id) ? null : { id, label: id };
160
+ }
161
+ /** `state "Label" as id` or `state id <<choice>>` — the non-composite forms. */
162
+ function parseStateDecl(rest, graph) {
163
+ if (rest === '')
164
+ return true;
165
+ if (rest.startsWith('"')) {
166
+ const close = rest.indexOf('"', 1);
167
+ if (close === -1)
168
+ return null;
169
+ const label = rest.slice(1, close);
170
+ const after = rest.slice(close + 1).trim();
171
+ const { id, classes } = takeTags(after.startsWith('as') ? after.slice(2).trim() : label);
172
+ const idx = graph.nodeLabel(id, decodeHtmlEntities(label));
173
+ if (idx === null)
174
+ return null;
175
+ for (const cls of classes)
176
+ graph.addClass(idx, cls);
177
+ return true;
178
+ }
179
+ let shape = 'round';
180
+ let token = rest;
181
+ let stereotyped = false;
182
+ const pos = rest.indexOf('<<');
183
+ if (pos !== -1) {
184
+ const stereo = rest
185
+ .slice(pos + 2)
186
+ .replace(/>>$/, '')
187
+ .trim();
188
+ if (stereo === 'choice')
189
+ shape = 'diamond';
190
+ token = rest.slice(0, pos).trim();
191
+ stereotyped = true;
192
+ }
193
+ const { id, classes } = takeTags(token);
194
+ if (id === '' || /\s/.test(id))
195
+ return null;
196
+ const idx = graph.nodeIndex(id, stereotyped ? id : null, shape);
197
+ if (idx === null)
198
+ return null;
199
+ for (const cls of classes)
200
+ graph.addClass(idx, cls);
201
+ return true;
202
+ }
203
+ /** `A --> B: label`, including chains `A --> B --> C`. */
204
+ function parseTransition(st, graph) {
205
+ let rest = st;
206
+ let prev = null;
207
+ for (;;) {
208
+ const split = splitOnce(rest, '-->');
209
+ if (!split)
210
+ break;
211
+ const [lhs, rhs] = split;
212
+ const fromTok = takeTags(lhs.trimEnd().replace(/-+$/, '').trim());
213
+ let from;
214
+ if (prev !== null) {
215
+ // Mid-chain: the source is the previous target, so nothing may precede.
216
+ if (fromTok.id !== '')
217
+ return null;
218
+ from = prev;
219
+ }
220
+ else {
221
+ if (fromTok.id === '')
222
+ return null;
223
+ const f = stateEndpoint(graph, fromTok.id, true);
224
+ if (f === null)
225
+ return null;
226
+ for (const cls of fromTok.classes)
227
+ graph.addClass(f, cls);
228
+ from = f;
229
+ }
230
+ // After the label colon it is all label — mermaid never chains past a
231
+ // label, so an arrow inside one (`: go "x --> y"`) is text, not a link.
232
+ const nextArrow = rhs.indexOf('-->');
233
+ const colon = splitColon(rhs);
234
+ const labelFirst = colon !== null && (nextArrow === -1 || colon[0].length < nextArrow);
235
+ const toPart = labelFirst ? colon[0] : nextArrow === -1 ? rhs : rhs.slice(0, nextArrow);
236
+ const label = labelFirst ? nonEmpty(decodeHtmlEntities(colon[1].trim())) : null;
237
+ const tail = labelFirst || nextArrow === -1 ? '' : rhs.slice(nextArrow);
238
+ const toTok = takeTags(toPart.trimStart().replace(/^>+/, '').trimEnd().replace(/-+$/, '').trim());
239
+ if (toTok.id === '')
240
+ return null;
241
+ const to = stateEndpoint(graph, toTok.id, false);
242
+ if (to === null)
243
+ return null;
244
+ for (const cls of toTok.classes)
245
+ graph.addClass(to, cls);
246
+ if (!graph.pushEdge({ from, to, label, headTo: 'arrow', headFrom: 'none', line: 'solid' })) {
247
+ return true;
248
+ }
249
+ prev = to;
250
+ rest = tail;
251
+ }
252
+ return true;
253
+ }
254
+ /**
255
+ * `[*]` is start or end depending on which side of the arrow it sits, and is
256
+ * scoped to the enclosing composite — each frame has its own start and end.
257
+ */
258
+ function stateEndpoint(graph, id, isSource) {
259
+ if (id === '[*]') {
260
+ const scope = graph.curGroup === null ? '' : ` g${graph.curGroup}`;
261
+ return graph.nodeIndex(`[*]${isSource ? 'start' : 'end'}${scope}`, '●', 'round');
262
+ }
263
+ return graph.nodeIndex(id, null, 'round');
264
+ }
265
+ /** `id: description`, or a bare state name; either may carry `:::` tags. */
266
+ function parseStateDesc(st, graph) {
267
+ const split = splitColon(st);
268
+ const { id, classes } = takeTags((split ? split[0] : st).trim());
269
+ let idx;
270
+ if (split) {
271
+ const desc = split[1].trim();
272
+ if (id === '' || /\s/.test(id) || desc === '')
273
+ return null;
274
+ // Repeated descriptions accumulate (mermaid stacks them as lines; here
275
+ // they join and wrap). A bare node's label is its id — that one replaces.
276
+ const prev = graph.index.get(id);
277
+ const before = prev === undefined ? null : graph.nodes[prev].label;
278
+ const text = decodeHtmlEntities(desc);
279
+ idx = graph.nodeLabel(id, before !== null && before !== id ? `${before} ${text}` : text);
280
+ }
281
+ else {
282
+ if (id === '' || /\s/.test(id))
283
+ return null;
284
+ idx = graph.nodeIndex(id, null, 'round');
285
+ }
286
+ if (idx === null)
287
+ return null;
288
+ for (const cls of classes)
289
+ graph.addClass(idx, cls);
290
+ return true;
291
+ }
292
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `timeline`: periods and their events as a vertical list — one row per
3
+ * event, the period named on its first row. Lenient: an unreadable statement
4
+ * is dropped and recorded in `warnings`.
5
+ */
6
+ import { Canvas, drawText } from '../canvas.js';
7
+ import { MAX_EDGES } from '../graph.js';
8
+ import { decodeHtmlEntities, fitLabel, MAX_LABEL } from '../labels.js';
9
+ import { headerKind, nonEmpty, statementsOf, words } from '../statements.js';
10
+ import { stringWidth } from '../width.js';
11
+ export const timeline = {
12
+ kind: 'timeline',
13
+ headers: ['timeline'],
14
+ render(src) {
15
+ const parsed = parseTimeline(src);
16
+ if (parsed === null)
17
+ return null;
18
+ const { title, rows, warnings } = parsed;
19
+ const periodW = Math.max(...rows.map((r) => (r.section ? 0 : stringWidth(r.period))));
20
+ const top = title === null ? 0 : 1;
21
+ const width = Math.max(title === null ? 0 : stringWidth(title) + 6, ...rows.map((r) => (r.section ? stringWidth(r.event) : periodW + 3 + stringWidth(r.event))));
22
+ const canvas = new Canvas(width, top + rows.length);
23
+ if (title !== null) {
24
+ const t = ` ${title} `;
25
+ const x = Math.max(0, Math.floor((width - stringWidth(t) - 4) / 2));
26
+ drawText(canvas, '──', x, 0, 'edge');
27
+ drawText(canvas, t, x + 2, 0, 'title');
28
+ drawText(canvas, '──', x + 2 + stringWidth(t), 0, 'edge');
29
+ }
30
+ rows.forEach((row, i) => {
31
+ const y = top + i;
32
+ if (row.section) {
33
+ drawText(canvas, row.event, 0, y, 'title');
34
+ return;
35
+ }
36
+ drawText(canvas, row.period, 0, y, 'text');
37
+ if (row.event === '')
38
+ return;
39
+ drawText(canvas, '─', periodW + 1, y, 'edge');
40
+ drawText(canvas, row.event, periodW + 3, y, 'edgeLabel');
41
+ });
42
+ return { canvas, warnings, classDefs: {} };
43
+ },
44
+ };
45
+ function parseTimeline(src) {
46
+ const statements = statementsOf(src);
47
+ if (headerKind(statements) !== 'timeline')
48
+ return null;
49
+ let title = null;
50
+ const rows = [];
51
+ const warnings = [];
52
+ let truncated = false;
53
+ let lastPeriod = false;
54
+ for (const st of statements.slice(1)) {
55
+ if (rows.length >= MAX_EDGES) {
56
+ truncated = true;
57
+ break;
58
+ }
59
+ const first = words(st)[0]?.toLowerCase();
60
+ if (first === 'title') {
61
+ title = nonEmpty(st.slice(st.toLowerCase().indexOf('title') + 5).trim());
62
+ continue;
63
+ }
64
+ if (first === 'section') {
65
+ const name = st.slice(st.toLowerCase().indexOf('section') + 7).trim();
66
+ rows.push({ period: '', event: clean(name), section: true });
67
+ lastPeriod = false;
68
+ continue;
69
+ }
70
+ // `period : event : event`; a statement of only `: event`s continues the
71
+ // previous period (the `;`-split form of mermaid's multi-line events).
72
+ const parts = st.split(':').map((p) => clean(p));
73
+ const period = parts.shift() ?? '';
74
+ if (period === '' && parts.length > 0 && lastPeriod) {
75
+ for (const event of parts)
76
+ rows.push({ period: '', event });
77
+ continue;
78
+ }
79
+ // A bare period renders event-less, as mermaid draws it.
80
+ if (period !== '' && parts.length === 0) {
81
+ rows.push({ period, event: '' });
82
+ lastPeriod = true;
83
+ continue;
84
+ }
85
+ if (period === '' || parts.some((p) => p === '')) {
86
+ warnings.push(`dropped, unreadable statement: "${st}"`);
87
+ continue;
88
+ }
89
+ parts.forEach((event, i) => {
90
+ rows.push({ period: i === 0 ? period : '', event });
91
+ });
92
+ lastPeriod = true;
93
+ }
94
+ if (truncated)
95
+ warnings.push(`diagram truncated: event cap (${MAX_EDGES}) reached`);
96
+ return rows.length === 0 ? null : { title, rows, warnings };
97
+ }
98
+ const clean = (s) => fitLabel(decodeHtmlEntities(s.trim()), MAX_LABEL);
99
+ //# sourceMappingURL=timeline.js.map
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The shared diagram model. Flowchart, state, class and ER sources all parse
3
+ * into a `Graph`; only sequence diagrams have their own model.
4
+ */
5
+ import { asciiUpper } from './labels.js';
6
+ /** Caps that keep layout bounded; exceeding one drops the diagram to fallback. */
7
+ export const MAX_NODES = 128;
8
+ export const MAX_EDGES = 512;
9
+ export const MAX_GROUPS = 24;
10
+ export const MAX_GROUP_DEPTH = 6;
11
+ /** Class members / ER attributes listed per box before eliding with `…`. */
12
+ export const MAX_MEMBERS = 8;
13
+ /** `LR`/`RL`/`BT` as written in a header or `direction` statement; else `down`. */
14
+ export function parseDir(token) {
15
+ switch (asciiUpper(token)) {
16
+ case 'LR':
17
+ return 'right';
18
+ case 'RL':
19
+ return 'left';
20
+ case 'BT':
21
+ return 'up';
22
+ default:
23
+ return 'down';
24
+ }
25
+ }
26
+ export class Graph {
27
+ nodes = [];
28
+ edges = [];
29
+ index = new Map();
30
+ groups = [];
31
+ /** Innermost subgraph each node was declared in, parallel to `nodes`. */
32
+ nodeGroup = [];
33
+ curGroup = null;
34
+ /**
35
+ * Set when a size cap was hit, naming the cap. The parser stops consuming
36
+ * statements and renders the prefix, warning about the truncation — a
37
+ * streamed diagram that outgrows a cap can never shrink back under it, so
38
+ * a stable truncated render beats flipping to the source box for good.
39
+ */
40
+ truncated = null;
41
+ /**
42
+ * Source the grammar could not read and dropped. Parsing is lenient in
43
+ * every grammar: a statement either contributes what parsed or is dropped
44
+ * and recorded here, so the reader can tell a clean diagram from one that
45
+ * is missing something they wrote.
46
+ */
47
+ warnings = [];
48
+ /** Parsed `classDef` declarations: name -> property map. */
49
+ classDefs = {};
50
+ dir = 'down';
51
+ constructor(dir = 'down') {
52
+ this.dir = dir;
53
+ }
54
+ /**
55
+ * Index of `id`, creating the node if new. A later declaration carrying a
56
+ * label overwrites the placeholder one an edge created. Returns `null` once
57
+ * `MAX_NODES` is reached, which truncates the parse.
58
+ */
59
+ nodeIndex(id, label, shape) {
60
+ const existing = this.index.get(id);
61
+ if (existing !== undefined) {
62
+ if (label !== null) {
63
+ this.nodes[existing].label = label;
64
+ this.nodes[existing].shape = shape;
65
+ }
66
+ return existing;
67
+ }
68
+ if (this.nodes.length >= MAX_NODES) {
69
+ this.truncated ??= `node cap (${MAX_NODES}) reached`;
70
+ return null;
71
+ }
72
+ this.index.set(id, this.nodes.length);
73
+ this.nodes.push({ label: label ?? id, shape });
74
+ this.nodeGroup.push(this.curGroup);
75
+ return this.nodes.length - 1;
76
+ }
77
+ /** Set a node's label without disturbing its shape, creating it if new. */
78
+ nodeLabel(id, label) {
79
+ const existing = this.index.get(id);
80
+ if (existing !== undefined) {
81
+ this.nodes[existing].label = label;
82
+ return existing;
83
+ }
84
+ return this.nodeIndex(id, label, 'round');
85
+ }
86
+ /** Attach an author class name to a node, ignoring a repeat. */
87
+ addClass(idx, name) {
88
+ const node = this.nodes[idx];
89
+ node.classes ??= [];
90
+ if (!node.classes.includes(name))
91
+ node.classes.push(name);
92
+ }
93
+ /**
94
+ * Apply collected `[ids, names]` class assignments. Run after the statement
95
+ * walk so a `class A,B name` (or `:::` tag) may precede the nodes it names;
96
+ * unknown ids are ignored.
97
+ */
98
+ applyClasses(assignments) {
99
+ for (const [ids, names] of assignments) {
100
+ for (const id of ids) {
101
+ const idx = this.index.get(id.trim());
102
+ if (idx === undefined)
103
+ continue;
104
+ for (const name of names) {
105
+ if (name.trim() !== '')
106
+ this.addClass(idx, name.trim());
107
+ }
108
+ }
109
+ }
110
+ }
111
+ /** Apply `[id, url]` link targets; the last one per id wins, unknown ids
112
+ * are ignored. Deferred like `applyClasses`, for the same ordering reason. */
113
+ applyHrefs(hrefs) {
114
+ for (const [id, url] of hrefs) {
115
+ const idx = this.index.get(id.trim());
116
+ if (idx !== undefined)
117
+ this.nodes[idx].href = url;
118
+ }
119
+ }
120
+ /**
121
+ * Record an unreadable statement; the diagram renders without it. Skipped
122
+ * once truncated — a cap-caused parse failure is not the statement's fault.
123
+ */
124
+ drop(st) {
125
+ if (this.truncated === null)
126
+ this.warnings.push(`dropped, unreadable statement: "${st}"`);
127
+ }
128
+ /** Append an edge, or flag `truncated` when `MAX_EDGES` is reached. */
129
+ pushEdge(edge) {
130
+ if (this.edges.length >= MAX_EDGES) {
131
+ this.truncated ??= `edge cap (${MAX_EDGES}) reached`;
132
+ return false;
133
+ }
134
+ this.edges.push(edge);
135
+ return true;
136
+ }
137
+ }
138
+ //# sourceMappingURL=graph.js.map
@@ -0,0 +1,59 @@
1
+ import { stripControls } from './labels.js';
2
+ import { diagramFor } from './registry.js';
3
+ import { frontmatterTitle } from './statements.js';
4
+ import { stringWidth } from './width.js';
5
+ export { classSgr, DEFAULT_THEME, toAnsi } from './ansi.js';
6
+ export { contrastOn, resolveClassStyle } from './class-style.js';
7
+ export { diagramKind } from './registry.js';
8
+ export { sourceBox } from './source-box.js';
9
+ /**
10
+ * Render a Mermaid source block as Unicode box-drawing art.
11
+ *
12
+ * Supported: `graph`/`flowchart` (including `subgraph`), `stateDiagram`,
13
+ * `classDiagram`, `erDiagram`, `sequenceDiagram`, `pie`, `mindmap`,
14
+ * `timeline` and `gitGraph`.
15
+ *
16
+ * The diagram is laid out at whatever size it needs; `art.width` reports the
17
+ * columns that turned out to be. Deciding what to do when that exceeds the
18
+ * space at hand is the caller's — `sourceBox` is the usual answer:
19
+ *
20
+ * ```ts
21
+ * const art = render(src)
22
+ * show(art && art.width <= cols ? art : sourceBox(src, cols))
23
+ * ```
24
+ *
25
+ * `null` means there is no art to show: blank input, a diagram type this
26
+ * renderer does not draw, a source in which not one statement parsed, or a
27
+ * diagram large enough that laying it out is refused. `diagramKind` separates
28
+ * the middle two.
29
+ *
30
+ * Rendering is best-effort in every grammar: a statement either contributes
31
+ * what parsed or is dropped, and a diagram over a size cap renders its prefix.
32
+ * Everything given up on is listed in `art.warnings` — advisory only, never a
33
+ * reason to withhold the art.
34
+ */
35
+ export function render(src) {
36
+ src = stripControls(src);
37
+ if (src.trim() === '')
38
+ return null;
39
+ const drawn = diagramFor(src)?.render(src) ?? null;
40
+ if (drawn === null)
41
+ return null;
42
+ const art = drawn.canvas.toLines();
43
+ // A frontmatter `title:` is centred above the art, in the `title` role.
44
+ const title = frontmatterTitle(src);
45
+ if (title !== null) {
46
+ const tw = stringWidth(title);
47
+ art.width = Math.max(art.width, tw);
48
+ const pad = ' '.repeat(Math.floor((art.width - tw) / 2));
49
+ art.plain.unshift(pad + title, '');
50
+ art.styled.unshift(pad === ''
51
+ ? [{ text: title, role: 'title' }]
52
+ : [
53
+ { text: pad, role: 'none' },
54
+ { text: title, role: 'title' },
55
+ ], []);
56
+ }
57
+ return { ...art, classDefs: drawn.classDefs, warnings: drawn.warnings };
58
+ }
59
+ //# sourceMappingURL=index.js.map