@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,44 @@
|
|
|
1
|
+
import { measure } from '../measure.js';
|
|
2
|
+
import { selfLayoutAlign, selfLayoutWidth } from '../layout.js';
|
|
3
|
+
const TEXT_ALIGNS = ['left', 'center', 'right'];
|
|
4
|
+
function isTextAlign(value) {
|
|
5
|
+
return TEXT_ALIGNS.includes(value);
|
|
6
|
+
}
|
|
7
|
+
/** `width=fit` with no `title` and an unhelpfully-shaped body (nothing to measure a sensible box from) falls back to this column count rather than the full available width. */
|
|
8
|
+
const FIT_DEFAULT_WIDTH = 40;
|
|
9
|
+
/** Frame border + one space of interior padding on each side. */
|
|
10
|
+
const FRAME_OVERHEAD = 2;
|
|
11
|
+
/**
|
|
12
|
+
* `:::card{title="..." text=left|center|right} ... :::` — a titled panel.
|
|
13
|
+
* `title` is optional; the title is woven into the frame's top edge, not a
|
|
14
|
+
* separate line, when given. `text` aligns the body inside the frame;
|
|
15
|
+
* absent/invalid behaves as `left`.
|
|
16
|
+
*
|
|
17
|
+
* Registered `selfLayout` (see `callout.ts`'s doc comment for why): this
|
|
18
|
+
* component draws a real box-drawn frame, which a generic post-render
|
|
19
|
+
* narrow/pad would corrupt, so it reads `ctx.layout` and sizes its own frame.
|
|
20
|
+
*/
|
|
21
|
+
export const Card = (attributes, children, ctx) => {
|
|
22
|
+
const title = attributes.title ?? null;
|
|
23
|
+
const titleText = title ? ctx.text(title) : undefined;
|
|
24
|
+
const rawTextAlign = attributes.text;
|
|
25
|
+
const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
|
|
26
|
+
const naturalWidth = titleText
|
|
27
|
+
? measure(titleText) + FRAME_OVERHEAD * 2
|
|
28
|
+
: FIT_DEFAULT_WIDTH;
|
|
29
|
+
const boxWidth = selfLayoutWidth(ctx.layout, ctx.width, naturalWidth);
|
|
30
|
+
const innerWidth = Math.max(1, boxWidth - FRAME_OVERHEAD);
|
|
31
|
+
const childrenText = children({ width: innerWidth });
|
|
32
|
+
const body = childrenText
|
|
33
|
+
? childrenText
|
|
34
|
+
.split('\n')
|
|
35
|
+
.map((line) => align === 'left' ? line : ctx.pad(line, innerWidth, align))
|
|
36
|
+
.join('\n')
|
|
37
|
+
: '';
|
|
38
|
+
const framed = ctx.frame(body, {
|
|
39
|
+
style: 'solid',
|
|
40
|
+
title: titleText,
|
|
41
|
+
width: boxWidth,
|
|
42
|
+
});
|
|
43
|
+
return selfLayoutAlign(framed, ctx.layout, ctx.width);
|
|
44
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AnsiComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `:::cell ... :::` — a transparent grouping container whose only job is
|
|
4
|
+
* letting several blocks count as ONE cell of `:::row`.
|
|
5
|
+
*
|
|
6
|
+
* `row.ts` no longer discovers its cells by splitting a flattened string on
|
|
7
|
+
* a blank-line heuristic: it walks its own directive's top-level children
|
|
8
|
+
* directly (`registry.ts`'s `AnsiChildren.parts`), so a `cell` grouping more
|
|
9
|
+
* than one block renders those blocks exactly as it would standalone,
|
|
10
|
+
* blank line and all, with no collapsing needed to keep a cell boundary
|
|
11
|
+
* unambiguous for `row.ts`.
|
|
12
|
+
*
|
|
13
|
+
* `text` aligns this cell's own content when rendered STANDALONE (outside a
|
|
14
|
+
* `row`); nested inside a `row`, the row's own `text` decides every cell's
|
|
15
|
+
* alignment uniformly instead, for the same reason `row.ts` does not look
|
|
16
|
+
* inside a cell's own attributes when it places columns.
|
|
17
|
+
*/
|
|
18
|
+
export declare const Cell: AnsiComponent;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const TEXT_ALIGNS = ['left', 'center', 'right'];
|
|
2
|
+
function isTextAlign(value) {
|
|
3
|
+
return TEXT_ALIGNS.includes(value);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* `:::cell ... :::` — a transparent grouping container whose only job is
|
|
7
|
+
* letting several blocks count as ONE cell of `:::row`.
|
|
8
|
+
*
|
|
9
|
+
* `row.ts` no longer discovers its cells by splitting a flattened string on
|
|
10
|
+
* a blank-line heuristic: it walks its own directive's top-level children
|
|
11
|
+
* directly (`registry.ts`'s `AnsiChildren.parts`), so a `cell` grouping more
|
|
12
|
+
* than one block renders those blocks exactly as it would standalone,
|
|
13
|
+
* blank line and all, with no collapsing needed to keep a cell boundary
|
|
14
|
+
* unambiguous for `row.ts`.
|
|
15
|
+
*
|
|
16
|
+
* `text` aligns this cell's own content when rendered STANDALONE (outside a
|
|
17
|
+
* `row`); nested inside a `row`, the row's own `text` decides every cell's
|
|
18
|
+
* alignment uniformly instead, for the same reason `row.ts` does not look
|
|
19
|
+
* inside a cell's own attributes when it places columns.
|
|
20
|
+
*/
|
|
21
|
+
export const Cell = (attributes, children, ctx) => {
|
|
22
|
+
const rawTextAlign = attributes.text;
|
|
23
|
+
const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : undefined;
|
|
24
|
+
const childrenText = children();
|
|
25
|
+
if (!align || align === 'left')
|
|
26
|
+
return childrenText;
|
|
27
|
+
return childrenText
|
|
28
|
+
.split('\n')
|
|
29
|
+
.map((line) => ctx.pad(line, ctx.width, align))
|
|
30
|
+
.join('\n');
|
|
31
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AnsiComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `::chart{kind=line|bar values="1,3,2,5"}` — a dependency-free, hand-rolled
|
|
4
|
+
* chart. Data binding (§8) mirrors `@markii/html`'s `Chart`: a bound `data`
|
|
5
|
+
* array of numbers (or `{value}` objects) takes priority over the static
|
|
6
|
+
* `values=` attribute; non-numeric/non-finite entries are dropped and the
|
|
7
|
+
* point count capped at `MAX_POINTS`. An empty or all-invalid series renders
|
|
8
|
+
* a small neutral "no data" line rather than a broken chart.
|
|
9
|
+
*
|
|
10
|
+
* Terminal form: a `line` chart is a single sparkline row (`▁▂▃▄▅▆▇█`),
|
|
11
|
+
* flanked by its minimum and maximum as plain numbers. A `bar` chart is one
|
|
12
|
+
* horizontal `█` bar per point, its own value as the row's label, labels
|
|
13
|
+
* right-aligned to the widest one so every bar starts at the same column.
|
|
14
|
+
*/
|
|
15
|
+
export declare const Chart: AnsiComponent;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { pad } from '../box.js';
|
|
2
|
+
import { measure } from '../measure.js';
|
|
3
|
+
import { safeRead } from '../resolve.js';
|
|
4
|
+
import { dataStateSuffix, failureToken } from '../failure-presentation.js';
|
|
5
|
+
const CHART_KINDS = ['line', 'bar'];
|
|
6
|
+
function isChartKind(value) {
|
|
7
|
+
return CHART_KINDS.includes(value);
|
|
8
|
+
}
|
|
9
|
+
const DEFAULT_KIND = 'line';
|
|
10
|
+
/** Hard cap on rendered points, independent of source (mirrors `@markii/html`'s `Chart`). */
|
|
11
|
+
const MAX_POINTS = 200;
|
|
12
|
+
function parseNumericString(raw) {
|
|
13
|
+
const trimmed = raw.trim();
|
|
14
|
+
if (trimmed === '')
|
|
15
|
+
return undefined;
|
|
16
|
+
const parsed = Number(trimmed);
|
|
17
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
18
|
+
}
|
|
19
|
+
function coercePoint(entry) {
|
|
20
|
+
if (typeof entry === 'number')
|
|
21
|
+
return Number.isFinite(entry) ? entry : undefined;
|
|
22
|
+
if (typeof entry === 'string')
|
|
23
|
+
return parseNumericString(entry);
|
|
24
|
+
if (entry !== null && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
25
|
+
const value = entry.value;
|
|
26
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
27
|
+
return value;
|
|
28
|
+
if (typeof value === 'string')
|
|
29
|
+
return parseNumericString(value);
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Builds the numeric point series to plot, a bound `data` array taking priority over the static `values=` attribute. Ported from `@markii/html`'s `resolvePoints`. */
|
|
34
|
+
function resolvePoints(data, dataStatus, rawValues) {
|
|
35
|
+
if (dataStatus !== 'missing' &&
|
|
36
|
+
dataStatus !== 'error' &&
|
|
37
|
+
Array.isArray(data)) {
|
|
38
|
+
const points = [];
|
|
39
|
+
for (const entry of data) {
|
|
40
|
+
const point = coercePoint(entry);
|
|
41
|
+
if (point !== undefined)
|
|
42
|
+
points.push(point);
|
|
43
|
+
if (points.length >= MAX_POINTS)
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
return points;
|
|
47
|
+
}
|
|
48
|
+
if (rawValues) {
|
|
49
|
+
const points = [];
|
|
50
|
+
for (const token of rawValues.split(',')) {
|
|
51
|
+
const point = coercePoint(token.trim());
|
|
52
|
+
if (point !== undefined)
|
|
53
|
+
points.push(point);
|
|
54
|
+
if (points.length >= MAX_POINTS)
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
return points;
|
|
58
|
+
}
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
/** `▁▂▃▄▅▆▇█`, the eight-step block-height ramp a line chart's sparkline row is built from. */
|
|
62
|
+
const SPARKLINE_CHARS = '▁▂▃▄▅▆▇█';
|
|
63
|
+
function formatPoint(value) {
|
|
64
|
+
return Number.isFinite(value)
|
|
65
|
+
? String(Math.round(value * 100) / 100)
|
|
66
|
+
: String(value);
|
|
67
|
+
}
|
|
68
|
+
function sparkline(points) {
|
|
69
|
+
const min = Math.min(...points);
|
|
70
|
+
const max = Math.max(...points);
|
|
71
|
+
const range = max - min;
|
|
72
|
+
return points
|
|
73
|
+
.map((point) => {
|
|
74
|
+
const normalized = range > 0 ? (point - min) / range : 0.5;
|
|
75
|
+
const index = Math.min(SPARKLINE_CHARS.length - 1, Math.max(0, Math.round(normalized * (SPARKLINE_CHARS.length - 1))));
|
|
76
|
+
return SPARKLINE_CHARS[index];
|
|
77
|
+
})
|
|
78
|
+
.join('');
|
|
79
|
+
}
|
|
80
|
+
/** `██████` bar length for `point`, scaled against the series' largest magnitude and capped at `budget` columns. */
|
|
81
|
+
function barLength(point, maxMagnitude, budget) {
|
|
82
|
+
if (maxMagnitude <= 0)
|
|
83
|
+
return 0;
|
|
84
|
+
return Math.max(0, Math.min(budget, Math.round((Math.abs(point) / maxMagnitude) * budget)));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* `::chart{kind=line|bar values="1,3,2,5"}` — a dependency-free, hand-rolled
|
|
88
|
+
* chart. Data binding (§8) mirrors `@markii/html`'s `Chart`: a bound `data`
|
|
89
|
+
* array of numbers (or `{value}` objects) takes priority over the static
|
|
90
|
+
* `values=` attribute; non-numeric/non-finite entries are dropped and the
|
|
91
|
+
* point count capped at `MAX_POINTS`. An empty or all-invalid series renders
|
|
92
|
+
* a small neutral "no data" line rather than a broken chart.
|
|
93
|
+
*
|
|
94
|
+
* Terminal form: a `line` chart is a single sparkline row (`▁▂▃▄▅▆▇█`),
|
|
95
|
+
* flanked by its minimum and maximum as plain numbers. A `bar` chart is one
|
|
96
|
+
* horizontal `█` bar per point, its own value as the row's label, labels
|
|
97
|
+
* right-aligned to the widest one so every bar starts at the same column.
|
|
98
|
+
*/
|
|
99
|
+
export const Chart = (attributes, _children, ctx) => {
|
|
100
|
+
const { data, dataStatus, dataFailureKind } = ctx;
|
|
101
|
+
const rawKind = attributes.kind ?? DEFAULT_KIND;
|
|
102
|
+
const kind = isChartKind(rawKind) ? rawKind : DEFAULT_KIND;
|
|
103
|
+
const bound = safeRead(() => resolvePoints(data, dataStatus, attributes.values), () => resolvePoints(undefined, 'missing', attributes.values));
|
|
104
|
+
const points = bound.fields;
|
|
105
|
+
const suffix = dataStateSuffix(dataStatus, dataFailureKind);
|
|
106
|
+
const token = failureToken(dataFailureKind);
|
|
107
|
+
const styledSuffix = suffix
|
|
108
|
+
? token
|
|
109
|
+
? ctx.style(suffix, token)
|
|
110
|
+
: ctx.dim(suffix)
|
|
111
|
+
: '';
|
|
112
|
+
if (points.length === 0) {
|
|
113
|
+
return `${ctx.dim('no data')}${styledSuffix}`;
|
|
114
|
+
}
|
|
115
|
+
if (kind === 'line') {
|
|
116
|
+
const min = ctx.dim(formatPoint(Math.min(...points)));
|
|
117
|
+
const max = ctx.dim(formatPoint(Math.max(...points)));
|
|
118
|
+
return `${min} ${sparkline(points)} ${max}${styledSuffix}`;
|
|
119
|
+
}
|
|
120
|
+
const labels = points.map((point) => formatPoint(point));
|
|
121
|
+
const labelWidth = Math.max(...labels.map((label) => measure(label)));
|
|
122
|
+
const budget = Math.max(1, ctx.width - labelWidth - 1);
|
|
123
|
+
const maxMagnitude = Math.max(...points.map((point) => Math.abs(point)));
|
|
124
|
+
const lines = points.map((point, index) => {
|
|
125
|
+
const bar = '█'.repeat(barLength(point, maxMagnitude, budget));
|
|
126
|
+
return `${pad(labels[index], labelWidth, 'right')} ${bar}`;
|
|
127
|
+
});
|
|
128
|
+
return `${lines.join('\n')}${styledSuffix}`;
|
|
129
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AnsiComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `:::details{title="..." open} ... :::` — a collapsible disclosure. A
|
|
4
|
+
* terminal has no collapse/expand affordance (see `render.ts`'s top comment
|
|
5
|
+
* on interactive components), so this ALWAYS shows the body: the bare
|
|
6
|
+
* `open` attribute instead only changes the marker glyph (`▾` for `open`,
|
|
7
|
+
* `▸` for folded-by-default), a quiet hint at the note's own authored
|
|
8
|
+
* default rather than a control that does anything here. The marker line
|
|
9
|
+
* also names the section a "collapsible section" so a reader understands
|
|
10
|
+
* why they are seeing an indented block with no directive name attached.
|
|
11
|
+
*/
|
|
12
|
+
export declare const Details: AnsiComponent;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { indentBlock } from '../box.js';
|
|
2
|
+
const DEFAULT_TITLE = 'Details';
|
|
3
|
+
/** Columns the indent prefix (" ") consumes from the enclosing width. */
|
|
4
|
+
const INDENT_WIDTH = 2;
|
|
5
|
+
/**
|
|
6
|
+
* `:::details{title="..." open} ... :::` — a collapsible disclosure. A
|
|
7
|
+
* terminal has no collapse/expand affordance (see `render.ts`'s top comment
|
|
8
|
+
* on interactive components), so this ALWAYS shows the body: the bare
|
|
9
|
+
* `open` attribute instead only changes the marker glyph (`▾` for `open`,
|
|
10
|
+
* `▸` for folded-by-default), a quiet hint at the note's own authored
|
|
11
|
+
* default rather than a control that does anything here. The marker line
|
|
12
|
+
* also names the section a "collapsible section" so a reader understands
|
|
13
|
+
* why they are seeing an indented block with no directive name attached.
|
|
14
|
+
*/
|
|
15
|
+
export const Details = (attributes, children, ctx) => {
|
|
16
|
+
const title = attributes.title ?? DEFAULT_TITLE;
|
|
17
|
+
const open = Object.hasOwn(attributes, 'open');
|
|
18
|
+
const glyph = open ? '▾' : '▸';
|
|
19
|
+
const marker = ctx.dim(`${glyph} ${ctx.bold(ctx.text(title))} (collapsible section, shown expanded)`);
|
|
20
|
+
const innerWidth = Math.max(1, ctx.width - INDENT_WIDTH);
|
|
21
|
+
const childrenText = children({ width: innerWidth });
|
|
22
|
+
if (!childrenText)
|
|
23
|
+
return marker;
|
|
24
|
+
return `${marker}\n${indentBlock(childrenText, ' ')}`;
|
|
25
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { measure } from '../measure.js';
|
|
2
|
+
import { selfLayoutAlign, selfLayoutWidth } from '../layout.js';
|
|
3
|
+
const DIVIDER_VARIANTS = [
|
|
4
|
+
'line',
|
|
5
|
+
'dots',
|
|
6
|
+
'ornament',
|
|
7
|
+
];
|
|
8
|
+
function isDividerVariant(value) {
|
|
9
|
+
return DIVIDER_VARIANTS.includes(value);
|
|
10
|
+
}
|
|
11
|
+
const LABEL_ALIGNS = ['left', 'center', 'right'];
|
|
12
|
+
function isDividerLabelAlign(value) {
|
|
13
|
+
return LABEL_ALIGNS.includes(value);
|
|
14
|
+
}
|
|
15
|
+
const ORNAMENT_GLYPH = '❖';
|
|
16
|
+
/** The rule character each non-ornament variant draws with. */
|
|
17
|
+
const RULE_CHAR = { line: '─', dots: '·' };
|
|
18
|
+
/** How much of a labeled rule's rule fill sits on the SHORT side when the label is pushed left/right (the rest fills the long side). */
|
|
19
|
+
const SHORT_SIDE_RULE = 2;
|
|
20
|
+
/** Builds a full-`width` rule with `label` woven in at `align`, e.g. `── Part 2 ─────────`. */
|
|
21
|
+
function labeledRule(ruleChar, label, width, align) {
|
|
22
|
+
const labelText = ` ${label} `;
|
|
23
|
+
const remaining = Math.max(0, width - measure(labelText));
|
|
24
|
+
let left;
|
|
25
|
+
if (align === 'left')
|
|
26
|
+
left = Math.min(SHORT_SIDE_RULE, remaining);
|
|
27
|
+
else if (align === 'right')
|
|
28
|
+
left = remaining - Math.min(SHORT_SIDE_RULE, remaining);
|
|
29
|
+
else
|
|
30
|
+
left = Math.floor(remaining / 2);
|
|
31
|
+
const right = remaining - left;
|
|
32
|
+
return `${ruleChar.repeat(left)}${labelText}${ruleChar.repeat(right)}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* `::divider` / `::divider{label="..." variant="line|dots|ornament"
|
|
36
|
+
* label-align="left|center|right"}` — a leaf directive drawing a section
|
|
37
|
+
* break. Unknown/missing `variant` falls back to `line`; unknown/missing
|
|
38
|
+
* `label-align` falls back to `center`. Terminal form: a full-width rule
|
|
39
|
+
* (`─` for `line`, `·` for `dots`) with the label woven into it at
|
|
40
|
+
* `label-align`, or, for `ornament`, no hairline at all — just the `❖` glyph
|
|
41
|
+
* (doubled around the label, when there is one) placed at `label-align`
|
|
42
|
+
* within the width. Dimmed, matching the plain thematic-break rendering.
|
|
43
|
+
*
|
|
44
|
+
* Registered `selfLayout` (`registry.ts`'s `AnsiRegistryEntry.selfLayout`):
|
|
45
|
+
* a full-width rule is ONE long run with no spaces in it, so a generic
|
|
46
|
+
* post-render `applyLayout` narrowing would treat the whole rule as a
|
|
47
|
+
* single over-long "word" and hard-break it into several shorter lines
|
|
48
|
+
* (`../box.ts`'s `wrap`'s `breakLongWord`) instead of drawing one shorter
|
|
49
|
+
* rule — this component instead reads `ctx.layout` and draws its own rule
|
|
50
|
+
* at the resolved width from the start, exactly like `card`/`callout`.
|
|
51
|
+
*/
|
|
52
|
+
const FIT_DEFAULT_WIDTH = 10;
|
|
53
|
+
export const Divider = (attributes, _children, ctx) => {
|
|
54
|
+
const rawVariant = attributes.variant ?? 'line';
|
|
55
|
+
const variant = isDividerVariant(rawVariant)
|
|
56
|
+
? rawVariant
|
|
57
|
+
: 'line';
|
|
58
|
+
const rawLabel = attributes.label ?? null;
|
|
59
|
+
const label = rawLabel ? ctx.text(rawLabel) : null;
|
|
60
|
+
const rawLabelAlign = attributes['label-align'];
|
|
61
|
+
const labelAlign = rawLabelAlign && isDividerLabelAlign(rawLabelAlign)
|
|
62
|
+
? rawLabelAlign
|
|
63
|
+
: 'center';
|
|
64
|
+
const naturalWidth = label ? measure(label) + 4 : FIT_DEFAULT_WIDTH;
|
|
65
|
+
const width = selfLayoutWidth(ctx.layout, ctx.width, naturalWidth);
|
|
66
|
+
let line;
|
|
67
|
+
if (variant === 'ornament') {
|
|
68
|
+
const inner = label
|
|
69
|
+
? `${ORNAMENT_GLYPH} ${label} ${ORNAMENT_GLYPH}`
|
|
70
|
+
: ORNAMENT_GLYPH;
|
|
71
|
+
line = ctx.pad(inner, width, labelAlign);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const ruleChar = RULE_CHAR[variant];
|
|
75
|
+
line = label
|
|
76
|
+
? labeledRule(ruleChar, label, width, labelAlign)
|
|
77
|
+
: ctx.rule(width, ruleChar);
|
|
78
|
+
}
|
|
79
|
+
return selfLayoutAlign(ctx.dim(line), ctx.layout, ctx.width);
|
|
80
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AnsiComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `:::figure{src="..." alt="..."} caption markdown :::` — an image with a
|
|
4
|
+
* rich (markdown) caption. `src` is required; a missing `src` shows only the
|
|
5
|
+
* caption, matching the graceful-degradation spirit of the unknown-directive
|
|
6
|
+
* fallback rather than throwing.
|
|
7
|
+
*
|
|
8
|
+
* Security: `src` bypasses `@markii/core`'s hast-level URL sanitizer (it is
|
|
9
|
+
* a directive attribute, not a markdown image), so this component closes
|
|
10
|
+
* that gap itself with `@markii/core`'s `isSafeUrl` — the same allowlist the
|
|
11
|
+
* sanitizer uses — and drops the image line entirely on a refusal, replacing
|
|
12
|
+
* it with a short labeled marker (a terminal has no tooltip to carry the
|
|
13
|
+
* full sentence out of the text flow) plus the full sentence reported to
|
|
14
|
+
* `onDiagnostic`; matches `@markii/html`'s `Figure` in behavior, not in the
|
|
15
|
+
* inline wording.
|
|
16
|
+
*
|
|
17
|
+
* Terminal form: alt text, then the (possibly host-resolved) src, then the
|
|
18
|
+
* caption, each on its own line, dimmed except the caption itself.
|
|
19
|
+
*/
|
|
20
|
+
export declare const Figure: AnsiComponent;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { isSafeUrl } from '@markii/core';
|
|
2
|
+
import { reportDiagnostic } from '@markii/stdlib';
|
|
3
|
+
import { resolveImageAttribute } from '../image-resolve.js';
|
|
4
|
+
import { unsafeImageSrcLabel, unsafeImageSrcTitle, } from '../failure-presentation.js';
|
|
5
|
+
const DEFAULT_ALT = '';
|
|
6
|
+
const DIRECTIVE_NAME = 'figure';
|
|
7
|
+
/**
|
|
8
|
+
* `:::figure{src="..." alt="..."} caption markdown :::` — an image with a
|
|
9
|
+
* rich (markdown) caption. `src` is required; a missing `src` shows only the
|
|
10
|
+
* caption, matching the graceful-degradation spirit of the unknown-directive
|
|
11
|
+
* fallback rather than throwing.
|
|
12
|
+
*
|
|
13
|
+
* Security: `src` bypasses `@markii/core`'s hast-level URL sanitizer (it is
|
|
14
|
+
* a directive attribute, not a markdown image), so this component closes
|
|
15
|
+
* that gap itself with `@markii/core`'s `isSafeUrl` — the same allowlist the
|
|
16
|
+
* sanitizer uses — and drops the image line entirely on a refusal, replacing
|
|
17
|
+
* it with a short labeled marker (a terminal has no tooltip to carry the
|
|
18
|
+
* full sentence out of the text flow) plus the full sentence reported to
|
|
19
|
+
* `onDiagnostic`; matches `@markii/html`'s `Figure` in behavior, not in the
|
|
20
|
+
* inline wording.
|
|
21
|
+
*
|
|
22
|
+
* Terminal form: alt text, then the (possibly host-resolved) src, then the
|
|
23
|
+
* caption, each on its own line, dimmed except the caption itself.
|
|
24
|
+
*/
|
|
25
|
+
export const Figure = (attributes, children, ctx) => {
|
|
26
|
+
const rawSrc = attributes.src ?? null;
|
|
27
|
+
const alt = attributes.alt ?? DEFAULT_ALT;
|
|
28
|
+
const refused = Boolean(rawSrc) && !isSafeUrl(rawSrc);
|
|
29
|
+
const safeSrc = rawSrc && !refused ? rawSrc : null;
|
|
30
|
+
const src = safeSrc
|
|
31
|
+
? ctx.text(resolveImageAttribute(safeSrc, ctx.resolveImageSrc))
|
|
32
|
+
: null;
|
|
33
|
+
const lines = [];
|
|
34
|
+
if (rawSrc) {
|
|
35
|
+
if (alt)
|
|
36
|
+
lines.push(ctx.dim(`alt: ${ctx.text(alt)}`));
|
|
37
|
+
if (refused) {
|
|
38
|
+
reportDiagnostic(ctx.onDiagnostic, {
|
|
39
|
+
kind: 'unsafe-image-src',
|
|
40
|
+
directive: DIRECTIVE_NAME,
|
|
41
|
+
message: unsafeImageSrcTitle(DIRECTIVE_NAME),
|
|
42
|
+
});
|
|
43
|
+
lines.push(ctx.dim(`[${unsafeImageSrcLabel(DIRECTIVE_NAME)}]`));
|
|
44
|
+
}
|
|
45
|
+
else if (src) {
|
|
46
|
+
lines.push(ctx.dim(src));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const childrenText = children();
|
|
50
|
+
if (childrenText)
|
|
51
|
+
lines.push(childrenText);
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AnsiRegistry } from '../registry.js';
|
|
2
|
+
export { Badge } from './badge.js';
|
|
3
|
+
export type { BadgeVariant } from './badge.js';
|
|
4
|
+
export { Callout } from './callout.js';
|
|
5
|
+
export type { CalloutType } from './callout.js';
|
|
6
|
+
export { Card } from './card.js';
|
|
7
|
+
export { Cell } from './cell.js';
|
|
8
|
+
export { Chart } from './chart.js';
|
|
9
|
+
export { Details } from './details.js';
|
|
10
|
+
export { Divider } from './divider.js';
|
|
11
|
+
export type { DividerVariant } from './divider.js';
|
|
12
|
+
export { Figure } from './figure.js';
|
|
13
|
+
export { Kbd } from './kbd.js';
|
|
14
|
+
export { createLayoutWrapper, layoutWrapperPresetAxis, LAYOUT_WRAPPER_PRESETS, } from './layout-wrapper.js';
|
|
15
|
+
export type { LayoutWrapperPreset } from './layout-wrapper.js';
|
|
16
|
+
export { Progress } from './progress.js';
|
|
17
|
+
export { Rating } from './rating.js';
|
|
18
|
+
export { Row, ROW_COLUMN_THRESHOLD } from './row.js';
|
|
19
|
+
export { Stat } from './stat.js';
|
|
20
|
+
export { Tab, DEFAULT_TAB_LABEL } from './tab.js';
|
|
21
|
+
export { Tabs } from './tabs.js';
|
|
22
|
+
export { Table } from './table.js';
|
|
23
|
+
export { drawTableGrid, measureTableGridWidth, MIN_COLUMN_WIDTH, } from './table-grid.js';
|
|
24
|
+
/**
|
|
25
|
+
* The built-in standard components, pre-registered under their names —
|
|
26
|
+
* matching `@markii/html`'s `defaultHtmlRegistry` and `@markii/react`'s
|
|
27
|
+
* `defaultRegistry` in shape and coverage: the same 23 names. `card`,
|
|
28
|
+
* `callout`, `divider`, and `table` are marked `selfLayout` (`registry.ts`'s
|
|
29
|
+
* `AnsiRegistryEntry.selfLayout`) because they draw a single long
|
|
30
|
+
* frame/bar/rule/grid a generic post-render `applyLayout` narrow/pad would
|
|
31
|
+
* corrupt or hard-break mid-glyph; see `card.ts`'s and `divider.ts`'s doc
|
|
32
|
+
* comments.
|
|
33
|
+
*/
|
|
34
|
+
export declare const defaultAnsiRegistry: AnsiRegistry;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { getContract } from '@markii/stdlib';
|
|
2
|
+
import { createAnsiRegistry } from '../registry.js';
|
|
3
|
+
import { Badge } from './badge.js';
|
|
4
|
+
import { Callout } from './callout.js';
|
|
5
|
+
import { Card } from './card.js';
|
|
6
|
+
import { Cell } from './cell.js';
|
|
7
|
+
import { Chart } from './chart.js';
|
|
8
|
+
import { Details } from './details.js';
|
|
9
|
+
import { Divider } from './divider.js';
|
|
10
|
+
import { Figure } from './figure.js';
|
|
11
|
+
import { Kbd } from './kbd.js';
|
|
12
|
+
import { createLayoutWrapper, layoutWrapperPresetAxis, } from './layout-wrapper.js';
|
|
13
|
+
import { Progress } from './progress.js';
|
|
14
|
+
import { Rating } from './rating.js';
|
|
15
|
+
import { Row } from './row.js';
|
|
16
|
+
import { Stat } from './stat.js';
|
|
17
|
+
import { Tab } from './tab.js';
|
|
18
|
+
import { Tabs } from './tabs.js';
|
|
19
|
+
import { Table } from './table.js';
|
|
20
|
+
export { Badge } from './badge.js';
|
|
21
|
+
export { Callout } from './callout.js';
|
|
22
|
+
export { Card } from './card.js';
|
|
23
|
+
export { Cell } from './cell.js';
|
|
24
|
+
export { Chart } from './chart.js';
|
|
25
|
+
export { Details } from './details.js';
|
|
26
|
+
export { Divider } from './divider.js';
|
|
27
|
+
export { Figure } from './figure.js';
|
|
28
|
+
export { Kbd } from './kbd.js';
|
|
29
|
+
export { createLayoutWrapper, layoutWrapperPresetAxis, LAYOUT_WRAPPER_PRESETS, } from './layout-wrapper.js';
|
|
30
|
+
export { Progress } from './progress.js';
|
|
31
|
+
export { Rating } from './rating.js';
|
|
32
|
+
export { Row, ROW_COLUMN_THRESHOLD } from './row.js';
|
|
33
|
+
export { Stat } from './stat.js';
|
|
34
|
+
export { Tab, DEFAULT_TAB_LABEL } from './tab.js';
|
|
35
|
+
export { Tabs } from './tabs.js';
|
|
36
|
+
export { Table } from './table.js';
|
|
37
|
+
export { drawTableGrid, measureTableGridWidth, MIN_COLUMN_WIDTH, } from './table-grid.js';
|
|
38
|
+
/**
|
|
39
|
+
* Derives a registry entry's `inline` flag from `@markii/stdlib`'s standard
|
|
40
|
+
* component contract for `name`, matching `@markii/html`'s
|
|
41
|
+
* `inlineFromContract`: `kind: 'inline'` -> `inline: true`, otherwise
|
|
42
|
+
* `false`. Falls back to `false` if `name` has no standard contract.
|
|
43
|
+
*/
|
|
44
|
+
function inlineFromContract(name) {
|
|
45
|
+
return getContract(name)?.kind === 'inline';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* One layout-wrapper registration: the shared wrapper component bound to
|
|
49
|
+
* `preset`, plus the `layout` axis that preset sets by its own name.
|
|
50
|
+
* Matches `@markii/html`'s `layoutWrapperEntry`.
|
|
51
|
+
*/
|
|
52
|
+
function layoutWrapperEntry(preset) {
|
|
53
|
+
return {
|
|
54
|
+
component: createLayoutWrapper(preset),
|
|
55
|
+
inline: inlineFromContract(preset),
|
|
56
|
+
layout: layoutWrapperPresetAxis(preset),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The built-in standard components, pre-registered under their names —
|
|
61
|
+
* matching `@markii/html`'s `defaultHtmlRegistry` and `@markii/react`'s
|
|
62
|
+
* `defaultRegistry` in shape and coverage: the same 23 names. `card`,
|
|
63
|
+
* `callout`, `divider`, and `table` are marked `selfLayout` (`registry.ts`'s
|
|
64
|
+
* `AnsiRegistryEntry.selfLayout`) because they draw a single long
|
|
65
|
+
* frame/bar/rule/grid a generic post-render `applyLayout` narrow/pad would
|
|
66
|
+
* corrupt or hard-break mid-glyph; see `card.ts`'s and `divider.ts`'s doc
|
|
67
|
+
* comments.
|
|
68
|
+
*/
|
|
69
|
+
export const defaultAnsiRegistry = createAnsiRegistry({
|
|
70
|
+
callout: {
|
|
71
|
+
component: Callout,
|
|
72
|
+
inline: inlineFromContract('callout'),
|
|
73
|
+
selfLayout: true,
|
|
74
|
+
},
|
|
75
|
+
kbd: { component: Kbd, inline: inlineFromContract('kbd') },
|
|
76
|
+
rating: { component: Rating, inline: inlineFromContract('rating') },
|
|
77
|
+
divider: {
|
|
78
|
+
component: Divider,
|
|
79
|
+
inline: inlineFromContract('divider'),
|
|
80
|
+
selfLayout: true,
|
|
81
|
+
},
|
|
82
|
+
details: { component: Details, inline: inlineFromContract('details') },
|
|
83
|
+
card: {
|
|
84
|
+
component: Card,
|
|
85
|
+
inline: inlineFromContract('card'),
|
|
86
|
+
selfLayout: true,
|
|
87
|
+
},
|
|
88
|
+
badge: { component: Badge, inline: inlineFromContract('badge') },
|
|
89
|
+
figure: { component: Figure, inline: inlineFromContract('figure') },
|
|
90
|
+
tabs: { component: Tabs, inline: inlineFromContract('tabs') },
|
|
91
|
+
tab: { component: Tab, inline: inlineFromContract('tab') },
|
|
92
|
+
row: { component: Row, inline: inlineFromContract('row') },
|
|
93
|
+
cell: { component: Cell, inline: inlineFromContract('cell') },
|
|
94
|
+
table: {
|
|
95
|
+
component: Table,
|
|
96
|
+
inline: inlineFromContract('table'),
|
|
97
|
+
selfLayout: true,
|
|
98
|
+
},
|
|
99
|
+
stat: { component: Stat, inline: inlineFromContract('stat') },
|
|
100
|
+
progress: { component: Progress, inline: inlineFromContract('progress') },
|
|
101
|
+
chart: { component: Chart, inline: inlineFromContract('chart') },
|
|
102
|
+
center: layoutWrapperEntry('center'),
|
|
103
|
+
left: layoutWrapperEntry('left'),
|
|
104
|
+
right: layoutWrapperEntry('right'),
|
|
105
|
+
wide: layoutWrapperEntry('wide'),
|
|
106
|
+
narrow: layoutWrapperEntry('narrow'),
|
|
107
|
+
full: layoutWrapperEntry('full'),
|
|
108
|
+
fit: layoutWrapperEntry('fit'),
|
|
109
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AnsiComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `:kbd[Ctrl+S]` — a styled keycap for an inline text directive. Terminal
|
|
4
|
+
* form: bracketed bold text, e.g. `[Ctrl+S]`. Takes no attributes; its inner
|
|
5
|
+
* content (already rendered plain text) is the key label.
|
|
6
|
+
*/
|
|
7
|
+
export declare const Kbd: AnsiComponent;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `:kbd[Ctrl+S]` — a styled keycap for an inline text directive. Terminal
|
|
3
|
+
* form: bracketed bold text, e.g. `[Ctrl+S]`. Takes no attributes; its inner
|
|
4
|
+
* content (already rendered plain text) is the key label.
|
|
5
|
+
*/
|
|
6
|
+
export const Kbd = (_attributes, children, ctx) => {
|
|
7
|
+
return `[${ctx.bold(children())}]`;
|
|
8
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { LayoutAxis } from '@markii/stdlib';
|
|
2
|
+
import type { AnsiComponent } from '../registry.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 declare const LAYOUT_WRAPPER_PRESETS: readonly ["center", "left", "right", "wide", "narrow", "full", "fit"];
|
|
14
|
+
export type LayoutWrapperPreset = (typeof LAYOUT_WRAPPER_PRESETS)[number];
|
|
15
|
+
/**
|
|
16
|
+
* Creates the registry component for one of docs/format.md's layout-wrapper
|
|
17
|
+
* container names. One shared implementation, bound to `preset` at
|
|
18
|
+
* registration time, matching `@markii/html`'s `createLayoutWrapper` in
|
|
19
|
+
* spirit: it never reads `attributes` at all — `render.ts` already stripped
|
|
20
|
+
* both reserved keys before this ever runs.
|
|
21
|
+
*
|
|
22
|
+
* A wrapper sets ONE axis by its own NAME (docs/spec.md §3): `preset` itself
|
|
23
|
+
* IS that axis's value (`center` sets `align: 'center'`; `narrow` sets
|
|
24
|
+
* `width: 'narrow'`), so it is applied unconditionally, whatever the author
|
|
25
|
+
* wrote for that axis's own reserved attribute (already discarded — the
|
|
26
|
+
* name always wins). The OTHER axis, when the author supplied it, arrives
|
|
27
|
+
* as `ctx.layout` (`render.ts` resolved it on this wrapper's behalf, since
|
|
28
|
+
* this wrapper is the directive's registered `layout` scope). Both are
|
|
29
|
+
* merged into one `ResolvedLayoutPresets` and applied together via
|
|
30
|
+
* `../layout.js`'s `applyLayout`, so `:::center{width=fit}` narrows AND
|
|
31
|
+
* centers in one pass.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createLayoutWrapper(preset: LayoutWrapperPreset): AnsiComponent;
|
|
34
|
+
/** The layout axis `preset` sets by its own name. Mirrors `@markii/html`'s `layoutWrapperPresetAxis`. */
|
|
35
|
+
export declare function layoutWrapperPresetAxis(preset: LayoutWrapperPreset): LayoutAxis;
|