@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,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,4 @@
1
+ import type { AnsiComponent } from '../registry.js';
2
+ export type DividerVariant = 'line' | 'dots' | 'ornament';
3
+ export type DividerLabelAlign = 'left' | 'center' | 'right';
4
+ export declare const Divider: AnsiComponent;
@@ -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;