@markii/ansi 0.15.0 → 0.17.0-next.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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/dist/ansi.d.ts +26 -0
  3. package/dist/ansi.js +51 -0
  4. package/dist/components/badge.d.ts +1 -1
  5. package/dist/components/badge.js +5 -4
  6. package/dist/components/callout.d.ts +38 -13
  7. package/dist/components/callout.js +68 -31
  8. package/dist/components/card.d.ts +34 -2
  9. package/dist/components/card.js +55 -18
  10. package/dist/components/cell.d.ts +5 -11
  11. package/dist/components/cell.js +11 -15
  12. package/dist/components/chart.d.ts +4 -10
  13. package/dist/components/chart.js +16 -24
  14. package/dist/components/details-string.d.ts +26 -0
  15. package/dist/components/details-string.js +39 -0
  16. package/dist/components/details.d.ts +32 -10
  17. package/dist/components/details.js +28 -25
  18. package/dist/components/divider.js +12 -16
  19. package/dist/components/figure.d.ts +5 -16
  20. package/dist/components/figure.js +9 -19
  21. package/dist/components/index.d.ts +14 -10
  22. package/dist/components/index.js +19 -16
  23. package/dist/components/kbd.d.ts +1 -1
  24. package/dist/components/kbd.js +3 -2
  25. package/dist/components/layout-wrapper.d.ts +7 -22
  26. package/dist/components/layout-wrapper.js +9 -23
  27. package/dist/components/progress.d.ts +3 -11
  28. package/dist/components/progress.js +8 -17
  29. package/dist/components/rating.d.ts +1 -2
  30. package/dist/components/rating.js +3 -4
  31. package/dist/components/row.d.ts +35 -20
  32. package/dist/components/row.js +35 -51
  33. package/dist/components/stat.d.ts +3 -8
  34. package/dist/components/stat.js +5 -11
  35. package/dist/components/tab.d.ts +9 -8
  36. package/dist/components/tab.js +12 -10
  37. package/dist/components/table-grid.d.ts +2 -1
  38. package/dist/components/table-grid.js +14 -12
  39. package/dist/components/table.d.ts +5 -10
  40. package/dist/components/table.js +8 -13
  41. package/dist/components/tabs-string.d.ts +17 -0
  42. package/dist/components/tabs-string.js +30 -0
  43. package/dist/components/tabs.d.ts +31 -14
  44. package/dist/components/tabs.js +55 -30
  45. package/dist/failure-presentation.d.ts +15 -0
  46. package/dist/failure-presentation.js +22 -0
  47. package/dist/glyphs.d.ts +73 -0
  48. package/dist/glyphs.js +88 -0
  49. package/dist/index.d.ts +6 -4
  50. package/dist/index.js +10 -7
  51. package/dist/ink-string.d.ts +23 -0
  52. package/dist/ink-string.js +65 -0
  53. package/dist/interactive.d.ts +66 -0
  54. package/dist/interactive.js +207 -0
  55. package/dist/layout.js +1 -2
  56. package/dist/pack-loader.d.ts +64 -0
  57. package/dist/pack-loader.js +172 -0
  58. package/dist/pack-script.d.ts +44 -0
  59. package/dist/pack-script.js +135 -0
  60. package/dist/registry.d.ts +106 -100
  61. package/dist/registry.js +17 -9
  62. package/dist/render.d.ts +63 -25
  63. package/dist/render.js +1001 -380
  64. package/dist/text-grid.d.ts +90 -0
  65. package/dist/text-grid.js +300 -0
  66. package/package.json +9 -5
  67. package/dist/box.d.ts +0 -47
  68. package/dist/box.js +0 -209
  69. package/dist/measure.d.ts +0 -25
  70. package/dist/measure.js +0 -108
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Hand-written width measurement, padding, wrapping, and box-drawing for the
3
+ * handful of STANDARD COMPONENTS that draw their own fixed-width glyphs
4
+ * (`card`, `callout`, `divider`, `chart`, `table`, and the plain-markdown
5
+ * table grid in `render.tsx`). Everything else in this engine wraps prose
6
+ * through Ink's own layout (`ink-string.ts`'s doc comment and AGENTS.md's
7
+ * batch-10 brief: "Drop wrap/pad/columns/frame/rule: Ink's layout replaces
8
+ * them").
9
+ *
10
+ * A self-drawing component cannot lean on Ink for this: the string it builds
11
+ * (a box-drawing frame, a sparkline, a table grid) is a fixed block of
12
+ * characters that has to be CORRECT the moment it is built, because Ink's
13
+ * `<Text>` renders it verbatim rather than reflowing pre-drawn glyphs. This
14
+ * module is therefore kept as a small, PRIVATE (not exported from
15
+ * `src/index.ts`) implementation detail of those few components, replacing
16
+ * the old public `measure.ts`/`box.ts` modules the batch-10 brief has this
17
+ * engine delete. The width-measurement approximation is unchanged from the
18
+ * old `measure.ts`: Latin/CJK/emoji/combining-mark ranges, not a full
19
+ * Unicode grapheme-segmentation table (see the doc comment below for the one
20
+ * documented limitation).
21
+ */
22
+ import type { FrameEdges, GlyphSet } from './glyphs.js';
23
+ /** Removes every SGR and OSC 8 escape sequence this engine can itself produce, so `measureWidth` counts only what a terminal actually draws. */
24
+ export declare function stripEscapes(text: string): string;
25
+ /**
26
+ * The display width of `text`, ANSI escapes stripped first. This is a
27
+ * deliberate approximation of UAX #11 (see `text-grid.ts`'s module comment
28
+ * and the former `measure.ts`'s identical documented limitation: a
29
+ * zero-width-joiner emoji sequence measures as the sum of its parts).
30
+ */
31
+ export declare function measureWidth(text: string): number;
32
+ /** Greedy word wrap to `width` columns, used only by a self-drawing component's own internal text (its title, its caption). Not exposed on `AnsiRenderContext`: ordinary prose wraps through Ink's own layout instead. */
33
+ export declare function wrapText(text: string, width: number): string[];
34
+ /** Pads `text` to `width` columns with spaces, aligned `left`/`center`/`right`. */
35
+ export declare function padText(text: string, width: number, align: 'left' | 'center' | 'right'): string;
36
+ /** `frameBlock`'s options: mirrors the old `box.ts`'s `FrameOptions`. */
37
+ export interface FrameOptions {
38
+ style: 'solid' | 'dashed';
39
+ title?: string;
40
+ width: number;
41
+ /** The render's glyph set (`./glyphs.ts`), so a frame follows the box style. */
42
+ glyphs: GlyphSet;
43
+ }
44
+ /**
45
+ * The edge glyphs for a frame of `style`. Exposed so a caller that draws
46
+ * its OWN edges (`render.tsx`'s Ink-`Box`-bordered `card`) can match
47
+ * `frameBlock`'s glyphs exactly without duplicating them.
48
+ */
49
+ export declare function frameGlyphs(style: 'solid' | 'dashed', glyphs: GlyphSet): FrameEdges;
50
+ /**
51
+ * Just the top edge of a self-drawing frame, with an optional title woven
52
+ * in. Factored out of `frameBlock` for `render.tsx`'s `card`: an Ink `Box`
53
+ * border draws a plain edge perfectly well on its own (left, right, and an
54
+ * untitled top/bottom), but has no primitive for weaving text into a
55
+ * border line, so a titled card still builds this one line by hand and
56
+ * disables Ink's own top border for it.
57
+ */
58
+ export declare function frameTopLine(options: FrameOptions): string;
59
+ /** Draws a box around `block`, one glyph set for `'solid'`, another for `'dashed'`, with an optional title woven into the top edge. */
60
+ export declare function frameBlock(block: string, options: FrameOptions): string;
61
+ /** The `left`/`right`/`top`/`bottom`/`topLeft`/`topRight`/`bottomLeft`/`bottomRight` glyph set Ink's `Box` `borderStyle` prop takes, matching `frameBlock`'s own character set exactly for `style`. Corners are always the shared set, exactly like `frameBlock`'s (a dashed frame still has solid corners). */
62
+ export declare function inkBorderStyle(style: 'solid' | 'dashed', glyphs: GlyphSet): {
63
+ topLeft: string;
64
+ top: string;
65
+ topRight: string;
66
+ right: string;
67
+ bottomRight: string;
68
+ bottom: string;
69
+ bottomLeft: string;
70
+ left: string;
71
+ };
72
+ /** The three-value per-line alignment every self-drawing component's `text=` attribute and every generic `align=` layout preset ultimately reduces to — the ONE spelling both `padText` (a pre-built string) and a caller pre-justifying a line before handing it to Ink share. */
73
+ export declare const TEXT_ALIGNS: readonly ["left", "center", "right"];
74
+ export type TextAlign = (typeof TEXT_ALIGNS)[number];
75
+ export declare function isTextAlign(value: string): value is TextAlign;
76
+ /** `text` cut to at most `width` terminal columns, with no ellipsis (so no glyph outside the engine's own table). Plain text only: an escape sequence would be measured and cut like prose. */
77
+ export declare function cutToWidth(text: string, width: number): string;
78
+ /** A full-width horizontal rule, `char` repeated to `width` columns. */
79
+ export declare function ruleLine(width: number, char: string): string;
80
+ /** Prefixes every line of `block` (split on `\n`) with `prefix`, e.g. a blockquote's bar. */
81
+ export declare function indentBlock(block: string, prefix: string): string;
82
+ /**
83
+ * Places `blocks` side by side, each padded (left-aligned) to its entry in
84
+ * `widths`, joined by `gutter` spaces, top-aligned. The string emitter of
85
+ * `row` (`render.tsx`'s `renderRowString`, reached when a `row` sits inside
86
+ * a body drawn as a string) places its cells with this. The Ink element
87
+ * emitter (`renderRowElement`) uses a flex `Box` with the same cell widths
88
+ * and gutter, both read from `components/row.ts`'s `resolveRowLayout`.
89
+ */
90
+ export declare function columnsBlock(blocks: readonly string[], widths: readonly number[], gutter: number): string;
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Hand-written width measurement, padding, wrapping, and box-drawing for the
3
+ * handful of STANDARD COMPONENTS that draw their own fixed-width glyphs
4
+ * (`card`, `callout`, `divider`, `chart`, `table`, and the plain-markdown
5
+ * table grid in `render.tsx`). Everything else in this engine wraps prose
6
+ * through Ink's own layout (`ink-string.ts`'s doc comment and AGENTS.md's
7
+ * batch-10 brief: "Drop wrap/pad/columns/frame/rule: Ink's layout replaces
8
+ * them").
9
+ *
10
+ * A self-drawing component cannot lean on Ink for this: the string it builds
11
+ * (a box-drawing frame, a sparkline, a table grid) is a fixed block of
12
+ * characters that has to be CORRECT the moment it is built, because Ink's
13
+ * `<Text>` renders it verbatim rather than reflowing pre-drawn glyphs. This
14
+ * module is therefore kept as a small, PRIVATE (not exported from
15
+ * `src/index.ts`) implementation detail of those few components, replacing
16
+ * the old public `measure.ts`/`box.ts` modules the batch-10 brief has this
17
+ * engine delete. The width-measurement approximation is unchanged from the
18
+ * old `measure.ts`: Latin/CJK/emoji/combining-mark ranges, not a full
19
+ * Unicode grapheme-segmentation table (see the doc comment below for the one
20
+ * documented limitation).
21
+ */
22
+ /** SGR: `ESC [ ... m`. */
23
+ const SGR_PATTERN = /\x1b\[[0-9;]*m/g;
24
+ /** OSC 8 hyperlink open/close: `ESC ] 8 ; ; ... BEL`. */
25
+ const OSC8_PATTERN = /\x1b\]8;;[^\x07\x1b]*\x07/g;
26
+ const ESCAPE_PATTERN = /\x1b\[[0-9;]*m|\x1b\]8;;[^\x07\x1b]*\x07/g;
27
+ /** Removes every SGR and OSC 8 escape sequence this engine can itself produce, so `measureWidth` counts only what a terminal actually draws. */
28
+ export function stripEscapes(text) {
29
+ return text.replace(OSC8_PATTERN, '').replace(SGR_PATTERN, '');
30
+ }
31
+ const COMBINING_RANGES = [
32
+ [0x0300, 0x036f],
33
+ [0x1ab0, 0x1aff],
34
+ [0x1dc0, 0x1dff],
35
+ [0x20d0, 0x20f0],
36
+ [0xfe00, 0xfe0f],
37
+ [0xfe20, 0xfe2f],
38
+ ];
39
+ const ZERO_WIDTH_RANGES = [
40
+ [0x200b, 0x200d],
41
+ [0x2060, 0x2060],
42
+ [0xfeff, 0xfeff],
43
+ ];
44
+ const WIDE_RANGES = [
45
+ [0x1100, 0x115f],
46
+ [0x2e80, 0x303e],
47
+ [0x3041, 0x33ff],
48
+ [0x3400, 0x4dbf],
49
+ [0x4e00, 0x9fff],
50
+ [0xa000, 0xa4cf],
51
+ [0xac00, 0xd7a3],
52
+ [0xf900, 0xfaff],
53
+ [0xfe30, 0xfe6f],
54
+ [0xff00, 0xff60],
55
+ [0xffe0, 0xffe6],
56
+ [0x20000, 0x2fffd],
57
+ [0x30000, 0x3fffd],
58
+ ];
59
+ const EMOJI_RANGES = [
60
+ [0x1f300, 0x1f64f],
61
+ [0x1f680, 0x1f6ff],
62
+ [0x1f900, 0x1f9ff],
63
+ [0x1fa70, 0x1faff],
64
+ ];
65
+ const EMOJI_PRESENTATION_LOW = 0x2600;
66
+ const EMOJI_PRESENTATION_HIGH = 0x27bf;
67
+ const VARIATION_SELECTOR_16 = 0xfe0f;
68
+ function inRanges(code, ranges) {
69
+ for (const [low, high] of ranges) {
70
+ if (code >= low && code <= high)
71
+ return true;
72
+ }
73
+ return false;
74
+ }
75
+ function codePointWidth(code, next) {
76
+ if (inRanges(code, COMBINING_RANGES))
77
+ return 0;
78
+ if (inRanges(code, ZERO_WIDTH_RANGES))
79
+ return 0;
80
+ if (inRanges(code, WIDE_RANGES))
81
+ return 2;
82
+ if (inRanges(code, EMOJI_RANGES))
83
+ return 2;
84
+ if (code >= EMOJI_PRESENTATION_LOW &&
85
+ code <= EMOJI_PRESENTATION_HIGH &&
86
+ next === VARIATION_SELECTOR_16) {
87
+ return 2;
88
+ }
89
+ return 1;
90
+ }
91
+ /**
92
+ * The display width of `text`, ANSI escapes stripped first. This is a
93
+ * deliberate approximation of UAX #11 (see `text-grid.ts`'s module comment
94
+ * and the former `measure.ts`'s identical documented limitation: a
95
+ * zero-width-joiner emoji sequence measures as the sum of its parts).
96
+ */
97
+ export function measureWidth(text) {
98
+ const stripped = stripEscapes(text);
99
+ const codePoints = Array.from(stripped, (char) => char.codePointAt(0) ?? 0);
100
+ let width = 0;
101
+ for (let index = 0; index < codePoints.length; index += 1) {
102
+ width += codePointWidth(codePoints[index], codePoints[index + 1]);
103
+ }
104
+ return width;
105
+ }
106
+ function pushChars(atoms, plain) {
107
+ for (const char of plain)
108
+ atoms.push({ text: char, width: measureWidth(char) });
109
+ }
110
+ function tokenize(text) {
111
+ const atoms = [];
112
+ let lastIndex = 0;
113
+ let match;
114
+ ESCAPE_PATTERN.lastIndex = 0;
115
+ while ((match = ESCAPE_PATTERN.exec(text)) !== null) {
116
+ if (match.index > lastIndex)
117
+ pushChars(atoms, text.slice(lastIndex, match.index));
118
+ atoms.push({ text: match[0], width: 0 });
119
+ lastIndex = match.index + match[0].length;
120
+ }
121
+ if (lastIndex < text.length)
122
+ pushChars(atoms, text.slice(lastIndex));
123
+ return atoms;
124
+ }
125
+ function breakLongWord(word, width) {
126
+ const atoms = tokenize(word);
127
+ const chunks = [];
128
+ let current = '';
129
+ let currentWidth = 0;
130
+ for (const atom of atoms) {
131
+ if (currentWidth + atom.width > width && current !== '') {
132
+ chunks.push(current);
133
+ current = '';
134
+ currentWidth = 0;
135
+ }
136
+ current += atom.text;
137
+ currentWidth += atom.width;
138
+ }
139
+ if (current !== '')
140
+ chunks.push(current);
141
+ return chunks.length > 0 ? chunks : [''];
142
+ }
143
+ /** Greedy word wrap to `width` columns, used only by a self-drawing component's own internal text (its title, its caption). Not exposed on `AnsiRenderContext`: ordinary prose wraps through Ink's own layout instead. */
144
+ export function wrapText(text, width) {
145
+ const w = Math.max(1, width);
146
+ const lines = [];
147
+ for (const hardLine of text.split('\n')) {
148
+ const words = hardLine.split(/ +/).filter((word) => word !== '');
149
+ if (words.length === 0) {
150
+ lines.push('');
151
+ continue;
152
+ }
153
+ let current = '';
154
+ let currentWidth = 0;
155
+ for (const word of words) {
156
+ const wordWidth = measureWidth(word);
157
+ if (wordWidth > w) {
158
+ if (current !== '') {
159
+ lines.push(current);
160
+ current = '';
161
+ currentWidth = 0;
162
+ }
163
+ lines.push(...breakLongWord(word, w));
164
+ continue;
165
+ }
166
+ if (current === '') {
167
+ current = word;
168
+ currentWidth = wordWidth;
169
+ continue;
170
+ }
171
+ if (currentWidth + 1 + wordWidth <= w) {
172
+ current += ` ${word}`;
173
+ currentWidth += 1 + wordWidth;
174
+ }
175
+ else {
176
+ lines.push(current);
177
+ current = word;
178
+ currentWidth = wordWidth;
179
+ }
180
+ }
181
+ if (current !== '')
182
+ lines.push(current);
183
+ }
184
+ return lines;
185
+ }
186
+ /** Pads `text` to `width` columns with spaces, aligned `left`/`center`/`right`. */
187
+ export function padText(text, width, align) {
188
+ const total = Math.max(0, width - measureWidth(text));
189
+ if (total === 0)
190
+ return text;
191
+ if (align === 'right')
192
+ return ' '.repeat(total) + text;
193
+ if (align === 'center') {
194
+ const left = Math.floor(total / 2);
195
+ const right = total - left;
196
+ return ' '.repeat(left) + text + ' '.repeat(right);
197
+ }
198
+ return text + ' '.repeat(total);
199
+ }
200
+ /**
201
+ * The edge glyphs for a frame of `style`. Exposed so a caller that draws
202
+ * its OWN edges (`render.tsx`'s Ink-`Box`-bordered `card`) can match
203
+ * `frameBlock`'s glyphs exactly without duplicating them.
204
+ */
205
+ export function frameGlyphs(style, glyphs) {
206
+ return style === 'dashed' ? glyphs.dashed : glyphs.solid;
207
+ }
208
+ /**
209
+ * Just the top edge of a self-drawing frame, with an optional title woven
210
+ * in. Factored out of `frameBlock` for `render.tsx`'s `card`: an Ink `Box`
211
+ * border draws a plain edge perfectly well on its own (left, right, and an
212
+ * untitled top/bottom), but has no primitive for weaving text into a
213
+ * border line, so a titled card still builds this one line by hand and
214
+ * disables Ink's own top border for it.
215
+ */
216
+ export function frameTopLine(options) {
217
+ const { style: kind, title, width, glyphs } = options;
218
+ const { horizontal } = frameGlyphs(kind, glyphs);
219
+ const innerWidth = Math.max(1, width - 2);
220
+ if (title) {
221
+ const label = ` ${title} `;
222
+ const remaining = Math.max(0, innerWidth - measureWidth(label) - 1);
223
+ return `${glyphs.topLeft}${horizontal}${label}${horizontal.repeat(remaining)}${glyphs.topRight}`;
224
+ }
225
+ return `${glyphs.topLeft}${horizontal.repeat(innerWidth)}${glyphs.topRight}`;
226
+ }
227
+ /** Draws a box around `block`, one glyph set for `'solid'`, another for `'dashed'`, with an optional title woven into the top edge. */
228
+ export function frameBlock(block, options) {
229
+ const { style: kind, width, glyphs } = options;
230
+ const { horizontal, vertical } = frameGlyphs(kind, glyphs);
231
+ const innerWidth = Math.max(1, width - 2);
232
+ const top = frameTopLine(options);
233
+ const bottom = `${glyphs.bottomLeft}${horizontal.repeat(innerWidth)}${glyphs.bottomRight}`;
234
+ const lines = block
235
+ .split('\n')
236
+ .map((line) => `${vertical}${padText(line, innerWidth, 'left')}${vertical}`);
237
+ return [top, ...lines, bottom].join('\n');
238
+ }
239
+ /** The `left`/`right`/`top`/`bottom`/`topLeft`/`topRight`/`bottomLeft`/`bottomRight` glyph set Ink's `Box` `borderStyle` prop takes, matching `frameBlock`'s own character set exactly for `style`. Corners are always the shared set, exactly like `frameBlock`'s (a dashed frame still has solid corners). */
240
+ export function inkBorderStyle(style, glyphs) {
241
+ const { horizontal, vertical } = frameGlyphs(style, glyphs);
242
+ return {
243
+ topLeft: glyphs.topLeft,
244
+ top: horizontal,
245
+ topRight: glyphs.topRight,
246
+ right: vertical,
247
+ bottomRight: glyphs.bottomRight,
248
+ bottom: horizontal,
249
+ bottomLeft: glyphs.bottomLeft,
250
+ left: vertical,
251
+ };
252
+ }
253
+ /** The three-value per-line alignment every self-drawing component's `text=` attribute and every generic `align=` layout preset ultimately reduces to — the ONE spelling both `padText` (a pre-built string) and a caller pre-justifying a line before handing it to Ink share. */
254
+ export const TEXT_ALIGNS = ['left', 'center', 'right'];
255
+ export function isTextAlign(value) {
256
+ return TEXT_ALIGNS.includes(value);
257
+ }
258
+ /** `text` cut to at most `width` terminal columns, with no ellipsis (so no glyph outside the engine's own table). Plain text only: an escape sequence would be measured and cut like prose. */
259
+ export function cutToWidth(text, width) {
260
+ let out = '';
261
+ let used = 0;
262
+ for (const char of text) {
263
+ const next = measureWidth(char);
264
+ if (used + next > width)
265
+ break;
266
+ out += char;
267
+ used += next;
268
+ }
269
+ return out;
270
+ }
271
+ /** A full-width horizontal rule, `char` repeated to `width` columns. */
272
+ export function ruleLine(width, char) {
273
+ return char.repeat(Math.max(0, width));
274
+ }
275
+ /** Prefixes every line of `block` (split on `\n`) with `prefix`, e.g. a blockquote's bar. */
276
+ export function indentBlock(block, prefix) {
277
+ return block
278
+ .split('\n')
279
+ .map((line) => `${prefix}${line}`)
280
+ .join('\n');
281
+ }
282
+ /**
283
+ * Places `blocks` side by side, each padded (left-aligned) to its entry in
284
+ * `widths`, joined by `gutter` spaces, top-aligned. The string emitter of
285
+ * `row` (`render.tsx`'s `renderRowString`, reached when a `row` sits inside
286
+ * a body drawn as a string) places its cells with this. The Ink element
287
+ * emitter (`renderRowElement`) uses a flex `Box` with the same cell widths
288
+ * and gutter, both read from `components/row.ts`'s `resolveRowLayout`.
289
+ */
290
+ export function columnsBlock(blocks, widths, gutter) {
291
+ const gap = ' '.repeat(Math.max(0, gutter));
292
+ const lineArrays = blocks.map((block) => block.split('\n'));
293
+ const rowCount = Math.max(0, ...lineArrays.map((lines) => lines.length));
294
+ const rows = [];
295
+ for (let row = 0; row < rowCount; row += 1) {
296
+ const cells = lineArrays.map((lines, index) => padText(lines[row] ?? '', widths[index] ?? 0, 'left'));
297
+ rows.push(cells.join(gap).replace(/[ ]+$/, ''));
298
+ }
299
+ return rows.join('\n');
300
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@markii/ansi",
3
- "version": "0.15.0",
4
- "description": "A framework-free terminal renderer for Markii (.mk.md): a registry-driven hast-to-ANSI-text engine. Zero React; for a terminal, a pipe, or any host that wants a plain string with optional color.",
3
+ "version": "0.17.0-next.0",
4
+ "description": "The terminal renderer for Markii (.mk.md): a registry-driven hast-to-terminal engine built on Ink. Renders a note once as a string with optional color, for a pipe or a log, or as a live Ink tree a host can mount.",
5
5
  "keywords": [
6
6
  "markdown",
7
7
  "markii",
@@ -51,11 +51,15 @@
51
51
  "regenerate:fixtures": "npm run build:dist && node scripts/regenerate-ansi-fixtures.ts"
52
52
  },
53
53
  "dependencies": {
54
- "@markii/core": "^0.15.0",
55
- "@markii/stdlib": "^0.15.0"
54
+ "@markii/core": "^0.17.0-next.0",
55
+ "@markii/pack": "^0.17.0-next.0",
56
+ "@markii/stdlib": "^0.17.0-next.0",
57
+ "ink": "^7.1.1",
58
+ "react": "^19.2.0"
56
59
  },
57
60
  "devDependencies": {
58
61
  "@types/hast": "^3.0.4",
59
- "@markii/runtime": "^0.15.0"
62
+ "@markii/runtime": "^0.17.0-next.0",
63
+ "@types/react": "^19.2.0"
60
64
  }
61
65
  }
package/dist/box.d.ts DELETED
@@ -1,47 +0,0 @@
1
- /**
2
- * Greedy word wrap to `width` columns. An existing `\n` is a hard break
3
- * (each hard-broken segment is wrapped independently); a word wider than
4
- * `width` on its own is broken at the width boundary via `breakLongWord`
5
- * rather than overflowing the line.
6
- */
7
- export declare function wrap(text: string, width: number): string[];
8
- /** Pads `text` to `width` columns with spaces, aligned `left`/`center`/`right`. A `text` already at or over `width` is returned unchanged. */
9
- export declare function pad(text: string, width: number, align: 'left' | 'center' | 'right'): string;
10
- /**
11
- * Re-wraps a block of text that was already wrapped to a WIDER budget than
12
- * `width` (the shape a component's pre-rendered `childrenText` always
13
- * arrives in: `render.ts` wraps a directive's body to the full directive
14
- * width before the component ever runs, since a container component that
15
- * draws its own frame or indent — `card`, `callout`, `details`, `figure` —
16
- * only learns it needs a NARROWER inner budget once it starts drawing).
17
- * Every existing `\n` is a hard break, each hard-broken line re-wrapped
18
- * independently if it still overflows `width`; a line already inside the
19
- * budget passes through unchanged. Mirrors the same pattern `render.ts`'s
20
- * `unknownDirective` fallback already uses for its dashed frame.
21
- */
22
- export declare function rewrapBlock(text: string, width: number): string;
23
- /** Prefixes every line of `block` (split on `\n`) with `prefix`, e.g. a blockquote's `│ `. */
24
- export declare function indentBlock(block: string, prefix: string): string;
25
- /**
26
- * Places `blocks` side by side, each padded (left-aligned) to its entry in
27
- * `widths`, joined by `gutter` spaces, top-aligned: a block with fewer
28
- * lines than its neighbors gets its missing rows filled with blank,
29
- * full-width padding rather than leaving a ragged gap.
30
- */
31
- export declare function columns(blocks: readonly string[], widths: readonly number[], gutter: number): string;
32
- /** `frame`'s options: which glyph set to draw with, an optional title woven into the top edge, and the frame's total outer width (borders included). */
33
- export interface FrameOptions {
34
- style: 'solid' | 'dashed';
35
- title?: string;
36
- width: number;
37
- }
38
- /**
39
- * Draws a box around `block`: `┌ ─ ┐ │ └ ┘` for `'solid'`, `┌ ╌ ┐ ┆ └ ┘` for
40
- * `'dashed'` (the corners are shared; only the edge glyphs change). A
41
- * `title` is woven into the top edge as `┌─ title ────┐`. Every content
42
- * line is padded to the frame's inner width, so every drawn line is exactly
43
- * `options.width` columns wide.
44
- */
45
- export declare function frame(block: string, options: FrameOptions): string;
46
- /** A full-width horizontal rule, `char` (default `─`) repeated to `width` columns. */
47
- export declare function rule(width: number, char?: string): string;
package/dist/box.js DELETED
@@ -1,209 +0,0 @@
1
- import { measure } from './measure.js';
2
- /**
3
- * Hand-written, width-aware layout helpers: wrapping, padding, side-by-side
4
- * columns, and box drawing. Every measurement goes through `./measure.ts`,
5
- * so these helpers wrap and pad correctly around colored text, wide CJK
6
- * characters, and emoji, not just plain ASCII. None of these read the
7
- * theme; they take and return plain strings, leaving color entirely to
8
- * `./style.ts` and the caller.
9
- *
10
- * `wrap`'s word-splitting collapses runs of spaces to one, the ordinary
11
- * behavior of a text wrapper: exact inter-word spacing is not something a
12
- * terminal reflow is expected to preserve.
13
- */
14
- const ESCAPE_PATTERN = /\x1b\[[0-9;]*m|\x1b\]8;;[^\x07\x1b]*\x07/g;
15
- /**
16
- * Splits `text` into atoms an escape sequence never gets split across: each
17
- * escape sequence is its own zero-width atom, and every other code point is
18
- * its own atom carrying `measure`'s width for that single code point. A
19
- * combining mark or variation selector that only reads as zero-width in
20
- * context with its BASE character (see `./measure.ts`'s module comment)
21
- * still measures correctly here, because it is itself in the zero-width/
22
- * combining ranges regardless of what precedes it; the one case this
23
- * under- or over-counts by a column is the dingbat-plus-variation-selector
24
- * pair (`☀️`), which needs to see both code points at once to know it
25
- * should measure 2 — an accepted instance of `./measure.ts`'s documented
26
- * approximation.
27
- */
28
- function tokenize(text) {
29
- const atoms = [];
30
- let lastIndex = 0;
31
- let match;
32
- ESCAPE_PATTERN.lastIndex = 0;
33
- while ((match = ESCAPE_PATTERN.exec(text)) !== null) {
34
- if (match.index > lastIndex) {
35
- pushChars(atoms, text.slice(lastIndex, match.index));
36
- }
37
- atoms.push({ text: match[0], width: 0 });
38
- lastIndex = match.index + match[0].length;
39
- }
40
- if (lastIndex < text.length)
41
- pushChars(atoms, text.slice(lastIndex));
42
- return atoms;
43
- }
44
- function pushChars(atoms, plain) {
45
- for (const char of plain) {
46
- atoms.push({ text: char, width: measure(char) });
47
- }
48
- }
49
- /** Breaks one over-long word into chunks of at most `width` columns, never splitting an escape sequence. */
50
- function breakLongWord(word, width) {
51
- const atoms = tokenize(word);
52
- const chunks = [];
53
- let current = '';
54
- let currentWidth = 0;
55
- for (const atom of atoms) {
56
- if (currentWidth + atom.width > width && current !== '') {
57
- chunks.push(current);
58
- current = '';
59
- currentWidth = 0;
60
- }
61
- current += atom.text;
62
- currentWidth += atom.width;
63
- }
64
- if (current !== '')
65
- chunks.push(current);
66
- return chunks.length > 0 ? chunks : [''];
67
- }
68
- /**
69
- * Greedy word wrap to `width` columns. An existing `\n` is a hard break
70
- * (each hard-broken segment is wrapped independently); a word wider than
71
- * `width` on its own is broken at the width boundary via `breakLongWord`
72
- * rather than overflowing the line.
73
- */
74
- export function wrap(text, width) {
75
- const w = Math.max(1, width);
76
- const lines = [];
77
- for (const hardLine of text.split('\n')) {
78
- const words = hardLine.split(/ +/).filter((word) => word !== '');
79
- if (words.length === 0) {
80
- lines.push('');
81
- continue;
82
- }
83
- let current = '';
84
- let currentWidth = 0;
85
- for (const word of words) {
86
- const wordWidth = measure(word);
87
- if (wordWidth > w) {
88
- if (current !== '') {
89
- lines.push(current);
90
- current = '';
91
- currentWidth = 0;
92
- }
93
- lines.push(...breakLongWord(word, w));
94
- continue;
95
- }
96
- if (current === '') {
97
- current = word;
98
- currentWidth = wordWidth;
99
- continue;
100
- }
101
- if (currentWidth + 1 + wordWidth <= w) {
102
- current += ` ${word}`;
103
- currentWidth += 1 + wordWidth;
104
- }
105
- else {
106
- lines.push(current);
107
- current = word;
108
- currentWidth = wordWidth;
109
- }
110
- }
111
- if (current !== '')
112
- lines.push(current);
113
- }
114
- return lines;
115
- }
116
- /** Pads `text` to `width` columns with spaces, aligned `left`/`center`/`right`. A `text` already at or over `width` is returned unchanged. */
117
- export function pad(text, width, align) {
118
- const total = Math.max(0, width - measure(text));
119
- if (total === 0)
120
- return text;
121
- if (align === 'right')
122
- return ' '.repeat(total) + text;
123
- if (align === 'center') {
124
- const left = Math.floor(total / 2);
125
- const right = total - left;
126
- return ' '.repeat(left) + text + ' '.repeat(right);
127
- }
128
- return text + ' '.repeat(total);
129
- }
130
- /**
131
- * Re-wraps a block of text that was already wrapped to a WIDER budget than
132
- * `width` (the shape a component's pre-rendered `childrenText` always
133
- * arrives in: `render.ts` wraps a directive's body to the full directive
134
- * width before the component ever runs, since a container component that
135
- * draws its own frame or indent — `card`, `callout`, `details`, `figure` —
136
- * only learns it needs a NARROWER inner budget once it starts drawing).
137
- * Every existing `\n` is a hard break, each hard-broken line re-wrapped
138
- * independently if it still overflows `width`; a line already inside the
139
- * budget passes through unchanged. Mirrors the same pattern `render.ts`'s
140
- * `unknownDirective` fallback already uses for its dashed frame.
141
- */
142
- export function rewrapBlock(text, width) {
143
- if (!text)
144
- return text;
145
- return text
146
- .split('\n')
147
- .flatMap((line) => wrap(line, width))
148
- .join('\n');
149
- }
150
- /** Prefixes every line of `block` (split on `\n`) with `prefix`, e.g. a blockquote's `│ `. */
151
- export function indentBlock(block, prefix) {
152
- return block
153
- .split('\n')
154
- .map((line) => `${prefix}${line}`)
155
- .join('\n');
156
- }
157
- /**
158
- * Places `blocks` side by side, each padded (left-aligned) to its entry in
159
- * `widths`, joined by `gutter` spaces, top-aligned: a block with fewer
160
- * lines than its neighbors gets its missing rows filled with blank,
161
- * full-width padding rather than leaving a ragged gap.
162
- */
163
- export function columns(blocks, widths, gutter) {
164
- const gap = ' '.repeat(Math.max(0, gutter));
165
- const lineArrays = blocks.map((block) => block.split('\n'));
166
- const rowCount = Math.max(0, ...lineArrays.map((lines) => lines.length));
167
- const rows = [];
168
- for (let row = 0; row < rowCount; row += 1) {
169
- const cells = lineArrays.map((lines, index) => pad(lines[row] ?? '', widths[index] ?? 0, 'left'));
170
- // The padding on the LAST column is dropped again. It buys nothing: no
171
- // column follows it to be aligned against, and left in place it would
172
- // end every row with a run of spaces, which shows up as trailing
173
- // whitespace in a committed fixture and in anything that pipes this
174
- // output to a file. Padding between columns is what does the work.
175
- rows.push(cells.join(gap).replace(/[ ]+$/, ''));
176
- }
177
- return rows.join('\n');
178
- }
179
- /**
180
- * Draws a box around `block`: `┌ ─ ┐ │ └ ┘` for `'solid'`, `┌ ╌ ┐ ┆ └ ┘` for
181
- * `'dashed'` (the corners are shared; only the edge glyphs change). A
182
- * `title` is woven into the top edge as `┌─ title ────┐`. Every content
183
- * line is padded to the frame's inner width, so every drawn line is exactly
184
- * `options.width` columns wide.
185
- */
186
- export function frame(block, options) {
187
- const { style: kind, title, width } = options;
188
- const horizontal = kind === 'dashed' ? '╌' : '─';
189
- const vertical = kind === 'dashed' ? '┆' : '│';
190
- const innerWidth = Math.max(1, width - 2);
191
- let top;
192
- if (title) {
193
- const label = ` ${title} `;
194
- const remaining = Math.max(0, innerWidth - measure(label) - 1);
195
- top = `┌${horizontal}${label}${horizontal.repeat(remaining)}┐`;
196
- }
197
- else {
198
- top = `┌${horizontal.repeat(innerWidth)}┐`;
199
- }
200
- const bottom = `└${horizontal.repeat(innerWidth)}┘`;
201
- const lines = block
202
- .split('\n')
203
- .map((line) => `${vertical}${pad(line, innerWidth, 'left')}${vertical}`);
204
- return [top, ...lines, bottom].join('\n');
205
- }
206
- /** A full-width horizontal rule, `char` (default `─`) repeated to `width` columns. */
207
- export function rule(width, char = '─') {
208
- return char.repeat(Math.max(0, width));
209
- }