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