@markii/ansi 0.16.0 → 0.17.0-next.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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/dist/components/callout.d.ts +15 -1
  3. package/dist/components/callout.js +32 -19
  4. package/dist/components/card.d.ts +10 -0
  5. package/dist/components/card.js +21 -13
  6. package/dist/components/chart.d.ts +1 -1
  7. package/dist/components/chart.js +8 -9
  8. package/dist/components/details-string.js +1 -1
  9. package/dist/components/details.d.ts +3 -1
  10. package/dist/components/details.js +7 -6
  11. package/dist/components/divider.js +3 -5
  12. package/dist/components/progress.js +2 -3
  13. package/dist/components/rating.js +2 -2
  14. package/dist/components/row.d.ts +2 -0
  15. package/dist/components/row.js +3 -1
  16. package/dist/components/stat.js +1 -2
  17. package/dist/components/table-grid.d.ts +2 -1
  18. package/dist/components/table-grid.js +13 -10
  19. package/dist/components/table.js +1 -1
  20. package/dist/components/tabs.d.ts +3 -1
  21. package/dist/components/tabs.js +25 -7
  22. package/dist/failure-presentation.d.ts +28 -0
  23. package/dist/failure-presentation.js +41 -0
  24. package/dist/glyphs.d.ts +73 -0
  25. package/dist/glyphs.js +88 -0
  26. package/dist/index.d.ts +5 -1
  27. package/dist/index.js +4 -0
  28. package/dist/ink-string.d.ts +11 -0
  29. package/dist/ink-string.js +24 -0
  30. package/dist/interactive.d.ts +31 -6
  31. package/dist/interactive.js +155 -12
  32. package/dist/pack-loader.d.ts +64 -0
  33. package/dist/pack-loader.js +172 -0
  34. package/dist/pack-script.d.ts +44 -0
  35. package/dist/pack-script.js +135 -0
  36. package/dist/registry.d.ts +15 -0
  37. package/dist/render.d.ts +48 -1
  38. package/dist/render.js +441 -85
  39. package/dist/text-grid.d.ts +43 -7
  40. package/dist/text-grid.js +69 -18
  41. package/package.json +6 -5
@@ -0,0 +1,64 @@
1
+ import type { PackManifest } from '@markii/pack';
2
+ import type { AnsiRegistry, AnsiRegistryEntry } from './registry.js';
3
+ /**
4
+ * This engine's id in a pack manifest's per-component `sources` map
5
+ * (docs/packs.md). A component with an `ansi` source has a terminal view.
6
+ */
7
+ export declare const ANSI_ENGINE_ID = "ansi";
8
+ /**
9
+ * The component modules a host hands to `loadAnsiPack`, keyed by the
10
+ * manifest's LOCAL component name. Each is the `{ component, inline? }`
11
+ * pair a pack's `terminal.js` registered (`./pack-script.ts`).
12
+ */
13
+ export type AnsiPackComponentModules = Record<string, AnsiRegistryEntry>;
14
+ /**
15
+ * Returns `value` with every string in it run through
16
+ * `stripControlCharacters`, so a pack that prints a stored value cannot
17
+ * print a control character a script smuggled in. Arrays and plain objects
18
+ * are copied (null-prototype for objects, hasOwn-guarded), never mutated.
19
+ * Anything past `MAX_SANITIZE_DEPTH`, and every non-JSON value, becomes
20
+ * `undefined`. Never throws: a throwing getter reads as `undefined`.
21
+ */
22
+ export declare function sanitizeValueForPack(value: unknown, depth?: number): unknown;
23
+ /** What `loadAnsiPack` reports about a pack it could not run. */
24
+ export interface LoadAnsiPackResult {
25
+ registry: AnsiRegistry;
26
+ /** Present when the pack has components but none has an `ansi` source: the pack contributes nothing. `engine` names the engines it does declare. */
27
+ dropped?: {
28
+ name: string;
29
+ engine: string;
30
+ };
31
+ }
32
+ /**
33
+ * Loads one pack's terminal components into a namespaced `AnsiRegistry`,
34
+ * mirroring `@markii/react`'s `loadPack`: gating is per component (only
35
+ * components with an `ansi` source register; the rest fall back to the
36
+ * unknown-component box), a manifest component with no module is skipped,
37
+ * a stray module is ignored, and a declared `kind` decides the `inline`
38
+ * flag. Reads are `Object.hasOwn`-guarded. Never throws.
39
+ */
40
+ export declare function loadAnsiPack(manifest: PackManifest, componentModules: AnsiPackComponentModules): LoadAnsiPackResult;
41
+ /** One pack ready to install: its manifest plus its registered component modules. */
42
+ export interface AnsiPackToInstall {
43
+ manifest: PackManifest;
44
+ componentModules: AnsiPackComponentModules;
45
+ }
46
+ export type InstallAnsiPacksResult = {
47
+ ok: true;
48
+ registry: AnsiRegistry;
49
+ dropped: {
50
+ name: string;
51
+ engine: string;
52
+ }[];
53
+ } | {
54
+ ok: false;
55
+ collisions: readonly string[];
56
+ };
57
+ /**
58
+ * Installs several packs into one registry on top of `base`, rejecting the
59
+ * whole install when two share a namespace (the install-time rule in
60
+ * docs/packs.md), exactly like `@markii/react`'s `installPacks`. A host
61
+ * that wants keep-first behavior (a bundled pack wins over a user pack of
62
+ * the same namespace) filters the list before calling.
63
+ */
64
+ export declare function installAnsiPacks(packs: readonly AnsiPackToInstall[], base?: AnsiRegistry): InstallAnsiPacksResult;
@@ -0,0 +1,172 @@
1
+ import { composeDirectiveName, detectNamespaceCollisions, packComponentsForEngine, packDeclaredEngines, packSupportsEngine, } from '@markii/pack';
2
+ import { createAnsiRegistry, mergeAnsiRegistries } from './registry.js';
3
+ import { stripControlCharacters } from './sanitize.js';
4
+ /**
5
+ * This engine's id in a pack manifest's per-component `sources` map
6
+ * (docs/packs.md). A component with an `ansi` source has a terminal view.
7
+ */
8
+ export const ANSI_ENGINE_ID = 'ansi';
9
+ /** The deepest a stored value is walked when sanitizing it for a pack. */
10
+ const MAX_SANITIZE_DEPTH = 16;
11
+ /**
12
+ * Returns `value` with every string in it run through
13
+ * `stripControlCharacters`, so a pack that prints a stored value cannot
14
+ * print a control character a script smuggled in. Arrays and plain objects
15
+ * are copied (null-prototype for objects, hasOwn-guarded), never mutated.
16
+ * Anything past `MAX_SANITIZE_DEPTH`, and every non-JSON value, becomes
17
+ * `undefined`. Never throws: a throwing getter reads as `undefined`.
18
+ */
19
+ export function sanitizeValueForPack(value, depth = 0) {
20
+ if (depth > MAX_SANITIZE_DEPTH)
21
+ return undefined;
22
+ if (typeof value === 'string')
23
+ return stripControlCharacters(value);
24
+ if (value === null ||
25
+ typeof value === 'number' ||
26
+ typeof value === 'boolean') {
27
+ return value;
28
+ }
29
+ try {
30
+ if (Array.isArray(value)) {
31
+ return value.map((item) => sanitizeValueForPack(item, depth + 1));
32
+ }
33
+ if (typeof value === 'object') {
34
+ const out = Object.create(null);
35
+ for (const key of Object.keys(value)) {
36
+ if (!Object.hasOwn(value, key))
37
+ continue;
38
+ out[stripControlCharacters(key)] = sanitizeValueForPack(value[key], depth + 1);
39
+ }
40
+ return out;
41
+ }
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ return undefined;
47
+ }
48
+ /** A copy of `attributes` with every string value (and key) stripped of control characters. A bare attribute stays `null`. */
49
+ function sanitizeAttributes(attributes) {
50
+ const out = Object.create(null);
51
+ try {
52
+ for (const key of Object.keys(attributes)) {
53
+ if (!Object.hasOwn(attributes, key))
54
+ continue;
55
+ const raw = attributes[key];
56
+ out[stripControlCharacters(key)] =
57
+ typeof raw === 'string' ? stripControlCharacters(raw) : raw;
58
+ }
59
+ }
60
+ catch {
61
+ // A hostile attribute object yields whatever copied so far.
62
+ }
63
+ return out;
64
+ }
65
+ /**
66
+ * The ctx a pack component receives: the engine's ctx with `resolve` and
67
+ * `data` replaced by sanitized-copy versions. `ctx.text` stays the engine's
68
+ * sanitizer, the way a pack prints any other string it composes.
69
+ */
70
+ function packContext(ctx) {
71
+ const packCtx = {
72
+ ...ctx,
73
+ resolve: (name) => {
74
+ const resolution = ctx.resolve(name);
75
+ return {
76
+ ...resolution,
77
+ value: sanitizeValueForPack(resolution.value),
78
+ ...(resolution.error !== undefined
79
+ ? { error: stripControlCharacters(resolution.error) }
80
+ : {}),
81
+ };
82
+ },
83
+ };
84
+ if ('data' in ctx)
85
+ packCtx.data = sanitizeValueForPack(ctx.data);
86
+ if (ctx.dataError !== undefined) {
87
+ packCtx.dataError = stripControlCharacters(ctx.dataError);
88
+ }
89
+ return packCtx;
90
+ }
91
+ /**
92
+ * Wraps a pack's component so the engine, not the pack, decides what text
93
+ * reaches it. The wrapper sanitizes the attributes and the stored-value
94
+ * view of the ctx before calling the pack, and a throw inside the pack's
95
+ * render is contained by the walk's existing per-component boundary.
96
+ *
97
+ * This bounds what AUTHOR text can do through a pack. It cannot bound the
98
+ * pack's own code: a terminal pack runs in the host's process with the
99
+ * host's privileges and can write to stdout itself (docs/security.md).
100
+ */
101
+ function wrapPackComponent(component) {
102
+ // Called as a plain function, like every registry component: it returns
103
+ // a string or an Ink element (the engine lays the element out at the
104
+ // directive's width). Components the pack's view renders INSIDE that
105
+ // element are ordinary React components and may use hooks.
106
+ return function PackComponent(props) {
107
+ return component({
108
+ attributes: sanitizeAttributes(props.attributes),
109
+ children: props.children,
110
+ ctx: packContext(props.ctx),
111
+ });
112
+ };
113
+ }
114
+ /**
115
+ * Loads one pack's terminal components into a namespaced `AnsiRegistry`,
116
+ * mirroring `@markii/react`'s `loadPack`: gating is per component (only
117
+ * components with an `ansi` source register; the rest fall back to the
118
+ * unknown-component box), a manifest component with no module is skipped,
119
+ * a stray module is ignored, and a declared `kind` decides the `inline`
120
+ * flag. Reads are `Object.hasOwn`-guarded. Never throws.
121
+ */
122
+ export function loadAnsiPack(manifest, componentModules) {
123
+ if (!packSupportsEngine(manifest, ANSI_ENGINE_ID)) {
124
+ return {
125
+ registry: createAnsiRegistry(),
126
+ dropped: { name: manifest.name, engine: packDeclaredEngines(manifest) },
127
+ };
128
+ }
129
+ const entries = {};
130
+ for (const { localName, kind } of packComponentsForEngine(manifest, ANSI_ENGINE_ID)) {
131
+ if (!Object.hasOwn(componentModules, localName))
132
+ continue;
133
+ const module = componentModules[localName];
134
+ if (module == null || typeof module.component !== 'function')
135
+ continue;
136
+ const composed = composeDirectiveName(manifest.name, localName);
137
+ if (!composed.ok)
138
+ continue;
139
+ const entry = {
140
+ component: wrapPackComponent(module.component),
141
+ };
142
+ const inline = kind === undefined ? module.inline : kind === 'inline';
143
+ if (inline !== undefined)
144
+ entry.inline = inline;
145
+ entries[composed.name] = entry;
146
+ }
147
+ return { registry: createAnsiRegistry(entries) };
148
+ }
149
+ /**
150
+ * Installs several packs into one registry on top of `base`, rejecting the
151
+ * whole install when two share a namespace (the install-time rule in
152
+ * docs/packs.md), exactly like `@markii/react`'s `installPacks`. A host
153
+ * that wants keep-first behavior (a bundled pack wins over a user pack of
154
+ * the same namespace) filters the list before calling.
155
+ */
156
+ export function installAnsiPacks(packs, base = createAnsiRegistry()) {
157
+ const collisions = detectNamespaceCollisions(packs.map((pack) => pack.manifest.name));
158
+ if (collisions.length > 0) {
159
+ return {
160
+ ok: false,
161
+ collisions: collisions.map((collision) => collision.namespace),
162
+ };
163
+ }
164
+ const dropped = [];
165
+ const loaded = packs.map((pack) => {
166
+ const result = loadAnsiPack(pack.manifest, pack.componentModules);
167
+ if (result.dropped)
168
+ dropped.push(result.dropped);
169
+ return result.registry;
170
+ });
171
+ return { ok: true, registry: mergeAnsiRegistries(base, ...loaded), dropped };
172
+ }
@@ -0,0 +1,44 @@
1
+ import type { AnsiPackToInstall } from './pack-loader.js';
2
+ /**
3
+ * The terminal pack registration contract (docs/packs.md). A pack's
4
+ * prebuilt `terminal.js` is an ESM module. On evaluation it calls
5
+ *
6
+ * globalThis.__markiiRegisterTerminalPack(manifestJson, componentModules)
7
+ *
8
+ * with the raw `pack.json` text and an own-property map of
9
+ * `{ component, inline? }` keyed by local component name, the same shape
10
+ * `__markiiRegisterPack` takes for the webview. React and Ink are NOT
11
+ * bundled into the script. The host sets `globalThis.__markiiReactInk`
12
+ * (the React instance Ink uses) and `globalThis.__markiiInk` (Ink's
13
+ * exports) and a pack reads them only inside render-time functions, so
14
+ * evaluating the script with both undefined must not throw.
15
+ *
16
+ * SECURITY: evaluating this script runs the pack's code in the calling
17
+ * process with that process's full privileges. Only a host's own device-
18
+ * local install store and bundled packs may reach `loadTerminalPackScript`:
19
+ * never a note, a bundle, the working directory, or an environment
20
+ * variable (docs/security.md).
21
+ */
22
+ export declare const TERMINAL_REGISTER_GLOBAL = "__markiiRegisterTerminalPack";
23
+ export declare const TERMINAL_REACT_GLOBAL = "__markiiReactInk";
24
+ export declare const TERMINAL_INK_GLOBAL = "__markiiInk";
25
+ /**
26
+ * Publishes this package's own React and Ink instances for pack scripts to
27
+ * read at render time. Idempotent. Left in place for the life of the
28
+ * process, since a pack reads them each time its component renders.
29
+ */
30
+ export declare function installTerminalPackGlobals(): void;
31
+ export type LoadTerminalPackScriptResult = {
32
+ ok: true;
33
+ pack: AnsiPackToInstall;
34
+ } | {
35
+ ok: false;
36
+ reason: string;
37
+ };
38
+ /**
39
+ * Evaluates one pack's `terminal.js` text and returns the validated pack
40
+ * ready for `installAnsiPacks`, or the reason it could not be used. Never
41
+ * rejects. SECURITY: runs the script's code in this process; see the
42
+ * module comment for who may call it.
43
+ */
44
+ export declare function loadTerminalPackScript(scriptText: string): Promise<LoadTerminalPackScriptResult>;
@@ -0,0 +1,135 @@
1
+ import * as React from 'react';
2
+ import * as Ink from 'ink';
3
+ import { parsePackManifest } from '@markii/pack';
4
+ /**
5
+ * The terminal pack registration contract (docs/packs.md). A pack's
6
+ * prebuilt `terminal.js` is an ESM module. On evaluation it calls
7
+ *
8
+ * globalThis.__markiiRegisterTerminalPack(manifestJson, componentModules)
9
+ *
10
+ * with the raw `pack.json` text and an own-property map of
11
+ * `{ component, inline? }` keyed by local component name, the same shape
12
+ * `__markiiRegisterPack` takes for the webview. React and Ink are NOT
13
+ * bundled into the script. The host sets `globalThis.__markiiReactInk`
14
+ * (the React instance Ink uses) and `globalThis.__markiiInk` (Ink's
15
+ * exports) and a pack reads them only inside render-time functions, so
16
+ * evaluating the script with both undefined must not throw.
17
+ *
18
+ * SECURITY: evaluating this script runs the pack's code in the calling
19
+ * process with that process's full privileges. Only a host's own device-
20
+ * local install store and bundled packs may reach `loadTerminalPackScript`:
21
+ * never a note, a bundle, the working directory, or an environment
22
+ * variable (docs/security.md).
23
+ */
24
+ export const TERMINAL_REGISTER_GLOBAL = '__markiiRegisterTerminalPack';
25
+ export const TERMINAL_REACT_GLOBAL = '__markiiReactInk';
26
+ export const TERMINAL_INK_GLOBAL = '__markiiInk';
27
+ /**
28
+ * Publishes this package's own React and Ink instances for pack scripts to
29
+ * read at render time. Idempotent. Left in place for the life of the
30
+ * process, since a pack reads them each time its component renders.
31
+ */
32
+ export function installTerminalPackGlobals() {
33
+ const g = globalThis;
34
+ g[TERMINAL_REACT_GLOBAL] = React;
35
+ g[TERMINAL_INK_GLOBAL] = Ink;
36
+ }
37
+ function isPlainObject(value) {
38
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
39
+ }
40
+ /** Own-property validation of the registered map, same rule as the webview convention. */
41
+ function toComponentModules(value) {
42
+ if (!isPlainObject(value))
43
+ return undefined;
44
+ const out = Object.create(null);
45
+ for (const key of Object.keys(value)) {
46
+ if (!Object.hasOwn(value, key))
47
+ continue;
48
+ const entry = value[key];
49
+ if (!isPlainObject(entry) || typeof entry.component !== 'function') {
50
+ return undefined;
51
+ }
52
+ if (Object.hasOwn(entry, 'inline') && typeof entry.inline !== 'boolean') {
53
+ return undefined;
54
+ }
55
+ out[key] = {
56
+ component: entry.component,
57
+ ...(typeof entry.inline === 'boolean' ? { inline: entry.inline } : {}),
58
+ };
59
+ }
60
+ return out;
61
+ }
62
+ // Evaluations are serialized: the register hook is one process global.
63
+ let queue = Promise.resolve();
64
+ let nonce = 0;
65
+ async function evaluate(scriptText) {
66
+ installTerminalPackGlobals();
67
+ const g = globalThis;
68
+ const previous = g[TERMINAL_REGISTER_GLOBAL];
69
+ const registrations = [];
70
+ g[TERMINAL_REGISTER_GLOBAL] = (manifestJson, modules) => {
71
+ registrations.push({ manifestJson, modules });
72
+ };
73
+ try {
74
+ nonce += 1;
75
+ const source = `${scriptText}\n//# markiiTerminalPack=${String(nonce)}`;
76
+ const url = `data:text/javascript;base64,${Buffer.from(source, 'utf8').toString('base64')}`;
77
+ try {
78
+ await import(/* @vite-ignore */ url);
79
+ }
80
+ catch (error) {
81
+ const detail = error instanceof Error ? error.message : String(error);
82
+ return {
83
+ ok: false,
84
+ reason: `terminal.js threw while loading: ${detail}`,
85
+ };
86
+ }
87
+ }
88
+ finally {
89
+ if (previous === undefined)
90
+ delete g[TERMINAL_REGISTER_GLOBAL];
91
+ else
92
+ g[TERMINAL_REGISTER_GLOBAL] = previous;
93
+ }
94
+ if (registrations.length !== 1) {
95
+ return {
96
+ ok: false,
97
+ reason: registrations.length === 0
98
+ ? `terminal.js did not call ${TERMINAL_REGISTER_GLOBAL}`
99
+ : `terminal.js called ${TERMINAL_REGISTER_GLOBAL} ${String(registrations.length)} times; exactly one registration is allowed`,
100
+ };
101
+ }
102
+ const { manifestJson, modules } = registrations[0];
103
+ if (typeof manifestJson !== 'string') {
104
+ return {
105
+ ok: false,
106
+ reason: 'terminal.js did not provide a manifest JSON string',
107
+ };
108
+ }
109
+ const parsed = parsePackManifest(manifestJson);
110
+ if (!parsed.ok) {
111
+ return {
112
+ ok: false,
113
+ reason: `terminal.js manifest is invalid (${parsed.errors.join('; ')})`,
114
+ };
115
+ }
116
+ const componentModules = toComponentModules(modules);
117
+ if (componentModules === undefined) {
118
+ return { ok: false, reason: 'terminal.js registered malformed components' };
119
+ }
120
+ return { ok: true, pack: { manifest: parsed.manifest, componentModules } };
121
+ }
122
+ /**
123
+ * Evaluates one pack's `terminal.js` text and returns the validated pack
124
+ * ready for `installAnsiPacks`, or the reason it could not be used. Never
125
+ * rejects. SECURITY: runs the script's code in this process; see the
126
+ * module comment for who may call it.
127
+ */
128
+ export function loadTerminalPackScript(scriptText) {
129
+ const run = queue.then(() => evaluate(scriptText));
130
+ queue = run.catch(() => undefined);
131
+ return run.catch((error) => ({
132
+ ok: false,
133
+ reason: `terminal.js could not be evaluated: ${error instanceof Error ? error.message : String(error)}`,
134
+ }));
135
+ }
@@ -3,6 +3,7 @@ import type { FailureKind, ValueStatus } from './value-types.js';
3
3
  import type { LayoutAxis, OnDiagnostic } from '@markii/stdlib';
4
4
  import type { ColorLevel } from './ansi.js';
5
5
  import type { AnsiTheme, Tier1Token } from './theme.js';
6
+ import type { GlyphSet } from './glyphs.js';
6
7
  import type { ResolvedLayoutAttributes } from './layout.js';
7
8
  import type { ResolveImageSrc } from './image-resolve.js';
8
9
  import type { ResolveHref } from './href-resolve.js';
@@ -63,6 +64,12 @@ export interface AnsiRenderContext {
63
64
  color: ColorLevel;
64
65
  /** The resolved theme for this render. */
65
66
  theme: AnsiTheme;
67
+ /**
68
+ * The glyphs for this render's box style (`./glyphs.ts`). A component that
69
+ * draws a frame, rule, bar, or symbol takes it from here, never from a
70
+ * literal, so `box: 'ascii'` reaches it.
71
+ */
72
+ glyphs: GlyphSet;
66
73
  /**
67
74
  * Whether this render is in INTERACTIVE mode (a live viewer), as opposed
68
75
  * to the default render-once mode. NAME COLLISION, documented at both
@@ -109,6 +116,14 @@ export interface AnsiRenderContext {
109
116
  dataStatus?: ValueStatus;
110
117
  dataError?: string;
111
118
  dataFailureKind?: FailureKind;
119
+ /**
120
+ * The engine's quiet trailing marker for this binding's state, e.g.
121
+ * ` (script error)` or ` (stale)`, and `''` for a fresh or missing one.
122
+ * Present with `data`. A component that cannot import the engine's
123
+ * failure-presentation module (a terminal pack, which only sees this
124
+ * context) appends it so the wording stays in one place.
125
+ */
126
+ dataSuffix?: string;
112
127
  /**
113
128
  * The resolved width/align presets for the axis a layout-scope entry does
114
129
  * not already own — this engine's counterpart to `@markii/html`'s
package/dist/render.d.ts CHANGED
@@ -4,27 +4,67 @@ import type { MarkNode } from '@markii/core';
4
4
  import { type ColorOption } from './ansi.js';
5
5
  import { type AnsiTheme } from './theme.js';
6
6
  import type { AnsiRegistry } from './registry.js';
7
- import { type OnDiagnostic } from '@markii/stdlib';
7
+ import { type OnDiagnostic, type UnavailableComponent } from '@markii/stdlib';
8
8
  import { type ResolveImageSrc } from './image-resolve.js';
9
9
  import { type ResolveHref } from './href-resolve.js';
10
10
  import type { AnsiValueStore, AnsiVaultStore } from './value-types.js';
11
+ import { type BoxStyle } from './glyphs.js';
11
12
  type MarkRoot = ReturnType<typeof parse>;
12
13
  export interface ElementTabPanel {
13
14
  label: string;
14
15
  body: ReactNode;
15
16
  }
17
+ /** A fenced code block as offered to `RenderMarkOptions.renderCode`. */
18
+ export interface RenderCodeBlock {
19
+ /** The info-string language, or `undefined` when the fence names none. */
20
+ readonly lang: string | undefined;
21
+ /** The code text, control characters removed and tabs expanded. */
22
+ readonly code: string;
23
+ }
24
+ /** The `renderCode` callback: a string for a code block, or `undefined` to draw it as usual. */
25
+ export type RenderCode = (code: RenderCodeBlock) => string | undefined;
16
26
  /**
17
27
  * Options for `renderMarkToAnsi`/`renderMarkNodeToAnsi`/`renderMarkInlineToAnsi`.
18
28
  * `store`/`vault` are offered both here and as positional parameters; when
19
29
  * both are given, the option wins.
20
30
  */
21
31
  export interface RenderMarkOptions {
32
+ /**
33
+ * Replaces the output for an ordinary fenced code block. Called with the
34
+ * info-string language (`undefined` when the fence names none) and the
35
+ * code text, already stripped of control characters and with tabs
36
+ * expanded (the engine's own sanitizer, so author escapes cannot reach
37
+ * the terminal through a host that echoes it). Return a string to use it,
38
+ * or `undefined` to draw the block as usual. The returned string is
39
+ * HOST-TRUSTED and inserted as-is, so the host may add its own SGR
40
+ * styling and owns keeping control characters out of anything it adds.
41
+ * Never called for script fences or inline code. A callback that throws
42
+ * draws the block as usual and reports a `'code-renderer-failed'` event
43
+ * through `onDiagnostic`. Element results are not supported: fenced blocks
44
+ * are drawn as strings in this engine.
45
+ */
46
+ readonly renderCode?: RenderCode;
22
47
  readonly width?: number;
23
48
  readonly color?: ColorOption;
24
49
  readonly theme?: AnsiTheme;
50
+ /**
51
+ * `'unicode'` (the default) draws frames, rules, bars, and markers with
52
+ * box-drawing and symbol characters; `'ascii'` draws every one of them
53
+ * with 7-bit characters, for a terminal that cannot show the others. The
54
+ * engine never chooses this from the environment: the host does.
55
+ */
56
+ readonly box?: BoxStyle;
25
57
  readonly resolveImageSrc?: ResolveImageSrc;
26
58
  readonly resolveHref?: ResolveHref;
27
59
  readonly onDiagnostic?: OnDiagnostic;
60
+ /**
61
+ * Components of installed packs that this render cannot draw, as plain
62
+ * data from the host. A directive named here renders the usual fallback
63
+ * with the label "no terminal view for" instead of "unknown component",
64
+ * and `onDiagnostic` gets a `'no-engine-view'` event. Omitted, the render
65
+ * is unchanged.
66
+ */
67
+ readonly unavailableComponents?: readonly UnavailableComponent[];
28
68
  readonly store?: AnsiValueStore;
29
69
  readonly vault?: AnsiVaultStore;
30
70
  /**
@@ -37,6 +77,13 @@ export interface RenderMarkOptions {
37
77
  * every panel/always-open exactly as the pre-Ink engine did.
38
78
  */
39
79
  readonly interactive?: boolean;
80
+ /**
81
+ * `buildMarkElement` only: the terminal's row count. Gives the live view
82
+ * a viewport of this many rows minus its one-row status line, scrolled
83
+ * with the keys in the legend. Omitted, the live view is laid out at its
84
+ * full height. The render-once string path never reads it.
85
+ */
86
+ readonly viewportRows?: number;
40
87
  }
41
88
  /**
42
89
  * Renders Markii text to a plain string (optionally carrying ANSI SGR/OSC 8