@markii/ansi 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ansi.d.ts +79 -0
- package/dist/ansi.js +136 -0
- package/dist/box.d.ts +47 -0
- package/dist/box.js +209 -0
- package/dist/components/badge.d.ts +10 -0
- package/dist/components/badge.js +44 -0
- package/dist/components/callout.d.ts +20 -0
- package/dist/components/callout.js +71 -0
- package/dist/components/card.d.ts +12 -0
- package/dist/components/card.js +44 -0
- package/dist/components/cell.d.ts +18 -0
- package/dist/components/cell.js +31 -0
- package/dist/components/chart.d.ts +15 -0
- package/dist/components/chart.js +129 -0
- package/dist/components/details.d.ts +12 -0
- package/dist/components/details.js +25 -0
- package/dist/components/divider.d.ts +4 -0
- package/dist/components/divider.js +80 -0
- package/dist/components/figure.d.ts +20 -0
- package/dist/components/figure.js +53 -0
- package/dist/components/index.d.ts +34 -0
- package/dist/components/index.js +109 -0
- package/dist/components/kbd.d.ts +7 -0
- package/dist/components/kbd.js +8 -0
- package/dist/components/layout-wrapper.d.ts +35 -0
- package/dist/components/layout-wrapper.js +62 -0
- package/dist/components/progress.d.ts +15 -0
- package/dist/components/progress.js +78 -0
- package/dist/components/rating.d.ts +9 -0
- package/dist/components/rating.js +28 -0
- package/dist/components/row.d.ts +26 -0
- package/dist/components/row.js +67 -0
- package/dist/components/stat.d.ts +13 -0
- package/dist/components/stat.js +79 -0
- package/dist/components/tab.d.ts +13 -0
- package/dist/components/tab.js +17 -0
- package/dist/components/table-grid.d.ts +34 -0
- package/dist/components/table-grid.js +124 -0
- package/dist/components/table.d.ts +16 -0
- package/dist/components/table.js +101 -0
- package/dist/components/tabs.d.ts +18 -0
- package/dist/components/tabs.js +30 -0
- package/dist/failure-presentation.d.ts +58 -0
- package/dist/failure-presentation.js +124 -0
- package/dist/href-resolve.d.ts +21 -0
- package/dist/href-resolve.js +25 -0
- package/dist/image-resolve.d.ts +41 -0
- package/dist/image-resolve.js +28 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/layout.d.ts +73 -0
- package/dist/layout.js +144 -0
- package/dist/measure.d.ts +25 -0
- package/dist/measure.js +108 -0
- package/dist/registry.d.ts +227 -0
- package/dist/registry.js +121 -0
- package/dist/render.d.ts +69 -0
- package/dist/render.js +889 -0
- package/dist/resolve.d.ts +60 -0
- package/dist/resolve.js +152 -0
- package/dist/sanitize.d.ts +51 -0
- package/dist/sanitize.js +101 -0
- package/dist/style.d.ts +9 -0
- package/dist/style.js +13 -0
- package/dist/theme.d.ts +36 -0
- package/dist/theme.js +79 -0
- package/dist/url-resolve.d.ts +54 -0
- package/dist/url-resolve.js +82 -0
- package/dist/value-format.d.ts +13 -0
- package/dist/value-format.js +16 -0
- package/dist/value-types.d.ts +37 -0
- package/dist/value-types.js +17 -0
- package/package.json +61 -0
package/dist/layout.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { ALIGN_PRESETS, LAYOUT_ATTRIBUTE_KEYS, WIDTH_PRESETS, } from '@markii/stdlib';
|
|
2
|
+
import { pad, wrap } from './box.js';
|
|
3
|
+
import { measure } from './measure.js';
|
|
4
|
+
/**
|
|
5
|
+
* The terminal counterpart of `@markii/html`'s `layout.ts`: the same closed
|
|
6
|
+
* `width`/`align` vocabulary from `@markii/stdlib`, but resolved to COLUMN
|
|
7
|
+
* ARITHMETIC instead of a CSS class, since there is no stylesheet here. The
|
|
8
|
+
* two reserved keys are still always stripped off a directive's attributes
|
|
9
|
+
* before a component sees them, whether or not their value turns out to be
|
|
10
|
+
* valid, exactly like the other two engines.
|
|
11
|
+
*/
|
|
12
|
+
export { LAYOUT_ATTRIBUTE_KEYS };
|
|
13
|
+
const NORMAL_WIDTH = 'normal';
|
|
14
|
+
function widthPresetFor(value) {
|
|
15
|
+
if (value === null || value === undefined || value === '')
|
|
16
|
+
return undefined;
|
|
17
|
+
if (value === NORMAL_WIDTH)
|
|
18
|
+
return undefined;
|
|
19
|
+
return WIDTH_PRESETS.includes(value)
|
|
20
|
+
? value
|
|
21
|
+
: undefined;
|
|
22
|
+
}
|
|
23
|
+
function alignPresetFor(value) {
|
|
24
|
+
if (value === null || value === undefined || value === '')
|
|
25
|
+
return undefined;
|
|
26
|
+
return ALIGN_PRESETS.includes(value)
|
|
27
|
+
? value
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Splits `width`/`align` off `attributes`, returning the remaining
|
|
32
|
+
* attributes untouched plus the presets those two attributes resolved to,
|
|
33
|
+
* if any. Same rules as `@markii/html`'s `resolveLayoutAttributes`: both
|
|
34
|
+
* keys are stripped whenever present regardless of validity; an invalid or
|
|
35
|
+
* hostile value never produces a preset; `ownedAxis` (a layout wrapper's own
|
|
36
|
+
* axis) strips its attribute but produces no preset for it, since the
|
|
37
|
+
* directive's NAME already decided that axis. Never throws.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveLayoutAttributes(attributes, ownedAxis) {
|
|
40
|
+
let rest = attributes;
|
|
41
|
+
const resolved = {};
|
|
42
|
+
if (Object.hasOwn(rest, 'width')) {
|
|
43
|
+
const { width, ...remainder } = rest;
|
|
44
|
+
rest = remainder;
|
|
45
|
+
const preset = ownedAxis === 'width' ? undefined : widthPresetFor(width);
|
|
46
|
+
if (preset)
|
|
47
|
+
resolved.width = preset;
|
|
48
|
+
}
|
|
49
|
+
if (Object.hasOwn(rest, 'align')) {
|
|
50
|
+
const { align, ...remainder } = rest;
|
|
51
|
+
rest = remainder;
|
|
52
|
+
const preset = ownedAxis === 'align' ? undefined : alignPresetFor(align);
|
|
53
|
+
if (preset)
|
|
54
|
+
resolved.align = preset;
|
|
55
|
+
}
|
|
56
|
+
return Object.keys(resolved).length > 0
|
|
57
|
+
? { attributes: rest, resolved }
|
|
58
|
+
: { attributes: rest };
|
|
59
|
+
}
|
|
60
|
+
/** The minimum column count `narrow` ever shrinks to, regardless of how small `width` is. */
|
|
61
|
+
export const NARROW_MINIMUM = 20;
|
|
62
|
+
/** The widest line already present in `block`, or `1` for an empty block (never a zero-width target). */
|
|
63
|
+
function maxLineWidth(block) {
|
|
64
|
+
let max = 0;
|
|
65
|
+
for (const line of block.split('\n'))
|
|
66
|
+
max = Math.max(max, measure(line));
|
|
67
|
+
return max || 1;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Turns `layout`'s resolved presets into an actual re-wrapped, aligned
|
|
71
|
+
* block, given `width` columns of budget. Mapping (all documented here,
|
|
72
|
+
* the one place it is decided): `narrow` halves `width` (rounded, never
|
|
73
|
+
* below `NARROW_MINIMUM`); `wide` and `full` both use the entire `width`
|
|
74
|
+
* (a terminal has no notion of "wider than the column" to distinguish
|
|
75
|
+
* them); `fit` shrinks to `block`'s own widest existing line, never wider
|
|
76
|
+
* than `width`. `align` then places the (possibly narrowed) block within
|
|
77
|
+
* the full `width` using `./box.js`'s `pad`, which is where `align`'s
|
|
78
|
+
* `left`/`center`/`right` meaning comes from — this function invents none
|
|
79
|
+
* of its own.
|
|
80
|
+
*/
|
|
81
|
+
export function applyLayout(block, layout, width) {
|
|
82
|
+
if (!layout)
|
|
83
|
+
return block;
|
|
84
|
+
let target = width;
|
|
85
|
+
if (layout.width === 'narrow') {
|
|
86
|
+
target = Math.max(NARROW_MINIMUM, Math.round(width / 2));
|
|
87
|
+
}
|
|
88
|
+
else if (layout.width === 'fit') {
|
|
89
|
+
target = Math.min(width, maxLineWidth(block));
|
|
90
|
+
}
|
|
91
|
+
const narrowed = target < width
|
|
92
|
+
? block
|
|
93
|
+
.split('\n')
|
|
94
|
+
.flatMap((line) => wrap(line, Math.max(1, target)))
|
|
95
|
+
.join('\n')
|
|
96
|
+
: block;
|
|
97
|
+
if (!layout.align)
|
|
98
|
+
return narrowed;
|
|
99
|
+
return narrowed
|
|
100
|
+
.split('\n')
|
|
101
|
+
.map((line) => pad(line, width, layout.align))
|
|
102
|
+
.join('\n');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The target width a SELF-DRAWING box component (`card`, `callout`, `table`,
|
|
106
|
+
* `chart`; see `registry.ts`'s `AnsiRegistryEntry.selfLayout`) should draw
|
|
107
|
+
* its own frame at, given the resolved `width` preset. Mirrors the width
|
|
108
|
+
* half of `applyLayout`'s mapping exactly (`narrow` halves `width`, floored
|
|
109
|
+
* at `NARROW_MINIMUM`; `wide`/`full`/absent use the whole budget; `fit`
|
|
110
|
+
* shrinks to the box's own natural content width, capped at `width`), so a
|
|
111
|
+
* component that draws at this width up front needs no further narrowing:
|
|
112
|
+
* `applyLayout`'s `wrap` step becomes a no-op on an already-correctly-sized
|
|
113
|
+
* line. `naturalWidth` is the box's own widest line at its full content size
|
|
114
|
+
* (only consulted for `fit`); a caller that draws lazily may pass a function
|
|
115
|
+
* to defer that measurement until it is known to matter.
|
|
116
|
+
*/
|
|
117
|
+
export function selfLayoutWidth(layout, width, naturalWidth) {
|
|
118
|
+
if (!layout?.width)
|
|
119
|
+
return width;
|
|
120
|
+
if (layout.width === 'narrow') {
|
|
121
|
+
return Math.max(NARROW_MINIMUM, Math.round(width / 2));
|
|
122
|
+
}
|
|
123
|
+
if (layout.width === 'fit') {
|
|
124
|
+
const natural = typeof naturalWidth === 'function' ? naturalWidth() : naturalWidth;
|
|
125
|
+
return Math.min(width, Math.max(1, natural));
|
|
126
|
+
}
|
|
127
|
+
return width;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Pads a SELF-DRAWING box's already-drawn frame (every line already
|
|
131
|
+
* `boxWidth` columns wide) within the full `width` per the resolved `align`
|
|
132
|
+
* preset — the align half of `applyLayout`, factored out so a `selfLayout`
|
|
133
|
+
* component can align its own frame without going through the generic
|
|
134
|
+
* narrow/wrap path that would otherwise re-wrap (and corrupt) its
|
|
135
|
+
* box-drawing characters. A no-op when `layout.align` is absent.
|
|
136
|
+
*/
|
|
137
|
+
export function selfLayoutAlign(block, layout, width) {
|
|
138
|
+
if (!layout?.align)
|
|
139
|
+
return block;
|
|
140
|
+
return block
|
|
141
|
+
.split('\n')
|
|
142
|
+
.map((line) => pad(line, width, layout.align))
|
|
143
|
+
.join('\n');
|
|
144
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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;
|
package/dist/measure.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
/** SGR: `ESC [ ... m`. */
|
|
17
|
+
const SGR_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
18
|
+
/** OSC 8 hyperlink open/close: `ESC ] 8 ; ; ... BEL`. */
|
|
19
|
+
const OSC8_PATTERN = /\x1b\]8;;[^\x07\x1b]*\x07/g;
|
|
20
|
+
/** Removes every SGR and OSC 8 escape sequence this engine can itself produce, so `measure` counts only what a terminal actually draws. */
|
|
21
|
+
export function stripAnsi(text) {
|
|
22
|
+
return text.replace(OSC8_PATTERN, '').replace(SGR_PATTERN, '');
|
|
23
|
+
}
|
|
24
|
+
/** Combining marks: measure 0, since a terminal draws them layered onto the preceding column rather than advancing the cursor. */
|
|
25
|
+
const COMBINING_RANGES = [
|
|
26
|
+
[0x0300, 0x036f],
|
|
27
|
+
[0x1ab0, 0x1aff],
|
|
28
|
+
[0x1dc0, 0x1dff],
|
|
29
|
+
[0x20d0, 0x20f0],
|
|
30
|
+
[0xfe00, 0xfe0f],
|
|
31
|
+
[0xfe20, 0xfe2f],
|
|
32
|
+
];
|
|
33
|
+
/** Zero-width characters: measure 0 (zero-width space/joiner/non-joiner, word joiner, byte-order mark used as a zero-width no-break space). */
|
|
34
|
+
const ZERO_WIDTH_RANGES = [
|
|
35
|
+
[0x200b, 0x200d],
|
|
36
|
+
[0x2060, 0x2060],
|
|
37
|
+
[0xfeff, 0xfeff],
|
|
38
|
+
];
|
|
39
|
+
/** East Asian Wide and Fullwidth: measure 2. */
|
|
40
|
+
const WIDE_RANGES = [
|
|
41
|
+
[0x1100, 0x115f],
|
|
42
|
+
[0x2e80, 0x303e],
|
|
43
|
+
[0x3041, 0x33ff],
|
|
44
|
+
[0x3400, 0x4dbf],
|
|
45
|
+
[0x4e00, 0x9fff],
|
|
46
|
+
[0xa000, 0xa4cf],
|
|
47
|
+
[0xac00, 0xd7a3],
|
|
48
|
+
[0xf900, 0xfaff],
|
|
49
|
+
[0xfe30, 0xfe6f],
|
|
50
|
+
[0xff00, 0xff60],
|
|
51
|
+
[0xffe0, 0xffe6],
|
|
52
|
+
[0x20000, 0x2fffd],
|
|
53
|
+
[0x30000, 0x3fffd],
|
|
54
|
+
];
|
|
55
|
+
/** Common emoji ranges: measure 2. U+2600-U+27BF is included only when followed by the U+FE0F variation selector (see `measureText`). */
|
|
56
|
+
const EMOJI_RANGES = [
|
|
57
|
+
[0x1f300, 0x1f64f],
|
|
58
|
+
[0x1f680, 0x1f6ff],
|
|
59
|
+
[0x1f900, 0x1f9ff],
|
|
60
|
+
[0x1fa70, 0x1faff],
|
|
61
|
+
];
|
|
62
|
+
const EMOJI_PRESENTATION_LOW = 0x2600;
|
|
63
|
+
const EMOJI_PRESENTATION_HIGH = 0x27bf;
|
|
64
|
+
const VARIATION_SELECTOR_16 = 0xfe0f;
|
|
65
|
+
function inRanges(code, ranges) {
|
|
66
|
+
for (const [low, high] of ranges) {
|
|
67
|
+
if (code >= low && code <= high)
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The display width of one code point, given the NEXT code point in the
|
|
74
|
+
* stream (needed only to check the emoji-variation-selector rule for the
|
|
75
|
+
* dingbat range U+2600-U+27BF).
|
|
76
|
+
*/
|
|
77
|
+
function codePointWidth(code, next) {
|
|
78
|
+
if (inRanges(code, COMBINING_RANGES))
|
|
79
|
+
return 0;
|
|
80
|
+
if (inRanges(code, ZERO_WIDTH_RANGES))
|
|
81
|
+
return 0;
|
|
82
|
+
if (inRanges(code, WIDE_RANGES))
|
|
83
|
+
return 2;
|
|
84
|
+
if (inRanges(code, EMOJI_RANGES))
|
|
85
|
+
return 2;
|
|
86
|
+
if (code >= EMOJI_PRESENTATION_LOW &&
|
|
87
|
+
code <= EMOJI_PRESENTATION_HIGH &&
|
|
88
|
+
next === VARIATION_SELECTOR_16) {
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The display width of `text`: ANSI escapes are stripped first, then every
|
|
95
|
+
* remaining code point (iterated as a code point, not a UTF-16 code unit,
|
|
96
|
+
* so a surrogate pair counts once) contributes its width per
|
|
97
|
+
* `codePointWidth`'s rules. This is what every wrapping/padding/column
|
|
98
|
+
* helper in `./box.ts` measures against.
|
|
99
|
+
*/
|
|
100
|
+
export function measure(text) {
|
|
101
|
+
const stripped = stripAnsi(text);
|
|
102
|
+
const codePoints = Array.from(stripped, (char) => char.codePointAt(0) ?? 0);
|
|
103
|
+
let width = 0;
|
|
104
|
+
for (let index = 0; index < codePoints.length; index += 1) {
|
|
105
|
+
width += codePointWidth(codePoints[index], codePoints[index + 1]);
|
|
106
|
+
}
|
|
107
|
+
return width;
|
|
108
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry contract for the terminal engine: the string-emitting twin of
|
|
3
|
+
* `@markii/html`'s `registry.ts`, itself the string-emitting twin of
|
|
4
|
+
* `@markii/react`'s `registry.ts`. A component here is a plain function from
|
|
5
|
+
* attributes and already-rendered children TEXT to an output string — no
|
|
6
|
+
* hast, no React, no HTML. The alias, merge, and hostile-configuration rules
|
|
7
|
+
* are kept identical to both existing renderers, so a note resolves the same
|
|
8
|
+
* way in all three.
|
|
9
|
+
*/
|
|
10
|
+
import type { FailureKind, ValueStatus } from './value-types.js';
|
|
11
|
+
import type { LayoutAxis, OnDiagnostic } from '@markii/stdlib';
|
|
12
|
+
import type { ColorLevel } from './ansi.js';
|
|
13
|
+
import type { AnsiTheme, Tier1Token } from './theme.js';
|
|
14
|
+
import type { ResolvedLayoutAttributes } from './layout.js';
|
|
15
|
+
import type { ResolveImageSrc } from './image-resolve.js';
|
|
16
|
+
import type { ResolveHref } from './href-resolve.js';
|
|
17
|
+
/**
|
|
18
|
+
* Attributes parsed off a directive, e.g. `{type=warning title="Careful"}`. A
|
|
19
|
+
* bare attribute (present but valueless, e.g. `{collapsed}`) arrives as
|
|
20
|
+
* `null`. A key that was never written is simply absent.
|
|
21
|
+
*/
|
|
22
|
+
export type DirectiveAttributes = Record<string, string | null | undefined>;
|
|
23
|
+
/**
|
|
24
|
+
* A `data=`/`:value[...]` name resolved against the render's value store
|
|
25
|
+
* (and, for an `@`-prefixed name, its vault) — this engine's read-only view
|
|
26
|
+
* of `./resolve.js`'s `StorePathResolution`.
|
|
27
|
+
*/
|
|
28
|
+
export interface ValueResolution {
|
|
29
|
+
value: unknown;
|
|
30
|
+
status: ValueStatus;
|
|
31
|
+
error?: string;
|
|
32
|
+
failureKind?: FailureKind;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The render context handed to every component. Mirrors
|
|
36
|
+
* `@markii/html`'s `HtmlRenderContext` field for field, with the HTML-only
|
|
37
|
+
* `esc` swapped for `text` (this engine's control-character sanitizer, see
|
|
38
|
+
* `./sanitize.js`'s `stripControlCharacters`) and a set of terminal-only
|
|
39
|
+
* fields added: `width`/`indent` (the layout budget this directive's own
|
|
40
|
+
* output has to work with), `color`/`theme` (what this render resolved),
|
|
41
|
+
* and the box/style helpers pre-bound to both.
|
|
42
|
+
*/
|
|
43
|
+
export interface AnsiRenderContext {
|
|
44
|
+
/** The columns available to this directive's own output. A component wraps its own text to this width. */
|
|
45
|
+
width: number;
|
|
46
|
+
/**
|
|
47
|
+
* The prefix an enclosing block (a blockquote, a nested list) has already
|
|
48
|
+
* applied to every line above this directive. Informational only: a
|
|
49
|
+
* component emits its own block UNINDENTED, and the walk applies the
|
|
50
|
+
* prefix afterward, exactly as `width` is already the post-indent budget.
|
|
51
|
+
*/
|
|
52
|
+
indent: string;
|
|
53
|
+
/** The resolved color depth for this render. */
|
|
54
|
+
color: ColorLevel;
|
|
55
|
+
/** The resolved theme for this render. */
|
|
56
|
+
theme: AnsiTheme;
|
|
57
|
+
/** Applies `theme`'s color for `token` at this render's `color` level. Unchanged at `'none'` or for a `null` theme entry. */
|
|
58
|
+
style(text: string, token: Tier1Token): string;
|
|
59
|
+
/** SGR bold, pre-bound to this render's color level. */
|
|
60
|
+
bold(text: string): string;
|
|
61
|
+
/** SGR dim/faint, pre-bound to this render's color level. */
|
|
62
|
+
dim(text: string): string;
|
|
63
|
+
/** SGR italic, pre-bound to this render's color level. */
|
|
64
|
+
italic(text: string): string;
|
|
65
|
+
/** SGR underline, pre-bound to this render's color level. */
|
|
66
|
+
underline(text: string): string;
|
|
67
|
+
/** SGR inverse/reverse video, pre-bound to this render's color level. */
|
|
68
|
+
inverse(text: string): string;
|
|
69
|
+
/** Greedy word wrap, pre-bound to nothing (width is explicit here since a component may wrap narrower than its own `ctx.width`, e.g. inside its own frame). */
|
|
70
|
+
wrap(text: string, width: number): string[];
|
|
71
|
+
/** Pads `text` to `width` columns, aligned `left`/`center`/`right`. */
|
|
72
|
+
pad(text: string, width: number, align: 'left' | 'center' | 'right'): string;
|
|
73
|
+
/** Places blocks side by side; see `./box.js`'s `columns`. */
|
|
74
|
+
columns(blocks: readonly string[], widths: readonly number[], gutter: number): string;
|
|
75
|
+
/** Draws a box around `block`; see `./box.js`'s `frame`. */
|
|
76
|
+
frame(block: string, options: {
|
|
77
|
+
style: 'solid' | 'dashed';
|
|
78
|
+
title?: string;
|
|
79
|
+
width: number;
|
|
80
|
+
}): string;
|
|
81
|
+
/** A full-width horizontal rule; see `./box.js`'s `rule`. */
|
|
82
|
+
rule(width: number, char?: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* The sanitizer a component MUST run any author-supplied string through
|
|
85
|
+
* before printing it (this engine's counterpart to the HTML context's
|
|
86
|
+
* `esc`): strips every control character an untrusted note could use to
|
|
87
|
+
* move the cursor or smuggle an escape sequence (`./sanitize.js`'s
|
|
88
|
+
* `stripControlCharacters`).
|
|
89
|
+
*/
|
|
90
|
+
text(value: string): string;
|
|
91
|
+
/** Resolves a `data=`/`:value[...]` name against the current render's store/vault. Never throws. */
|
|
92
|
+
resolve(name: string): ValueResolution;
|
|
93
|
+
/** The quiet missing/stale/failure-tinted marker for `name`. Never throws. */
|
|
94
|
+
valueMarker(name: string, format?: string, decimals?: string): string;
|
|
95
|
+
data?: unknown;
|
|
96
|
+
dataStatus?: ValueStatus;
|
|
97
|
+
dataError?: string;
|
|
98
|
+
dataFailureKind?: FailureKind;
|
|
99
|
+
/**
|
|
100
|
+
* The resolved width/align presets for the axis a layout-scope entry does
|
|
101
|
+
* not already own — this engine's counterpart to `@markii/html`'s
|
|
102
|
+
* `layoutClassName`, carried as data instead of a class name since there
|
|
103
|
+
* is no stylesheet here.
|
|
104
|
+
*/
|
|
105
|
+
layout?: ResolvedLayoutAttributes['resolved'];
|
|
106
|
+
resolveImageSrc?: ResolveImageSrc;
|
|
107
|
+
resolveHref?: ResolveHref;
|
|
108
|
+
onDiagnostic?: OnDiagnostic;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* How a component wants ONE render of its children to happen: at a
|
|
112
|
+
* narrower `width` than the directive itself was rendered at (a self-layout
|
|
113
|
+
* box sizing its own inner budget), and/or a different `indent`. Omitting a
|
|
114
|
+
* field inherits the value the enclosing directive was rendered with.
|
|
115
|
+
*/
|
|
116
|
+
export interface AnsiChildrenOptions {
|
|
117
|
+
width?: number;
|
|
118
|
+
indent?: string;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* One top-level child of a directive's own body, independently renderable
|
|
122
|
+
* at its own width — the piece `row` needs and no other standard component
|
|
123
|
+
* does: it must decide each cell's column width BEFORE that cell's content
|
|
124
|
+
* (including a self-drawing box like `card`) is rendered, not re-wrap an
|
|
125
|
+
* already-drawn frame afterward. `name` is the child's own resolved
|
|
126
|
+
* directive name (e.g. `'cell'`) when the child is itself a directive, and
|
|
127
|
+
* `undefined` for a plain block (a paragraph, a list) or a directive with a
|
|
128
|
+
* different name — enough for a container to recognize a semantically
|
|
129
|
+
* tagged child without ever seeing a hast node.
|
|
130
|
+
*/
|
|
131
|
+
export interface AnsiChildPart {
|
|
132
|
+
readonly name?: string;
|
|
133
|
+
render(options?: AnsiChildrenOptions): string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The children a component receives. Calling it directly renders the
|
|
137
|
+
* directive's WHOLE body as one flattened string — what every component
|
|
138
|
+
* except `row` wants, and what used to be eagerly computed as a plain
|
|
139
|
+
* string before the component ever ran. `options` narrows the width/indent
|
|
140
|
+
* for that one call (a self-layout box's inner budget); omitted, it renders
|
|
141
|
+
* at the width/indent the directive itself was given.
|
|
142
|
+
*
|
|
143
|
+
* `.parts` exposes each of the directive's own top-level children
|
|
144
|
+
* separately, in document order, each independently renderable at its own
|
|
145
|
+
* width via its own `render(options)` — see `AnsiChildPart`.
|
|
146
|
+
*
|
|
147
|
+
* Laziness is the point: nothing under this directive is rendered until a
|
|
148
|
+
* component actually calls `children()` or one of `.parts`' `render()`
|
|
149
|
+
* functions, so a container can decide sizes top-down before anything below
|
|
150
|
+
* it draws a single character.
|
|
151
|
+
*/
|
|
152
|
+
export type AnsiChildren = ((options?: AnsiChildrenOptions) => string) & {
|
|
153
|
+
readonly parts: readonly AnsiChildPart[];
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* One registry component: receives the directive's raw string attributes
|
|
157
|
+
* (bare attributes as `null`), a lazy handle onto its inner markdown
|
|
158
|
+
* (`AnsiChildren`, see above), and the render context, and returns the text
|
|
159
|
+
* to emit. Attribute parsing, validation, and defaulting are the
|
|
160
|
+
* component's own job, exactly as in the other two engines' contracts.
|
|
161
|
+
*/
|
|
162
|
+
export type AnsiComponent = (attributes: DirectiveAttributes, children: AnsiChildren, ctx: AnsiRenderContext) => string;
|
|
163
|
+
/** One registry entry: the component plus whether it is meant to be used inline vs as a block, and whether it is a layout scope. Mirrors `@markii/html`'s `HtmlRegistryEntry`. */
|
|
164
|
+
export interface AnsiRegistryEntry {
|
|
165
|
+
component: AnsiComponent;
|
|
166
|
+
inline?: boolean;
|
|
167
|
+
layout?: LayoutAxis;
|
|
168
|
+
/**
|
|
169
|
+
* Marks a component that draws its OWN box (a frame, a table grid) rather
|
|
170
|
+
* than plain wrapped text — `card`, `callout`, `table`, `chart` in the
|
|
171
|
+
* standard set. `applyLayout`'s generic re-wrap/pad would corrupt a
|
|
172
|
+
* pre-drawn frame's box-drawing characters if `render.ts` narrowed it
|
|
173
|
+
* AFTER the component already drew it at the full width. A `selfLayout`
|
|
174
|
+
* component instead receives the resolved `width`/`align` presets as
|
|
175
|
+
* `ctx.layout` (the same field a layout-WRAPPER scope receives) and is
|
|
176
|
+
* trusted to size its own frame correctly; `render.ts` then skips its
|
|
177
|
+
* usual post-render `applyLayout` call for it, exactly as it already does
|
|
178
|
+
* for a layout-wrapper scope. Unlike `layout: LayoutAxis`, this is not
|
|
179
|
+
* "this directive's name sets an axis" — it is "this directive draws pixels
|
|
180
|
+
* that a generic wrap/pad would break."
|
|
181
|
+
*/
|
|
182
|
+
selfLayout?: boolean;
|
|
183
|
+
}
|
|
184
|
+
/** One alias: a second name for an existing component, optionally carrying preset attributes. */
|
|
185
|
+
export interface RegistryAlias {
|
|
186
|
+
name: string;
|
|
187
|
+
attributes?: DirectiveAttributes;
|
|
188
|
+
}
|
|
189
|
+
/** Alias name -> what it stands for. */
|
|
190
|
+
export type RegistryAliases = Record<string, RegistryAlias>;
|
|
191
|
+
/** The symbol an alias table hangs off a registry under. Mirrors `@markii/html`'s `REGISTRY_ALIASES`. */
|
|
192
|
+
export declare const REGISTRY_ALIASES: unique symbol;
|
|
193
|
+
/** Directive name -> component registration, plus an optional alias table under `REGISTRY_ALIASES`. */
|
|
194
|
+
export interface AnsiRegistry {
|
|
195
|
+
[name: string]: AnsiRegistryEntry;
|
|
196
|
+
[REGISTRY_ALIASES]?: RegistryAliases;
|
|
197
|
+
}
|
|
198
|
+
/** Reads a registry's alias table, or `undefined` if it has none. Returned as-is; treat as read-only. */
|
|
199
|
+
export declare function registryAliases(registry: AnsiRegistry): RegistryAliases | undefined;
|
|
200
|
+
/**
|
|
201
|
+
* Creates a registry from a plain object of entries plus an optional alias
|
|
202
|
+
* table. The returned map has a `null` prototype so a directive named
|
|
203
|
+
* `constructor`, `toString`, `hasOwnProperty`, etc. cannot resolve to an
|
|
204
|
+
* inherited member.
|
|
205
|
+
*/
|
|
206
|
+
export declare function createAnsiRegistry(entries?: AnsiRegistry, aliases?: RegistryAliases): AnsiRegistry;
|
|
207
|
+
/** Merges any number of registries, later ones taking precedence, into a null-prototype map. Alias tables merge per name. */
|
|
208
|
+
export declare function mergeAnsiRegistries(...registries: AnsiRegistry[]): AnsiRegistry;
|
|
209
|
+
/** Reads `entry.component`, or `undefined` if `entry` is nullish or the read itself throws. */
|
|
210
|
+
export declare function readRegistryComponent(entry: AnsiRegistryEntry | undefined): AnsiComponent | undefined;
|
|
211
|
+
/** The layout axis the component registered under `name` owns, or `undefined`. Mirrors `@markii/html`'s `registryLayoutAxis`. */
|
|
212
|
+
export declare function registryLayoutAxis(registry: AnsiRegistry, name: string): LayoutAxis | undefined;
|
|
213
|
+
/** Whether the component registered under `name` is marked `selfLayout` (see `AnsiRegistryEntry`'s doc comment). Fails permissive (`false`) on a throwing `.selfLayout` getter, matching `registryLayoutAxis`'s defensiveness. */
|
|
214
|
+
export declare function registrySelfLayout(registry: AnsiRegistry, name: string): boolean;
|
|
215
|
+
/** A directive name and attributes after alias resolution. */
|
|
216
|
+
export interface ResolvedDirective {
|
|
217
|
+
name: string;
|
|
218
|
+
attributes: DirectiveAttributes;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Resolves one directive name through the registry's alias table. Same four
|
|
222
|
+
* rules as `@markii/html`'s `resolveDirectiveAlias`: a real component wins
|
|
223
|
+
* over any alias; an unaliased name passes through; an alias is followed
|
|
224
|
+
* exactly one hop; author attributes win over the alias's presets. Never
|
|
225
|
+
* throws.
|
|
226
|
+
*/
|
|
227
|
+
export declare function resolveDirectiveAlias(registry: AnsiRegistry, name: string, attributes: DirectiveAttributes): ResolvedDirective;
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry contract for the terminal engine: the string-emitting twin of
|
|
3
|
+
* `@markii/html`'s `registry.ts`, itself the string-emitting twin of
|
|
4
|
+
* `@markii/react`'s `registry.ts`. A component here is a plain function from
|
|
5
|
+
* attributes and already-rendered children TEXT to an output string — no
|
|
6
|
+
* hast, no React, no HTML. The alias, merge, and hostile-configuration rules
|
|
7
|
+
* are kept identical to both existing renderers, so a note resolves the same
|
|
8
|
+
* way in all three.
|
|
9
|
+
*/
|
|
10
|
+
/** The symbol an alias table hangs off a registry under. Mirrors `@markii/html`'s `REGISTRY_ALIASES`. */
|
|
11
|
+
export const REGISTRY_ALIASES = Symbol('markii.ansi.registry.aliases');
|
|
12
|
+
/** Reads a registry's alias table, or `undefined` if it has none. Returned as-is; treat as read-only. */
|
|
13
|
+
export function registryAliases(registry) {
|
|
14
|
+
return registry[REGISTRY_ALIASES];
|
|
15
|
+
}
|
|
16
|
+
function mergeAliasTables(tables) {
|
|
17
|
+
const present = tables.filter((table) => table !== undefined);
|
|
18
|
+
if (present.length === 0)
|
|
19
|
+
return undefined;
|
|
20
|
+
const merged = Object.create(null);
|
|
21
|
+
for (const table of present) {
|
|
22
|
+
for (const name of Object.keys(table))
|
|
23
|
+
merged[name] = table[name];
|
|
24
|
+
}
|
|
25
|
+
return merged;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Creates a registry from a plain object of entries plus an optional alias
|
|
29
|
+
* table. The returned map has a `null` prototype so a directive named
|
|
30
|
+
* `constructor`, `toString`, `hasOwnProperty`, etc. cannot resolve to an
|
|
31
|
+
* inherited member.
|
|
32
|
+
*/
|
|
33
|
+
export function createAnsiRegistry(entries = {}, aliases) {
|
|
34
|
+
const registry = Object.assign(Object.create(null), entries);
|
|
35
|
+
const merged = mergeAliasTables([registryAliases(entries), aliases]);
|
|
36
|
+
if (merged)
|
|
37
|
+
registry[REGISTRY_ALIASES] = merged;
|
|
38
|
+
return registry;
|
|
39
|
+
}
|
|
40
|
+
/** Merges any number of registries, later ones taking precedence, into a null-prototype map. Alias tables merge per name. */
|
|
41
|
+
export function mergeAnsiRegistries(...registries) {
|
|
42
|
+
const merged = Object.assign(Object.create(null), ...registries);
|
|
43
|
+
const aliases = mergeAliasTables(registries.map(registryAliases));
|
|
44
|
+
if (aliases)
|
|
45
|
+
merged[REGISTRY_ALIASES] = aliases;
|
|
46
|
+
else
|
|
47
|
+
delete merged[REGISTRY_ALIASES];
|
|
48
|
+
return merged;
|
|
49
|
+
}
|
|
50
|
+
/** Reads `entry.component`, or `undefined` if `entry` is nullish or the read itself throws. */
|
|
51
|
+
export function readRegistryComponent(entry) {
|
|
52
|
+
if (!entry)
|
|
53
|
+
return undefined;
|
|
54
|
+
try {
|
|
55
|
+
return entry.component ?? undefined;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** The layout axis the component registered under `name` owns, or `undefined`. Mirrors `@markii/html`'s `registryLayoutAxis`. */
|
|
62
|
+
export function registryLayoutAxis(registry, name) {
|
|
63
|
+
const entry = Object.hasOwn(registry, name) ? registry[name] : undefined;
|
|
64
|
+
if (readRegistryComponent(entry) == null)
|
|
65
|
+
return undefined;
|
|
66
|
+
try {
|
|
67
|
+
const axis = entry?.layout;
|
|
68
|
+
return axis === 'width' || axis === 'align' ? axis : undefined;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Whether the component registered under `name` is marked `selfLayout` (see `AnsiRegistryEntry`'s doc comment). Fails permissive (`false`) on a throwing `.selfLayout` getter, matching `registryLayoutAxis`'s defensiveness. */
|
|
75
|
+
export function registrySelfLayout(registry, name) {
|
|
76
|
+
const entry = Object.hasOwn(registry, name) ? registry[name] : undefined;
|
|
77
|
+
if (readRegistryComponent(entry) == null)
|
|
78
|
+
return false;
|
|
79
|
+
try {
|
|
80
|
+
return entry?.selfLayout === true;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function hasComponent(registry, name) {
|
|
87
|
+
return (Object.hasOwn(registry, name) &&
|
|
88
|
+
readRegistryComponent(registry[name]) != null);
|
|
89
|
+
}
|
|
90
|
+
function mergeAliasAttributes(preset, author) {
|
|
91
|
+
if (!preset)
|
|
92
|
+
return author;
|
|
93
|
+
const result = {};
|
|
94
|
+
for (const [key, value] of Object.entries(preset))
|
|
95
|
+
result[key] = value;
|
|
96
|
+
for (const [key, value] of Object.entries(author))
|
|
97
|
+
result[key] = value;
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Resolves one directive name through the registry's alias table. Same four
|
|
102
|
+
* rules as `@markii/html`'s `resolveDirectiveAlias`: a real component wins
|
|
103
|
+
* over any alias; an unaliased name passes through; an alias is followed
|
|
104
|
+
* exactly one hop; author attributes win over the alias's presets. Never
|
|
105
|
+
* throws.
|
|
106
|
+
*/
|
|
107
|
+
export function resolveDirectiveAlias(registry, name, attributes) {
|
|
108
|
+
if (hasComponent(registry, name))
|
|
109
|
+
return { name, attributes };
|
|
110
|
+
const aliases = registryAliases(registry);
|
|
111
|
+
if (!aliases || !Object.hasOwn(aliases, name))
|
|
112
|
+
return { name, attributes };
|
|
113
|
+
const alias = aliases[name];
|
|
114
|
+
if (typeof alias?.name !== 'string' || alias.name === '') {
|
|
115
|
+
return { name, attributes };
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
name: alias.name,
|
|
119
|
+
attributes: mergeAliasAttributes(alias.attributes, attributes),
|
|
120
|
+
};
|
|
121
|
+
}
|