@hncode/hncode 0.5.5 → 0.6.0

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 (56) hide show
  1. package/package.json +37 -38
  2. package/src/agent.js +67 -2
  3. package/src/image-row.js +111 -0
  4. package/src/latex.js +1557 -0
  5. package/src/llm.js +19 -4
  6. package/src/mermaid/ansi.js +55 -0
  7. package/src/mermaid/canvas.js +449 -0
  8. package/src/mermaid/class-style.js +57 -0
  9. package/src/mermaid/css-colors.js +153 -0
  10. package/src/mermaid/diagrams/class.js +289 -0
  11. package/src/mermaid/diagrams/er.js +201 -0
  12. package/src/mermaid/diagrams/flowchart.js +488 -0
  13. package/src/mermaid/diagrams/gitgraph.js +205 -0
  14. package/src/mermaid/diagrams/mindmap.js +101 -0
  15. package/src/mermaid/diagrams/pie.js +104 -0
  16. package/src/mermaid/diagrams/sequence.js +282 -0
  17. package/src/mermaid/diagrams/state.js +292 -0
  18. package/src/mermaid/diagrams/timeline.js +99 -0
  19. package/src/mermaid/graph.js +138 -0
  20. package/src/mermaid/index.js +59 -0
  21. package/src/mermaid/labels.js +339 -0
  22. package/src/mermaid/layout-seq.js +225 -0
  23. package/src/mermaid/layout.js +1454 -0
  24. package/src/mermaid/registry.js +54 -0
  25. package/src/mermaid/source-box.js +102 -0
  26. package/src/mermaid/statements.js +235 -0
  27. package/src/mermaid/types.js +1 -0
  28. package/src/mermaid/width.js +29 -0
  29. package/src/screen-diff.js +512 -0
  30. package/src/screen-paint.js +128 -0
  31. package/src/screen.js +277 -0
  32. package/src/subagent-types.js +136 -147
  33. package/src/term-caps.js +10 -4
  34. package/src/tex/font.js +292 -0
  35. package/src/tex/fonts/LICENSE.txt +11 -0
  36. package/src/tex/fonts/STIX2Math.ttf +0 -0
  37. package/src/tex/index.js +293 -0
  38. package/src/tex/kitty.js +186 -0
  39. package/src/tex/layout.js +446 -0
  40. package/src/tex/parse.js +331 -0
  41. package/src/tex/raster.js +216 -0
  42. package/src/tex/render.js +167 -0
  43. package/src/tex/sixel.js +326 -0
  44. package/src/tex/svg.js +237 -0
  45. package/src/tool-result.js +2 -2
  46. package/src/tools/agent-swarm.js +3 -15
  47. package/src/tools/agent.js +454 -464
  48. package/src/tools/ask-user-question.js +195 -206
  49. package/src/tools/bash.js +39 -3
  50. package/src/tools/edit.js +10 -112
  51. package/src/tools/fetch-url.js +130 -120
  52. package/src/tools/memory.js +80 -88
  53. package/src/tools/read.js +27 -23
  54. package/src/tools/todo.js +8 -13
  55. package/src/tools/validate-args.js +78 -0
  56. package/src/tui.js +1346 -496
package/src/llm.js CHANGED
@@ -256,7 +256,12 @@ function buildBody(cfg, messages, streaming = true, opts = {}) {
256
256
  const { system, messages: am } = toAnthropic(messages);
257
257
  const body = {
258
258
  model: cfg.innerModel, system, messages: am,
259
- tools: opts.noTools ? undefined : anthropicToolDefs(opts.tools || llmToolsList),
259
+ // `tool_choice: none` is the protocol-level refusal. Omitting the tool definitions entirely, which
260
+ // is what `noTools` used to do, leaves the model free to EMIT the syntax from memory — a summariser
261
+ // that answers with `<|tool_call|>{…}` instead of a summary. Declaring the tools and forbidding their
262
+ // use is the only form both providers accept.
263
+ tools: opts.toolChoice === 'none' ? anthropicToolDefs(opts.tools || llmToolsList) : (opts.noTools ? undefined : anthropicToolDefs(opts.tools || llmToolsList)),
264
+ tool_choice: opts.toolChoice === 'none' ? { type: 'none' } : undefined,
260
265
  ...common, ...wire,
261
266
  };
262
267
  if (!body.tools || !body.tools.length) delete body.tools;
@@ -296,9 +301,19 @@ function buildBody(cfg, messages, streaming = true, opts = {}) {
296
301
  model: cfg.innerModel, messages: toOpenAi(messages),
297
302
  ...common, ...wire,
298
303
  };
299
- // Only attach a NON-EMPTY tools array: `tools: []` is rejected by some strict
300
- // gateways, and an instance that registered no list means "no tools here".
301
- if (!opts.noTools && defs.length) body.tools = defs;
304
+ // `tool_choice: 'none'` NEEDS THE TOOLS DECLARED alongside it. OpenAI rejects the field without them, so
305
+ // the list is sent and the choice forbids use — the same shape as the Anthropic branch, and the only form
306
+ // that actually stops a model answering with a tool call instead of a summary.
307
+ if (opts.toolChoice === 'none') {
308
+ if (defs.length) {
309
+ body.tools = defs;
310
+ body.tool_choice = 'none';
311
+ }
312
+ } else if (!opts.noTools && defs.length) {
313
+ // Only attach a NON-EMPTY tools array: `tools: []` is rejected by some strict
314
+ // gateways, and an instance that registered no list means "no tools here".
315
+ body.tools = defs;
316
+ }
302
317
  return JSON.stringify(applyPromptCache('openai', body, {
303
318
  enabled: cfg.promptCache !== false,
304
319
  sessionKey: cfg.sessionId,
@@ -0,0 +1,55 @@
1
+ import { contrastOn, resolveClassStyle } from './class-style.js';
2
+ const ESC = String.fromCharCode(27);
3
+ const OSC8 = `${ESC}]8;;`;
4
+ const ST = `${ESC}\\`;
5
+ /** Dim frame, plain labels, cyan connectors. Readable on light and dark. */
6
+ export const DEFAULT_THEME = {
7
+ border: '2',
8
+ edge: '36',
9
+ edgeLabel: '2;36',
10
+ title: '1',
11
+ };
12
+ const rgb = (hex, sgr) => `${sgr};2;${[1, 3, 5].map((i) => Number.parseInt(hex.slice(i, i + 2), 16)).join(';')}`;
13
+ /**
14
+ * The truecolor SGR a class style gives a span of the given role, or
15
+ * undefined when the style says nothing about it (fall back to the theme).
16
+ * `stroke` colors borders, `color` text; `fill` backs every painted cell,
17
+ * with a black/white foreground picked for contrast when none was declared.
18
+ * A style that colors nothing for this role keeps `fallback` (the theme's
19
+ * SGR), so a bold-only class bolds the themed look instead of replacing it.
20
+ */
21
+ export function classSgr(st, role, fallback) {
22
+ const p = [];
23
+ const fg = role === 'border' ? (st.stroke ?? st.color) : role === 'edge' ? undefined : st.color;
24
+ const backed = fg ?? (st.fill === undefined ? undefined : contrastOn(st.fill));
25
+ if (backed !== undefined)
26
+ p.push(rgb(backed, 38));
27
+ if (st.fill !== undefined)
28
+ p.push(rgb(st.fill, 48));
29
+ if (p.length === 0 && fallback !== undefined)
30
+ p.push(fallback);
31
+ if (st.bold === true)
32
+ p.unshift('1');
33
+ return p.length > 0 ? p.join(';') : undefined;
34
+ }
35
+ /**
36
+ * Render art to ANSI-coloured lines. Spans that carry author classes are
37
+ * styled from `art.classDefs` (best effort — see `resolveClassStyle`),
38
+ * overriding the role theme; everything else follows `theme`.
39
+ *
40
+ * A convenience over mapping `art.styled` yourself — reach for that directly
41
+ * when your TUI has its own styling model.
42
+ */
43
+ export function toAnsi(art, theme = DEFAULT_THEME) {
44
+ return art.styled.map((row) => row
45
+ .map((span) => {
46
+ const cls = resolveClassStyle(span.classes, art.classDefs);
47
+ const sgr = cls !== null ? classSgr(cls, span.role, theme[span.role]) : theme[span.role];
48
+ const text = sgr === undefined ? span.text : `${ESC}[${sgr}m${span.text}${ESC}[0m`;
49
+ // OSC 8 hyperlink around the whole run; terminals without support
50
+ // ignore the sequences and print the text unchanged.
51
+ return span.href === undefined ? text : `${OSC8}${span.href}${ST}${text}${OSC8}${ST}`;
52
+ })
53
+ .join(''));
54
+ }
55
+ //# sourceMappingURL=ansi.js.map
@@ -0,0 +1,449 @@
1
+ import { measured } from './width.js';
2
+ /**
3
+ * Sentinel occupying the trailing column of a wide glyph. Never emitted: the
4
+ * line builder skips it so a CJK character claims two cells of layout but
5
+ * contributes one character of output.
6
+ */
7
+ export const CONT = String.fromCharCode(0);
8
+ /** Connection direction bits, combined into a box-drawing glyph by `maskChar`. */
9
+ export const U = 1;
10
+ export const D = 2;
11
+ export const L = 4;
12
+ export const R = 8;
13
+ /** Line styles, tracked per cell so crossing edges keep their own stroke. */
14
+ export const STY_DOT = 1;
15
+ export const STY_THICK = 2;
16
+ export const STY_SOLID = 4;
17
+ /**
18
+ * A grid of cells. Edges accumulate as direction bits rather than glyphs so
19
+ * that crossings and junctions resolve correctly whatever order they are drawn
20
+ * in; `finalizeMask` turns the accumulated bits into characters at the end.
21
+ *
22
+ * `occupied` marks cells claimed by a box, which edge bits must not overwrite.
23
+ */
24
+ export class Canvas {
25
+ w;
26
+ h;
27
+ ch;
28
+ role;
29
+ /** Space-joined author classes per cell, or undefined; see `Span.classes`. */
30
+ tag;
31
+ /** Link target per cell, or undefined; see `Span.href`. */
32
+ href;
33
+ mask;
34
+ style;
35
+ occupied;
36
+ curStyle = STY_SOLID;
37
+ /** Author classes stamped on cells painted while set, like `curStyle`. */
38
+ curTag;
39
+ /** Link target stamped on cells painted while set, like `curTag`. */
40
+ curHref;
41
+ constructor(w, h) {
42
+ const n = w * h;
43
+ this.w = w;
44
+ this.h = h;
45
+ this.ch = new Array(n).fill(' ');
46
+ this.role = new Array(n).fill('none');
47
+ this.tag = new Array(n).fill(undefined);
48
+ this.href = new Array(n).fill(undefined);
49
+ this.mask = new Uint8Array(n);
50
+ this.style = new Uint8Array(n);
51
+ this.occupied = new Uint8Array(n);
52
+ }
53
+ idx(x, y) {
54
+ return y * this.w + x;
55
+ }
56
+ set(x, y, c, role) {
57
+ if (x >= this.w || y >= this.h)
58
+ return;
59
+ const i = this.idx(x, y);
60
+ // A literal tab measures one cell here but jumps to the terminal's tab
61
+ // stop there, desyncing every column after it (the source box expands
62
+ // tabs for the same reason). Same width, safe glyph.
63
+ this.ch[i] = c === '\t' ? ' ' : c;
64
+ this.role[i] = role;
65
+ if (this.curTag !== undefined)
66
+ this.tag[i] = this.curTag;
67
+ if (this.curHref !== undefined)
68
+ this.href[i] = this.curHref;
69
+ }
70
+ /**
71
+ * Accumulate direction bits on a free cell.
72
+ *
73
+ * `role` is the role to claim the cell for; `border` cells are never
74
+ * reclassified, so a connector meeting a box keeps the box's styling.
75
+ */
76
+ addBits(x, y, bits, role = 'edge') {
77
+ if (x >= this.w || y >= this.h)
78
+ return;
79
+ const i = this.idx(x, y);
80
+ if (this.occupied[i])
81
+ return;
82
+ this.mask[i] |= bits;
83
+ this.style[i] |= this.curStyle;
84
+ if (this.role[i] !== 'border')
85
+ this.role[i] = role;
86
+ if (this.curTag !== undefined)
87
+ this.tag[i] = this.curTag;
88
+ if (this.curHref !== undefined)
89
+ this.href[i] = this.curHref;
90
+ }
91
+ /** Stamp a finished sub-canvas (a subgraph frame's contents) at an offset. */
92
+ blit(sub, ox, oy) {
93
+ for (let sy = 0; sy < sub.h; sy++) {
94
+ for (let sx = 0; sx < sub.w; sx++) {
95
+ const x = ox + sx;
96
+ const y = oy + sy;
97
+ if (x >= this.w || y >= this.h)
98
+ continue;
99
+ const si = sub.idx(sx, sy);
100
+ const di = this.idx(x, y);
101
+ this.ch[di] = sub.ch[si];
102
+ this.role[di] = sub.role[si];
103
+ this.tag[di] = sub.tag[si];
104
+ this.href[di] = sub.href[si];
105
+ this.style[di] = sub.style[si];
106
+ this.occupied[di] = 1;
107
+ }
108
+ }
109
+ }
110
+ /** Add direction bits even to an occupied cell, so an edge can meet a border. */
111
+ junction(x, y, bits) {
112
+ if (x >= this.w || y >= this.h)
113
+ return;
114
+ const i = this.idx(x, y);
115
+ this.mask[i] |= bits;
116
+ if (this.role[i] !== 'border')
117
+ this.role[i] = 'edge';
118
+ }
119
+ segV(x, y0, y1) {
120
+ const a = Math.min(y0, y1);
121
+ const b = Math.max(y0, y1);
122
+ for (let y = a; y <= b; y++) {
123
+ let bits = 0;
124
+ if (y > a)
125
+ bits |= U;
126
+ if (y < b)
127
+ bits |= D;
128
+ this.addBits(x, y, bits);
129
+ }
130
+ }
131
+ segH(y, x0, x1) {
132
+ const a = Math.min(x0, x1);
133
+ const b = Math.max(x0, x1);
134
+ for (let x = a; x <= b; x++) {
135
+ let bits = 0;
136
+ if (x > a)
137
+ bits |= L;
138
+ if (x < b)
139
+ bits |= R;
140
+ this.addBits(x, y, bits);
141
+ }
142
+ }
143
+ /** Resolve accumulated direction bits into glyphs, honouring line style. */
144
+ finalizeMask() {
145
+ for (let i = 0; i < this.ch.length; i++) {
146
+ if (this.mask[i] === 0)
147
+ continue;
148
+ if (this.ch[i] === ' ') {
149
+ const c = maskChar(this.mask[i]);
150
+ this.ch[i] =
151
+ this.style[i] === STY_DOT ? dottedChar(c) : this.style[i] === STY_THICK ? thickChar(c) : c;
152
+ }
153
+ else if (this.ch[i] === '═' || this.ch[i] === '║') {
154
+ this.ch[i] = doubleTee(this.ch[i], this.mask[i]);
155
+ }
156
+ }
157
+ }
158
+ /**
159
+ * Mirror top-to-bottom for `BT`. Rows reorder but within-row text does not,
160
+ * so labels stay readable; box-drawing glyphs flip to match.
161
+ */
162
+ flipVertical() {
163
+ for (let y = 0; y < Math.floor(this.h / 2); y++) {
164
+ const y2 = this.h - 1 - y;
165
+ for (let x = 0; x < this.w; x++) {
166
+ const i = this.idx(x, y);
167
+ const j = this.idx(x, y2);
168
+ [this.ch[i], this.ch[j]] = [this.ch[j], this.ch[i]];
169
+ [this.role[i], this.role[j]] = [this.role[j], this.role[i]];
170
+ [this.tag[i], this.tag[j]] = [this.tag[j], this.tag[i]];
171
+ [this.href[i], this.href[j]] = [this.href[j], this.href[i]];
172
+ }
173
+ }
174
+ for (let i = 0; i < this.ch.length; i++) {
175
+ if (!textRole(this.role[i]))
176
+ this.ch[i] = flipGlyphV(this.ch[i]);
177
+ }
178
+ }
179
+ /**
180
+ * Mirror left-to-right for `RL`. Mirroring reverses each row, so after
181
+ * flipping glyphs each text/label run is reversed back to reading order.
182
+ */
183
+ flipHorizontal() {
184
+ for (let y = 0; y < this.h; y++) {
185
+ for (let x = 0; x < Math.floor(this.w / 2); x++) {
186
+ const x2 = this.w - 1 - x;
187
+ const i = this.idx(x, y);
188
+ const j = this.idx(x2, y);
189
+ [this.ch[i], this.ch[j]] = [this.ch[j], this.ch[i]];
190
+ [this.role[i], this.role[j]] = [this.role[j], this.role[i]];
191
+ [this.tag[i], this.tag[j]] = [this.tag[j], this.tag[i]];
192
+ [this.href[i], this.href[j]] = [this.href[j], this.href[i]];
193
+ }
194
+ }
195
+ for (let i = 0; i < this.ch.length; i++) {
196
+ if (!textRole(this.role[i]))
197
+ this.ch[i] = flipGlyphH(this.ch[i]);
198
+ }
199
+ for (let y = 0; y < this.h; y++) {
200
+ let x = 0;
201
+ while (x < this.w) {
202
+ const role = this.role[this.idx(x, y)];
203
+ if (role === 'text' || role === 'edgeLabel') {
204
+ const start = this.idx(x, y);
205
+ while (x < this.w && this.role[this.idx(x, y)] === role)
206
+ x++;
207
+ const end = this.idx(x, y);
208
+ reverseSlice(this.ch, start, end);
209
+ }
210
+ else {
211
+ x++;
212
+ }
213
+ }
214
+ }
215
+ }
216
+ /** Group each row into runs of one role and tag, dropping continuations. */
217
+ toLines() {
218
+ const plain = [];
219
+ const styled = [];
220
+ let width = 0;
221
+ for (let y = 0; y < this.h; y++) {
222
+ // A trailing CONT counts as painted: it is the second cell of a wide
223
+ // glyph, so the row really does reach that column.
224
+ let last = 0;
225
+ for (let x = this.w - 1; x >= 0; x--) {
226
+ if (this.ch[this.idx(x, y)] !== ' ') {
227
+ last = x + 1;
228
+ break;
229
+ }
230
+ }
231
+ width = Math.max(width, last);
232
+ const spans = [];
233
+ const push = (text, role, tag, href) => {
234
+ if (text === '')
235
+ return;
236
+ const span = { text, role };
237
+ if (tag !== undefined)
238
+ span.classes = tag.split(' ');
239
+ if (href !== undefined)
240
+ span.href = href;
241
+ spans.push(span);
242
+ };
243
+ let plainRow = '';
244
+ let run = '';
245
+ let runRole = 'none';
246
+ let runTag;
247
+ let runHref;
248
+ for (let x = 0; x < last; x++) {
249
+ const i = this.idx(x, y);
250
+ const c = this.ch[i];
251
+ if (c === CONT)
252
+ continue;
253
+ plainRow += c;
254
+ if ((this.role[i] !== runRole || this.tag[i] !== runTag || this.href[i] !== runHref) &&
255
+ run !== '') {
256
+ push(run, runRole, runTag, runHref);
257
+ run = '';
258
+ }
259
+ runRole = this.role[i];
260
+ runTag = this.tag[i];
261
+ runHref = this.href[i];
262
+ run += c;
263
+ }
264
+ push(run, runRole, runTag, runHref);
265
+ styled.push(spans);
266
+ // Only ASCII spaces, which is all a blank cell ever holds. Trimming `\s`
267
+ // would eat a trailing NBSP that `styled` keeps, desyncing the two.
268
+ plain.push(plainRow.replace(/ +$/, ''));
269
+ }
270
+ let first = 0;
271
+ while (first < plain.length && plain[first] === '')
272
+ first++;
273
+ let end = plain.length;
274
+ while (end > first && plain[end - 1] === '')
275
+ end--;
276
+ return { plain: plain.slice(first, end), styled: styled.slice(first, end), width };
277
+ }
278
+ }
279
+ function reverseSlice(arr, start, end) {
280
+ for (let i = start, j = end - 1; i < j; i++, j--) {
281
+ ;
282
+ [arr[i], arr[j]] = [arr[j], arr[i]];
283
+ }
284
+ }
285
+ /**
286
+ * Paint `text` at `x, y`, one grapheme cluster per cell.
287
+ *
288
+ * A wide cluster claims a second cell, marked with `CONT` so the line builder
289
+ * emits one character for it rather than a stray space.
290
+ */
291
+ export function drawText(canvas, text, x, y, role) {
292
+ let cur = x;
293
+ for (const [cluster, cw] of measured(text)) {
294
+ if (cw === 0)
295
+ continue;
296
+ canvas.set(cur, y, cluster, role);
297
+ for (let k = 1; k < cw; k++)
298
+ canvas.set(cur + k, y, CONT, role);
299
+ cur += cw;
300
+ }
301
+ }
302
+ /**
303
+ * Paint `text` at `x, y`, clearing any edge bits underneath first.
304
+ *
305
+ * Used where text sits on top of a drawn line (sequence messages, dividers,
306
+ * compartment rows) and must win over it.
307
+ */
308
+ export function drawTextOverEdges(canvas, text, x, y, role) {
309
+ let cur = x;
310
+ for (const [cluster, cw] of measured(text)) {
311
+ if (cw === 0)
312
+ continue;
313
+ for (let k = 0; k < cw; k++) {
314
+ if (cur + k < canvas.w && y < canvas.h)
315
+ canvas.mask[canvas.idx(cur + k, y)] = 0;
316
+ canvas.set(cur + k, y, k === 0 ? cluster : CONT, role);
317
+ }
318
+ cur += cw;
319
+ }
320
+ }
321
+ export function maskChar(mask) {
322
+ switch (mask) {
323
+ case 0:
324
+ return ' ';
325
+ case U:
326
+ case D:
327
+ case U | D:
328
+ return '│';
329
+ case L:
330
+ case R:
331
+ case L | R:
332
+ return '─';
333
+ case D | R:
334
+ return '┌';
335
+ case D | L:
336
+ return '┐';
337
+ case U | R:
338
+ return '└';
339
+ case U | L:
340
+ return '┘';
341
+ case U | D | R:
342
+ return '├';
343
+ case U | D | L:
344
+ return '┤';
345
+ case D | L | R:
346
+ return '┬';
347
+ case U | L | R:
348
+ return '┴';
349
+ default:
350
+ return '┼';
351
+ }
352
+ }
353
+ /** An edge teeing into a double-line border: the mixed single/double glyphs. */
354
+ function doubleTee(c, mask) {
355
+ if (c === '═') {
356
+ if (mask & U && mask & D)
357
+ return '╪';
358
+ if (mask & D)
359
+ return '╤';
360
+ if (mask & U)
361
+ return '╧';
362
+ }
363
+ else {
364
+ if (mask & L && mask & R)
365
+ return '╫';
366
+ if (mask & R)
367
+ return '╟';
368
+ if (mask & L)
369
+ return '╢';
370
+ }
371
+ return c;
372
+ }
373
+ const DOTTED = { '─': '╌', '│': '╎' };
374
+ const THICK = {
375
+ '─': '━',
376
+ '│': '┃',
377
+ '┌': '┏',
378
+ '┐': '┓',
379
+ '└': '┗',
380
+ '┘': '┛',
381
+ '├': '┣',
382
+ '┤': '┫',
383
+ '┬': '┳',
384
+ '┴': '┻',
385
+ '┼': '╋',
386
+ };
387
+ const FLIP_V = {
388
+ '╔': '╚',
389
+ '╚': '╔',
390
+ '╗': '╝',
391
+ '╝': '╗',
392
+ '╤': '╧',
393
+ '╧': '╤',
394
+ '┌': '└',
395
+ '└': '┌',
396
+ '┐': '┘',
397
+ '┘': '┐',
398
+ '┏': '┗',
399
+ '┗': '┏',
400
+ '┓': '┛',
401
+ '┛': '┓',
402
+ '╭': '╰',
403
+ '╰': '╭',
404
+ '╮': '╯',
405
+ '╯': '╮',
406
+ '┬': '┴',
407
+ '┴': '┬',
408
+ '┳': '┻',
409
+ '┻': '┳',
410
+ '▼': '▲',
411
+ '▲': '▼',
412
+ '▽': '△',
413
+ '△': '▽',
414
+ };
415
+ const FLIP_H = {
416
+ '╔': '╗',
417
+ '╗': '╔',
418
+ '╚': '╝',
419
+ '╝': '╚',
420
+ '╟': '╢',
421
+ '╢': '╟',
422
+ '┌': '┐',
423
+ '┐': '┌',
424
+ '└': '┘',
425
+ '┘': '└',
426
+ '┏': '┓',
427
+ '┓': '┏',
428
+ '┗': '┛',
429
+ '┛': '┗',
430
+ '╭': '╮',
431
+ '╮': '╭',
432
+ '╰': '╯',
433
+ '╯': '╰',
434
+ '├': '┤',
435
+ '┤': '├',
436
+ '┣': '┫',
437
+ '┫': '┣',
438
+ '▶': '◄',
439
+ '◄': '▶',
440
+ '▷': '◁',
441
+ '◁': '▷',
442
+ };
443
+ const dottedChar = (c) => DOTTED[c] ?? c;
444
+ const thickChar = (c) => THICK[c] ?? c;
445
+ const flipGlyphV = (c) => FLIP_V[c] ?? c;
446
+ const flipGlyphH = (c) => FLIP_H[c] ?? c;
447
+ /** User-authored cells: flips reorder them but must not remap their glyphs. */
448
+ const textRole = (r) => r === 'text' || r === 'edgeLabel' || r === 'title';
449
+ //# sourceMappingURL=canvas.js.map
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Best-effort interpretation of `classDef` styles for a cell grid.
3
+ *
4
+ * A terminal cell can express a foreground, a background and boldness —
5
+ * nothing else. `fill` is the node background, `stroke` its border,
6
+ * `color` its text; every other property is silently ignored.
7
+ */
8
+ import { NAMED_COLORS } from './css-colors.js';
9
+ /** `#rgb`, `#rrggbb`, `rgb(r,g,b)` or a CSS color name → `#rrggbb`; else null. */
10
+ function normalizeColor(v) {
11
+ const s = v.trim().toLowerCase();
12
+ if (/^#[0-9a-f]{6}$/.test(s))
13
+ return s;
14
+ if (/^#[0-9a-f]{3}$/.test(s))
15
+ return `#${s[1]}${s[1]}${s[2]}${s[2]}${s[3]}${s[3]}`;
16
+ const rgb = s.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
17
+ if (rgb !== null) {
18
+ const hex = (n) => Math.min(255, Number(n)).toString(16).padStart(2, '0');
19
+ return `#${hex(rgb[1])}${hex(rgb[2])}${hex(rgb[3])}`;
20
+ }
21
+ return NAMED_COLORS[s] ?? null;
22
+ }
23
+ /**
24
+ * The merged style of a span's classes (later classes win), or null when
25
+ * nothing terminal-expressible was declared.
26
+ */
27
+ export function resolveClassStyle(classes, classDefs) {
28
+ if (classes === undefined)
29
+ return null;
30
+ const out = {};
31
+ for (const name of classes) {
32
+ const props = classDefs[name];
33
+ if (props === undefined)
34
+ continue;
35
+ for (const [k, v] of Object.entries(props)) {
36
+ if (k === 'fill' || k === 'stroke' || k === 'color') {
37
+ const c = normalizeColor(v);
38
+ if (c !== null)
39
+ out[k] = c;
40
+ }
41
+ else if (k === 'font-weight') {
42
+ out.bold = v.trim() === 'bold' || v.trim() === 'bolder';
43
+ }
44
+ }
45
+ }
46
+ return Object.keys(out).length > 0 ? out : null;
47
+ }
48
+ /**
49
+ * Black or white, whichever reads on the given `#rrggbb` background — the
50
+ * guard that keeps `fill:#eee` legible on a dark terminal theme.
51
+ */
52
+ export function contrastOn(fill) {
53
+ const ch = (i) => Number.parseInt(fill.slice(i, i + 2), 16);
54
+ const yiq = (ch(1) * 299 + ch(3) * 587 + ch(5) * 114) / 1000;
55
+ return yiq >= 128 ? '#000000' : '#ffffff';
56
+ }
57
+ //# sourceMappingURL=class-style.js.map