@markii/ansi 0.15.0 → 0.16.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.
- package/dist/ansi.d.ts +26 -0
- package/dist/ansi.js +51 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +5 -4
- package/dist/components/callout.d.ts +24 -13
- package/dist/components/callout.js +45 -21
- package/dist/components/card.d.ts +24 -2
- package/dist/components/card.js +41 -12
- package/dist/components/cell.d.ts +5 -11
- package/dist/components/cell.js +11 -15
- package/dist/components/chart.d.ts +4 -10
- package/dist/components/chart.js +9 -16
- package/dist/components/details-string.d.ts +26 -0
- package/dist/components/details-string.js +39 -0
- package/dist/components/details.d.ts +30 -10
- package/dist/components/details.js +27 -25
- package/dist/components/divider.js +9 -11
- package/dist/components/figure.d.ts +5 -16
- package/dist/components/figure.js +9 -19
- package/dist/components/index.d.ts +14 -10
- package/dist/components/index.js +19 -16
- package/dist/components/kbd.d.ts +1 -1
- package/dist/components/kbd.js +3 -2
- package/dist/components/layout-wrapper.d.ts +7 -22
- package/dist/components/layout-wrapper.js +9 -23
- package/dist/components/progress.d.ts +3 -11
- package/dist/components/progress.js +6 -14
- package/dist/components/rating.d.ts +1 -2
- package/dist/components/rating.js +2 -3
- package/dist/components/row.d.ts +33 -20
- package/dist/components/row.js +33 -51
- package/dist/components/stat.d.ts +3 -8
- package/dist/components/stat.js +4 -9
- package/dist/components/tab.d.ts +9 -8
- package/dist/components/tab.js +12 -10
- package/dist/components/table-grid.js +1 -2
- package/dist/components/table.d.ts +5 -10
- package/dist/components/table.js +7 -12
- package/dist/components/tabs-string.d.ts +17 -0
- package/dist/components/tabs-string.js +30 -0
- package/dist/components/tabs.d.ts +29 -14
- package/dist/components/tabs.js +37 -30
- package/dist/index.d.ts +2 -4
- package/dist/index.js +6 -7
- package/dist/ink-string.d.ts +12 -0
- package/dist/ink-string.js +41 -0
- package/dist/interactive.d.ts +41 -0
- package/dist/interactive.js +64 -0
- package/dist/layout.js +1 -2
- package/dist/registry.d.ts +91 -100
- package/dist/registry.js +17 -9
- package/dist/render.d.ts +39 -24
- package/dist/render.js +676 -373
- package/dist/text-grid.d.ts +54 -0
- package/dist/text-grid.js +249 -0
- package/package.json +7 -4
- package/dist/box.d.ts +0 -47
- package/dist/box.js +0 -209
- package/dist/measure.d.ts +0 -25
- package/dist/measure.js +0 -108
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
/** Removes every SGR and OSC 8 escape sequence this engine can itself produce, so `measureWidth` counts only what a terminal actually draws. */
|
|
23
|
+
export declare function stripEscapes(text: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* The display width of `text`, ANSI escapes stripped first. This is a
|
|
26
|
+
* deliberate approximation of UAX #11 (see `text-grid.ts`'s module comment
|
|
27
|
+
* and the former `measure.ts`'s identical documented limitation: a
|
|
28
|
+
* zero-width-joiner emoji sequence measures as the sum of its parts).
|
|
29
|
+
*/
|
|
30
|
+
export declare function measureWidth(text: string): number;
|
|
31
|
+
/** 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. */
|
|
32
|
+
export declare function wrapText(text: string, width: number): string[];
|
|
33
|
+
/** Pads `text` to `width` columns with spaces, aligned `left`/`center`/`right`. */
|
|
34
|
+
export declare function padText(text: string, width: number, align: 'left' | 'center' | 'right'): string;
|
|
35
|
+
/** `frameBlock`'s options: mirrors the old `box.ts`'s `FrameOptions`. */
|
|
36
|
+
export interface FrameOptions {
|
|
37
|
+
style: 'solid' | 'dashed';
|
|
38
|
+
title?: string;
|
|
39
|
+
width: number;
|
|
40
|
+
}
|
|
41
|
+
/** Draws a box around `block`, one glyph set for `'solid'`, another for `'dashed'`, with an optional title woven into the top edge. */
|
|
42
|
+
export declare function frameBlock(block: string, options: FrameOptions): string;
|
|
43
|
+
/** A full-width horizontal rule, `char` (default `─`) repeated to `width` columns. */
|
|
44
|
+
export declare function ruleLine(width: number, char?: string): string;
|
|
45
|
+
/** Prefixes every line of `block` (split on `\n`) with `prefix`, e.g. a blockquote's `│ `. */
|
|
46
|
+
export declare function indentBlock(block: string, prefix: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Places `blocks` side by side, each padded (left-aligned) to its entry in
|
|
49
|
+
* `widths`, joined by `gutter` spaces, top-aligned. Used only by the plain
|
|
50
|
+
* GFM markdown table (`render.tsx`'s `renderGfmTable`) and
|
|
51
|
+
* `table-grid.ts`'s own row-drawing; `row`'s OWN columns are placed by a
|
|
52
|
+
* real Ink flex `Box` instead (`components/row.tsx`), not by this helper.
|
|
53
|
+
*/
|
|
54
|
+
export declare function columnsBlock(blocks: readonly string[], widths: readonly number[], gutter: number): string;
|
|
@@ -0,0 +1,249 @@
|
|
|
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
|
+
/** Draws a box around `block`, one glyph set for `'solid'`, another for `'dashed'`, with an optional title woven into the top edge. */
|
|
201
|
+
export function frameBlock(block, options) {
|
|
202
|
+
const { style: kind, title, width } = options;
|
|
203
|
+
const horizontal = kind === 'dashed' ? '╌' : '─';
|
|
204
|
+
const vertical = kind === 'dashed' ? '┆' : '│';
|
|
205
|
+
const innerWidth = Math.max(1, width - 2);
|
|
206
|
+
let top;
|
|
207
|
+
if (title) {
|
|
208
|
+
const label = ` ${title} `;
|
|
209
|
+
const remaining = Math.max(0, innerWidth - measureWidth(label) - 1);
|
|
210
|
+
top = `┌${horizontal}${label}${horizontal.repeat(remaining)}┐`;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
top = `┌${horizontal.repeat(innerWidth)}┐`;
|
|
214
|
+
}
|
|
215
|
+
const bottom = `└${horizontal.repeat(innerWidth)}┘`;
|
|
216
|
+
const lines = block
|
|
217
|
+
.split('\n')
|
|
218
|
+
.map((line) => `${vertical}${padText(line, innerWidth, 'left')}${vertical}`);
|
|
219
|
+
return [top, ...lines, bottom].join('\n');
|
|
220
|
+
}
|
|
221
|
+
/** A full-width horizontal rule, `char` (default `─`) repeated to `width` columns. */
|
|
222
|
+
export function ruleLine(width, char = '─') {
|
|
223
|
+
return char.repeat(Math.max(0, width));
|
|
224
|
+
}
|
|
225
|
+
/** Prefixes every line of `block` (split on `\n`) with `prefix`, e.g. a blockquote's `│ `. */
|
|
226
|
+
export function indentBlock(block, prefix) {
|
|
227
|
+
return block
|
|
228
|
+
.split('\n')
|
|
229
|
+
.map((line) => `${prefix}${line}`)
|
|
230
|
+
.join('\n');
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Places `blocks` side by side, each padded (left-aligned) to its entry in
|
|
234
|
+
* `widths`, joined by `gutter` spaces, top-aligned. Used only by the plain
|
|
235
|
+
* GFM markdown table (`render.tsx`'s `renderGfmTable`) and
|
|
236
|
+
* `table-grid.ts`'s own row-drawing; `row`'s OWN columns are placed by a
|
|
237
|
+
* real Ink flex `Box` instead (`components/row.tsx`), not by this helper.
|
|
238
|
+
*/
|
|
239
|
+
export function columnsBlock(blocks, widths, gutter) {
|
|
240
|
+
const gap = ' '.repeat(Math.max(0, gutter));
|
|
241
|
+
const lineArrays = blocks.map((block) => block.split('\n'));
|
|
242
|
+
const rowCount = Math.max(0, ...lineArrays.map((lines) => lines.length));
|
|
243
|
+
const rows = [];
|
|
244
|
+
for (let row = 0; row < rowCount; row += 1) {
|
|
245
|
+
const cells = lineArrays.map((lines, index) => padText(lines[row] ?? '', widths[index] ?? 0, 'left'));
|
|
246
|
+
rows.push(cells.join(gap).replace(/[ ]+$/, ''));
|
|
247
|
+
}
|
|
248
|
+
return rows.join('\n');
|
|
249
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markii/ansi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -51,11 +51,14 @@
|
|
|
51
51
|
"regenerate:fixtures": "npm run build:dist && node scripts/regenerate-ansi-fixtures.ts"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@markii/core": "^0.
|
|
55
|
-
"@markii/stdlib": "^0.
|
|
54
|
+
"@markii/core": "^0.16.0",
|
|
55
|
+
"@markii/stdlib": "^0.16.0",
|
|
56
|
+
"ink": "^7.1.1",
|
|
57
|
+
"react": "^19.2.0"
|
|
56
58
|
},
|
|
57
59
|
"devDependencies": {
|
|
58
60
|
"@types/hast": "^3.0.4",
|
|
59
|
-
"@markii/runtime": "^0.
|
|
61
|
+
"@markii/runtime": "^0.16.0",
|
|
62
|
+
"@types/react": "^19.2.0"
|
|
60
63
|
}
|
|
61
64
|
}
|
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
|
-
}
|
package/dist/measure.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hand-written display-width measurement (no dependency is permitted here;
|
|
3
|
-
* see AGENTS.md's Stack section and the batch brief). This is a DELIBERATE
|
|
4
|
-
* APPROXIMATION of Unicode's East Asian Width property (UAX #11), not a
|
|
5
|
-
* full Unicode width table: it covers the ranges most terminal content
|
|
6
|
-
* actually uses (Latin text, CJK, common emoji, combining marks) and gets
|
|
7
|
-
* one class of case knowingly wrong, documented below.
|
|
8
|
-
*
|
|
9
|
-
* KNOWN LIMITATION: a grapheme cluster joined by a zero-width joiner
|
|
10
|
-
* (U+200D), such as a multi-person or multi-skin-tone emoji sequence,
|
|
11
|
-
* measures as the sum of its parts rather than as the single glyph a
|
|
12
|
-
* terminal actually draws. Getting this exactly right needs a real
|
|
13
|
-
* grapheme-segmentation table, which this package deliberately does not
|
|
14
|
-
* carry.
|
|
15
|
-
*/
|
|
16
|
-
/** Removes every SGR and OSC 8 escape sequence this engine can itself produce, so `measure` counts only what a terminal actually draws. */
|
|
17
|
-
export declare function stripAnsi(text: string): string;
|
|
18
|
-
/**
|
|
19
|
-
* The display width of `text`: ANSI escapes are stripped first, then every
|
|
20
|
-
* remaining code point (iterated as a code point, not a UTF-16 code unit,
|
|
21
|
-
* so a surrogate pair counts once) contributes its width per
|
|
22
|
-
* `codePointWidth`'s rules. This is what every wrapping/padding/column
|
|
23
|
-
* helper in `./box.ts` measures against.
|
|
24
|
-
*/
|
|
25
|
-
export declare function measure(text: string): number;
|