@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/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;
|