@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,101 @@
1
+ import { deriveTableShape, formatValue, isNumericLike } from '@markii/stdlib';
2
+ import { selfLayoutAlign, selfLayoutWidth } from '../layout.js';
3
+ import { safeRead } from '../resolve.js';
4
+ import { dataStateSuffix, failureToken } from '../failure-presentation.js';
5
+ import { drawTableGrid, measureTableGridWidth } from './table-grid.js';
6
+ function parseColumns(raw) {
7
+ if (!raw)
8
+ return undefined;
9
+ const list = raw
10
+ .split(',')
11
+ .map((entry) => entry.trim())
12
+ .filter((entry) => entry.length > 0);
13
+ return list.length > 0 ? list : undefined;
14
+ }
15
+ function parseLimit(raw) {
16
+ if (!raw)
17
+ return undefined;
18
+ const trimmed = raw.trim();
19
+ if (!/^\d+$/.test(trimmed))
20
+ return undefined;
21
+ const parsed = Number(trimmed);
22
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
23
+ }
24
+ function limitRows(rows, limit) {
25
+ return limit === undefined ? rows : rows.slice(0, limit);
26
+ }
27
+ /** Renders one cell's display text: `format`/`decimals` apply only when the cell's raw value is itself numeric-like, matching `@markii/html`'s `Table` exactly. */
28
+ function renderCell(value, format, decimals) {
29
+ if (format && isNumericLike(value))
30
+ return formatValue(value, format, decimals);
31
+ return formatValue(value);
32
+ }
33
+ const TEXT_ALIGNS = ['left', 'center', 'right'];
34
+ function isTextAlign(value) {
35
+ return TEXT_ALIGNS.includes(value);
36
+ }
37
+ /**
38
+ * `::table{data=users columns="name,role" limit=10}` — a data-bound table.
39
+ * Data binding (§8) mirrors `@markii/html`'s `Table`: `@markii/stdlib`'s
40
+ * `deriveTableShape` decides the layout from the bound value's own shape.
41
+ * `format`/`decimals` apply to numeric cells only; `text` aligns every
42
+ * cell's content within its column; `caption`, when given, is a bold line
43
+ * above the grid. A missing/stale/failed binding degrades to the same quiet
44
+ * "no data" line `chart`/`progress` use, with the failure suffix appended.
45
+ *
46
+ * Registered `selfLayout` (see `card.ts`'s doc comment): draws real
47
+ * box-drawing glyphs (`./table-grid.ts`), so it sizes its own grid off
48
+ * `ctx.layout` instead of letting a generic post-render narrow/pad corrupt
49
+ * the borders.
50
+ */
51
+ export const Table = (attributes, _children, ctx) => {
52
+ const { data, dataStatus, dataFailureKind } = ctx;
53
+ const columnsOverride = parseColumns(attributes.columns);
54
+ const limit = parseLimit(attributes.limit);
55
+ const format = attributes.format ?? undefined;
56
+ const decimals = attributes.decimals ?? undefined;
57
+ const caption = attributes.caption ?? null;
58
+ const rawTextAlign = attributes.text;
59
+ const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
60
+ const bound = safeRead(() => dataStatus === 'missing' || dataStatus === 'error'
61
+ ? { kind: 'empty' }
62
+ : deriveTableShape(data, columnsOverride), () => ({ kind: 'empty' }));
63
+ const shape = bound.fields;
64
+ const suffix = dataStateSuffix(dataStatus, dataFailureKind);
65
+ const token = failureToken(dataFailureKind);
66
+ const styledSuffix = suffix
67
+ ? token
68
+ ? ctx.style(suffix, token)
69
+ : ctx.dim(suffix)
70
+ : '';
71
+ const captionLine = caption ? ctx.bold(ctx.text(caption)) : undefined;
72
+ if (shape.kind === 'empty') {
73
+ const emptyLine = `${ctx.dim('no data')}${styledSuffix}`;
74
+ return captionLine ? `${captionLine}\n${emptyLine}` : emptyLine;
75
+ }
76
+ const cell = (value) => ctx.text(renderCell(value, format, decimals));
77
+ let header;
78
+ let rows;
79
+ if (shape.kind === 'objects') {
80
+ header = shape.columns.map((column) => ctx.text(column));
81
+ rows = limitRows(shape.rows, limit).map((cells) => cells.map(cell));
82
+ }
83
+ else if (shape.kind === 'arrays') {
84
+ rows = limitRows(shape.rows, limit).map((cells) => cells.map(cell));
85
+ }
86
+ else if (shape.kind === 'primitives') {
87
+ rows = limitRows(shape.rows, limit).map((cells) => [cell(cells[0])]);
88
+ }
89
+ else {
90
+ rows = limitRows(shape.rows, limit).map(([key, value]) => [
91
+ ctx.text(key),
92
+ cell(value),
93
+ ]);
94
+ }
95
+ const naturalWidth = measureTableGridWidth(header, rows);
96
+ const boxWidth = selfLayoutWidth(ctx.layout, ctx.width, naturalWidth);
97
+ const grid = drawTableGrid(header, rows, boxWidth, (text) => ctx.bold(text), align);
98
+ const body = `${grid}${styledSuffix}`;
99
+ const withCaption = captionLine ? `${captionLine}\n${body}` : body;
100
+ return selfLayoutAlign(withCaption, ctx.layout, ctx.width);
101
+ };
@@ -0,0 +1,18 @@
1
+ import type { AnsiComponent } from '../registry.js';
2
+ /**
3
+ * `::::tabs :::tab{label="..."} ... ::: :::tab{label="..."} ... ::: ::::` —
4
+ * a tabbed panel switcher. A terminal has no click-driven tab switching (see
5
+ * `render.ts`'s top comment on interactive components), so every panel is
6
+ * shown, stacked, in document order — matching `@markii/html`'s `Tabs`
7
+ * faithfulness limitation of showing every panel rather than picking one.
8
+ *
9
+ * "The active tab marked": since every panel is always visible here, this
10
+ * engine treats the FIRST tab (document order) as the one a live host would
11
+ * show before any interaction, and marks only its heading line — every
12
+ * `tab` child's block is joined into `childrenText` with a blank line
13
+ * between them (`render.ts`'s `renderBlocks`), the same separator this
14
+ * engine uses between any two sibling blocks, so splitting on the first
15
+ * blank line reliably finds the boundary between the first tab's block and
16
+ * the rest without needing any information `tabs` does not have.
17
+ */
18
+ export declare const Tabs: AnsiComponent;
@@ -0,0 +1,30 @@
1
+ /** The visible marker appended to whichever tab is treated as "active" (see this module's doc comment for how that is decided). */
2
+ const ACTIVE_MARKER = ' (active)';
3
+ /**
4
+ * `::::tabs :::tab{label="..."} ... ::: :::tab{label="..."} ... ::: ::::` —
5
+ * a tabbed panel switcher. A terminal has no click-driven tab switching (see
6
+ * `render.ts`'s top comment on interactive components), so every panel is
7
+ * shown, stacked, in document order — matching `@markii/html`'s `Tabs`
8
+ * faithfulness limitation of showing every panel rather than picking one.
9
+ *
10
+ * "The active tab marked": since every panel is always visible here, this
11
+ * engine treats the FIRST tab (document order) as the one a live host would
12
+ * show before any interaction, and marks only its heading line — every
13
+ * `tab` child's block is joined into `childrenText` with a blank line
14
+ * between them (`render.ts`'s `renderBlocks`), the same separator this
15
+ * engine uses between any two sibling blocks, so splitting on the first
16
+ * blank line reliably finds the boundary between the first tab's block and
17
+ * the rest without needing any information `tabs` does not have.
18
+ */
19
+ export const Tabs = (_attributes, children) => {
20
+ const childrenText = children();
21
+ if (!childrenText.trim())
22
+ return '';
23
+ const blocks = childrenText.split('\n\n');
24
+ const [first, ...rest] = blocks;
25
+ if (first === undefined)
26
+ return childrenText;
27
+ const lines = first.split('\n');
28
+ lines[0] = `${lines[0] ?? ''}${ACTIVE_MARKER}`;
29
+ return [lines.join('\n'), ...rest].join('\n\n');
30
+ };
@@ -0,0 +1,58 @@
1
+ import type { FailureKind, ValueStatus } from './value-types.js';
2
+ import type { Tier1Token } from './theme.js';
3
+ /** The short phrase for `kind`, or `undefined` if absent or not one of the four taxonomy members. */
4
+ export declare function failurePhrase(kind: FailureKind | undefined): string | undefined;
5
+ /** The tooltip-equivalent text for a failed/missing binding: the short phrase for `kind`, with `error` appended when there is one. */
6
+ export declare function failureTitle(error: string | undefined, kind: FailureKind | undefined): string | undefined;
7
+ /**
8
+ * The theme token a failed binding's marker is colored with, this engine's
9
+ * terminal equivalent of `@markii/html`'s `failureKindClass`: `script-error`
10
+ * and `capability-denied` both read as `--mk-danger` (the note asked for
11
+ * something and could not get it, one way or another), `tier-blocked` reads
12
+ * as `--mk-warning` (a manual run would fix it, nothing is actually broken),
13
+ * `limit` reads as `--mk-limit`'s dedicated purple. `undefined` for an
14
+ * absent or out-of-taxonomy `kind`, in which case a caller applies no color
15
+ * at all rather than guessing one.
16
+ */
17
+ export declare function failureToken(kind: FailureKind | undefined): Tier1Token | undefined;
18
+ /**
19
+ * The quiet trailing marker text for a data-bound value's state: ` (stale)`
20
+ * for a stale value, ` (<phrase>)` for a failed one (built from the SAME
21
+ * `FAILURE_PHRASE` table `failureTitle` uses, so the two never say something
22
+ * different about the same failure), `''` for a fresh or plain-missing
23
+ * value (a plain miss already shows as the empty/placeholder marker
24
+ * `render.ts` builds; it needs no additional suffix). `status: 'error'`
25
+ * without a recognized `kind` still gets the generic wording via
26
+ * `failurePhrase`'s `undefined` fallback in the caller.
27
+ */
28
+ export declare function dataStateSuffix(status: ValueStatus | undefined, kind: FailureKind | undefined): string;
29
+ /**
30
+ * The marker text `render.ts` shows for an INLINE-registered component that
31
+ * receives no content (e.g. `::badge{label="x"}`, text put in an attribute
32
+ * instead of the directive body). Wording identical to `@markii/html`'s
33
+ * `emptyInlineTitle`.
34
+ */
35
+ export declare function emptyInlineTitle(name: string): string;
36
+ /** The marker text for a known attribute's value outside its closed enum. Wording identical to `@markii/html`'s `invalidAttributeValueTitle`. */
37
+ export declare function invalidAttributeValueTitle(directive: string, attribute: string, value: string): string;
38
+ /** The marker text for a component-built image whose `src` was refused as unsafe. Wording identical to `@markii/html`'s `unsafeImageSrcTitle`. */
39
+ export declare function unsafeImageSrcTitle(directive: string): string;
40
+ /**
41
+ * The INLINE marker text for a known attribute's value outside its closed
42
+ * enum, this engine's own departure from `@markii/html`'s identical wording:
43
+ * a terminal has no tooltip to carry `invalidAttributeValueTitle`'s full
44
+ * sentence out of the text flow, so printing that sentence inline would put
45
+ * the reason back in the page, the exact thing AGENTS.md's "clean is not
46
+ * silent" rule exists to prevent. This short label is what render.ts prints
47
+ * next to the component's own output; the full sentence still reaches a
48
+ * host's diagnostics surface via `onDiagnostic`.
49
+ */
50
+ export declare function invalidAttributeValueLabel(directive: string, attribute: string): string;
51
+ /**
52
+ * The INLINE marker text for a component-built image whose `src` was
53
+ * refused as unsafe, this engine's short counterpart to
54
+ * `unsafeImageSrcTitle` for the same reason `invalidAttributeValueLabel`
55
+ * exists: no tooltip channel, so the full sentence goes to `onDiagnostic`
56
+ * only.
57
+ */
58
+ export declare function unsafeImageSrcLabel(directive: string): string;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * This engine's port of `@markii/html`'s `failure-presentation.ts` — the ONE
3
+ * place UI wording for `@markii/runtime`'s failure taxonomy lives in this
4
+ * engine. Ported (not imported) for the same reason `./resolve.ts` is
5
+ * ported: each platform renderer independently implements the same
6
+ * presentation contract (docs/scripting.md), kept identical in wording so a
7
+ * failing name reads the same everywhere. `failure-presentation.drift.test.ts`
8
+ * is the executable proof: it reads `@markii/html`'s copy of this module as
9
+ * TEXT and asserts the phrases and title templates match byte for byte.
10
+ *
11
+ * The presentation contract (AGENTS.md's cleanliness principle) carries over
12
+ * unchanged: a failure never becomes body text. In a terminal it surfaces as
13
+ * a quiet trailing marker (`dataStateSuffix`) plus, where the caller wants
14
+ * it, a themed color (`failureToken`) — the terminal's equivalent of the
15
+ * HTML engine's `title` tooltip and modifier class, since neither exists in
16
+ * a terminal.
17
+ *
18
+ * Two notices (an out-of-enum attribute value, a refused image source) carry
19
+ * a full sentence in the browser engines' `title` tooltip, reached out of
20
+ * the text flow. A terminal has no tooltip, so printing that sentence inline
21
+ * would put the reason back in the rendered note. `invalidAttributeValueLabel`
22
+ * and `unsafeImageSrcLabel` are this engine's short inline labels for those
23
+ * two cases; the full sentence (`invalidAttributeValueTitle`/
24
+ * `unsafeImageSrcTitle`, unchanged) still reaches a host only through
25
+ * `onDiagnostic`.
26
+ */
27
+ /** Human-facing phrase per `FailureKind`. Identical wording to `@markii/html`'s `FAILURE_PHRASE`; kept in sync by `failure-presentation.drift.test.ts`. Null-prototype so an out-of-taxonomy `kind` can never resolve through the prototype chain. */
28
+ const FAILURE_PHRASE = Object.assign(Object.create(null), {
29
+ 'script-error': 'script error',
30
+ 'capability-denied': 'needs permission',
31
+ 'tier-blocked': 'requires manual run',
32
+ limit: 'limit exceeded',
33
+ });
34
+ /** The short phrase for `kind`, or `undefined` if absent or not one of the four taxonomy members. */
35
+ export function failurePhrase(kind) {
36
+ if (kind === undefined)
37
+ return undefined;
38
+ return Object.hasOwn(FAILURE_PHRASE, kind) ? FAILURE_PHRASE[kind] : undefined;
39
+ }
40
+ /** The tooltip-equivalent text for a failed/missing binding: the short phrase for `kind`, with `error` appended when there is one. */
41
+ export function failureTitle(error, kind) {
42
+ const phrase = failurePhrase(kind);
43
+ if (!phrase)
44
+ return error ? error : undefined;
45
+ return error ? `${phrase}: ${error}` : phrase;
46
+ }
47
+ /**
48
+ * The theme token a failed binding's marker is colored with, this engine's
49
+ * terminal equivalent of `@markii/html`'s `failureKindClass`: `script-error`
50
+ * and `capability-denied` both read as `--mk-danger` (the note asked for
51
+ * something and could not get it, one way or another), `tier-blocked` reads
52
+ * as `--mk-warning` (a manual run would fix it, nothing is actually broken),
53
+ * `limit` reads as `--mk-limit`'s dedicated purple. `undefined` for an
54
+ * absent or out-of-taxonomy `kind`, in which case a caller applies no color
55
+ * at all rather than guessing one.
56
+ */
57
+ export function failureToken(kind) {
58
+ if (kind === 'script-error' || kind === 'capability-denied')
59
+ return '--mk-danger';
60
+ if (kind === 'tier-blocked')
61
+ return '--mk-warning';
62
+ if (kind === 'limit')
63
+ return '--mk-limit';
64
+ return undefined;
65
+ }
66
+ /**
67
+ * The quiet trailing marker text for a data-bound value's state: ` (stale)`
68
+ * for a stale value, ` (<phrase>)` for a failed one (built from the SAME
69
+ * `FAILURE_PHRASE` table `failureTitle` uses, so the two never say something
70
+ * different about the same failure), `''` for a fresh or plain-missing
71
+ * value (a plain miss already shows as the empty/placeholder marker
72
+ * `render.ts` builds; it needs no additional suffix). `status: 'error'`
73
+ * without a recognized `kind` still gets the generic wording via
74
+ * `failurePhrase`'s `undefined` fallback in the caller.
75
+ */
76
+ export function dataStateSuffix(status, kind) {
77
+ if (status === 'error') {
78
+ const phrase = failurePhrase(kind) ?? 'error';
79
+ return ` (${phrase})`;
80
+ }
81
+ if (status === 'stale')
82
+ return ' (stale)';
83
+ return '';
84
+ }
85
+ /**
86
+ * The marker text `render.ts` shows for an INLINE-registered component that
87
+ * receives no content (e.g. `::badge{label="x"}`, text put in an attribute
88
+ * instead of the directive body). Wording identical to `@markii/html`'s
89
+ * `emptyInlineTitle`.
90
+ */
91
+ export function emptyInlineTitle(name) {
92
+ return `${name}: no content (an attribute may have been used where directive text was expected)`;
93
+ }
94
+ /** The marker text for a known attribute's value outside its closed enum. Wording identical to `@markii/html`'s `invalidAttributeValueTitle`. */
95
+ export function invalidAttributeValueTitle(directive, attribute, value) {
96
+ return `${directive}: "${value}" is not a valid ${attribute} value (ignored)`;
97
+ }
98
+ /** The marker text for a component-built image whose `src` was refused as unsafe. Wording identical to `@markii/html`'s `unsafeImageSrcTitle`. */
99
+ export function unsafeImageSrcTitle(directive) {
100
+ return `${directive}: image source was refused as unsafe and was not shown`;
101
+ }
102
+ /**
103
+ * The INLINE marker text for a known attribute's value outside its closed
104
+ * enum, this engine's own departure from `@markii/html`'s identical wording:
105
+ * a terminal has no tooltip to carry `invalidAttributeValueTitle`'s full
106
+ * sentence out of the text flow, so printing that sentence inline would put
107
+ * the reason back in the page, the exact thing AGENTS.md's "clean is not
108
+ * silent" rule exists to prevent. This short label is what render.ts prints
109
+ * next to the component's own output; the full sentence still reaches a
110
+ * host's diagnostics surface via `onDiagnostic`.
111
+ */
112
+ export function invalidAttributeValueLabel(directive, attribute) {
113
+ return `${directive}: ${attribute} ignored`;
114
+ }
115
+ /**
116
+ * The INLINE marker text for a component-built image whose `src` was
117
+ * refused as unsafe, this engine's short counterpart to
118
+ * `unsafeImageSrcTitle` for the same reason `invalidAttributeValueLabel`
119
+ * exists: no tooltip channel, so the full sentence goes to `onDiagnostic`
120
+ * only.
121
+ */
122
+ export function unsafeImageSrcLabel(directive) {
123
+ return `${directive}: image not shown`;
124
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The link-rewrite twin of `./image-resolve.js`'s `resolveImageSrc`:
3
+ * `renderMarkToAnsi`'s `resolveHref` option (`render.ts`'s
4
+ * `RenderMarkOptions`), applied to every link an ordinary markdown link
5
+ * produces. Ported verbatim from `@markii/html`'s `href-resolve.ts`.
6
+ *
7
+ * Same resolvability rule as images (no scheme, no protocol-relative
8
+ * `//host/...`, no bare `#fragment`, not empty), and the same
9
+ * `javascript:`/`vbscript:` refusal on the resolver's OWN return value,
10
+ * both shared with `./image-resolve.js` via `./url-resolve.js`.
11
+ */
12
+ export type ResolveHref = (href: string) => string | undefined;
13
+ /**
14
+ * The value one link should actually carry: `value` unchanged unless
15
+ * `resolveHref` is present, `value` is worth resolving at all, the resolver
16
+ * returns something, and that something passes `isSafeResolvedUrl`. A
17
+ * resolver that throws is treated exactly like one that returned
18
+ * `undefined`: `value` is kept, and the render is never broken over one
19
+ * link.
20
+ */
21
+ export declare function resolveHrefAttribute(value: string, resolveHref: ResolveHref | undefined): string;
@@ -0,0 +1,25 @@
1
+ import { isResolvableRelativeValue, isSafeResolvedUrl } from './url-resolve.js';
2
+ /**
3
+ * The value one link should actually carry: `value` unchanged unless
4
+ * `resolveHref` is present, `value` is worth resolving at all, the resolver
5
+ * returns something, and that something passes `isSafeResolvedUrl`. A
6
+ * resolver that throws is treated exactly like one that returned
7
+ * `undefined`: `value` is kept, and the render is never broken over one
8
+ * link.
9
+ */
10
+ export function resolveHrefAttribute(value, resolveHref) {
11
+ if (!resolveHref)
12
+ return value;
13
+ if (!isResolvableRelativeValue(value))
14
+ return value;
15
+ let resolved;
16
+ try {
17
+ resolved = resolveHref(value);
18
+ }
19
+ catch {
20
+ return value;
21
+ }
22
+ if (resolved === undefined)
23
+ return value;
24
+ return isSafeResolvedUrl(resolved) ? resolved : value;
25
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The shared logic behind `renderMarkToAnsi`'s `resolveImageSrc` option
3
+ * (see `render.ts`'s `RenderMarkOptions`), used everywhere an image
4
+ * reference reaches this engine's output: an ordinary markdown image and
5
+ * any future standard component that builds its own image reference from a
6
+ * directive attribute. Ported verbatim from `@markii/html`'s
7
+ * `image-resolve.ts` (itself mirroring `@markii/react`'s).
8
+ *
9
+ * A host resolver is only ever asked about a source that could plausibly
10
+ * be its own: one with no scheme, no protocol-relative `//host/...` form,
11
+ * no bare `#fragment`, and no empty/whitespace value — `./url-resolve.js`'s
12
+ * `isResolvableRelativeValue`, shared with `resolveHref`
13
+ * (`./href-resolve.js`) rather than duplicated.
14
+ *
15
+ * WHY THE RESULT CHECK IS NOT `isSafeUrl`. `isSafeUrl`'s allowlist
16
+ * (`http`/`https`/`mailto`/`tel`) exists to judge a URL an AUTHOR typed
17
+ * into the document, where any other scheme is suspicious. A resolver's
18
+ * RETURN VALUE is the opposite trust direction: it is the HOST's own
19
+ * answer for where its resolved image actually lives, and the two
20
+ * reference hosts already return values `isSafeUrl` would reject outright
21
+ * (a `data:` URI, an `app://` vault path). What still needs guarding
22
+ * against is a resolver, hostile or merely buggy, echoing a
23
+ * `javascript:`/`vbscript:` value back out — the one class of scheme that
24
+ * turns an image reference into a script-execution vector rather than an
25
+ * image request. `./url-resolve.js`'s `isSafeResolvedUrl` is a narrow
26
+ * denylist for exactly that, not a repeat of the author-facing allowlist.
27
+ */
28
+ /** The shape `renderMarkToAnsi`/`renderMarkNodeToAnsi` accept, and the one carried on `AnsiRenderContext` for a component that resolves its own image reference. */
29
+ export type ResolveImageSrc = (src: string) => string | undefined;
30
+ /**
31
+ * The value one image reference should actually carry: `value` unchanged
32
+ * unless `resolveImageSrc` is present, `value` is worth resolving at all,
33
+ * the resolver returns something, and that something passes
34
+ * `isSafeResolvedUrl` — so a resolver can never smuggle a `javascript:` URL
35
+ * past the sanitizer that already ran on everything else in the document,
36
+ * while a legitimate `data:`/`app:`/host-scheme result still reaches the
37
+ * output. A resolver that throws is treated exactly like one that returned
38
+ * `undefined`: `value` is kept, and the render is never broken over one
39
+ * image.
40
+ */
41
+ export declare function resolveImageAttribute(value: string, resolveImageSrc: ResolveImageSrc | undefined): string;
@@ -0,0 +1,28 @@
1
+ import { isResolvableRelativeValue, isSafeResolvedUrl } from './url-resolve.js';
2
+ /**
3
+ * The value one image reference should actually carry: `value` unchanged
4
+ * unless `resolveImageSrc` is present, `value` is worth resolving at all,
5
+ * the resolver returns something, and that something passes
6
+ * `isSafeResolvedUrl` — so a resolver can never smuggle a `javascript:` URL
7
+ * past the sanitizer that already ran on everything else in the document,
8
+ * while a legitimate `data:`/`app:`/host-scheme result still reaches the
9
+ * output. A resolver that throws is treated exactly like one that returned
10
+ * `undefined`: `value` is kept, and the render is never broken over one
11
+ * image.
12
+ */
13
+ export function resolveImageAttribute(value, resolveImageSrc) {
14
+ if (!resolveImageSrc)
15
+ return value;
16
+ if (!isResolvableRelativeValue(value))
17
+ return value;
18
+ let resolved;
19
+ try {
20
+ resolved = resolveImageSrc(value);
21
+ }
22
+ catch {
23
+ return value;
24
+ }
25
+ if (resolved === undefined)
26
+ return value;
27
+ return isSafeResolvedUrl(resolved) ? resolved : value;
28
+ }
@@ -0,0 +1,15 @@
1
+ export { renderMarkToAnsi, renderMarkNodeToAnsi, renderMarkInlineToAnsi, type RenderMarkOptions, } from './render.js';
2
+ export { fg, colorize, bold, dim, italic, underline, inverse, hyperlink, detectColorLevel, resolveColorOption, type ColorLevel, type ColorOption, type AnsiColor, } from './ansi.js';
3
+ export { defaultAnsiTheme, type AnsiTheme, type Tier1Token } from './theme.js';
4
+ export { style } from './style.js';
5
+ export { createAnsiRegistry, mergeAnsiRegistries, registryAliases, readRegistryComponent, resolveDirectiveAlias, REGISTRY_ALIASES, type DirectiveAttributes, type AnsiRegistry, type AnsiRegistryEntry, type AnsiComponent, type AnsiChildren, type AnsiChildPart, type AnsiChildrenOptions, type AnsiRenderContext, type RegistryAlias, type RegistryAliases, type ResolvedDirective, type ValueResolution, } from './registry.js';
6
+ export { resolveStorePath, resolveScopedPath, VAULT_NAME_PREFIX, type StorePathResolution, type ValueScope, } from './resolve.js';
7
+ export { failurePhrase, failureTitle, failureToken, dataStateSuffix, emptyInlineTitle, invalidAttributeValueLabel, invalidAttributeValueTitle, unsafeImageSrcLabel, unsafeImageSrcTitle, } from './failure-presentation.js';
8
+ export { stringifyStoredValue } from './value-format.js';
9
+ export { measure, stripAnsi } from './measure.js';
10
+ export { stripControlCharacters, sanitizeBlockText, sanitizeUrlText, BLOCK_TAB_WIDTH, } from './sanitize.js';
11
+ export { wrap, pad, indentBlock, columns, frame, rule, type FrameOptions, } from './box.js';
12
+ export { resolveLayoutAttributes, applyLayout, LAYOUT_ATTRIBUTE_KEYS, type ResolvedLayoutAttributes, type ResolvedLayoutPresets, type WidthPreset, type AlignPreset, } from './layout.js';
13
+ export { type ResolveImageSrc } from './image-resolve.js';
14
+ export { type ResolveHref } from './href-resolve.js';
15
+ export type { AnsiValueStore, AnsiVaultStore, FailureKind, StoredValue, ValueStatus, } from './value-types.js';
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ // @markii/ansi: a framework-free terminal renderer for Markii documents. It
2
+ // consumes @markii/core's sanitized hast and emits a plain string (optionally
3
+ // carrying ANSI SGR/OSC 8 escapes) for a terminal, a pipe, or any host that
4
+ // wants text with no HTML and no React runtime. It is a third platform
5
+ // renderer alongside @markii/html and @markii/react.
6
+ export { renderMarkToAnsi, renderMarkNodeToAnsi, renderMarkInlineToAnsi, } from './render.js';
7
+ export { fg, colorize, bold, dim, italic, underline, inverse, hyperlink, detectColorLevel, resolveColorOption, } from './ansi.js';
8
+ export { defaultAnsiTheme } from './theme.js';
9
+ export { style } from './style.js';
10
+ export { createAnsiRegistry, mergeAnsiRegistries, registryAliases, readRegistryComponent, resolveDirectiveAlias, REGISTRY_ALIASES, } from './registry.js';
11
+ export { resolveStorePath, resolveScopedPath, VAULT_NAME_PREFIX, } from './resolve.js';
12
+ export { failurePhrase, failureTitle, failureToken, dataStateSuffix, emptyInlineTitle, invalidAttributeValueLabel, invalidAttributeValueTitle, unsafeImageSrcLabel, unsafeImageSrcTitle, } from './failure-presentation.js';
13
+ export { stringifyStoredValue } from './value-format.js';
14
+ export { measure, stripAnsi } from './measure.js';
15
+ export { stripControlCharacters, sanitizeBlockText, sanitizeUrlText, BLOCK_TAB_WIDTH, } from './sanitize.js';
16
+ export { wrap, pad, indentBlock, columns, frame, rule, } from './box.js';
17
+ export { resolveLayoutAttributes, applyLayout, LAYOUT_ATTRIBUTE_KEYS, } from './layout.js';
@@ -0,0 +1,73 @@
1
+ import { ALIGN_PRESETS, LAYOUT_ATTRIBUTE_KEYS, WIDTH_PRESETS } from '@markii/stdlib';
2
+ import type { LayoutAxis } from '@markii/stdlib';
3
+ import type { DirectiveAttributes } from './registry.js';
4
+ /**
5
+ * The terminal counterpart of `@markii/html`'s `layout.ts`: the same closed
6
+ * `width`/`align` vocabulary from `@markii/stdlib`, but resolved to COLUMN
7
+ * ARITHMETIC instead of a CSS class, since there is no stylesheet here. The
8
+ * two reserved keys are still always stripped off a directive's attributes
9
+ * before a component sees them, whether or not their value turns out to be
10
+ * valid, exactly like the other two engines.
11
+ */
12
+ export { LAYOUT_ATTRIBUTE_KEYS };
13
+ export type WidthPreset = (typeof WIDTH_PRESETS)[number];
14
+ export type AlignPreset = (typeof ALIGN_PRESETS)[number];
15
+ /** The two layout attributes' resolved preset names, present only for a preset that actually applied. */
16
+ export interface ResolvedLayoutPresets {
17
+ width?: WidthPreset;
18
+ align?: AlignPreset;
19
+ }
20
+ export interface ResolvedLayoutAttributes {
21
+ /** `attributes` with every reserved layout key (present, valid or not) removed. */
22
+ attributes: DirectiveAttributes;
23
+ /** The resolved presets, or `undefined` if neither attribute produced one. */
24
+ resolved?: ResolvedLayoutPresets;
25
+ }
26
+ /**
27
+ * Splits `width`/`align` off `attributes`, returning the remaining
28
+ * attributes untouched plus the presets those two attributes resolved to,
29
+ * if any. Same rules as `@markii/html`'s `resolveLayoutAttributes`: both
30
+ * keys are stripped whenever present regardless of validity; an invalid or
31
+ * hostile value never produces a preset; `ownedAxis` (a layout wrapper's own
32
+ * axis) strips its attribute but produces no preset for it, since the
33
+ * directive's NAME already decided that axis. Never throws.
34
+ */
35
+ export declare function resolveLayoutAttributes(attributes: DirectiveAttributes, ownedAxis?: LayoutAxis): ResolvedLayoutAttributes;
36
+ /** The minimum column count `narrow` ever shrinks to, regardless of how small `width` is. */
37
+ export declare const NARROW_MINIMUM = 20;
38
+ /**
39
+ * Turns `layout`'s resolved presets into an actual re-wrapped, aligned
40
+ * block, given `width` columns of budget. Mapping (all documented here,
41
+ * the one place it is decided): `narrow` halves `width` (rounded, never
42
+ * below `NARROW_MINIMUM`); `wide` and `full` both use the entire `width`
43
+ * (a terminal has no notion of "wider than the column" to distinguish
44
+ * them); `fit` shrinks to `block`'s own widest existing line, never wider
45
+ * than `width`. `align` then places the (possibly narrowed) block within
46
+ * the full `width` using `./box.js`'s `pad`, which is where `align`'s
47
+ * `left`/`center`/`right` meaning comes from — this function invents none
48
+ * of its own.
49
+ */
50
+ export declare function applyLayout(block: string, layout: ResolvedLayoutPresets | undefined, width: number): string;
51
+ /**
52
+ * The target width a SELF-DRAWING box component (`card`, `callout`, `table`,
53
+ * `chart`; see `registry.ts`'s `AnsiRegistryEntry.selfLayout`) should draw
54
+ * its own frame at, given the resolved `width` preset. Mirrors the width
55
+ * half of `applyLayout`'s mapping exactly (`narrow` halves `width`, floored
56
+ * at `NARROW_MINIMUM`; `wide`/`full`/absent use the whole budget; `fit`
57
+ * shrinks to the box's own natural content width, capped at `width`), so a
58
+ * component that draws at this width up front needs no further narrowing:
59
+ * `applyLayout`'s `wrap` step becomes a no-op on an already-correctly-sized
60
+ * line. `naturalWidth` is the box's own widest line at its full content size
61
+ * (only consulted for `fit`); a caller that draws lazily may pass a function
62
+ * to defer that measurement until it is known to matter.
63
+ */
64
+ export declare function selfLayoutWidth(layout: ResolvedLayoutPresets | undefined, width: number, naturalWidth: number | (() => number)): number;
65
+ /**
66
+ * Pads a SELF-DRAWING box's already-drawn frame (every line already
67
+ * `boxWidth` columns wide) within the full `width` per the resolved `align`
68
+ * preset — the align half of `applyLayout`, factored out so a `selfLayout`
69
+ * component can align its own frame without going through the generic
70
+ * narrow/wrap path that would otherwise re-wrap (and corrupt) its
71
+ * box-drawing characters. A no-op when `layout.align` is absent.
72
+ */
73
+ export declare function selfLayoutAlign(block: string, layout: ResolvedLayoutPresets | undefined, width: number): string;