@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.
Files changed (73) hide show
  1. package/dist/ansi.d.ts +79 -0
  2. package/dist/ansi.js +136 -0
  3. package/dist/box.d.ts +47 -0
  4. package/dist/box.js +209 -0
  5. package/dist/components/badge.d.ts +10 -0
  6. package/dist/components/badge.js +44 -0
  7. package/dist/components/callout.d.ts +20 -0
  8. package/dist/components/callout.js +71 -0
  9. package/dist/components/card.d.ts +12 -0
  10. package/dist/components/card.js +44 -0
  11. package/dist/components/cell.d.ts +18 -0
  12. package/dist/components/cell.js +31 -0
  13. package/dist/components/chart.d.ts +15 -0
  14. package/dist/components/chart.js +129 -0
  15. package/dist/components/details.d.ts +12 -0
  16. package/dist/components/details.js +25 -0
  17. package/dist/components/divider.d.ts +4 -0
  18. package/dist/components/divider.js +80 -0
  19. package/dist/components/figure.d.ts +20 -0
  20. package/dist/components/figure.js +53 -0
  21. package/dist/components/index.d.ts +34 -0
  22. package/dist/components/index.js +109 -0
  23. package/dist/components/kbd.d.ts +7 -0
  24. package/dist/components/kbd.js +8 -0
  25. package/dist/components/layout-wrapper.d.ts +35 -0
  26. package/dist/components/layout-wrapper.js +62 -0
  27. package/dist/components/progress.d.ts +15 -0
  28. package/dist/components/progress.js +78 -0
  29. package/dist/components/rating.d.ts +9 -0
  30. package/dist/components/rating.js +28 -0
  31. package/dist/components/row.d.ts +26 -0
  32. package/dist/components/row.js +67 -0
  33. package/dist/components/stat.d.ts +13 -0
  34. package/dist/components/stat.js +79 -0
  35. package/dist/components/tab.d.ts +13 -0
  36. package/dist/components/tab.js +17 -0
  37. package/dist/components/table-grid.d.ts +34 -0
  38. package/dist/components/table-grid.js +124 -0
  39. package/dist/components/table.d.ts +16 -0
  40. package/dist/components/table.js +101 -0
  41. package/dist/components/tabs.d.ts +18 -0
  42. package/dist/components/tabs.js +30 -0
  43. package/dist/failure-presentation.d.ts +58 -0
  44. package/dist/failure-presentation.js +124 -0
  45. package/dist/href-resolve.d.ts +21 -0
  46. package/dist/href-resolve.js +25 -0
  47. package/dist/image-resolve.d.ts +41 -0
  48. package/dist/image-resolve.js +28 -0
  49. package/dist/index.d.ts +15 -0
  50. package/dist/index.js +17 -0
  51. package/dist/layout.d.ts +73 -0
  52. package/dist/layout.js +144 -0
  53. package/dist/measure.d.ts +25 -0
  54. package/dist/measure.js +108 -0
  55. package/dist/registry.d.ts +227 -0
  56. package/dist/registry.js +121 -0
  57. package/dist/render.d.ts +69 -0
  58. package/dist/render.js +889 -0
  59. package/dist/resolve.d.ts +60 -0
  60. package/dist/resolve.js +152 -0
  61. package/dist/sanitize.d.ts +51 -0
  62. package/dist/sanitize.js +101 -0
  63. package/dist/style.d.ts +9 -0
  64. package/dist/style.js +13 -0
  65. package/dist/theme.d.ts +36 -0
  66. package/dist/theme.js +79 -0
  67. package/dist/url-resolve.d.ts +54 -0
  68. package/dist/url-resolve.js +82 -0
  69. package/dist/value-format.d.ts +13 -0
  70. package/dist/value-format.js +16 -0
  71. package/dist/value-types.d.ts +37 -0
  72. package/dist/value-types.js +17 -0
  73. 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;
@@ -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;
@@ -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
+ }