@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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { layoutWrapperAxis } from '@markii/stdlib';
|
|
2
|
+
import { applyLayout } from '../layout.js';
|
|
3
|
+
/**
|
|
4
|
+
* The closed set of layout-wrapper container names (docs/format.md):
|
|
5
|
+
* aliases of the one shared implementation below (`createLayoutWrapper`),
|
|
6
|
+
* matching `@markii/html`'s `layout-wrapper.ts`. There is deliberately no
|
|
7
|
+
* `normal` alias: the default needs no wrapper at all. Every name here is
|
|
8
|
+
* ALSO one of `@markii/stdlib`'s own width/align preset values (`center`/
|
|
9
|
+
* `left`/`right` are align presets, `wide`/`narrow`/`full`/`fit` are width
|
|
10
|
+
* presets), which is what lets `createLayoutWrapper` use the preset name
|
|
11
|
+
* itself as that axis's value with no separate lookup table.
|
|
12
|
+
*/
|
|
13
|
+
export const LAYOUT_WRAPPER_PRESETS = [
|
|
14
|
+
'center',
|
|
15
|
+
'left',
|
|
16
|
+
'right',
|
|
17
|
+
'wide',
|
|
18
|
+
'narrow',
|
|
19
|
+
'full',
|
|
20
|
+
'fit',
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Creates the registry component for one of docs/format.md's layout-wrapper
|
|
24
|
+
* container names. One shared implementation, bound to `preset` at
|
|
25
|
+
* registration time, matching `@markii/html`'s `createLayoutWrapper` in
|
|
26
|
+
* spirit: it never reads `attributes` at all — `render.ts` already stripped
|
|
27
|
+
* both reserved keys before this ever runs.
|
|
28
|
+
*
|
|
29
|
+
* A wrapper sets ONE axis by its own NAME (docs/spec.md §3): `preset` itself
|
|
30
|
+
* IS that axis's value (`center` sets `align: 'center'`; `narrow` sets
|
|
31
|
+
* `width: 'narrow'`), so it is applied unconditionally, whatever the author
|
|
32
|
+
* wrote for that axis's own reserved attribute (already discarded — the
|
|
33
|
+
* name always wins). The OTHER axis, when the author supplied it, arrives
|
|
34
|
+
* as `ctx.layout` (`render.ts` resolved it on this wrapper's behalf, since
|
|
35
|
+
* this wrapper is the directive's registered `layout` scope). Both are
|
|
36
|
+
* merged into one `ResolvedLayoutPresets` and applied together via
|
|
37
|
+
* `../layout.js`'s `applyLayout`, so `:::center{width=fit}` narrows AND
|
|
38
|
+
* centers in one pass.
|
|
39
|
+
*/
|
|
40
|
+
export function createLayoutWrapper(preset) {
|
|
41
|
+
const ownAxis = layoutWrapperAxis(preset);
|
|
42
|
+
if (ownAxis === undefined) {
|
|
43
|
+
// Unreachable for the closed preset list above.
|
|
44
|
+
throw new Error(`"${preset}" is not a layout-wrapper name`);
|
|
45
|
+
}
|
|
46
|
+
const own = ownAxis === 'align'
|
|
47
|
+
? { align: preset }
|
|
48
|
+
: { width: preset };
|
|
49
|
+
return (_attributes, children, ctx) => {
|
|
50
|
+
const merged = { ...own, ...ctx.layout };
|
|
51
|
+
return applyLayout(children(), merged, ctx.width);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** The layout axis `preset` sets by its own name. Mirrors `@markii/html`'s `layoutWrapperPresetAxis`. */
|
|
55
|
+
export function layoutWrapperPresetAxis(preset) {
|
|
56
|
+
const axis = layoutWrapperAxis(preset);
|
|
57
|
+
if (axis === undefined) {
|
|
58
|
+
// Unreachable for the closed preset list above.
|
|
59
|
+
throw new Error(`"${preset}" is not a layout-wrapper name`);
|
|
60
|
+
}
|
|
61
|
+
return axis;
|
|
62
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AnsiComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `::progress{value=3 max=5 label="tasks"}` — a meter bar. Data binding (§8)
|
|
4
|
+
* mirrors `@markii/html`'s `Progress`: a bound number supplies `value`; a
|
|
5
|
+
* bound object may supply `value`/`max` — explicit attributes always win.
|
|
6
|
+
* Non-numeric/`NaN`/`Infinity` input falls back to `0` (value) or the
|
|
7
|
+
* default `max` of `1`; the effective value is clamped to `[0, max]`.
|
|
8
|
+
* Terminal form: `label bar percent`, the bar drawn with `█` (filled) and
|
|
9
|
+
* `░` (empty) at up to `MAX_BAR_WIDTH` columns, narrower when the label and
|
|
10
|
+
* percent text leave less room. `format`/`decimals`, when given, format the
|
|
11
|
+
* `value/max` fraction for the percent readout in place of the default
|
|
12
|
+
* rounded integer percent, matching `@markii/html` exactly. A failed/stale
|
|
13
|
+
* binding appends the quiet failure suffix after the percent.
|
|
14
|
+
*/
|
|
15
|
+
export declare const Progress: AnsiComponent;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { formatValue } from '@markii/stdlib';
|
|
2
|
+
import { measure } from '../measure.js';
|
|
3
|
+
import { safeRead } from '../resolve.js';
|
|
4
|
+
import { dataStateSuffix, failureToken } from '../failure-presentation.js';
|
|
5
|
+
const DEFAULT_MAX = 1;
|
|
6
|
+
function parseFiniteNumber(raw, fallback) {
|
|
7
|
+
if (raw === null || raw === undefined)
|
|
8
|
+
return fallback;
|
|
9
|
+
const parsed = Number(raw);
|
|
10
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
11
|
+
}
|
|
12
|
+
function clamp(value, min, max) {
|
|
13
|
+
return Math.min(Math.max(value, min), max);
|
|
14
|
+
}
|
|
15
|
+
function coerceNumber(value) {
|
|
16
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
17
|
+
return value;
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
const parsed = Number(value);
|
|
20
|
+
if (Number.isFinite(parsed))
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
function readProgressFields(data) {
|
|
26
|
+
if (typeof data === 'number') {
|
|
27
|
+
return { value: Number.isFinite(data) ? data : undefined };
|
|
28
|
+
}
|
|
29
|
+
if (data !== null && typeof data === 'object' && !Array.isArray(data)) {
|
|
30
|
+
const record = data;
|
|
31
|
+
return { value: coerceNumber(record.value), max: coerceNumber(record.max) };
|
|
32
|
+
}
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
/** Block characters the bar fills with, and its widest allowed footprint (columns), sized down further when the label/percent text leaves less room. */
|
|
36
|
+
const FILLED_CHAR = '█';
|
|
37
|
+
const EMPTY_CHAR = '░';
|
|
38
|
+
const MAX_BAR_WIDTH = 30;
|
|
39
|
+
const MIN_BAR_WIDTH = 3;
|
|
40
|
+
/**
|
|
41
|
+
* `::progress{value=3 max=5 label="tasks"}` — a meter bar. Data binding (§8)
|
|
42
|
+
* mirrors `@markii/html`'s `Progress`: a bound number supplies `value`; a
|
|
43
|
+
* bound object may supply `value`/`max` — explicit attributes always win.
|
|
44
|
+
* Non-numeric/`NaN`/`Infinity` input falls back to `0` (value) or the
|
|
45
|
+
* default `max` of `1`; the effective value is clamped to `[0, max]`.
|
|
46
|
+
* Terminal form: `label bar percent`, the bar drawn with `█` (filled) and
|
|
47
|
+
* `░` (empty) at up to `MAX_BAR_WIDTH` columns, narrower when the label and
|
|
48
|
+
* percent text leave less room. `format`/`decimals`, when given, format the
|
|
49
|
+
* `value/max` fraction for the percent readout in place of the default
|
|
50
|
+
* rounded integer percent, matching `@markii/html` exactly. A failed/stale
|
|
51
|
+
* binding appends the quiet failure suffix after the percent.
|
|
52
|
+
*/
|
|
53
|
+
export const Progress = (attributes, _children, ctx) => {
|
|
54
|
+
const { data, dataStatus, dataFailureKind } = ctx;
|
|
55
|
+
const bound = safeRead(() => dataStatus === 'missing' || dataStatus === 'error'
|
|
56
|
+
? {}
|
|
57
|
+
: readProgressFields(data), () => ({}));
|
|
58
|
+
const fromData = bound.fields;
|
|
59
|
+
const rawMax = parseFiniteNumber(attributes.max, fromData.max ?? DEFAULT_MAX);
|
|
60
|
+
const max = rawMax > 0 ? rawMax : DEFAULT_MAX;
|
|
61
|
+
const rawValue = parseFiniteNumber(attributes.value, fromData.value ?? 0);
|
|
62
|
+
const value = clamp(rawValue, 0, max);
|
|
63
|
+
const percent = clamp((value / max) * 100, 0, 100);
|
|
64
|
+
const label = attributes.label ?? null;
|
|
65
|
+
const rawFormat = attributes.format ?? undefined;
|
|
66
|
+
const percentText = rawFormat
|
|
67
|
+
? formatValue(percent / 100, rawFormat, attributes.decimals ?? undefined)
|
|
68
|
+
: `${String(Math.round(percent))}%`;
|
|
69
|
+
const labelPart = label ? `${ctx.text(label)} ` : '';
|
|
70
|
+
const suffix = dataStateSuffix(dataStatus, dataFailureKind);
|
|
71
|
+
const token = failureToken(dataFailureKind);
|
|
72
|
+
const percentPart = ` ${ctx.text(percentText)}${suffix ? (token ? ctx.style(suffix, token) : ctx.dim(suffix)) : ''}`;
|
|
73
|
+
const overhead = measure(labelPart) + measure(percentText) + 1;
|
|
74
|
+
const barWidth = Math.max(MIN_BAR_WIDTH, Math.min(MAX_BAR_WIDTH, ctx.width - overhead));
|
|
75
|
+
const filledCount = Math.round((percent / 100) * barWidth);
|
|
76
|
+
const bar = FILLED_CHAR.repeat(filledCount) + EMPTY_CHAR.repeat(barWidth - filledCount);
|
|
77
|
+
return `${labelPart}${bar}${percentPart}`;
|
|
78
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AnsiComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `::rating{value=3 max=5}` — a leaf directive rendering a row of stars.
|
|
4
|
+
* Both attributes are optional and clamped to sane bounds; malformed input
|
|
5
|
+
* degrades gracefully instead of throwing. Terminal form: filled (`★`) and
|
|
6
|
+
* empty (`☆`) stars, one per position up to `max`. Matches
|
|
7
|
+
* `@markii/html`'s `Rating` clamping rules exactly.
|
|
8
|
+
*/
|
|
9
|
+
export declare const Rating: AnsiComponent;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const DEFAULT_MAX = 5;
|
|
2
|
+
const MIN_MAX = 1;
|
|
3
|
+
const MAX_MAX = 20;
|
|
4
|
+
function parseCount(raw, fallback) {
|
|
5
|
+
if (raw === null || raw === undefined)
|
|
6
|
+
return fallback;
|
|
7
|
+
const parsed = Number.parseInt(raw, 10);
|
|
8
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
9
|
+
}
|
|
10
|
+
function clamp(value, min, max) {
|
|
11
|
+
return Math.min(Math.max(value, min), max);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `::rating{value=3 max=5}` — a leaf directive rendering a row of stars.
|
|
15
|
+
* Both attributes are optional and clamped to sane bounds; malformed input
|
|
16
|
+
* degrades gracefully instead of throwing. Terminal form: filled (`★`) and
|
|
17
|
+
* empty (`☆`) stars, one per position up to `max`. Matches
|
|
18
|
+
* `@markii/html`'s `Rating` clamping rules exactly.
|
|
19
|
+
*/
|
|
20
|
+
export const Rating = (attributes) => {
|
|
21
|
+
const max = clamp(parseCount(attributes.max, DEFAULT_MAX), MIN_MAX, MAX_MAX);
|
|
22
|
+
const value = clamp(parseCount(attributes.value, 0), 0, max);
|
|
23
|
+
let stars = '';
|
|
24
|
+
for (let index = 0; index < max; index += 1) {
|
|
25
|
+
stars += index < value ? '★' : '☆';
|
|
26
|
+
}
|
|
27
|
+
return stars;
|
|
28
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AnsiComponent } from '../registry.js';
|
|
2
|
+
/** Below this width, cells stack vertically instead of sitting side by side — a named constant per the batch brief, not a magic number. */
|
|
3
|
+
export declare const ROW_COLUMN_THRESHOLD = 60;
|
|
4
|
+
/**
|
|
5
|
+
* `:::row{cols=2|3|4 text=left|center|right} ... :::` — docs/format.md's one
|
|
6
|
+
* layout container. An absent/invalid `cols` auto-fits: every direct block
|
|
7
|
+
* child becomes one column, all in a single row of columns. A `cols` value
|
|
8
|
+
* smaller than the child count wraps into further grid rows, `cols` cells at
|
|
9
|
+
* a time — the terminal counterpart of the CSS grid wrap a live host does.
|
|
10
|
+
* Below `ROW_COLUMN_THRESHOLD` columns, or with only one cell, cells stack
|
|
11
|
+
* vertically instead (a plain blank-line-separated list) — a fixed-width
|
|
12
|
+
* terminal has no responsive reflow, so this is the one width breakpoint
|
|
13
|
+
* that stands in for it.
|
|
14
|
+
*
|
|
15
|
+
* Each cell is one of the directive's own top-level children
|
|
16
|
+
* (`registry.ts`'s `AnsiChildren.parts`: a `:::cell` directive grouping
|
|
17
|
+
* several blocks, or any other block standing for itself, per docs/format.md's
|
|
18
|
+
* "a row counts its direct block children as its cells"), rendered through
|
|
19
|
+
* that part's own `render({ width })` at the ACTUAL column width decided
|
|
20
|
+
* below, rather than rendered once at the row's full width and then
|
|
21
|
+
* re-wrapped into a narrower column. A cell containing a self-drawing box
|
|
22
|
+
* (a nested `card`/`table`) therefore draws that box at the real column
|
|
23
|
+
* width in the first place, instead of having an already-drawn frame
|
|
24
|
+
* mangled by a later re-wrap.
|
|
25
|
+
*/
|
|
26
|
+
export declare const Row: AnsiComponent;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { columns } from '../box.js';
|
|
2
|
+
const COLS_VALUES = ['2', '3', '4'];
|
|
3
|
+
function isColsValue(value) {
|
|
4
|
+
return COLS_VALUES.includes(value);
|
|
5
|
+
}
|
|
6
|
+
const TEXT_ALIGNS = ['left', 'center', 'right'];
|
|
7
|
+
function isTextAlign(value) {
|
|
8
|
+
return TEXT_ALIGNS.includes(value);
|
|
9
|
+
}
|
|
10
|
+
/** Below this width, cells stack vertically instead of sitting side by side — a named constant per the batch brief, not a magic number. */
|
|
11
|
+
export const ROW_COLUMN_THRESHOLD = 60;
|
|
12
|
+
/** Spaces between adjacent columns. */
|
|
13
|
+
const GUTTER = 1;
|
|
14
|
+
/**
|
|
15
|
+
* `:::row{cols=2|3|4 text=left|center|right} ... :::` — docs/format.md's one
|
|
16
|
+
* layout container. An absent/invalid `cols` auto-fits: every direct block
|
|
17
|
+
* child becomes one column, all in a single row of columns. A `cols` value
|
|
18
|
+
* smaller than the child count wraps into further grid rows, `cols` cells at
|
|
19
|
+
* a time — the terminal counterpart of the CSS grid wrap a live host does.
|
|
20
|
+
* Below `ROW_COLUMN_THRESHOLD` columns, or with only one cell, cells stack
|
|
21
|
+
* vertically instead (a plain blank-line-separated list) — a fixed-width
|
|
22
|
+
* terminal has no responsive reflow, so this is the one width breakpoint
|
|
23
|
+
* that stands in for it.
|
|
24
|
+
*
|
|
25
|
+
* Each cell is one of the directive's own top-level children
|
|
26
|
+
* (`registry.ts`'s `AnsiChildren.parts`: a `:::cell` directive grouping
|
|
27
|
+
* several blocks, or any other block standing for itself, per docs/format.md's
|
|
28
|
+
* "a row counts its direct block children as its cells"), rendered through
|
|
29
|
+
* that part's own `render({ width })` at the ACTUAL column width decided
|
|
30
|
+
* below, rather than rendered once at the row's full width and then
|
|
31
|
+
* re-wrapped into a narrower column. A cell containing a self-drawing box
|
|
32
|
+
* (a nested `card`/`table`) therefore draws that box at the real column
|
|
33
|
+
* width in the first place, instead of having an already-drawn frame
|
|
34
|
+
* mangled by a later re-wrap.
|
|
35
|
+
*/
|
|
36
|
+
export const Row = (attributes, children, ctx) => {
|
|
37
|
+
const rawTextAlign = attributes.text;
|
|
38
|
+
const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
|
|
39
|
+
const cellParts = children.parts;
|
|
40
|
+
if (cellParts.length === 0)
|
|
41
|
+
return '';
|
|
42
|
+
const rawCols = attributes.cols ?? '';
|
|
43
|
+
const requestedCols = isColsValue(rawCols)
|
|
44
|
+
? Number(rawCols)
|
|
45
|
+
: cellParts.length;
|
|
46
|
+
const columnCount = Math.max(1, Math.min(requestedCols, cellParts.length));
|
|
47
|
+
const place = (text, width) => align === 'left'
|
|
48
|
+
? text
|
|
49
|
+
: text
|
|
50
|
+
.split('\n')
|
|
51
|
+
.map((line) => ctx.pad(line, width, align))
|
|
52
|
+
.join('\n');
|
|
53
|
+
if (ctx.width < ROW_COLUMN_THRESHOLD || columnCount <= 1) {
|
|
54
|
+
return cellParts
|
|
55
|
+
.map((part) => place(part.render({ width: ctx.width }), ctx.width))
|
|
56
|
+
.join('\n\n');
|
|
57
|
+
}
|
|
58
|
+
const colWidth = Math.max(1, Math.floor((ctx.width - GUTTER * (columnCount - 1)) / columnCount));
|
|
59
|
+
const placedCells = cellParts.map((part) => place(part.render({ width: colWidth }), colWidth));
|
|
60
|
+
const gridRows = [];
|
|
61
|
+
for (let index = 0; index < placedCells.length; index += columnCount) {
|
|
62
|
+
const rowCells = placedCells.slice(index, index + columnCount);
|
|
63
|
+
const widths = rowCells.map(() => colWidth);
|
|
64
|
+
gridRows.push(columns(rowCells, widths, GUTTER));
|
|
65
|
+
}
|
|
66
|
+
return gridRows.join('\n\n');
|
|
67
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AnsiComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `::stat{value=42 label="stars" trend=up}` — a big value plus label. Data
|
|
4
|
+
* binding (§8) mirrors `@markii/html`'s `Stat` exactly: a bound number/string
|
|
5
|
+
* supplies `value`; a bound object may supply `value`/`label`/`delta`/`trend`
|
|
6
|
+
* — an explicit attribute always wins. Missing value renders `—` rather than
|
|
7
|
+
* a blank line. Terminal form: a muted label line, then a bold value line
|
|
8
|
+
* (with `delta` appended, colored by `trend` when recognized). A failed or
|
|
9
|
+
* stale binding appends `failure-presentation.ts`'s quiet suffix to the
|
|
10
|
+
* value line — the terminal has no tooltip channel, so the reason has to
|
|
11
|
+
* reach the text itself (AGENTS.md's "clean is not silent").
|
|
12
|
+
*/
|
|
13
|
+
export declare const Stat: AnsiComponent;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { formatValue } from '@markii/stdlib';
|
|
2
|
+
import { safeRead } from '../resolve.js';
|
|
3
|
+
import { dataStateSuffix, failureToken } from '../failure-presentation.js';
|
|
4
|
+
const EMPTY_VALUE = '—';
|
|
5
|
+
const TRENDS = ['up', 'down', 'flat'];
|
|
6
|
+
function isTrend(value) {
|
|
7
|
+
return TRENDS.includes(value);
|
|
8
|
+
}
|
|
9
|
+
function coerceField(value) {
|
|
10
|
+
if (typeof value === 'string')
|
|
11
|
+
return value;
|
|
12
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
13
|
+
return String(value);
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
function readStatFields(data) {
|
|
18
|
+
if (typeof data === 'number' || typeof data === 'string') {
|
|
19
|
+
return { value: String(data) };
|
|
20
|
+
}
|
|
21
|
+
if (data !== null && typeof data === 'object' && !Array.isArray(data)) {
|
|
22
|
+
const record = data;
|
|
23
|
+
return {
|
|
24
|
+
value: coerceField(record.value),
|
|
25
|
+
label: coerceField(record.label),
|
|
26
|
+
delta: coerceField(record.delta),
|
|
27
|
+
trend: coerceField(record.trend),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
/** Explicit directive attributes win over the bound `data` object's own fields. */
|
|
33
|
+
function pick(attribute, fromData) {
|
|
34
|
+
return attribute ?? fromData ?? undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `::stat{value=42 label="stars" trend=up}` — a big value plus label. Data
|
|
38
|
+
* binding (§8) mirrors `@markii/html`'s `Stat` exactly: a bound number/string
|
|
39
|
+
* supplies `value`; a bound object may supply `value`/`label`/`delta`/`trend`
|
|
40
|
+
* — an explicit attribute always wins. Missing value renders `—` rather than
|
|
41
|
+
* a blank line. Terminal form: a muted label line, then a bold value line
|
|
42
|
+
* (with `delta` appended, colored by `trend` when recognized). A failed or
|
|
43
|
+
* stale binding appends `failure-presentation.ts`'s quiet suffix to the
|
|
44
|
+
* value line — the terminal has no tooltip channel, so the reason has to
|
|
45
|
+
* reach the text itself (AGENTS.md's "clean is not silent").
|
|
46
|
+
*/
|
|
47
|
+
export const Stat = (attributes, _children, ctx) => {
|
|
48
|
+
const { data, dataStatus, dataFailureKind } = ctx;
|
|
49
|
+
const bound = safeRead(() => dataStatus === 'missing' || dataStatus === 'error'
|
|
50
|
+
? {}
|
|
51
|
+
: readStatFields(data), () => ({}));
|
|
52
|
+
const fromData = bound.fields;
|
|
53
|
+
const value = pick(attributes.value, fromData.value);
|
|
54
|
+
const formattedValue = formatValue(value, attributes.format ?? undefined, attributes.decimals ?? undefined);
|
|
55
|
+
const label = pick(attributes.label, fromData.label);
|
|
56
|
+
const delta = pick(attributes.delta, fromData.delta);
|
|
57
|
+
const rawTrend = pick(attributes.trend, fromData.trend);
|
|
58
|
+
const trend = rawTrend && isTrend(rawTrend) ? rawTrend : undefined;
|
|
59
|
+
const lines = [];
|
|
60
|
+
if (label)
|
|
61
|
+
lines.push(ctx.style(ctx.text(label), '--mk-muted'));
|
|
62
|
+
let valueLine = ctx.bold(ctx.text(formattedValue || EMPTY_VALUE));
|
|
63
|
+
if (delta) {
|
|
64
|
+
const deltaToken = trend === 'up'
|
|
65
|
+
? '--mk-success'
|
|
66
|
+
: trend === 'down'
|
|
67
|
+
? '--mk-danger'
|
|
68
|
+
: undefined;
|
|
69
|
+
const deltaText = ctx.text(delta);
|
|
70
|
+
valueLine += ` ${deltaToken ? ctx.style(deltaText, deltaToken) : deltaText}`;
|
|
71
|
+
}
|
|
72
|
+
const suffix = dataStateSuffix(dataStatus, dataFailureKind);
|
|
73
|
+
if (suffix) {
|
|
74
|
+
const token = failureToken(dataFailureKind);
|
|
75
|
+
valueLine += token ? ctx.style(suffix, token) : ctx.dim(suffix);
|
|
76
|
+
}
|
|
77
|
+
lines.push(valueLine);
|
|
78
|
+
return lines.join('\n');
|
|
79
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AnsiComponent } from '../registry.js';
|
|
2
|
+
export declare const DEFAULT_TAB_LABEL = "Tab";
|
|
3
|
+
/**
|
|
4
|
+
* `:::tab{label="..."} ... :::` — one panel of a `tabs` component
|
|
5
|
+
* (`tabs.ts`). Unlike `@markii/html`'s `Tab` (which never reads `label` at
|
|
6
|
+
* all — see that module's `tabs.ts` for why a string-based engine usually
|
|
7
|
+
* can't reach a child directive's own attributes from its parent), THIS
|
|
8
|
+
* engine's `tab` reads its OWN `label` directly, since it is rendered as
|
|
9
|
+
* its OWN directive, not inspected by `tabs` from the outside: the label
|
|
10
|
+
* becomes a bold heading line above the panel body. Rendered standalone
|
|
11
|
+
* (outside a `tabs` parent) it shows exactly the same heading and panel.
|
|
12
|
+
*/
|
|
13
|
+
export declare const Tab: AnsiComponent;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const DEFAULT_TAB_LABEL = 'Tab';
|
|
2
|
+
/**
|
|
3
|
+
* `:::tab{label="..."} ... :::` — one panel of a `tabs` component
|
|
4
|
+
* (`tabs.ts`). Unlike `@markii/html`'s `Tab` (which never reads `label` at
|
|
5
|
+
* all — see that module's `tabs.ts` for why a string-based engine usually
|
|
6
|
+
* can't reach a child directive's own attributes from its parent), THIS
|
|
7
|
+
* engine's `tab` reads its OWN `label` directly, since it is rendered as
|
|
8
|
+
* its OWN directive, not inspected by `tabs` from the outside: the label
|
|
9
|
+
* becomes a bold heading line above the panel body. Rendered standalone
|
|
10
|
+
* (outside a `tabs` parent) it shows exactly the same heading and panel.
|
|
11
|
+
*/
|
|
12
|
+
export const Tab = (attributes, children, ctx) => {
|
|
13
|
+
const label = attributes.label ?? DEFAULT_TAB_LABEL;
|
|
14
|
+
const heading = ctx.bold(ctx.text(label));
|
|
15
|
+
const childrenText = children();
|
|
16
|
+
return childrenText ? `${heading}\n${childrenText}` : heading;
|
|
17
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE box-drawing/width-negotiation routine every table this engine
|
|
3
|
+
* draws goes through: the data-bound `::table` component (`./table.ts`) and
|
|
4
|
+
* `render.ts`'s plain GFM markdown table both call `drawTableGrid`, so there
|
|
5
|
+
* is exactly one place that decides how a table's columns share the
|
|
6
|
+
* available width and exactly one set of box-drawing glyphs in the whole
|
|
7
|
+
* engine (`┌ ┬ ┐ ├ ┼ ┤ └ ┴ ┘ ─ │`).
|
|
8
|
+
*
|
|
9
|
+
* Column-width negotiation: every cell (header included) is measured at its
|
|
10
|
+
* natural width; if the natural total fits `width`, every column gets its
|
|
11
|
+
* natural width. Otherwise the widest column is shrunk one column at a time
|
|
12
|
+
* (ties broken by the leftmost widest column) until the total fits or every
|
|
13
|
+
* column has hit `MIN_COLUMN_WIDTH`, whichever comes first — a table wider
|
|
14
|
+
* than `width` even at every column's minimum is allowed to overflow rather
|
|
15
|
+
* than produce unreadably thin columns; nothing in this engine truncates
|
|
16
|
+
* text outright. A column narrower than its content wraps that cell's text
|
|
17
|
+
* inside its own box (`./box.ts`'s `wrap`), so a row's height grows to fit
|
|
18
|
+
* its tallest cell rather than losing text.
|
|
19
|
+
*/
|
|
20
|
+
/** The floor a column is shrunk to before a table is allowed to overflow `width`. Chosen so a wrapped cell still reads as more than one letter per line. */
|
|
21
|
+
export declare const MIN_COLUMN_WIDTH = 3;
|
|
22
|
+
/**
|
|
23
|
+
* Draws a box-drawn table. `header`, when given, is the bold first row,
|
|
24
|
+
* separated from the body by its own rule; `rows` is every remaining row.
|
|
25
|
+
* Every string is display text ALREADY sanitized/formatted by the caller
|
|
26
|
+
* (this module escapes nothing of its own — there is no HTML/ANSI injection
|
|
27
|
+
* risk in a plain string, only in the escape SEQUENCES `boldFn` itself
|
|
28
|
+
* generates, which this module never constructs by hand). Returns a
|
|
29
|
+
* complete grid at most `width` columns wide when the natural content
|
|
30
|
+
* allows it, otherwise as narrow as `MIN_COLUMN_WIDTH` per column permits.
|
|
31
|
+
*/
|
|
32
|
+
export declare function drawTableGrid(header: readonly string[] | undefined, rows: readonly (readonly string[])[], width: number, boldFn?: (text: string) => string, align?: 'left' | 'center' | 'right'): string;
|
|
33
|
+
/** The widest a `drawTableGrid` result's lines get, for a `selfLayout` component (`./table.ts`) that needs to know its own natural footprint (the `fit` width preset). */
|
|
34
|
+
export declare function measureTableGridWidth(header: readonly string[] | undefined, rows: readonly (readonly string[])[]): number;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { pad, wrap } from '../box.js';
|
|
2
|
+
import { measure } from '../measure.js';
|
|
3
|
+
/**
|
|
4
|
+
* The ONE box-drawing/width-negotiation routine every table this engine
|
|
5
|
+
* draws goes through: the data-bound `::table` component (`./table.ts`) and
|
|
6
|
+
* `render.ts`'s plain GFM markdown table both call `drawTableGrid`, so there
|
|
7
|
+
* is exactly one place that decides how a table's columns share the
|
|
8
|
+
* available width and exactly one set of box-drawing glyphs in the whole
|
|
9
|
+
* engine (`┌ ┬ ┐ ├ ┼ ┤ └ ┴ ┘ ─ │`).
|
|
10
|
+
*
|
|
11
|
+
* Column-width negotiation: every cell (header included) is measured at its
|
|
12
|
+
* natural width; if the natural total fits `width`, every column gets its
|
|
13
|
+
* natural width. Otherwise the widest column is shrunk one column at a time
|
|
14
|
+
* (ties broken by the leftmost widest column) until the total fits or every
|
|
15
|
+
* column has hit `MIN_COLUMN_WIDTH`, whichever comes first — a table wider
|
|
16
|
+
* than `width` even at every column's minimum is allowed to overflow rather
|
|
17
|
+
* than produce unreadably thin columns; nothing in this engine truncates
|
|
18
|
+
* text outright. A column narrower than its content wraps that cell's text
|
|
19
|
+
* inside its own box (`./box.ts`'s `wrap`), so a row's height grows to fit
|
|
20
|
+
* its tallest cell rather than losing text.
|
|
21
|
+
*/
|
|
22
|
+
/** The floor a column is shrunk to before a table is allowed to overflow `width`. Chosen so a wrapped cell still reads as more than one letter per line. */
|
|
23
|
+
export const MIN_COLUMN_WIDTH = 3;
|
|
24
|
+
/** Per-column border/padding overhead: one leading space, one trailing space, shared with `measureOverhead` below. */
|
|
25
|
+
const CELL_PADDING = 2;
|
|
26
|
+
function measureOverhead(columnCount) {
|
|
27
|
+
// One vertical border before each column plus one closing border, plus
|
|
28
|
+
// one space of padding on each side of every column's content.
|
|
29
|
+
return columnCount + 1 + columnCount * CELL_PADDING;
|
|
30
|
+
}
|
|
31
|
+
function naturalColumnWidths(header, rows, columnCount) {
|
|
32
|
+
const widths = new Array(columnCount).fill(1);
|
|
33
|
+
const consider = (cells) => {
|
|
34
|
+
for (let c = 0; c < columnCount; c += 1) {
|
|
35
|
+
const width = measure(cells?.[c] ?? '');
|
|
36
|
+
if (width > widths[c])
|
|
37
|
+
widths[c] = width;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
consider(header);
|
|
41
|
+
for (const row of rows)
|
|
42
|
+
consider(row);
|
|
43
|
+
return widths;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Shrinks `widths` (in place, returning a new array) until their total plus
|
|
47
|
+
* `overhead` fits `budget`, or every column is at `MIN_COLUMN_WIDTH`. Each
|
|
48
|
+
* step removes one column from the widest-remaining pool, so several
|
|
49
|
+
* over-wide columns shrink roughly evenly rather than one column absorbing
|
|
50
|
+
* the whole deficit.
|
|
51
|
+
*/
|
|
52
|
+
function negotiateColumnWidths(natural, width) {
|
|
53
|
+
const widths = [...natural];
|
|
54
|
+
const columnCount = widths.length;
|
|
55
|
+
const overhead = measureOverhead(columnCount);
|
|
56
|
+
const budget = Math.max(columnCount * MIN_COLUMN_WIDTH, width - overhead);
|
|
57
|
+
let total = widths.reduce((sum, w) => sum + w, 0);
|
|
58
|
+
while (total > budget) {
|
|
59
|
+
let widest = -1;
|
|
60
|
+
for (let c = 0; c < columnCount; c += 1) {
|
|
61
|
+
if (widths[c] > MIN_COLUMN_WIDTH &&
|
|
62
|
+
(widest === -1 || widths[c] > widths[widest])) {
|
|
63
|
+
widest = c;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (widest === -1)
|
|
67
|
+
break; // every column is already at the floor
|
|
68
|
+
widths[widest] = widths[widest] - 1;
|
|
69
|
+
total -= 1;
|
|
70
|
+
}
|
|
71
|
+
return widths;
|
|
72
|
+
}
|
|
73
|
+
/** Wraps every cell in `row` to its column's width, returning the row's cells as line arrays and the row's resulting height. */
|
|
74
|
+
function wrapRow(row, widths) {
|
|
75
|
+
const lines = widths.map((w, c) => wrap(row[c] ?? '', w));
|
|
76
|
+
const height = Math.max(1, ...lines.map((l) => l.length));
|
|
77
|
+
return { lines, height };
|
|
78
|
+
}
|
|
79
|
+
function drawRow(row, widths, boldRow, boldFn, align) {
|
|
80
|
+
const { lines, height } = wrapRow(row, widths);
|
|
81
|
+
const rowLines = [];
|
|
82
|
+
for (let line = 0; line < height; line += 1) {
|
|
83
|
+
const cells = widths.map((w, c) => {
|
|
84
|
+
const text = pad(lines[c]?.[line] ?? '', w, align);
|
|
85
|
+
return boldRow ? boldFn(text) : text;
|
|
86
|
+
});
|
|
87
|
+
rowLines.push(`│ ${cells.join(' │ ')} │`);
|
|
88
|
+
}
|
|
89
|
+
return rowLines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
function drawSeparator(widths, left, mid, right) {
|
|
92
|
+
return `${left}${widths.map((w) => '─'.repeat(w + CELL_PADDING)).join(mid)}${right}`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Draws a box-drawn table. `header`, when given, is the bold first row,
|
|
96
|
+
* separated from the body by its own rule; `rows` is every remaining row.
|
|
97
|
+
* Every string is display text ALREADY sanitized/formatted by the caller
|
|
98
|
+
* (this module escapes nothing of its own — there is no HTML/ANSI injection
|
|
99
|
+
* risk in a plain string, only in the escape SEQUENCES `boldFn` itself
|
|
100
|
+
* generates, which this module never constructs by hand). Returns a
|
|
101
|
+
* complete grid at most `width` columns wide when the natural content
|
|
102
|
+
* allows it, otherwise as narrow as `MIN_COLUMN_WIDTH` per column permits.
|
|
103
|
+
*/
|
|
104
|
+
export function drawTableGrid(header, rows, width, boldFn = (text) => text, align = 'left') {
|
|
105
|
+
const columnCount = Math.max(header?.length ?? 0, ...rows.map((row) => row.length), 1);
|
|
106
|
+
const natural = naturalColumnWidths(header, rows, columnCount);
|
|
107
|
+
const widths = negotiateColumnWidths(natural, width);
|
|
108
|
+
const lines = [];
|
|
109
|
+
lines.push(drawSeparator(widths, '┌', '┬', '┐'));
|
|
110
|
+
if (header) {
|
|
111
|
+
lines.push(drawRow(header, widths, true, boldFn, align));
|
|
112
|
+
lines.push(drawSeparator(widths, '├', '┼', '┤'));
|
|
113
|
+
}
|
|
114
|
+
for (const row of rows)
|
|
115
|
+
lines.push(drawRow(row, widths, false, boldFn, align));
|
|
116
|
+
lines.push(drawSeparator(widths, '└', '┴', '┘'));
|
|
117
|
+
return lines.join('\n');
|
|
118
|
+
}
|
|
119
|
+
/** The widest a `drawTableGrid` result's lines get, for a `selfLayout` component (`./table.ts`) that needs to know its own natural footprint (the `fit` width preset). */
|
|
120
|
+
export function measureTableGridWidth(header, rows) {
|
|
121
|
+
const columnCount = Math.max(header?.length ?? 0, ...rows.map((row) => row.length), 1);
|
|
122
|
+
const natural = naturalColumnWidths(header, rows, columnCount);
|
|
123
|
+
return natural.reduce((sum, w) => sum + w, 0) + measureOverhead(columnCount);
|
|
124
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AnsiComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `::table{data=users columns="name,role" limit=10}` — a data-bound table.
|
|
4
|
+
* Data binding (§8) mirrors `@markii/html`'s `Table`: `@markii/stdlib`'s
|
|
5
|
+
* `deriveTableShape` decides the layout from the bound value's own shape.
|
|
6
|
+
* `format`/`decimals` apply to numeric cells only; `text` aligns every
|
|
7
|
+
* cell's content within its column; `caption`, when given, is a bold line
|
|
8
|
+
* above the grid. A missing/stale/failed binding degrades to the same quiet
|
|
9
|
+
* "no data" line `chart`/`progress` use, with the failure suffix appended.
|
|
10
|
+
*
|
|
11
|
+
* Registered `selfLayout` (see `card.ts`'s doc comment): draws real
|
|
12
|
+
* box-drawing glyphs (`./table-grid.ts`), so it sizes its own grid off
|
|
13
|
+
* `ctx.layout` instead of letting a generic post-render narrow/pad corrupt
|
|
14
|
+
* the borders.
|
|
15
|
+
*/
|
|
16
|
+
export declare const Table: AnsiComponent;
|