@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.
- package/LICENSE +21 -0
- package/dist/components/callout.d.ts +15 -1
- package/dist/components/callout.js +32 -19
- package/dist/components/card.d.ts +10 -0
- package/dist/components/card.js +21 -13
- package/dist/components/chart.d.ts +1 -1
- package/dist/components/chart.js +8 -9
- package/dist/components/details-string.js +1 -1
- package/dist/components/details.d.ts +3 -1
- package/dist/components/details.js +7 -6
- package/dist/components/divider.js +3 -5
- package/dist/components/progress.js +2 -3
- package/dist/components/rating.js +2 -2
- package/dist/components/row.d.ts +2 -0
- package/dist/components/row.js +3 -1
- package/dist/components/stat.js +1 -2
- package/dist/components/table-grid.d.ts +2 -1
- package/dist/components/table-grid.js +13 -10
- package/dist/components/table.js +1 -1
- package/dist/components/tabs.d.ts +3 -1
- package/dist/components/tabs.js +25 -7
- package/dist/failure-presentation.d.ts +28 -0
- package/dist/failure-presentation.js +41 -0
- package/dist/glyphs.d.ts +73 -0
- package/dist/glyphs.js +88 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -0
- package/dist/ink-string.d.ts +11 -0
- package/dist/ink-string.js +24 -0
- package/dist/interactive.d.ts +31 -6
- package/dist/interactive.js +155 -12
- package/dist/pack-loader.d.ts +64 -0
- package/dist/pack-loader.js +172 -0
- package/dist/pack-script.d.ts +44 -0
- package/dist/pack-script.js +135 -0
- package/dist/registry.d.ts +15 -0
- package/dist/render.d.ts +48 -1
- package/dist/render.js +441 -85
- package/dist/text-grid.d.ts +43 -7
- package/dist/text-grid.js +69 -18
- 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
|
+
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -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
|