@markii/ansi 0.0.1 → 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
package/dist/ansi.d.ts
CHANGED
|
@@ -39,6 +39,32 @@ export declare const italic: (text: string, level: ColorLevel) => string;
|
|
|
39
39
|
export declare const underline: (text: string, level: ColorLevel) => string;
|
|
40
40
|
/** SGR inverse/reverse video (7). Returns `text` unchanged at `'none'`. */
|
|
41
41
|
export declare const inverse: (text: string, level: ColorLevel) => string;
|
|
42
|
+
/**
|
|
43
|
+
* Repairs an SGR escape span that would otherwise cross an embedded `\n`
|
|
44
|
+
* inside `text`, so every physical LINE is independently self-contained:
|
|
45
|
+
* whatever SGR codes are still "open" (emitted since the last full reset)
|
|
46
|
+
* at a line boundary are closed with a reset before the `\n` and reopened
|
|
47
|
+
* at the start of the next line.
|
|
48
|
+
*
|
|
49
|
+
* This is the SYSTEMATIC fix for a real Ink behavior discovered during
|
|
50
|
+
* batch 10's post-phase-2 review: Ink's `<Text>` does not treat an embedded
|
|
51
|
+
* raw ANSI escape as transparent across an embedded newline the way a
|
|
52
|
+
* terminal receiving the same bytes directly would — it resets SGR state
|
|
53
|
+
* at every line boundary within one `<Text>` value and does not reopen it.
|
|
54
|
+
* Every multi-line string this engine hands to a single `<Text>` node
|
|
55
|
+
* (`render.tsx`'s `styledText` helper) MUST pass through this function
|
|
56
|
+
* first: a dim-wrapped multi-line frame, a bold-and-underlined heading that
|
|
57
|
+
* wrapped onto more than one line, a paragraph whose bold span happened to
|
|
58
|
+
* straddle a wrap point — anything this engine itself line-wrapped (with
|
|
59
|
+
* `text-grid.ts`'s `wrapText`) while carrying an open style. It is a no-op,
|
|
60
|
+
* fast-pathed, for a string with no `\n` at all.
|
|
61
|
+
*
|
|
62
|
+
* Every SGR code this engine's own helpers ever emit closes with a FULL
|
|
63
|
+
* reset (`\x1b[0m`), never a narrow one (`ansi.ts`'s `wrapSgr`), so
|
|
64
|
+
* tracking "what is currently open" only ever needs a flat list cleared by
|
|
65
|
+
* the next `\x1b[0m` (or a bare `\x1b[m`) — never per-attribute bookkeeping.
|
|
66
|
+
*/
|
|
67
|
+
export declare function carrySgrAcrossLines(text: string): string;
|
|
42
68
|
/**
|
|
43
69
|
* OSC 8: `ESC ] 8 ; ; url BEL text ESC ] 8 ; ; BEL`. Emitted only when
|
|
44
70
|
* `level` is not `'none'`; at `'none'` this returns `text` UNCHANGED, and
|
package/dist/ansi.js
CHANGED
|
@@ -63,6 +63,57 @@ export const italic = sgrWrap(3);
|
|
|
63
63
|
export const underline = sgrWrap(4);
|
|
64
64
|
/** SGR inverse/reverse video (7). Returns `text` unchanged at `'none'`. */
|
|
65
65
|
export const inverse = sgrWrap(7);
|
|
66
|
+
/** Matches one SGR sequence, capturing its raw parameter string (empty for a bare `ESC [ m`). */
|
|
67
|
+
const SGR_TOKEN = /\x1b\[([0-9;]*)m/g;
|
|
68
|
+
/**
|
|
69
|
+
* Repairs an SGR escape span that would otherwise cross an embedded `\n`
|
|
70
|
+
* inside `text`, so every physical LINE is independently self-contained:
|
|
71
|
+
* whatever SGR codes are still "open" (emitted since the last full reset)
|
|
72
|
+
* at a line boundary are closed with a reset before the `\n` and reopened
|
|
73
|
+
* at the start of the next line.
|
|
74
|
+
*
|
|
75
|
+
* This is the SYSTEMATIC fix for a real Ink behavior discovered during
|
|
76
|
+
* batch 10's post-phase-2 review: Ink's `<Text>` does not treat an embedded
|
|
77
|
+
* raw ANSI escape as transparent across an embedded newline the way a
|
|
78
|
+
* terminal receiving the same bytes directly would — it resets SGR state
|
|
79
|
+
* at every line boundary within one `<Text>` value and does not reopen it.
|
|
80
|
+
* Every multi-line string this engine hands to a single `<Text>` node
|
|
81
|
+
* (`render.tsx`'s `styledText` helper) MUST pass through this function
|
|
82
|
+
* first: a dim-wrapped multi-line frame, a bold-and-underlined heading that
|
|
83
|
+
* wrapped onto more than one line, a paragraph whose bold span happened to
|
|
84
|
+
* straddle a wrap point — anything this engine itself line-wrapped (with
|
|
85
|
+
* `text-grid.ts`'s `wrapText`) while carrying an open style. It is a no-op,
|
|
86
|
+
* fast-pathed, for a string with no `\n` at all.
|
|
87
|
+
*
|
|
88
|
+
* Every SGR code this engine's own helpers ever emit closes with a FULL
|
|
89
|
+
* reset (`\x1b[0m`), never a narrow one (`ansi.ts`'s `wrapSgr`), so
|
|
90
|
+
* tracking "what is currently open" only ever needs a flat list cleared by
|
|
91
|
+
* the next `\x1b[0m` (or a bare `\x1b[m`) — never per-attribute bookkeeping.
|
|
92
|
+
*/
|
|
93
|
+
export function carrySgrAcrossLines(text) {
|
|
94
|
+
if (!text.includes('\n'))
|
|
95
|
+
return text;
|
|
96
|
+
const lines = text.split('\n');
|
|
97
|
+
let openCodes = [];
|
|
98
|
+
const result = [];
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
const prefix = openCodes.map((params) => `${ESC}[${params}m`).join('');
|
|
101
|
+
SGR_TOKEN.lastIndex = 0;
|
|
102
|
+
let match;
|
|
103
|
+
while ((match = SGR_TOKEN.exec(line)) !== null) {
|
|
104
|
+
const params = match[1] ?? '';
|
|
105
|
+
if (params === '' || params === '0') {
|
|
106
|
+
openCodes = [];
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
openCodes = [...openCodes, params];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const suffix = openCodes.length > 0 ? RESET : '';
|
|
113
|
+
result.push(`${prefix}${line}${suffix}`);
|
|
114
|
+
}
|
|
115
|
+
return result.join('\n');
|
|
116
|
+
}
|
|
66
117
|
/**
|
|
67
118
|
* OSC 8: `ESC ] 8 ; ; url BEL text ESC ] 8 ; ; BEL`. Emitted only when
|
|
68
119
|
* `level` is not `'none'`; at `'none'` this returns `text` UNCHANGED, and
|
package/dist/components/badge.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { childrenText } from '../registry.js';
|
|
1
2
|
const BADGE_VARIANTS = [
|
|
2
3
|
'neutral',
|
|
3
4
|
'info',
|
|
@@ -31,14 +32,14 @@ const TOKEN_NAME = {
|
|
|
31
32
|
* variant. At color level `'none'` there is no inverse video to distinguish
|
|
32
33
|
* a badge from surrounding text, so it degrades to `[label]` instead.
|
|
33
34
|
*/
|
|
34
|
-
export const Badge = (attributes, children, ctx) => {
|
|
35
|
+
export const Badge = ({ attributes, children, ctx }) => {
|
|
35
36
|
const rawVariant = attributes.variant ?? DEFAULT_VARIANT;
|
|
36
37
|
const variant = isBadgeVariant(rawVariant)
|
|
37
38
|
? rawVariant
|
|
38
39
|
: DEFAULT_VARIANT;
|
|
39
|
-
const
|
|
40
|
+
const text = childrenText(children);
|
|
40
41
|
if (ctx.color === 'none')
|
|
41
|
-
return `[${
|
|
42
|
+
return `[${text}]`;
|
|
42
43
|
const token = TOKEN_NAME[VARIANT_TOKEN[variant]];
|
|
43
|
-
return ctx.inverse(ctx.style(` ${
|
|
44
|
+
return ctx.inverse(ctx.style(` ${text} `, token));
|
|
44
45
|
};
|
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ResolvedLayoutPresets } from '../layout.js';
|
|
2
|
+
import { type AnsiComponent, type DirectiveAttributes } from '../registry.js';
|
|
2
3
|
export type CalloutType = 'info' | 'warning' | 'danger';
|
|
4
|
+
/**
|
|
5
|
+
* The inner budget `callout`'s body renders at, given ONLY its own
|
|
6
|
+
* attributes and the outer width — never the body itself. Mirrors
|
|
7
|
+
* `card.ts`'s `resolveCardInnerWidth`: `render.tsx`'s walk calls this
|
|
8
|
+
* BEFORE building children, so a callout's body (and any self-drawing
|
|
9
|
+
* component nested inside it, like a `card`) is built exactly once, at the
|
|
10
|
+
* correct width, never built wide and re-wrapped narrower afterward — see
|
|
11
|
+
* `resolveCardInnerWidth`'s doc comment for why that re-wrap-afterward
|
|
12
|
+
* shape was a real, shipped regression.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveCalloutInnerWidth(attributes: DirectiveAttributes, layout: ResolvedLayoutPresets | undefined, outerWidth: number): number;
|
|
3
15
|
/**
|
|
4
16
|
* `:::callout{type=info|warning|danger title="..." text=left|center|right}` —
|
|
5
17
|
* a colored aside/warning/danger box. Unknown/missing `type` falls back to
|
|
6
|
-
* `info
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* width. `text` aligns the icon/title/body lines within that remaining
|
|
11
|
-
* width; absent/invalid `text` behaves as `left` (no padding needed).
|
|
18
|
+
* `info`. Terminal form: every line carries a colored left bar; the first
|
|
19
|
+
* line is the icon plus the type label, an optional bold title line
|
|
20
|
+
* follows, then the body. `text` aligns the icon/title/body lines within
|
|
21
|
+
* the remaining width.
|
|
12
22
|
*
|
|
13
|
-
* Registered `selfLayout
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
23
|
+
* Registered `selfLayout`: every line carries the colored bar prefix, which
|
|
24
|
+
* a generic post-render `applyLayout` narrowing would re-wrap right through,
|
|
25
|
+
* losing the bar on any continuation line. This component reads the
|
|
26
|
+
* resolved `width`/`align` off `ctx.layout` itself and sizes/places its own
|
|
27
|
+
* bar block, exactly like `card`/`table`/`divider`. `children` arrives
|
|
28
|
+
* ALREADY built at exactly this component's own inner width
|
|
29
|
+
* (`resolveCalloutInnerWidth` above) — this component never re-wraps.
|
|
19
30
|
*/
|
|
20
31
|
export declare const Callout: AnsiComponent;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { measureWidth, padText } from '../text-grid.js';
|
|
2
2
|
import { selfLayoutAlign, selfLayoutWidth } from '../layout.js';
|
|
3
|
+
import { childrenText, } from '../registry.js';
|
|
3
4
|
const CALLOUT_TYPES = ['info', 'warning', 'danger'];
|
|
4
5
|
const CALLOUT_ICONS = {
|
|
5
6
|
info: 'ℹ',
|
|
@@ -23,47 +24,70 @@ const TEXT_ALIGNS = ['left', 'center', 'right'];
|
|
|
23
24
|
function isTextAlign(value) {
|
|
24
25
|
return TEXT_ALIGNS.includes(value);
|
|
25
26
|
}
|
|
26
|
-
/** The left bar every line of a callout carries, marking it as one colored block at a glance
|
|
27
|
+
/** The left bar every line of a callout carries, marking it as one colored block at a glance. */
|
|
27
28
|
const BAR = '▌ ';
|
|
29
|
+
/** `headerText` for `type`, e.g. `ℹ Info` — needed by both `resolveCalloutInnerWidth` (before there is a component instance) and `Callout` itself. */
|
|
30
|
+
function headerTextFor(type) {
|
|
31
|
+
return `${CALLOUT_ICONS[type]} ${CALLOUT_LABELS[type]}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The inner budget `callout`'s body renders at, given ONLY its own
|
|
35
|
+
* attributes and the outer width — never the body itself. Mirrors
|
|
36
|
+
* `card.ts`'s `resolveCardInnerWidth`: `render.tsx`'s walk calls this
|
|
37
|
+
* BEFORE building children, so a callout's body (and any self-drawing
|
|
38
|
+
* component nested inside it, like a `card`) is built exactly once, at the
|
|
39
|
+
* correct width, never built wide and re-wrapped narrower afterward — see
|
|
40
|
+
* `resolveCardInnerWidth`'s doc comment for why that re-wrap-afterward
|
|
41
|
+
* shape was a real, shipped regression.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveCalloutInnerWidth(attributes, layout, outerWidth) {
|
|
44
|
+
const rawType = attributes.type ?? 'info';
|
|
45
|
+
const type = isCalloutType(rawType) ? rawType : 'info';
|
|
46
|
+
const title = attributes.title ?? null;
|
|
47
|
+
const naturalWidth = BAR.length +
|
|
48
|
+
Math.max(measureWidth(headerTextFor(type)), measureWidth(title ?? ''));
|
|
49
|
+
const boxWidth = selfLayoutWidth(layout, outerWidth, naturalWidth);
|
|
50
|
+
return Math.max(1, boxWidth - BAR.length);
|
|
51
|
+
}
|
|
28
52
|
/**
|
|
29
53
|
* `:::callout{type=info|warning|danger title="..." text=left|center|right}` —
|
|
30
54
|
* a colored aside/warning/danger box. Unknown/missing `type` falls back to
|
|
31
|
-
* `info
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* width. `text` aligns the icon/title/body lines within that remaining
|
|
36
|
-
* width; absent/invalid `text` behaves as `left` (no padding needed).
|
|
55
|
+
* `info`. Terminal form: every line carries a colored left bar; the first
|
|
56
|
+
* line is the icon plus the type label, an optional bold title line
|
|
57
|
+
* follows, then the body. `text` aligns the icon/title/body lines within
|
|
58
|
+
* the remaining width.
|
|
37
59
|
*
|
|
38
|
-
* Registered `selfLayout
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* `
|
|
43
|
-
*
|
|
60
|
+
* Registered `selfLayout`: every line carries the colored bar prefix, which
|
|
61
|
+
* a generic post-render `applyLayout` narrowing would re-wrap right through,
|
|
62
|
+
* losing the bar on any continuation line. This component reads the
|
|
63
|
+
* resolved `width`/`align` off `ctx.layout` itself and sizes/places its own
|
|
64
|
+
* bar block, exactly like `card`/`table`/`divider`. `children` arrives
|
|
65
|
+
* ALREADY built at exactly this component's own inner width
|
|
66
|
+
* (`resolveCalloutInnerWidth` above) — this component never re-wraps.
|
|
44
67
|
*/
|
|
45
|
-
export const Callout = (attributes, children, ctx) => {
|
|
68
|
+
export const Callout = ({ attributes, children, ctx }) => {
|
|
46
69
|
const rawType = attributes.type ?? 'info';
|
|
47
70
|
const type = isCalloutType(rawType) ? rawType : 'info';
|
|
48
71
|
const token = CALLOUT_TOKENS[type];
|
|
49
72
|
const title = attributes.title ?? null;
|
|
50
73
|
const rawTextAlign = attributes.text;
|
|
51
74
|
const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
|
|
52
|
-
const headerText =
|
|
75
|
+
const headerText = headerTextFor(type);
|
|
53
76
|
const titleText = title ? ctx.text(title) : undefined;
|
|
54
|
-
const naturalWidth = BAR.length +
|
|
77
|
+
const naturalWidth = BAR.length +
|
|
78
|
+
Math.max(measureWidth(headerText), measureWidth(titleText ?? ''));
|
|
55
79
|
const boxWidth = selfLayoutWidth(ctx.layout, ctx.width, naturalWidth);
|
|
56
80
|
const bar = ctx.style(BAR, token);
|
|
57
81
|
const innerWidth = Math.max(1, boxWidth - BAR.length);
|
|
58
|
-
const placeLine = (line) => align === 'left' ? line :
|
|
82
|
+
const placeLine = (line) => align === 'left' ? line : padText(line, innerWidth, align);
|
|
59
83
|
const headerLine = `${bar}${placeLine(ctx.style(headerText, token))}`;
|
|
60
84
|
const lines = [headerLine];
|
|
61
85
|
if (titleText) {
|
|
62
86
|
lines.push(`${bar}${placeLine(ctx.bold(titleText))}`);
|
|
63
87
|
}
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
for (const line of
|
|
88
|
+
const body = childrenText(children);
|
|
89
|
+
if (body) {
|
|
90
|
+
for (const line of body.split('\n')) {
|
|
67
91
|
lines.push(`${bar}${placeLine(line)}`);
|
|
68
92
|
}
|
|
69
93
|
}
|
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ResolvedLayoutPresets } from '../layout.js';
|
|
2
|
+
import { type AnsiComponent, type DirectiveAttributes } from '../registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* The inner budget `card`'s body renders at, given ONLY its own attributes
|
|
5
|
+
* and the outer width — never the body itself. This is deliberately a pure
|
|
6
|
+
* function callable BEFORE any children exist: `render.tsx`'s walk calls it
|
|
7
|
+
* to decide the width its OWN recursive children-builder uses, so `card`'s
|
|
8
|
+
* children are built ONCE, directly at the correct inner width, and never
|
|
9
|
+
* built wide and re-wrapped narrower afterward.
|
|
10
|
+
*
|
|
11
|
+
* That re-wrap-afterward shape was a real, shipped regression (caught
|
|
12
|
+
* post-phase-2 review): re-wrapping an ALREADY-RENDERED child block with a
|
|
13
|
+
* word-based wrapper corrupts anything in it that draws its own fixed-width
|
|
14
|
+
* glyphs (a nested `card`/`callout`'s frame, a nested `row`'s columns) — the
|
|
15
|
+
* word-wrapper has no notion that a run of box-drawing characters is not
|
|
16
|
+
* prose. Computing the inner width eagerly and building children exactly
|
|
17
|
+
* once at it is the fix, and it is what makes the deleted lazy-children API
|
|
18
|
+
* safe to have removed: this is its eager equivalent.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveCardInnerWidth(attributes: DirectiveAttributes, layout: ResolvedLayoutPresets | undefined, outerWidth: number): number;
|
|
2
21
|
/**
|
|
3
22
|
* `:::card{title="..." text=left|center|right} ... :::` — a titled panel.
|
|
4
23
|
* `title` is optional; the title is woven into the frame's top edge, not a
|
|
@@ -7,6 +26,9 @@ import type { AnsiComponent } from '../registry.js';
|
|
|
7
26
|
*
|
|
8
27
|
* Registered `selfLayout` (see `callout.ts`'s doc comment for why): this
|
|
9
28
|
* component draws a real box-drawn frame, which a generic post-render
|
|
10
|
-
* narrow/pad would corrupt, so it reads `ctx.layout` and sizes its own
|
|
29
|
+
* narrow/pad would corrupt, so it reads `ctx.layout` and sizes its own
|
|
30
|
+
* frame. `children` arrives ALREADY built at exactly this component's own
|
|
31
|
+
* inner width (`render.tsx`'s walk calls `resolveCardInnerWidth` above
|
|
32
|
+
* before building them) — this component never re-wraps.
|
|
11
33
|
*/
|
|
12
34
|
export declare const Card: AnsiComponent;
|
package/dist/components/card.js
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { measureWidth, frameBlock, padText } from '../text-grid.js';
|
|
2
2
|
import { selfLayoutAlign, selfLayoutWidth } from '../layout.js';
|
|
3
|
+
import { childrenText, } from '../registry.js';
|
|
3
4
|
const TEXT_ALIGNS = ['left', 'center', 'right'];
|
|
4
5
|
function isTextAlign(value) {
|
|
5
6
|
return TEXT_ALIGNS.includes(value);
|
|
6
7
|
}
|
|
7
|
-
/** `width=fit` with no `title` and an unhelpfully-shaped body
|
|
8
|
+
/** `width=fit` with no `title` and an unhelpfully-shaped body falls back to this column count rather than the full available width. */
|
|
8
9
|
const FIT_DEFAULT_WIDTH = 40;
|
|
9
10
|
/** Frame border + one space of interior padding on each side. */
|
|
10
11
|
const FRAME_OVERHEAD = 2;
|
|
12
|
+
/**
|
|
13
|
+
* The inner budget `card`'s body renders at, given ONLY its own attributes
|
|
14
|
+
* and the outer width — never the body itself. This is deliberately a pure
|
|
15
|
+
* function callable BEFORE any children exist: `render.tsx`'s walk calls it
|
|
16
|
+
* to decide the width its OWN recursive children-builder uses, so `card`'s
|
|
17
|
+
* children are built ONCE, directly at the correct inner width, and never
|
|
18
|
+
* built wide and re-wrapped narrower afterward.
|
|
19
|
+
*
|
|
20
|
+
* That re-wrap-afterward shape was a real, shipped regression (caught
|
|
21
|
+
* post-phase-2 review): re-wrapping an ALREADY-RENDERED child block with a
|
|
22
|
+
* word-based wrapper corrupts anything in it that draws its own fixed-width
|
|
23
|
+
* glyphs (a nested `card`/`callout`'s frame, a nested `row`'s columns) — the
|
|
24
|
+
* word-wrapper has no notion that a run of box-drawing characters is not
|
|
25
|
+
* prose. Computing the inner width eagerly and building children exactly
|
|
26
|
+
* once at it is the fix, and it is what makes the deleted lazy-children API
|
|
27
|
+
* safe to have removed: this is its eager equivalent.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveCardInnerWidth(attributes, layout, outerWidth) {
|
|
30
|
+
const title = attributes.title ?? null;
|
|
31
|
+
const naturalWidth = title
|
|
32
|
+
? measureWidth(title) + FRAME_OVERHEAD * 2
|
|
33
|
+
: FIT_DEFAULT_WIDTH;
|
|
34
|
+
const boxWidth = selfLayoutWidth(layout, outerWidth, naturalWidth);
|
|
35
|
+
return Math.max(1, boxWidth - FRAME_OVERHEAD);
|
|
36
|
+
}
|
|
11
37
|
/**
|
|
12
38
|
* `:::card{title="..." text=left|center|right} ... :::` — a titled panel.
|
|
13
39
|
* `title` is optional; the title is woven into the frame's top edge, not a
|
|
@@ -16,26 +42,29 @@ const FRAME_OVERHEAD = 2;
|
|
|
16
42
|
*
|
|
17
43
|
* Registered `selfLayout` (see `callout.ts`'s doc comment for why): this
|
|
18
44
|
* component draws a real box-drawn frame, which a generic post-render
|
|
19
|
-
* narrow/pad would corrupt, so it reads `ctx.layout` and sizes its own
|
|
45
|
+
* narrow/pad would corrupt, so it reads `ctx.layout` and sizes its own
|
|
46
|
+
* frame. `children` arrives ALREADY built at exactly this component's own
|
|
47
|
+
* inner width (`render.tsx`'s walk calls `resolveCardInnerWidth` above
|
|
48
|
+
* before building them) — this component never re-wraps.
|
|
20
49
|
*/
|
|
21
|
-
export const Card = (attributes, children, ctx) => {
|
|
50
|
+
export const Card = ({ attributes, children, ctx }) => {
|
|
22
51
|
const title = attributes.title ?? null;
|
|
23
52
|
const titleText = title ? ctx.text(title) : undefined;
|
|
24
53
|
const rawTextAlign = attributes.text;
|
|
25
54
|
const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
|
|
26
55
|
const naturalWidth = titleText
|
|
27
|
-
?
|
|
56
|
+
? measureWidth(titleText) + FRAME_OVERHEAD * 2
|
|
28
57
|
: FIT_DEFAULT_WIDTH;
|
|
29
58
|
const boxWidth = selfLayoutWidth(ctx.layout, ctx.width, naturalWidth);
|
|
30
59
|
const innerWidth = Math.max(1, boxWidth - FRAME_OVERHEAD);
|
|
31
|
-
const
|
|
32
|
-
const body =
|
|
33
|
-
?
|
|
60
|
+
const bodyText = childrenText(children);
|
|
61
|
+
const body = align === 'left'
|
|
62
|
+
? bodyText
|
|
63
|
+
: bodyText
|
|
34
64
|
.split('\n')
|
|
35
|
-
.map((line) =>
|
|
36
|
-
.join('\n')
|
|
37
|
-
|
|
38
|
-
const framed = ctx.frame(body, {
|
|
65
|
+
.map((line) => padText(line, innerWidth, align))
|
|
66
|
+
.join('\n');
|
|
67
|
+
const framed = frameBlock(body, {
|
|
39
68
|
style: 'solid',
|
|
40
69
|
title: titleText,
|
|
41
70
|
width: boxWidth,
|
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AnsiComponent } from '../registry.js';
|
|
2
2
|
/**
|
|
3
3
|
* `:::cell ... :::` — a transparent grouping container whose only job is
|
|
4
4
|
* letting several blocks count as ONE cell of `:::row`.
|
|
5
5
|
*
|
|
6
|
-
* `row.ts` no longer discovers its cells by splitting a flattened string on
|
|
7
|
-
* a blank-line heuristic: it walks its own directive's top-level children
|
|
8
|
-
* directly (`registry.ts`'s `AnsiChildren.parts`), so a `cell` grouping more
|
|
9
|
-
* than one block renders those blocks exactly as it would standalone,
|
|
10
|
-
* blank line and all, with no collapsing needed to keep a cell boundary
|
|
11
|
-
* unambiguous for `row.ts`.
|
|
12
|
-
*
|
|
13
6
|
* `text` aligns this cell's own content when rendered STANDALONE (outside a
|
|
14
|
-
* `row`)
|
|
15
|
-
*
|
|
16
|
-
*
|
|
7
|
+
* `row`) or nested inside one: `render.tsx`'s `row` handling still runs
|
|
8
|
+
* each cell through the ordinary directive pipeline (so this component's
|
|
9
|
+
* own alignment applies first), then applies `row`'s OWN `text` uniformly
|
|
10
|
+
* on top — see `components/row.ts`'s doc comment.
|
|
17
11
|
*/
|
|
18
12
|
export declare const Cell: AnsiComponent;
|
package/dist/components/cell.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { padText } from '../text-grid.js';
|
|
2
|
+
import { childrenText } from '../registry.js';
|
|
1
3
|
const TEXT_ALIGNS = ['left', 'center', 'right'];
|
|
2
4
|
function isTextAlign(value) {
|
|
3
5
|
return TEXT_ALIGNS.includes(value);
|
|
@@ -6,26 +8,20 @@ function isTextAlign(value) {
|
|
|
6
8
|
* `:::cell ... :::` — a transparent grouping container whose only job is
|
|
7
9
|
* letting several blocks count as ONE cell of `:::row`.
|
|
8
10
|
*
|
|
9
|
-
* `row.ts` no longer discovers its cells by splitting a flattened string on
|
|
10
|
-
* a blank-line heuristic: it walks its own directive's top-level children
|
|
11
|
-
* directly (`registry.ts`'s `AnsiChildren.parts`), so a `cell` grouping more
|
|
12
|
-
* than one block renders those blocks exactly as it would standalone,
|
|
13
|
-
* blank line and all, with no collapsing needed to keep a cell boundary
|
|
14
|
-
* unambiguous for `row.ts`.
|
|
15
|
-
*
|
|
16
11
|
* `text` aligns this cell's own content when rendered STANDALONE (outside a
|
|
17
|
-
* `row`)
|
|
18
|
-
*
|
|
19
|
-
*
|
|
12
|
+
* `row`) or nested inside one: `render.tsx`'s `row` handling still runs
|
|
13
|
+
* each cell through the ordinary directive pipeline (so this component's
|
|
14
|
+
* own alignment applies first), then applies `row`'s OWN `text` uniformly
|
|
15
|
+
* on top — see `components/row.ts`'s doc comment.
|
|
20
16
|
*/
|
|
21
|
-
export const Cell = (attributes, children, ctx) => {
|
|
17
|
+
export const Cell = ({ attributes, children, ctx }) => {
|
|
22
18
|
const rawTextAlign = attributes.text;
|
|
23
19
|
const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : undefined;
|
|
24
|
-
const
|
|
20
|
+
const text = childrenText(children);
|
|
25
21
|
if (!align || align === 'left')
|
|
26
|
-
return
|
|
27
|
-
return
|
|
22
|
+
return text;
|
|
23
|
+
return text
|
|
28
24
|
.split('\n')
|
|
29
|
-
.map((line) =>
|
|
25
|
+
.map((line) => padText(line, ctx.width, align))
|
|
30
26
|
.join('\n');
|
|
31
27
|
};
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import type { AnsiComponent } from '../registry.js';
|
|
2
2
|
/**
|
|
3
3
|
* `::chart{kind=line|bar values="1,3,2,5"}` — a dependency-free, hand-rolled
|
|
4
|
-
* chart. Data binding (§8)
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
* a small neutral "no data" line rather than a broken chart.
|
|
9
|
-
*
|
|
10
|
-
* Terminal form: a `line` chart is a single sparkline row (`▁▂▃▄▅▆▇█`),
|
|
11
|
-
* flanked by its minimum and maximum as plain numbers. A `bar` chart is one
|
|
12
|
-
* horizontal `█` bar per point, its own value as the row's label, labels
|
|
13
|
-
* right-aligned to the widest one so every bar starts at the same column.
|
|
4
|
+
* chart. Data binding (§8): a bound `data` array of numbers (or `{value}`
|
|
5
|
+
* objects) takes priority over the static `values=` attribute. Terminal
|
|
6
|
+
* form: a `line` chart is a single sparkline row, a `bar` chart is one
|
|
7
|
+
* horizontal `█` bar per point.
|
|
14
8
|
*/
|
|
15
9
|
export declare const Chart: AnsiComponent;
|
package/dist/components/chart.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { measure } from '../measure.js';
|
|
1
|
+
import { padText, measureWidth } from '../text-grid.js';
|
|
3
2
|
import { safeRead } from '../resolve.js';
|
|
4
3
|
import { dataStateSuffix, failureToken } from '../failure-presentation.js';
|
|
5
4
|
const CHART_KINDS = ['line', 'bar'];
|
|
@@ -30,7 +29,7 @@ function coercePoint(entry) {
|
|
|
30
29
|
}
|
|
31
30
|
return undefined;
|
|
32
31
|
}
|
|
33
|
-
/** Builds the numeric point series to plot, a bound `data` array taking priority over the static `values=` attribute.
|
|
32
|
+
/** Builds the numeric point series to plot, a bound `data` array taking priority over the static `values=` attribute. */
|
|
34
33
|
function resolvePoints(data, dataStatus, rawValues) {
|
|
35
34
|
if (dataStatus !== 'missing' &&
|
|
36
35
|
dataStatus !== 'error' &&
|
|
@@ -85,18 +84,12 @@ function barLength(point, maxMagnitude, budget) {
|
|
|
85
84
|
}
|
|
86
85
|
/**
|
|
87
86
|
* `::chart{kind=line|bar values="1,3,2,5"}` — a dependency-free, hand-rolled
|
|
88
|
-
* chart. Data binding (§8)
|
|
89
|
-
*
|
|
90
|
-
* `
|
|
91
|
-
*
|
|
92
|
-
* a small neutral "no data" line rather than a broken chart.
|
|
93
|
-
*
|
|
94
|
-
* Terminal form: a `line` chart is a single sparkline row (`▁▂▃▄▅▆▇█`),
|
|
95
|
-
* flanked by its minimum and maximum as plain numbers. A `bar` chart is one
|
|
96
|
-
* horizontal `█` bar per point, its own value as the row's label, labels
|
|
97
|
-
* right-aligned to the widest one so every bar starts at the same column.
|
|
87
|
+
* chart. Data binding (§8): a bound `data` array of numbers (or `{value}`
|
|
88
|
+
* objects) takes priority over the static `values=` attribute. Terminal
|
|
89
|
+
* form: a `line` chart is a single sparkline row, a `bar` chart is one
|
|
90
|
+
* horizontal `█` bar per point.
|
|
98
91
|
*/
|
|
99
|
-
export const Chart = (attributes,
|
|
92
|
+
export const Chart = ({ attributes, ctx }) => {
|
|
100
93
|
const { data, dataStatus, dataFailureKind } = ctx;
|
|
101
94
|
const rawKind = attributes.kind ?? DEFAULT_KIND;
|
|
102
95
|
const kind = isChartKind(rawKind) ? rawKind : DEFAULT_KIND;
|
|
@@ -118,12 +111,12 @@ export const Chart = (attributes, _children, ctx) => {
|
|
|
118
111
|
return `${min} ${sparkline(points)} ${max}${styledSuffix}`;
|
|
119
112
|
}
|
|
120
113
|
const labels = points.map((point) => formatPoint(point));
|
|
121
|
-
const labelWidth = Math.max(...labels.map((label) =>
|
|
114
|
+
const labelWidth = Math.max(...labels.map((label) => measureWidth(label)));
|
|
122
115
|
const budget = Math.max(1, ctx.width - labelWidth - 1);
|
|
123
116
|
const maxMagnitude = Math.max(...points.map((point) => Math.abs(point)));
|
|
124
117
|
const lines = points.map((point, index) => {
|
|
125
118
|
const bar = '█'.repeat(barLength(point, maxMagnitude, budget));
|
|
126
|
-
return `${
|
|
119
|
+
return `${padText(labels[index], labelWidth, 'right')} ${bar}`;
|
|
127
120
|
});
|
|
128
121
|
return `${lines.join('\n')}${styledSuffix}`;
|
|
129
122
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type AnsiComponent } from '../registry.js';
|
|
2
|
+
/** Columns the indent prefix (" ") consumes from the enclosing width. */
|
|
3
|
+
export declare const DETAILS_INDENT_WIDTH = 2;
|
|
4
|
+
/**
|
|
5
|
+
* The inner budget `details`'s body renders at, given only the outer width
|
|
6
|
+
* — never the body itself. Mirrors `card.ts`'s `resolveCardInnerWidth`:
|
|
7
|
+
* `render.tsx`'s walk calls this BEFORE building children (for both the
|
|
8
|
+
* STRING-mode fallback below and the real interactive element builder in
|
|
9
|
+
* `details.tsx`), so the body — and anything self-drawing nested inside it
|
|
10
|
+
* — is built exactly once, at the correct width, never re-wrapped
|
|
11
|
+
* afterward.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveDetailsInnerWidth(outerWidth: number): number;
|
|
14
|
+
/**
|
|
15
|
+
* The STRING-mode `details` registration (used by `render.tsx`'s registry
|
|
16
|
+
* lookups, and as the fallback rendering when a `details` directive is
|
|
17
|
+
* reached through STRING mode — nested inside a self-drawing container's
|
|
18
|
+
* own body, see `render.tsx`'s top comment). The real Ink/interactive
|
|
19
|
+
* version `render.tsx` builds directly for a `details` reached through
|
|
20
|
+
* ELEMENT mode is `InteractiveDetails` in `./details.tsx`; the two share no
|
|
21
|
+
* code because this one can only ever show the body (a plain string has no
|
|
22
|
+
* expand/collapse affordance), exactly like the pre-Ink engine. `children`
|
|
23
|
+
* arrives ALREADY built at exactly `resolveDetailsInnerWidth`'s width —
|
|
24
|
+
* this component never re-wraps.
|
|
25
|
+
*/
|
|
26
|
+
export declare const Details: AnsiComponent;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { indentBlock } from '../text-grid.js';
|
|
2
|
+
import { childrenText } from '../registry.js';
|
|
3
|
+
const DEFAULT_TITLE = 'Details';
|
|
4
|
+
/** Columns the indent prefix (" ") consumes from the enclosing width. */
|
|
5
|
+
export const DETAILS_INDENT_WIDTH = 2;
|
|
6
|
+
/**
|
|
7
|
+
* The inner budget `details`'s body renders at, given only the outer width
|
|
8
|
+
* — never the body itself. Mirrors `card.ts`'s `resolveCardInnerWidth`:
|
|
9
|
+
* `render.tsx`'s walk calls this BEFORE building children (for both the
|
|
10
|
+
* STRING-mode fallback below and the real interactive element builder in
|
|
11
|
+
* `details.tsx`), so the body — and anything self-drawing nested inside it
|
|
12
|
+
* — is built exactly once, at the correct width, never re-wrapped
|
|
13
|
+
* afterward.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveDetailsInnerWidth(outerWidth) {
|
|
16
|
+
return Math.max(1, outerWidth - DETAILS_INDENT_WIDTH);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The STRING-mode `details` registration (used by `render.tsx`'s registry
|
|
20
|
+
* lookups, and as the fallback rendering when a `details` directive is
|
|
21
|
+
* reached through STRING mode — nested inside a self-drawing container's
|
|
22
|
+
* own body, see `render.tsx`'s top comment). The real Ink/interactive
|
|
23
|
+
* version `render.tsx` builds directly for a `details` reached through
|
|
24
|
+
* ELEMENT mode is `InteractiveDetails` in `./details.tsx`; the two share no
|
|
25
|
+
* code because this one can only ever show the body (a plain string has no
|
|
26
|
+
* expand/collapse affordance), exactly like the pre-Ink engine. `children`
|
|
27
|
+
* arrives ALREADY built at exactly `resolveDetailsInnerWidth`'s width —
|
|
28
|
+
* this component never re-wraps.
|
|
29
|
+
*/
|
|
30
|
+
export const Details = ({ attributes, children, ctx }) => {
|
|
31
|
+
const title = attributes.title ?? DEFAULT_TITLE;
|
|
32
|
+
const open = Object.hasOwn(attributes, 'open');
|
|
33
|
+
const glyph = open ? '▾' : '▸';
|
|
34
|
+
const marker = ctx.dim(`${glyph} ${ctx.bold(ctx.text(title))} (collapsible section, shown expanded)`);
|
|
35
|
+
const body = childrenText(children);
|
|
36
|
+
if (!body)
|
|
37
|
+
return marker;
|
|
38
|
+
return `${marker}\n${indentBlock(body, ' ')}`;
|
|
39
|
+
};
|