@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/ansi.d.ts ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The escape-sequence primitives and color model this engine is built on.
3
+ * Every helper here returns a string with no escape state left open: a
4
+ * sequence a helper opens, it closes within the same returned string, so
5
+ * concatenating helper outputs can never leak color or style into text a
6
+ * later helper did not intend to touch.
7
+ *
8
+ * The engine never decides for itself whether color is appropriate: it has
9
+ * no access to `process`, `stdout`, or the environment (`detectColorLevel`
10
+ * takes both as explicit parameters), so a caller that wants automatic
11
+ * detection has to ask for it and hand back the answer.
12
+ */
13
+ /** What this engine will actually emit: no escapes at all, or one of three color depths. */
14
+ export type ColorLevel = 'none' | '16' | '256' | 'truecolor';
15
+ /** What a caller asks for. `'auto'` defers to `detectColorLevel`; see `resolveColorOption` for why an unresolved `'auto'` never reaches the renderer as a level on its own. */
16
+ export type ColorOption = 'auto' | 'never' | '16' | '256' | 'truecolor';
17
+ /**
18
+ * One theme color, carrying all three depths a terminal might support so
19
+ * `fg` can pick the right one without recomputing anything at render time.
20
+ * `ansi16` is an SGR foreground code: 30-37 for the eight standard colors,
21
+ * 90-97 for their bright counterparts.
22
+ */
23
+ export interface AnsiColor {
24
+ ansi16: number;
25
+ ansi256: number;
26
+ truecolor: readonly [number, number, number];
27
+ }
28
+ /** The SGR sequence that sets `color` as the foreground at `level`, or `''` at `'none'`. */
29
+ export declare function fg(color: AnsiColor, level: ColorLevel): string;
30
+ /** Wraps `text` in `color`'s foreground SGR at `level`, resetting after and restoring the color after any reset `text` carried. Unchanged at `'none'`. */
31
+ export declare function colorize(text: string, color: AnsiColor, level: ColorLevel): string;
32
+ /** SGR bold (1). Returns `text` unchanged at `'none'`. */
33
+ export declare const bold: (text: string, level: ColorLevel) => string;
34
+ /** SGR dim/faint (2). Returns `text` unchanged at `'none'`. */
35
+ export declare const dim: (text: string, level: ColorLevel) => string;
36
+ /** SGR italic (3). Returns `text` unchanged at `'none'`. */
37
+ export declare const italic: (text: string, level: ColorLevel) => string;
38
+ /** SGR underline (4). Returns `text` unchanged at `'none'`. */
39
+ export declare const underline: (text: string, level: ColorLevel) => string;
40
+ /** SGR inverse/reverse video (7). Returns `text` unchanged at `'none'`. */
41
+ export declare const inverse: (text: string, level: ColorLevel) => string;
42
+ /**
43
+ * OSC 8: `ESC ] 8 ; ; url BEL text ESC ] 8 ; ; BEL`. Emitted only when
44
+ * `level` is not `'none'`; at `'none'` this returns `text` UNCHANGED, and
45
+ * the CALLER is responsible for appending the `(url)` form itself (this
46
+ * function has no opinion on that wording; `render.ts` and the
47
+ * failure-presentation module own it). The split exists because a
48
+ * plain-text render (`'none'`) still wants the URL visible somewhere, while
49
+ * a color-capable terminal gets the real clickable link instead.
50
+ */
51
+ export declare function hyperlink(text: string, url: string, level: ColorLevel): string;
52
+ /**
53
+ * Reads color intent from an environment map and TTY flag, following (in
54
+ * order): `NO_COLOR` (any non-empty value forces `'none'`, per the
55
+ * no-color.org convention) beats everything else; `FORCE_COLOR` (`0` forces
56
+ * `'none'`, `1`/`true` forces `'16'`, `2` forces `'256'`, `3` forces
57
+ * `'truecolor'`) beats the TTY check; a non-TTY output forces `'none'`
58
+ * (piping to a file or another program should never carry escape codes
59
+ * unless the caller explicitly forced one above); `TERM=dumb` forces
60
+ * `'none'`; `COLORTERM` of `truecolor`/`24bit` forces `'truecolor'`; a
61
+ * `TERM` containing `256color` gives `'256'`; anything else that got this
62
+ * far gets the conservative `'16'`.
63
+ *
64
+ * Reads NOTHING itself: no `process`, no `globalThis`. A caller (a CLI's
65
+ * entry point, typically) supplies `env` and `isTTY` explicitly.
66
+ */
67
+ export declare function detectColorLevel(env: Record<string, string | undefined>, isTTY: boolean): ColorLevel;
68
+ /**
69
+ * Resolves a caller-supplied `ColorOption` to the `ColorLevel` this engine
70
+ * will actually use. `'never'` and `'auto'` BOTH resolve to `'none'` here:
71
+ * the engine has no environment knowledge of its own (see this module's top
72
+ * comment) and must never emit an escape it cannot justify, so `'auto'`
73
+ * detection is entirely the caller's job via `detectColorLevel` — a caller
74
+ * that wants automatic detection calls that function itself and passes the
75
+ * resulting level in directly, rather than passing `'auto'` through to this
76
+ * engine and hoping it guesses right. `undefined` behaves exactly like
77
+ * `'auto'`.
78
+ */
79
+ export declare function resolveColorOption(option: ColorOption | undefined): ColorLevel;
package/dist/ansi.js ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The escape-sequence primitives and color model this engine is built on.
3
+ * Every helper here returns a string with no escape state left open: a
4
+ * sequence a helper opens, it closes within the same returned string, so
5
+ * concatenating helper outputs can never leak color or style into text a
6
+ * later helper did not intend to touch.
7
+ *
8
+ * The engine never decides for itself whether color is appropriate: it has
9
+ * no access to `process`, `stdout`, or the environment (`detectColorLevel`
10
+ * takes both as explicit parameters), so a caller that wants automatic
11
+ * detection has to ask for it and hand back the answer.
12
+ */
13
+ const ESC = '\x1b';
14
+ const RESET = `${ESC}[0m`;
15
+ /** The SGR sequence that sets `color` as the foreground at `level`, or `''` at `'none'`. */
16
+ export function fg(color, level) {
17
+ if (level === 'none')
18
+ return '';
19
+ if (level === '16')
20
+ return `${ESC}[${color.ansi16}m`;
21
+ if (level === '256')
22
+ return `${ESC}[38;5;${color.ansi256}m`;
23
+ const [r, g, b] = color.truecolor;
24
+ return `${ESC}[38;2;${r};${g};${b}m`;
25
+ }
26
+ /**
27
+ * Wraps `text` in `open`, closes with a full reset, and re-opens `open`
28
+ * after every reset `text` already contained.
29
+ *
30
+ * Every helper in this module closes with SGR 0, which resets ALL
31
+ * attributes rather than only the one it set, because SGR has no "undo just
32
+ * this" code a renderer can rely on across terminals. That makes the
33
+ * helpers safe to concatenate but not, on its own, safe to NEST: a dim
34
+ * frame holding one colored word would lose its dim from that word onward,
35
+ * since the word's own reset clears the frame's attribute too. Re-opening
36
+ * after each inner reset restores the outer attribute for the remainder, so
37
+ * a component can style text that other components already styled without
38
+ * having to know what they did. The cost is one extra sequence per nesting
39
+ * point, which is invisible in the terminal and stable in a fixture.
40
+ */
41
+ function wrapSgr(text, open) {
42
+ const restored = text.includes(RESET)
43
+ ? text.split(RESET).join(`${RESET}${open}`)
44
+ : text;
45
+ return `${open}${restored}${RESET}`;
46
+ }
47
+ /** Wraps `text` in `color`'s foreground SGR at `level`, resetting after and restoring the color after any reset `text` carried. Unchanged at `'none'`. */
48
+ export function colorize(text, color, level) {
49
+ if (level === 'none')
50
+ return text;
51
+ return wrapSgr(text, fg(color, level));
52
+ }
53
+ function sgrWrap(code) {
54
+ return (text, level) => level === 'none' ? text : wrapSgr(text, `${ESC}[${code}m`);
55
+ }
56
+ /** SGR bold (1). Returns `text` unchanged at `'none'`. */
57
+ export const bold = sgrWrap(1);
58
+ /** SGR dim/faint (2). Returns `text` unchanged at `'none'`. */
59
+ export const dim = sgrWrap(2);
60
+ /** SGR italic (3). Returns `text` unchanged at `'none'`. */
61
+ export const italic = sgrWrap(3);
62
+ /** SGR underline (4). Returns `text` unchanged at `'none'`. */
63
+ export const underline = sgrWrap(4);
64
+ /** SGR inverse/reverse video (7). Returns `text` unchanged at `'none'`. */
65
+ export const inverse = sgrWrap(7);
66
+ /**
67
+ * OSC 8: `ESC ] 8 ; ; url BEL text ESC ] 8 ; ; BEL`. Emitted only when
68
+ * `level` is not `'none'`; at `'none'` this returns `text` UNCHANGED, and
69
+ * the CALLER is responsible for appending the `(url)` form itself (this
70
+ * function has no opinion on that wording; `render.ts` and the
71
+ * failure-presentation module own it). The split exists because a
72
+ * plain-text render (`'none'`) still wants the URL visible somewhere, while
73
+ * a color-capable terminal gets the real clickable link instead.
74
+ */
75
+ export function hyperlink(text, url, level) {
76
+ if (level === 'none')
77
+ return text;
78
+ return `${ESC}]8;;${url}\x07${text}${ESC}]8;;\x07`;
79
+ }
80
+ /**
81
+ * Reads color intent from an environment map and TTY flag, following (in
82
+ * order): `NO_COLOR` (any non-empty value forces `'none'`, per the
83
+ * no-color.org convention) beats everything else; `FORCE_COLOR` (`0` forces
84
+ * `'none'`, `1`/`true` forces `'16'`, `2` forces `'256'`, `3` forces
85
+ * `'truecolor'`) beats the TTY check; a non-TTY output forces `'none'`
86
+ * (piping to a file or another program should never carry escape codes
87
+ * unless the caller explicitly forced one above); `TERM=dumb` forces
88
+ * `'none'`; `COLORTERM` of `truecolor`/`24bit` forces `'truecolor'`; a
89
+ * `TERM` containing `256color` gives `'256'`; anything else that got this
90
+ * far gets the conservative `'16'`.
91
+ *
92
+ * Reads NOTHING itself: no `process`, no `globalThis`. A caller (a CLI's
93
+ * entry point, typically) supplies `env` and `isTTY` explicitly.
94
+ */
95
+ export function detectColorLevel(env, isTTY) {
96
+ if (env.NO_COLOR !== undefined && env.NO_COLOR !== '')
97
+ return 'none';
98
+ const forceColor = env.FORCE_COLOR;
99
+ if (forceColor !== undefined) {
100
+ if (forceColor === '0')
101
+ return 'none';
102
+ if (forceColor === '1' || forceColor === 'true')
103
+ return '16';
104
+ if (forceColor === '2')
105
+ return '256';
106
+ if (forceColor === '3')
107
+ return 'truecolor';
108
+ }
109
+ if (!isTTY)
110
+ return 'none';
111
+ if (env.TERM === 'dumb')
112
+ return 'none';
113
+ const colorterm = env.COLORTERM;
114
+ if (colorterm === 'truecolor' || colorterm === '24bit')
115
+ return 'truecolor';
116
+ if (env.TERM?.includes('256color'))
117
+ return '256';
118
+ return '16';
119
+ }
120
+ /**
121
+ * Resolves a caller-supplied `ColorOption` to the `ColorLevel` this engine
122
+ * will actually use. `'never'` and `'auto'` BOTH resolve to `'none'` here:
123
+ * the engine has no environment knowledge of its own (see this module's top
124
+ * comment) and must never emit an escape it cannot justify, so `'auto'`
125
+ * detection is entirely the caller's job via `detectColorLevel` — a caller
126
+ * that wants automatic detection calls that function itself and passes the
127
+ * resulting level in directly, rather than passing `'auto'` through to this
128
+ * engine and hoping it guesses right. `undefined` behaves exactly like
129
+ * `'auto'`.
130
+ */
131
+ export function resolveColorOption(option) {
132
+ if (option === '16' || option === '256' || option === 'truecolor') {
133
+ return option;
134
+ }
135
+ return 'none';
136
+ }
package/dist/box.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Greedy word wrap to `width` columns. An existing `\n` is a hard break
3
+ * (each hard-broken segment is wrapped independently); a word wider than
4
+ * `width` on its own is broken at the width boundary via `breakLongWord`
5
+ * rather than overflowing the line.
6
+ */
7
+ export declare function wrap(text: string, width: number): string[];
8
+ /** Pads `text` to `width` columns with spaces, aligned `left`/`center`/`right`. A `text` already at or over `width` is returned unchanged. */
9
+ export declare function pad(text: string, width: number, align: 'left' | 'center' | 'right'): string;
10
+ /**
11
+ * Re-wraps a block of text that was already wrapped to a WIDER budget than
12
+ * `width` (the shape a component's pre-rendered `childrenText` always
13
+ * arrives in: `render.ts` wraps a directive's body to the full directive
14
+ * width before the component ever runs, since a container component that
15
+ * draws its own frame or indent — `card`, `callout`, `details`, `figure` —
16
+ * only learns it needs a NARROWER inner budget once it starts drawing).
17
+ * Every existing `\n` is a hard break, each hard-broken line re-wrapped
18
+ * independently if it still overflows `width`; a line already inside the
19
+ * budget passes through unchanged. Mirrors the same pattern `render.ts`'s
20
+ * `unknownDirective` fallback already uses for its dashed frame.
21
+ */
22
+ export declare function rewrapBlock(text: string, width: number): string;
23
+ /** Prefixes every line of `block` (split on `\n`) with `prefix`, e.g. a blockquote's `│ `. */
24
+ export declare function indentBlock(block: string, prefix: string): string;
25
+ /**
26
+ * Places `blocks` side by side, each padded (left-aligned) to its entry in
27
+ * `widths`, joined by `gutter` spaces, top-aligned: a block with fewer
28
+ * lines than its neighbors gets its missing rows filled with blank,
29
+ * full-width padding rather than leaving a ragged gap.
30
+ */
31
+ export declare function columns(blocks: readonly string[], widths: readonly number[], gutter: number): string;
32
+ /** `frame`'s options: which glyph set to draw with, an optional title woven into the top edge, and the frame's total outer width (borders included). */
33
+ export interface FrameOptions {
34
+ style: 'solid' | 'dashed';
35
+ title?: string;
36
+ width: number;
37
+ }
38
+ /**
39
+ * Draws a box around `block`: `┌ ─ ┐ │ └ ┘` for `'solid'`, `┌ ╌ ┐ ┆ └ ┘` for
40
+ * `'dashed'` (the corners are shared; only the edge glyphs change). A
41
+ * `title` is woven into the top edge as `┌─ title ────┐`. Every content
42
+ * line is padded to the frame's inner width, so every drawn line is exactly
43
+ * `options.width` columns wide.
44
+ */
45
+ export declare function frame(block: string, options: FrameOptions): string;
46
+ /** A full-width horizontal rule, `char` (default `─`) repeated to `width` columns. */
47
+ export declare function rule(width: number, char?: string): string;
package/dist/box.js ADDED
@@ -0,0 +1,209 @@
1
+ import { measure } from './measure.js';
2
+ /**
3
+ * Hand-written, width-aware layout helpers: wrapping, padding, side-by-side
4
+ * columns, and box drawing. Every measurement goes through `./measure.ts`,
5
+ * so these helpers wrap and pad correctly around colored text, wide CJK
6
+ * characters, and emoji, not just plain ASCII. None of these read the
7
+ * theme; they take and return plain strings, leaving color entirely to
8
+ * `./style.ts` and the caller.
9
+ *
10
+ * `wrap`'s word-splitting collapses runs of spaces to one, the ordinary
11
+ * behavior of a text wrapper: exact inter-word spacing is not something a
12
+ * terminal reflow is expected to preserve.
13
+ */
14
+ const ESCAPE_PATTERN = /\x1b\[[0-9;]*m|\x1b\]8;;[^\x07\x1b]*\x07/g;
15
+ /**
16
+ * Splits `text` into atoms an escape sequence never gets split across: each
17
+ * escape sequence is its own zero-width atom, and every other code point is
18
+ * its own atom carrying `measure`'s width for that single code point. A
19
+ * combining mark or variation selector that only reads as zero-width in
20
+ * context with its BASE character (see `./measure.ts`'s module comment)
21
+ * still measures correctly here, because it is itself in the zero-width/
22
+ * combining ranges regardless of what precedes it; the one case this
23
+ * under- or over-counts by a column is the dingbat-plus-variation-selector
24
+ * pair (`☀️`), which needs to see both code points at once to know it
25
+ * should measure 2 — an accepted instance of `./measure.ts`'s documented
26
+ * approximation.
27
+ */
28
+ function tokenize(text) {
29
+ const atoms = [];
30
+ let lastIndex = 0;
31
+ let match;
32
+ ESCAPE_PATTERN.lastIndex = 0;
33
+ while ((match = ESCAPE_PATTERN.exec(text)) !== null) {
34
+ if (match.index > lastIndex) {
35
+ pushChars(atoms, text.slice(lastIndex, match.index));
36
+ }
37
+ atoms.push({ text: match[0], width: 0 });
38
+ lastIndex = match.index + match[0].length;
39
+ }
40
+ if (lastIndex < text.length)
41
+ pushChars(atoms, text.slice(lastIndex));
42
+ return atoms;
43
+ }
44
+ function pushChars(atoms, plain) {
45
+ for (const char of plain) {
46
+ atoms.push({ text: char, width: measure(char) });
47
+ }
48
+ }
49
+ /** Breaks one over-long word into chunks of at most `width` columns, never splitting an escape sequence. */
50
+ function breakLongWord(word, width) {
51
+ const atoms = tokenize(word);
52
+ const chunks = [];
53
+ let current = '';
54
+ let currentWidth = 0;
55
+ for (const atom of atoms) {
56
+ if (currentWidth + atom.width > width && current !== '') {
57
+ chunks.push(current);
58
+ current = '';
59
+ currentWidth = 0;
60
+ }
61
+ current += atom.text;
62
+ currentWidth += atom.width;
63
+ }
64
+ if (current !== '')
65
+ chunks.push(current);
66
+ return chunks.length > 0 ? chunks : [''];
67
+ }
68
+ /**
69
+ * Greedy word wrap to `width` columns. An existing `\n` is a hard break
70
+ * (each hard-broken segment is wrapped independently); a word wider than
71
+ * `width` on its own is broken at the width boundary via `breakLongWord`
72
+ * rather than overflowing the line.
73
+ */
74
+ export function wrap(text, width) {
75
+ const w = Math.max(1, width);
76
+ const lines = [];
77
+ for (const hardLine of text.split('\n')) {
78
+ const words = hardLine.split(/ +/).filter((word) => word !== '');
79
+ if (words.length === 0) {
80
+ lines.push('');
81
+ continue;
82
+ }
83
+ let current = '';
84
+ let currentWidth = 0;
85
+ for (const word of words) {
86
+ const wordWidth = measure(word);
87
+ if (wordWidth > w) {
88
+ if (current !== '') {
89
+ lines.push(current);
90
+ current = '';
91
+ currentWidth = 0;
92
+ }
93
+ lines.push(...breakLongWord(word, w));
94
+ continue;
95
+ }
96
+ if (current === '') {
97
+ current = word;
98
+ currentWidth = wordWidth;
99
+ continue;
100
+ }
101
+ if (currentWidth + 1 + wordWidth <= w) {
102
+ current += ` ${word}`;
103
+ currentWidth += 1 + wordWidth;
104
+ }
105
+ else {
106
+ lines.push(current);
107
+ current = word;
108
+ currentWidth = wordWidth;
109
+ }
110
+ }
111
+ if (current !== '')
112
+ lines.push(current);
113
+ }
114
+ return lines;
115
+ }
116
+ /** Pads `text` to `width` columns with spaces, aligned `left`/`center`/`right`. A `text` already at or over `width` is returned unchanged. */
117
+ export function pad(text, width, align) {
118
+ const total = Math.max(0, width - measure(text));
119
+ if (total === 0)
120
+ return text;
121
+ if (align === 'right')
122
+ return ' '.repeat(total) + text;
123
+ if (align === 'center') {
124
+ const left = Math.floor(total / 2);
125
+ const right = total - left;
126
+ return ' '.repeat(left) + text + ' '.repeat(right);
127
+ }
128
+ return text + ' '.repeat(total);
129
+ }
130
+ /**
131
+ * Re-wraps a block of text that was already wrapped to a WIDER budget than
132
+ * `width` (the shape a component's pre-rendered `childrenText` always
133
+ * arrives in: `render.ts` wraps a directive's body to the full directive
134
+ * width before the component ever runs, since a container component that
135
+ * draws its own frame or indent — `card`, `callout`, `details`, `figure` —
136
+ * only learns it needs a NARROWER inner budget once it starts drawing).
137
+ * Every existing `\n` is a hard break, each hard-broken line re-wrapped
138
+ * independently if it still overflows `width`; a line already inside the
139
+ * budget passes through unchanged. Mirrors the same pattern `render.ts`'s
140
+ * `unknownDirective` fallback already uses for its dashed frame.
141
+ */
142
+ export function rewrapBlock(text, width) {
143
+ if (!text)
144
+ return text;
145
+ return text
146
+ .split('\n')
147
+ .flatMap((line) => wrap(line, width))
148
+ .join('\n');
149
+ }
150
+ /** Prefixes every line of `block` (split on `\n`) with `prefix`, e.g. a blockquote's `│ `. */
151
+ export function indentBlock(block, prefix) {
152
+ return block
153
+ .split('\n')
154
+ .map((line) => `${prefix}${line}`)
155
+ .join('\n');
156
+ }
157
+ /**
158
+ * Places `blocks` side by side, each padded (left-aligned) to its entry in
159
+ * `widths`, joined by `gutter` spaces, top-aligned: a block with fewer
160
+ * lines than its neighbors gets its missing rows filled with blank,
161
+ * full-width padding rather than leaving a ragged gap.
162
+ */
163
+ export function columns(blocks, widths, gutter) {
164
+ const gap = ' '.repeat(Math.max(0, gutter));
165
+ const lineArrays = blocks.map((block) => block.split('\n'));
166
+ const rowCount = Math.max(0, ...lineArrays.map((lines) => lines.length));
167
+ const rows = [];
168
+ for (let row = 0; row < rowCount; row += 1) {
169
+ const cells = lineArrays.map((lines, index) => pad(lines[row] ?? '', widths[index] ?? 0, 'left'));
170
+ // The padding on the LAST column is dropped again. It buys nothing: no
171
+ // column follows it to be aligned against, and left in place it would
172
+ // end every row with a run of spaces, which shows up as trailing
173
+ // whitespace in a committed fixture and in anything that pipes this
174
+ // output to a file. Padding between columns is what does the work.
175
+ rows.push(cells.join(gap).replace(/[ ]+$/, ''));
176
+ }
177
+ return rows.join('\n');
178
+ }
179
+ /**
180
+ * Draws a box around `block`: `┌ ─ ┐ │ └ ┘` for `'solid'`, `┌ ╌ ┐ ┆ └ ┘` for
181
+ * `'dashed'` (the corners are shared; only the edge glyphs change). A
182
+ * `title` is woven into the top edge as `┌─ title ────┐`. Every content
183
+ * line is padded to the frame's inner width, so every drawn line is exactly
184
+ * `options.width` columns wide.
185
+ */
186
+ export function frame(block, options) {
187
+ const { style: kind, title, width } = options;
188
+ const horizontal = kind === 'dashed' ? '╌' : '─';
189
+ const vertical = kind === 'dashed' ? '┆' : '│';
190
+ const innerWidth = Math.max(1, width - 2);
191
+ let top;
192
+ if (title) {
193
+ const label = ` ${title} `;
194
+ const remaining = Math.max(0, innerWidth - measure(label) - 1);
195
+ top = `┌${horizontal}${label}${horizontal.repeat(remaining)}┐`;
196
+ }
197
+ else {
198
+ top = `┌${horizontal.repeat(innerWidth)}┐`;
199
+ }
200
+ const bottom = `└${horizontal.repeat(innerWidth)}┘`;
201
+ const lines = block
202
+ .split('\n')
203
+ .map((line) => `${vertical}${pad(line, innerWidth, 'left')}${vertical}`);
204
+ return [top, ...lines, bottom].join('\n');
205
+ }
206
+ /** A full-width horizontal rule, `char` (default `─`) repeated to `width` columns. */
207
+ export function rule(width, char = '─') {
208
+ return char.repeat(Math.max(0, width));
209
+ }
@@ -0,0 +1,10 @@
1
+ import type { AnsiComponent } from '../registry.js';
2
+ export type BadgeVariant = 'neutral' | 'info' | 'success' | 'warning' | 'danger';
3
+ /**
4
+ * `:badge[New]{variant=success}` — a status pill for an inline text
5
+ * directive. Unknown/missing `variant` falls back to `neutral` rather than
6
+ * throwing. Terminal form: an inverse-video chip, ` label ` colored for the
7
+ * variant. At color level `'none'` there is no inverse video to distinguish
8
+ * a badge from surrounding text, so it degrades to `[label]` instead.
9
+ */
10
+ export declare const Badge: AnsiComponent;
@@ -0,0 +1,44 @@
1
+ const BADGE_VARIANTS = [
2
+ 'neutral',
3
+ 'info',
4
+ 'success',
5
+ 'warning',
6
+ 'danger',
7
+ ];
8
+ const DEFAULT_VARIANT = 'neutral';
9
+ function isBadgeVariant(value) {
10
+ return BADGE_VARIANTS.includes(value);
11
+ }
12
+ /** Variant -> the theme token its chip's background reads (mirroring `@markii/html`'s color choice for the same variant). */
13
+ const VARIANT_TOKEN = {
14
+ neutral: 'muted',
15
+ info: 'info',
16
+ success: 'success',
17
+ warning: 'warning',
18
+ danger: 'danger',
19
+ };
20
+ const TOKEN_NAME = {
21
+ muted: '--mk-muted',
22
+ info: '--mk-info',
23
+ success: '--mk-success',
24
+ warning: '--mk-warning',
25
+ danger: '--mk-danger',
26
+ };
27
+ /**
28
+ * `:badge[New]{variant=success}` — a status pill for an inline text
29
+ * directive. Unknown/missing `variant` falls back to `neutral` rather than
30
+ * throwing. Terminal form: an inverse-video chip, ` label ` colored for the
31
+ * variant. At color level `'none'` there is no inverse video to distinguish
32
+ * a badge from surrounding text, so it degrades to `[label]` instead.
33
+ */
34
+ export const Badge = (attributes, children, ctx) => {
35
+ const rawVariant = attributes.variant ?? DEFAULT_VARIANT;
36
+ const variant = isBadgeVariant(rawVariant)
37
+ ? rawVariant
38
+ : DEFAULT_VARIANT;
39
+ const childrenText = children();
40
+ if (ctx.color === 'none')
41
+ return `[${childrenText}]`;
42
+ const token = TOKEN_NAME[VARIANT_TOKEN[variant]];
43
+ return ctx.inverse(ctx.style(` ${childrenText} `, token));
44
+ };
@@ -0,0 +1,20 @@
1
+ import type { AnsiComponent } from '../registry.js';
2
+ export type CalloutType = 'info' | 'warning' | 'danger';
3
+ /**
4
+ * `:::callout{type=info|warning|danger title="..." text=left|center|right}` —
5
+ * a colored aside/warning/danger box. Unknown/missing `type` falls back to
6
+ * `info` (`render.ts`'s generic invalid-enum notice covers reporting that,
7
+ * same as every other enum attribute). Terminal form: every line carries a
8
+ * colored left bar; the first line is the icon plus the type label, an
9
+ * optional bold title line follows, then the body wrapped to the remaining
10
+ * width. `text` aligns the icon/title/body lines within that remaining
11
+ * width; absent/invalid `text` behaves as `left` (no padding needed).
12
+ *
13
+ * Registered `selfLayout` (`registry.ts`'s `AnsiRegistryEntry.selfLayout`):
14
+ * every line carries the colored bar prefix, which a generic post-render
15
+ * `applyLayout` narrowing would re-wrap right through, losing the bar on any
16
+ * continuation line. This component instead reads the resolved
17
+ * `width`/`align` off `ctx.layout` itself and sizes/places its own bar
18
+ * block, exactly like `card`/`table`/`chart`.
19
+ */
20
+ export declare const Callout: AnsiComponent;
@@ -0,0 +1,71 @@
1
+ import { measure } from '../measure.js';
2
+ import { selfLayoutAlign, selfLayoutWidth } from '../layout.js';
3
+ const CALLOUT_TYPES = ['info', 'warning', 'danger'];
4
+ const CALLOUT_ICONS = {
5
+ info: 'ℹ',
6
+ warning: '▲',
7
+ danger: '✕',
8
+ };
9
+ const CALLOUT_LABELS = {
10
+ info: 'Info',
11
+ warning: 'Warning',
12
+ danger: 'Danger',
13
+ };
14
+ const CALLOUT_TOKENS = {
15
+ info: '--mk-info',
16
+ warning: '--mk-warning',
17
+ danger: '--mk-danger',
18
+ };
19
+ function isCalloutType(value) {
20
+ return CALLOUT_TYPES.includes(value);
21
+ }
22
+ const TEXT_ALIGNS = ['left', 'center', 'right'];
23
+ function isTextAlign(value) {
24
+ return TEXT_ALIGNS.includes(value);
25
+ }
26
+ /** The left bar every line of a callout carries, marking it as one colored block at a glance (`box.ts` reserves `│`/`┆` for frames, so this uses the half-block glyph instead). */
27
+ const BAR = '▌ ';
28
+ /**
29
+ * `:::callout{type=info|warning|danger title="..." text=left|center|right}` —
30
+ * a colored aside/warning/danger box. Unknown/missing `type` falls back to
31
+ * `info` (`render.ts`'s generic invalid-enum notice covers reporting that,
32
+ * same as every other enum attribute). Terminal form: every line carries a
33
+ * colored left bar; the first line is the icon plus the type label, an
34
+ * optional bold title line follows, then the body wrapped to the remaining
35
+ * width. `text` aligns the icon/title/body lines within that remaining
36
+ * width; absent/invalid `text` behaves as `left` (no padding needed).
37
+ *
38
+ * Registered `selfLayout` (`registry.ts`'s `AnsiRegistryEntry.selfLayout`):
39
+ * every line carries the colored bar prefix, which a generic post-render
40
+ * `applyLayout` narrowing would re-wrap right through, losing the bar on any
41
+ * continuation line. This component instead reads the resolved
42
+ * `width`/`align` off `ctx.layout` itself and sizes/places its own bar
43
+ * block, exactly like `card`/`table`/`chart`.
44
+ */
45
+ export const Callout = (attributes, children, ctx) => {
46
+ const rawType = attributes.type ?? 'info';
47
+ const type = isCalloutType(rawType) ? rawType : 'info';
48
+ const token = CALLOUT_TOKENS[type];
49
+ const title = attributes.title ?? null;
50
+ const rawTextAlign = attributes.text;
51
+ const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
52
+ const headerText = `${CALLOUT_ICONS[type]} ${CALLOUT_LABELS[type]}`;
53
+ const titleText = title ? ctx.text(title) : undefined;
54
+ const naturalWidth = BAR.length + Math.max(measure(headerText), measure(titleText ?? ''));
55
+ const boxWidth = selfLayoutWidth(ctx.layout, ctx.width, naturalWidth);
56
+ const bar = ctx.style(BAR, token);
57
+ const innerWidth = Math.max(1, boxWidth - BAR.length);
58
+ const placeLine = (line) => align === 'left' ? line : ctx.pad(line, innerWidth, align);
59
+ const headerLine = `${bar}${placeLine(ctx.style(headerText, token))}`;
60
+ const lines = [headerLine];
61
+ if (titleText) {
62
+ lines.push(`${bar}${placeLine(ctx.bold(titleText))}`);
63
+ }
64
+ const childrenText = children({ width: innerWidth });
65
+ if (childrenText) {
66
+ for (const line of childrenText.split('\n')) {
67
+ lines.push(`${bar}${placeLine(line)}`);
68
+ }
69
+ }
70
+ return selfLayoutAlign(lines.join('\n'), ctx.layout, ctx.width);
71
+ };
@@ -0,0 +1,12 @@
1
+ import type { AnsiComponent } from '../registry.js';
2
+ /**
3
+ * `:::card{title="..." text=left|center|right} ... :::` — a titled panel.
4
+ * `title` is optional; the title is woven into the frame's top edge, not a
5
+ * separate line, when given. `text` aligns the body inside the frame;
6
+ * absent/invalid behaves as `left`.
7
+ *
8
+ * Registered `selfLayout` (see `callout.ts`'s doc comment for why): this
9
+ * 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 frame.
11
+ */
12
+ export declare const Card: AnsiComponent;