@markii/ansi 0.17.0-next.0 → 0.17.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/failure-presentation.d.ts +15 -0
- package/dist/failure-presentation.js +23 -0
- package/dist/index.d.ts +1 -1
- package/dist/interactive.d.ts +19 -1
- package/dist/interactive.js +30 -0
- package/dist/pack-loader.js +3 -0
- package/dist/pack-script.d.ts +1 -1
- package/dist/pack-script.js +12 -2
- package/dist/registry.d.ts +10 -0
- package/dist/render.d.ts +31 -0
- package/dist/render.js +142 -17
- package/package.json +5 -5
|
@@ -37,6 +37,10 @@ export declare function emptyInlineTitle(name: string): string;
|
|
|
37
37
|
export declare function invalidAttributeValueTitle(directive: string, attribute: string, value: string): string;
|
|
38
38
|
/** The marker text for a component-built image whose `src` was refused as unsafe. Wording identical to `@markii/html`'s `unsafeImageSrcTitle`. */
|
|
39
39
|
export declare function unsafeImageSrcTitle(directive: string): string;
|
|
40
|
+
/** The diagnostic text for a host `renderCode` callback that threw; the code block rendered as usual. Wording identical to `@markii/html`'s `codeRendererFailedTitle`. */
|
|
41
|
+
export declare function codeRendererFailedTitle(): string;
|
|
42
|
+
/** The diagnostic text for a directive that names no registered component; the unknown-component box was shown. */
|
|
43
|
+
export declare function unknownDirectiveTitle(directive: string): string;
|
|
40
44
|
/** The diagnostic text for a registered block component written as an inline directive. Wording identical to `@markii/html`'s `formMismatchTitle`. */
|
|
41
45
|
export declare function formMismatchTitle(directive: string): string;
|
|
42
46
|
/**
|
|
@@ -71,3 +75,14 @@ export declare const NO_ENGINE_VIEW_LABEL = "no terminal view for";
|
|
|
71
75
|
* and is never printed in the note.
|
|
72
76
|
*/
|
|
73
77
|
export declare function noEngineViewTitle(directive: string, pack: string, engines: readonly string[]): string;
|
|
78
|
+
/**
|
|
79
|
+
* The fallback label for a directive of a pack whose terminal view script
|
|
80
|
+
* could not be loaded. Followed by the directive name in backticks, like the
|
|
81
|
+
* other fallback labels. `pack` is stripped by the caller's sanitizer.
|
|
82
|
+
*/
|
|
83
|
+
export declare function packLoadFailedLabel(pack: string): string;
|
|
84
|
+
/**
|
|
85
|
+
* The diagnostic text for the same case. The loader's own line (what threw)
|
|
86
|
+
* is on the diagnostics surface already; this one names the directive.
|
|
87
|
+
*/
|
|
88
|
+
export declare function packLoadFailedTitle(directive: string, pack: string): string;
|
|
@@ -99,6 +99,14 @@ export function invalidAttributeValueTitle(directive, attribute, value) {
|
|
|
99
99
|
export function unsafeImageSrcTitle(directive) {
|
|
100
100
|
return `${directive}: image source was refused as unsafe and was not shown`;
|
|
101
101
|
}
|
|
102
|
+
/** The diagnostic text for a host `renderCode` callback that threw; the code block rendered as usual. Wording identical to `@markii/html`'s `codeRendererFailedTitle`. */
|
|
103
|
+
export function codeRendererFailedTitle() {
|
|
104
|
+
return 'code block: the host code renderer failed, so the plain block was shown';
|
|
105
|
+
}
|
|
106
|
+
/** The diagnostic text for a directive that names no registered component; the unknown-component box was shown. */
|
|
107
|
+
export function unknownDirectiveTitle(directive) {
|
|
108
|
+
return `${directive}: unknown component, shown as a placeholder`;
|
|
109
|
+
}
|
|
102
110
|
/** The diagnostic text for a registered block component written as an inline directive. Wording identical to `@markii/html`'s `formMismatchTitle`. */
|
|
103
111
|
export function formMismatchTitle(directive) {
|
|
104
112
|
return `${directive}: block component was written inline and was not rendered`;
|
|
@@ -144,3 +152,18 @@ export function noEngineViewTitle(directive, pack, engines) {
|
|
|
144
152
|
: 'it has no views for any engine';
|
|
145
153
|
return `The ${pack} pack has no terminal view for this component; ${has}`;
|
|
146
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* The fallback label for a directive of a pack whose terminal view script
|
|
157
|
+
* could not be loaded. Followed by the directive name in backticks, like the
|
|
158
|
+
* other fallback labels. `pack` is stripped by the caller's sanitizer.
|
|
159
|
+
*/
|
|
160
|
+
export function packLoadFailedLabel(pack) {
|
|
161
|
+
return `the \`${pack}\` pack failed to load, no terminal view for`;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The diagnostic text for the same case. The loader's own line (what threw)
|
|
165
|
+
* is on the diagnostics surface already; this one names the directive.
|
|
166
|
+
*/
|
|
167
|
+
export function packLoadFailedTitle(directive, pack) {
|
|
168
|
+
return `The ${pack} pack failed to load in the terminal, so ${directive} has no view`;
|
|
169
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { renderMarkToAnsi, renderMarkNodeToAnsi, renderMarkInlineToAnsi, buildMarkElement, type RenderMarkOptions, } from './render.js';
|
|
1
|
+
export { renderMarkToAnsi, renderMarkNodeToAnsi, renderMarkInlineToAnsi, buildMarkElement, type RenderCode, type RenderCodeBlock, type RenderMarkOptions, } from './render.js';
|
|
2
2
|
export { fg, colorize, bold, dim, italic, underline, inverse, hyperlink, detectColorLevel, resolveColorOption, type ColorLevel, type ColorOption, type AnsiColor, } from './ansi.js';
|
|
3
3
|
export { defaultAnsiTheme, type AnsiTheme, type Tier1Token } from './theme.js';
|
|
4
4
|
export { BOX_STYLES, isBoxStyle, glyphsFor, type BoxStyle, type FrameEdges, type GlyphSet, } from './glyphs.js';
|
package/dist/interactive.d.ts
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
+
import { useInput } from 'ink';
|
|
2
3
|
import type { DOMElement } from 'ink';
|
|
3
4
|
import type { AnsiTheme } from './theme.js';
|
|
4
5
|
import type { ColorLevel } from './ansi.js';
|
|
6
|
+
/**
|
|
7
|
+
* Wraps a focusable pack component's live element. It draws a two-column
|
|
8
|
+
* gutter (the accent marker while the component has focus, blanks
|
|
9
|
+
* otherwise) and names the component's focus id for `usePackInput`. The
|
|
10
|
+
* pack draws inside the width that is left.
|
|
11
|
+
*/
|
|
12
|
+
export declare function PackFocusScope({ focusId, children, }: {
|
|
13
|
+
focusId: number;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
}): ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* Ink's `useInput`, for a terminal pack: the same handler and options, but
|
|
18
|
+
* keys reach the handler only while the pack component whose subtree calls
|
|
19
|
+
* it has focus. Outside a focusable pack component it never fires. A pack
|
|
20
|
+
* reads this as `__markiiInk.useInput`, so it never sees raw stdin.
|
|
21
|
+
*/
|
|
22
|
+
export declare function usePackInput(handler: Parameters<typeof useInput>[0], options?: Parameters<typeof useInput>[1]): void;
|
|
5
23
|
/** Whether the component identified by `focusId` is the currently focused one. `undefined` (a non-interactive render, or a component that never got a `focusId`) is never focused. */
|
|
6
24
|
export declare function useIsFocused(focusId: number | undefined): boolean;
|
|
7
25
|
/** Wraps `text` in the accent-colored focus marker when interactive AND `focusId` is the focused one; otherwise returns `text` unchanged. */
|
|
@@ -29,7 +47,7 @@ export declare function useFocusAnchor(focusId: number | undefined): (node: DOME
|
|
|
29
47
|
* yet (mount, or a reader who has not switched panels).
|
|
30
48
|
*/
|
|
31
49
|
export interface FocusableDescriptor {
|
|
32
|
-
kind: 'tabs' | 'details';
|
|
50
|
+
kind: 'tabs' | 'details' | 'pack';
|
|
33
51
|
label: string;
|
|
34
52
|
}
|
|
35
53
|
export interface InteractiveRootProps {
|
package/dist/interactive.js
CHANGED
|
@@ -11,6 +11,36 @@ const FocusContext = createContext({
|
|
|
11
11
|
reportActiveLabel: () => { },
|
|
12
12
|
registerAnchor: () => { },
|
|
13
13
|
});
|
|
14
|
+
/**
|
|
15
|
+
* The `focusId` of the pack component a subtree belongs to, or `undefined`
|
|
16
|
+
* outside one. Provided by `PackFocusScope`; read only by `usePackInput`.
|
|
17
|
+
*/
|
|
18
|
+
const PackFocusContext = createContext(undefined);
|
|
19
|
+
/**
|
|
20
|
+
* Wraps a focusable pack component's live element. It draws a two-column
|
|
21
|
+
* gutter (the accent marker while the component has focus, blanks
|
|
22
|
+
* otherwise) and names the component's focus id for `usePackInput`. The
|
|
23
|
+
* pack draws inside the width that is left.
|
|
24
|
+
*/
|
|
25
|
+
export function PackFocusScope({ focusId, children, }) {
|
|
26
|
+
const gutter = useFocusMarker('▌ ', focusId);
|
|
27
|
+
const focused = useIsFocused(focusId);
|
|
28
|
+
return (_jsx(PackFocusContext.Provider, { value: focusId, children: _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: focused ? gutter : ' ' }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: children })] }) }));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Ink's `useInput`, for a terminal pack: the same handler and options, but
|
|
32
|
+
* keys reach the handler only while the pack component whose subtree calls
|
|
33
|
+
* it has focus. Outside a focusable pack component it never fires. A pack
|
|
34
|
+
* reads this as `__markiiInk.useInput`, so it never sees raw stdin.
|
|
35
|
+
*/
|
|
36
|
+
export function usePackInput(handler, options = {}) {
|
|
37
|
+
const focusId = useContext(PackFocusContext);
|
|
38
|
+
const focused = useIsFocused(focusId);
|
|
39
|
+
useInput(handler, {
|
|
40
|
+
...options,
|
|
41
|
+
isActive: focused && options.isActive !== false,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
14
44
|
/** Whether the component identified by `focusId` is the currently focused one. `undefined` (a non-interactive render, or a component that never got a `focusId`) is never focused. */
|
|
15
45
|
export function useIsFocused(focusId) {
|
|
16
46
|
const api = useContext(FocusContext);
|
package/dist/pack-loader.js
CHANGED
|
@@ -142,6 +142,9 @@ export function loadAnsiPack(manifest, componentModules) {
|
|
|
142
142
|
const inline = kind === undefined ? module.inline : kind === 'inline';
|
|
143
143
|
if (inline !== undefined)
|
|
144
144
|
entry.inline = inline;
|
|
145
|
+
// Focus only makes sense for a block view.
|
|
146
|
+
if (module.focusable === true && inline !== true)
|
|
147
|
+
entry.focusable = true;
|
|
145
148
|
entries[composed.name] = entry;
|
|
146
149
|
}
|
|
147
150
|
return { registry: createAnsiRegistry(entries) };
|
package/dist/pack-script.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { AnsiPackToInstall } from './pack-loader.js';
|
|
|
6
6
|
* globalThis.__markiiRegisterTerminalPack(manifestJson, componentModules)
|
|
7
7
|
*
|
|
8
8
|
* with the raw `pack.json` text and an own-property map of
|
|
9
|
-
* `{ component, inline? }` keyed by local component name, the same shape
|
|
9
|
+
* `{ component, inline?, focusable? }` keyed by local component name, the same shape
|
|
10
10
|
* `__markiiRegisterPack` takes for the webview. React and Ink are NOT
|
|
11
11
|
* bundled into the script. The host sets `globalThis.__markiiReactInk`
|
|
12
12
|
* (the React instance Ink uses) and `globalThis.__markiiInk` (Ink's
|
package/dist/pack-script.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import * as Ink from 'ink';
|
|
3
|
+
import { usePackInput } from './interactive.js';
|
|
3
4
|
import { parsePackManifest } from '@markii/pack';
|
|
4
5
|
/**
|
|
5
6
|
* The terminal pack registration contract (docs/packs.md). A pack's
|
|
@@ -8,7 +9,7 @@ import { parsePackManifest } from '@markii/pack';
|
|
|
8
9
|
* globalThis.__markiiRegisterTerminalPack(manifestJson, componentModules)
|
|
9
10
|
*
|
|
10
11
|
* with the raw `pack.json` text and an own-property map of
|
|
11
|
-
* `{ component, inline? }` keyed by local component name, the same shape
|
|
12
|
+
* `{ component, inline?, focusable? }` keyed by local component name, the same shape
|
|
12
13
|
* `__markiiRegisterPack` takes for the webview. React and Ink are NOT
|
|
13
14
|
* bundled into the script. The host sets `globalThis.__markiiReactInk`
|
|
14
15
|
* (the React instance Ink uses) and `globalThis.__markiiInk` (Ink's
|
|
@@ -32,7 +33,9 @@ export const TERMINAL_INK_GLOBAL = '__markiiInk';
|
|
|
32
33
|
export function installTerminalPackGlobals() {
|
|
33
34
|
const g = globalThis;
|
|
34
35
|
g[TERMINAL_REACT_GLOBAL] = React;
|
|
35
|
-
|
|
36
|
+
// Ink's exports with `useInput` replaced by the focus-gated one: a pack
|
|
37
|
+
// gets keys while its component has focus and never raw stdin.
|
|
38
|
+
g[TERMINAL_INK_GLOBAL] = { ...Ink, useInput: usePackInput };
|
|
36
39
|
}
|
|
37
40
|
function isPlainObject(value) {
|
|
38
41
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -52,9 +55,16 @@ function toComponentModules(value) {
|
|
|
52
55
|
if (Object.hasOwn(entry, 'inline') && typeof entry.inline !== 'boolean') {
|
|
53
56
|
return undefined;
|
|
54
57
|
}
|
|
58
|
+
if (Object.hasOwn(entry, 'focusable') &&
|
|
59
|
+
typeof entry.focusable !== 'boolean') {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
55
62
|
out[key] = {
|
|
56
63
|
component: entry.component,
|
|
57
64
|
...(typeof entry.inline === 'boolean' ? { inline: entry.inline } : {}),
|
|
65
|
+
...(typeof entry.focusable === 'boolean'
|
|
66
|
+
? { focusable: entry.focusable }
|
|
67
|
+
: {}),
|
|
58
68
|
};
|
|
59
69
|
}
|
|
60
70
|
return out;
|
package/dist/registry.d.ts
CHANGED
|
@@ -171,6 +171,16 @@ export interface AnsiRegistryEntry {
|
|
|
171
171
|
* Ink primitives Yoga can reflow.
|
|
172
172
|
*/
|
|
173
173
|
selfLayout?: boolean;
|
|
174
|
+
/**
|
|
175
|
+
* Marks a BLOCK component whose returned Ink element the live viewer
|
|
176
|
+
* mounts and lets take focus, instead of drawing it as a static
|
|
177
|
+
* snapshot. It joins the focus order (up/down/j/k) and, while focused,
|
|
178
|
+
* its element's `useInput` calls receive keys (a pack reads
|
|
179
|
+
* `__markiiInk.useInput`, which is gated this way). In a render-once
|
|
180
|
+
* pass, or inside a framed body (a callout, a card) it renders as
|
|
181
|
+
* before: a snapshot with no focus.
|
|
182
|
+
*/
|
|
183
|
+
focusable?: boolean;
|
|
174
184
|
}
|
|
175
185
|
/** One alias: a second name for an existing component, optionally carrying preset attributes. */
|
|
176
186
|
export interface RegistryAlias {
|
package/dist/render.d.ts
CHANGED
|
@@ -14,12 +14,36 @@ export interface ElementTabPanel {
|
|
|
14
14
|
label: string;
|
|
15
15
|
body: ReactNode;
|
|
16
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;
|
|
17
26
|
/**
|
|
18
27
|
* Options for `renderMarkToAnsi`/`renderMarkNodeToAnsi`/`renderMarkInlineToAnsi`.
|
|
19
28
|
* `store`/`vault` are offered both here and as positional parameters; when
|
|
20
29
|
* both are given, the option wins.
|
|
21
30
|
*/
|
|
22
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;
|
|
23
47
|
readonly width?: number;
|
|
24
48
|
readonly color?: ColorOption;
|
|
25
49
|
readonly theme?: AnsiTheme;
|
|
@@ -33,6 +57,13 @@ export interface RenderMarkOptions {
|
|
|
33
57
|
readonly resolveImageSrc?: ResolveImageSrc;
|
|
34
58
|
readonly resolveHref?: ResolveHref;
|
|
35
59
|
readonly onDiagnostic?: OnDiagnostic;
|
|
60
|
+
/**
|
|
61
|
+
* Also report a directive that names no registered component as an
|
|
62
|
+
* `'unknown-directive'` event. Off by default: the fallback box already
|
|
63
|
+
* marks it in the output. A host that places findings in the note's
|
|
64
|
+
* source (a language server) turns it on.
|
|
65
|
+
*/
|
|
66
|
+
readonly reportUnknownDirectives?: boolean;
|
|
36
67
|
/**
|
|
37
68
|
* Components of installed packs that this render cannot draw, as plain
|
|
38
69
|
* data from the host. A directive named here renders the usual fallback
|
package/dist/render.js
CHANGED
|
@@ -15,13 +15,13 @@ import { sanitizeBlockText, sanitizeUrlText, stripControlCharacters, } from './s
|
|
|
15
15
|
import { readRegistryComponent, registryLayoutAxis, registrySelfLayout, resolveDirectiveAlias, } from './registry.js';
|
|
16
16
|
import { applyLayout, resolveLayoutAttributes, } from './layout.js';
|
|
17
17
|
import { resolveScopedPath } from './resolve.js';
|
|
18
|
-
import { dataStateSuffix, emptyInlineTitle, failureToken, formMismatchTitle, NO_ENGINE_VIEW_LABEL, noEngineViewTitle, invalidAttributeValueLabel, invalidAttributeValueTitle, unsafeImageSrcLabel, unsafeImageSrcTitle, } from './failure-presentation.js';
|
|
18
|
+
import { dataStateSuffix, emptyInlineTitle, failureToken, codeRendererFailedTitle, formMismatchTitle, NO_ENGINE_VIEW_LABEL, noEngineViewTitle, packLoadFailedLabel, packLoadFailedTitle, invalidAttributeValueLabel, invalidAttributeValueTitle, unsafeImageSrcLabel, unknownDirectiveTitle, unsafeImageSrcTitle, } from './failure-presentation.js';
|
|
19
19
|
import { createUnavailableComponentLookup, formatValue, getContract, reportDiagnostic, } from '@markii/stdlib';
|
|
20
20
|
import { resolveImageAttribute, } from './image-resolve.js';
|
|
21
21
|
import { resolveHrefAttribute } from './href-resolve.js';
|
|
22
22
|
import { defaultAnsiRegistry } from './components/index.js';
|
|
23
23
|
import { renderInkToString, renderInkToStringSync } from './ink-string.js';
|
|
24
|
-
import { InteractiveRoot } from './interactive.js';
|
|
24
|
+
import { InteractiveRoot, PackFocusScope } from './interactive.js';
|
|
25
25
|
import { glyphsFor } from './glyphs.js';
|
|
26
26
|
import { InteractiveTabs } from './components/tabs.js';
|
|
27
27
|
import { InteractiveDetails } from './components/details.js';
|
|
@@ -126,6 +126,29 @@ function nodeOrRootToHast(node) {
|
|
|
126
126
|
}
|
|
127
127
|
return { type: 'root', children };
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* A walk context whose `onDiagnostic` stamps each event with the source
|
|
131
|
+
* range of `element`, unless an inner directive already did. This is how a
|
|
132
|
+
* host (the language server) places a finding in the note without any
|
|
133
|
+
* component knowing about positions. A node with no position, or a render
|
|
134
|
+
* with no `onDiagnostic`, returns `wctx` itself.
|
|
135
|
+
*/
|
|
136
|
+
function withNodePosition(wctx, element) {
|
|
137
|
+
const downstream = wctx.onDiagnostic;
|
|
138
|
+
const range = element.position;
|
|
139
|
+
if (downstream === undefined || range === undefined)
|
|
140
|
+
return wctx;
|
|
141
|
+
const position = {
|
|
142
|
+
startLine: range.start.line,
|
|
143
|
+
startColumn: range.start.column,
|
|
144
|
+
endLine: range.end.line,
|
|
145
|
+
endColumn: range.end.column,
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
...wctx,
|
|
149
|
+
onDiagnostic: (event) => downstream(event.position === undefined ? { ...event, position } : event),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
129
152
|
function nextFocusId(wctx) {
|
|
130
153
|
const id = wctx.focusCounter.current;
|
|
131
154
|
wctx.focusCounter.current += 1;
|
|
@@ -208,8 +231,11 @@ function isRegisteredInline(entry) {
|
|
|
208
231
|
}
|
|
209
232
|
}
|
|
210
233
|
/** The fallback label line, worded for the reason and the form the directive was written in. Plain text (no escapes). */
|
|
211
|
-
function fallbackLabel(name, inline, reason) {
|
|
234
|
+
function fallbackLabel(name, inline, reason, pack) {
|
|
212
235
|
const code = `\`${stripControlCharacters(name)}\``;
|
|
236
|
+
if (reason === 'pack-load-failed') {
|
|
237
|
+
return `${packLoadFailedLabel(stripControlCharacters(pack ?? ''))} ${code}`;
|
|
238
|
+
}
|
|
213
239
|
if (reason === 'no-engine-view')
|
|
214
240
|
return `${NO_ENGINE_VIEW_LABEL} ${code}`;
|
|
215
241
|
if (reason === 'form-mismatch') {
|
|
@@ -220,8 +246,8 @@ function fallbackLabel(name, inline, reason) {
|
|
|
220
246
|
return `unknown component ${code}`;
|
|
221
247
|
}
|
|
222
248
|
/** The unknown-directive fallback: dim inline text for a text directive, a dashed dim frame for a block directive. Wording matches `@markii/html`'s `fallbackLabel` exactly. */
|
|
223
|
-
function unknownDirective(name, inline, childrenText, reason, width, color, glyphs) {
|
|
224
|
-
const label = fallbackLabel(name, inline, reason);
|
|
249
|
+
function unknownDirective(name, inline, childrenText, reason, width, color, glyphs, pack) {
|
|
250
|
+
const label = fallbackLabel(name, inline, reason, pack);
|
|
225
251
|
if (inline) {
|
|
226
252
|
return childrenText === ''
|
|
227
253
|
? dim(label, color)
|
|
@@ -380,10 +406,17 @@ function renderDirectiveContentString(name, kind, isBlock, attributes, childrenT
|
|
|
380
406
|
reportDiagnostic(wctx.onDiagnostic, {
|
|
381
407
|
kind: 'no-engine-view',
|
|
382
408
|
directive: name,
|
|
383
|
-
message:
|
|
409
|
+
message: missingView.loadFailed
|
|
410
|
+
? packLoadFailedTitle(name, missingView.pack)
|
|
411
|
+
: noEngineViewTitle(name, missingView.pack, missingView.engines),
|
|
384
412
|
});
|
|
385
|
-
return unknownDirective(name, inline, safeChildrenText(childrenText), 'no-engine-view', width, wctx.color, wctx.glyphs);
|
|
413
|
+
return unknownDirective(name, inline, safeChildrenText(childrenText), missingView.loadFailed ? 'pack-load-failed' : 'no-engine-view', width, wctx.color, wctx.glyphs, missingView.pack);
|
|
386
414
|
}
|
|
415
|
+
reportDiagnostic(wctx.onDiagnostic, {
|
|
416
|
+
kind: 'unknown-directive',
|
|
417
|
+
directive: name || '(unnamed)',
|
|
418
|
+
message: unknownDirectiveTitle(name || '(unnamed)'),
|
|
419
|
+
});
|
|
387
420
|
return unknownDirective(name || '(unnamed)', inline, safeChildrenText(childrenText), 'unregistered', width, wctx.color, wctx.glyphs);
|
|
388
421
|
}
|
|
389
422
|
if (isFormMismatch(entry, kind)) {
|
|
@@ -536,6 +569,9 @@ function renderBlockString(node, width, indent, wctx) {
|
|
|
536
569
|
const marker = renderScriptMarker(node, wctx.color, wctx.glyphs);
|
|
537
570
|
if (marker !== undefined)
|
|
538
571
|
return marker;
|
|
572
|
+
const custom = applyRenderCode(node, wctx);
|
|
573
|
+
if (custom !== undefined)
|
|
574
|
+
return custom;
|
|
539
575
|
return renderCodeBlock(node, wctx.color);
|
|
540
576
|
}
|
|
541
577
|
return wrapText(renderInlineChildrenString(node.children ?? [], width, indent, wctx), width).join('\n');
|
|
@@ -648,6 +684,35 @@ function renderScriptMarker(node, color, glyphs) {
|
|
|
648
684
|
return undefined;
|
|
649
685
|
}
|
|
650
686
|
}
|
|
687
|
+
/**
|
|
688
|
+
* Offers one ordinary fenced code block to the host's `renderCode`. The
|
|
689
|
+
* text handed over is already run through `sanitizeBlockText` (control
|
|
690
|
+
* characters dropped, tabs expanded), so a host that echoes it cannot
|
|
691
|
+
* smuggle author escapes into the terminal. Returns the host's string, or
|
|
692
|
+
* `undefined` to draw the block as usual: no callback, a decline, a
|
|
693
|
+
* non-string result, or a throw (reported through `onDiagnostic`).
|
|
694
|
+
*/
|
|
695
|
+
function applyRenderCode(node, wctx) {
|
|
696
|
+
if (!wctx.renderCode)
|
|
697
|
+
return undefined;
|
|
698
|
+
const codeNode = findChildElement(node, 'code');
|
|
699
|
+
if (!codeNode)
|
|
700
|
+
return undefined;
|
|
701
|
+
try {
|
|
702
|
+
const result = wctx.renderCode({
|
|
703
|
+
lang: getLanguage(codeNode) || undefined,
|
|
704
|
+
code: sanitizeBlockText(getCodeText(codeNode)),
|
|
705
|
+
});
|
|
706
|
+
return typeof result === 'string' ? result : undefined;
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
reportDiagnostic(wctx.onDiagnostic, {
|
|
710
|
+
kind: 'code-renderer-failed',
|
|
711
|
+
message: codeRendererFailedTitle(),
|
|
712
|
+
});
|
|
713
|
+
return undefined;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
651
716
|
function renderCodeBlock(node, color) {
|
|
652
717
|
const codeNode = findChildElement(node, 'code');
|
|
653
718
|
const language = getLanguage(codeNode);
|
|
@@ -775,7 +840,8 @@ function renderBlocksString(nodes, width, indent, wctx) {
|
|
|
775
840
|
}
|
|
776
841
|
return blocks.join('\n\n');
|
|
777
842
|
}
|
|
778
|
-
function renderDirectiveString(element, width, indent,
|
|
843
|
+
function renderDirectiveString(element, width, indent, walk) {
|
|
844
|
+
const wctx = withNodePosition(walk, element);
|
|
779
845
|
const written = stringProperty(element, 'data-mk-name') ?? '';
|
|
780
846
|
const kind = stringProperty(element, 'data-mk-kind');
|
|
781
847
|
const rawAttributes = parseAttributes(stringProperty(element, 'data-mk-attrs'));
|
|
@@ -1204,7 +1270,8 @@ function renderFigureElement(element, attributes, width, indent, wctx) {
|
|
|
1204
1270
|
return (_jsxs(Box, { flexDirection: "column", children: [headerElement, body] }));
|
|
1205
1271
|
}
|
|
1206
1272
|
/** Renders one directive as a real Ink element (block position only). Dispatches `row`/`tabs`/`details`/`card`/`figure` to their real builders; every other directive falls back to the string engine, wrapped in one `<Text>`. */
|
|
1207
|
-
function renderDirectiveElement(element, width, indent,
|
|
1273
|
+
function renderDirectiveElement(element, width, indent, walk) {
|
|
1274
|
+
const wctx = withNodePosition(walk, element);
|
|
1208
1275
|
const written = stringProperty(element, 'data-mk-name') ?? '';
|
|
1209
1276
|
const kind = stringProperty(element, 'data-mk-kind');
|
|
1210
1277
|
const rawAttributes = parseAttributes(stringProperty(element, 'data-mk-attrs'));
|
|
@@ -1275,8 +1342,48 @@ function renderDirectiveElement(element, width, indent, wctx) {
|
|
|
1275
1342
|
return styledText(componentError(name || '(unnamed)', false, '', width, wctx.color, wctx.glyphs));
|
|
1276
1343
|
}
|
|
1277
1344
|
}
|
|
1345
|
+
if (entry?.focusable === true && !formMismatch && wctx.interactive) {
|
|
1346
|
+
const focusable = renderFocusablePackElement(element, name, aliased, width, indent, wctx);
|
|
1347
|
+
if (focusable !== undefined)
|
|
1348
|
+
return focusable;
|
|
1349
|
+
}
|
|
1278
1350
|
return styledText(renderDirectiveString(element, width, indent, wctx));
|
|
1279
1351
|
}
|
|
1352
|
+
/**
|
|
1353
|
+
* A `focusable` registry component in the live viewer: the component runs
|
|
1354
|
+
* as usual, but an Ink element it returns is mounted (not drawn once) in a
|
|
1355
|
+
* `PackFocusScope`, so it joins the focus order and receives keys while
|
|
1356
|
+
* focused. Returns `undefined` when the component returns a string or
|
|
1357
|
+
* throws, so the caller falls back to the string path (which reports the
|
|
1358
|
+
* failure the usual way). The component draws two columns narrower: the
|
|
1359
|
+
* scope's gutter carries the focus marker.
|
|
1360
|
+
*/
|
|
1361
|
+
function renderFocusablePackElement(element, name, attributes, width, indent, wctx) {
|
|
1362
|
+
const component = readRegistryComponent(wctx.registry[name]);
|
|
1363
|
+
if (component == null)
|
|
1364
|
+
return undefined;
|
|
1365
|
+
const innerWidth = Math.max(1, width - 2);
|
|
1366
|
+
const binding = resolveDataAttribute(attributes, wctx.scope);
|
|
1367
|
+
const focusId = nextFocusId(wctx);
|
|
1368
|
+
recordFocusable(wctx, { kind: 'pack', label: stripControlCharacters(name) });
|
|
1369
|
+
const ctx = buildComponentContext(innerWidth, indent, wctx, binding, undefined, focusId);
|
|
1370
|
+
let result;
|
|
1371
|
+
try {
|
|
1372
|
+
result = component({
|
|
1373
|
+
attributes: binding.attributes,
|
|
1374
|
+
children: renderDirectiveChildrenString(element.children, innerWidth, indent, wctx),
|
|
1375
|
+
ctx,
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
catch {
|
|
1379
|
+
// Same as the string path: the failure is contained to a neutral box.
|
|
1380
|
+
return styledText(componentError(name || '(unnamed)', false, '', width, wctx.color, wctx.glyphs));
|
|
1381
|
+
}
|
|
1382
|
+
if (!isValidElement(result)) {
|
|
1383
|
+
return styledText(typeof result === 'string' ? result : '');
|
|
1384
|
+
}
|
|
1385
|
+
return _jsx(PackFocusScope, { focusId: focusId, children: result });
|
|
1386
|
+
}
|
|
1280
1387
|
/** Renders one block-level hast node as a real Ink element, or `null` for a node that carries no content (whitespace-only text). */
|
|
1281
1388
|
function renderBlockElement(node, width, indent, wctx) {
|
|
1282
1389
|
if (node.type === 'text') {
|
|
@@ -1355,7 +1462,7 @@ function renderFailureFallback(error, color) {
|
|
|
1355
1462
|
const message = error instanceof Error ? error.message : String(error);
|
|
1356
1463
|
return `${dim('failed to render document', color)}\n${stripControlCharacters(message)}\n`;
|
|
1357
1464
|
}
|
|
1358
|
-
function buildWalkContext(registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable) {
|
|
1465
|
+
function buildWalkContext(registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, renderCode) {
|
|
1359
1466
|
return {
|
|
1360
1467
|
registry,
|
|
1361
1468
|
scope,
|
|
@@ -1367,20 +1474,38 @@ function buildWalkContext(registry, scope, color, theme, glyphs, interactive, re
|
|
|
1367
1474
|
resolveHref,
|
|
1368
1475
|
onDiagnostic,
|
|
1369
1476
|
unavailable,
|
|
1477
|
+
renderCode,
|
|
1370
1478
|
focusCounter: { current: 0 },
|
|
1371
1479
|
focusables: undefined,
|
|
1372
1480
|
};
|
|
1373
1481
|
}
|
|
1482
|
+
/**
|
|
1483
|
+
* The `onDiagnostic` a render hands its walk: the caller's own, minus
|
|
1484
|
+
* `'unknown-directive'` events unless `reportUnknownDirectives` asked for
|
|
1485
|
+
* them. The fallback box already marks an unknown directive in the output,
|
|
1486
|
+
* so a host that does not place findings in a source (the viewer, an
|
|
1487
|
+
* export) keeps the quiet diagnostics surface it always had.
|
|
1488
|
+
*/
|
|
1489
|
+
function diagnosticsFor(options) {
|
|
1490
|
+
const downstream = options?.onDiagnostic;
|
|
1491
|
+
if (downstream === undefined || options?.reportUnknownDirectives === true) {
|
|
1492
|
+
return downstream;
|
|
1493
|
+
}
|
|
1494
|
+
return (event) => {
|
|
1495
|
+
if (event.kind !== 'unknown-directive')
|
|
1496
|
+
downstream(event);
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1374
1499
|
/** The columns assumed when a caller supplies no `width` option. */
|
|
1375
1500
|
const DEFAULT_WIDTH = 80;
|
|
1376
1501
|
/** Builds the root Ink element for `root`, given every resolved render input. Exported for `buildMarkElement` (the live-viewer entry point, phase 3) as well as this module's own three string-producing entry points. */
|
|
1377
|
-
function buildRootElement(root, registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, width) {
|
|
1378
|
-
const wctx = buildWalkContext(registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable);
|
|
1502
|
+
function buildRootElement(root, registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, width, renderCode) {
|
|
1503
|
+
const wctx = buildWalkContext(registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, renderCode);
|
|
1379
1504
|
const body = renderBlocksElement(root.children, width, '', wctx);
|
|
1380
1505
|
return (_jsx(Box, { width: width, flexDirection: "column", children: body ?? _jsx(Text, { children: " " }) }));
|
|
1381
1506
|
}
|
|
1382
|
-
async function renderRootToString(root, registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, width) {
|
|
1383
|
-
const element = buildRootElement(root, registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, width);
|
|
1507
|
+
async function renderRootToString(root, registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, width, renderCode) {
|
|
1508
|
+
const element = buildRootElement(root, registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, width, renderCode);
|
|
1384
1509
|
const raw = await renderInkToString(element, width);
|
|
1385
1510
|
return finalizeOutput(raw);
|
|
1386
1511
|
}
|
|
@@ -1405,7 +1530,7 @@ async function renderRootToString(root, registry, scope, color, theme, glyphs, i
|
|
|
1405
1530
|
export async function renderMarkToAnsi(text, registry = defaultAnsiRegistry, store, vault, options) {
|
|
1406
1531
|
const color = resolveColorOption(options?.color);
|
|
1407
1532
|
try {
|
|
1408
|
-
return await renderRootToString(toHast(text), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, color, options?.theme ?? defaultAnsiTheme, glyphsFor(options?.box), options?.interactive ?? false, options?.resolveImageSrc, options?.resolveHref, options
|
|
1533
|
+
return await renderRootToString(toHast(text), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, color, options?.theme ?? defaultAnsiTheme, glyphsFor(options?.box), options?.interactive ?? false, options?.resolveImageSrc, options?.resolveHref, diagnosticsFor(options), createUnavailableComponentLookup(options?.unavailableComponents), options?.width ?? DEFAULT_WIDTH, options?.renderCode);
|
|
1409
1534
|
}
|
|
1410
1535
|
catch (error) {
|
|
1411
1536
|
return renderFailureFallback(error, color);
|
|
@@ -1420,7 +1545,7 @@ export async function renderMarkToAnsi(text, registry = defaultAnsiRegistry, sto
|
|
|
1420
1545
|
export async function renderMarkNodeToAnsi(node, registry = defaultAnsiRegistry, store, vault, options) {
|
|
1421
1546
|
const color = resolveColorOption(options?.color);
|
|
1422
1547
|
try {
|
|
1423
|
-
return await renderRootToString(nodeOrRootToHast(node), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, color, options?.theme ?? defaultAnsiTheme, glyphsFor(options?.box), options?.interactive ?? false, options?.resolveImageSrc, options?.resolveHref, options
|
|
1548
|
+
return await renderRootToString(nodeOrRootToHast(node), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, color, options?.theme ?? defaultAnsiTheme, glyphsFor(options?.box), options?.interactive ?? false, options?.resolveImageSrc, options?.resolveHref, diagnosticsFor(options), createUnavailableComponentLookup(options?.unavailableComponents), options?.width ?? DEFAULT_WIDTH, options?.renderCode);
|
|
1424
1549
|
}
|
|
1425
1550
|
catch (error) {
|
|
1426
1551
|
return renderFailureFallback(error, color);
|
|
@@ -1484,7 +1609,7 @@ export function buildMarkElement(document, registry = defaultAnsiRegistry, optio
|
|
|
1484
1609
|
root = { type: 'root', children: [] };
|
|
1485
1610
|
}
|
|
1486
1611
|
const unavailable = createUnavailableComponentLookup(options?.unavailableComponents);
|
|
1487
|
-
const content = buildRootElement(root, registry, scope, color, theme, glyphs, interactive, options?.resolveImageSrc, options?.resolveHref, options
|
|
1612
|
+
const content = buildRootElement(root, registry, scope, color, theme, glyphs, interactive, options?.resolveImageSrc, options?.resolveHref, diagnosticsFor(options), unavailable, width, options?.renderCode);
|
|
1488
1613
|
if (!interactive)
|
|
1489
1614
|
return content;
|
|
1490
1615
|
// The walk above already assigned every focusable component's `focusId`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markii/ansi",
|
|
3
|
-
"version": "0.17.0-next.
|
|
3
|
+
"version": "0.17.0-next.2",
|
|
4
4
|
"description": "The terminal renderer for Markii (.mk.md): a registry-driven hast-to-terminal engine built on Ink. Renders a note once as a string with optional color, for a pipe or a log, or as a live Ink tree a host can mount.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -51,15 +51,15 @@
|
|
|
51
51
|
"regenerate:fixtures": "npm run build:dist && node scripts/regenerate-ansi-fixtures.ts"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@markii/core": "^0.17.0-next.
|
|
55
|
-
"@markii/pack": "^0.17.0-next.
|
|
56
|
-
"@markii/stdlib": "^0.17.0-next.
|
|
54
|
+
"@markii/core": "^0.17.0-next.2",
|
|
55
|
+
"@markii/pack": "^0.17.0-next.2",
|
|
56
|
+
"@markii/stdlib": "^0.17.0-next.2",
|
|
57
57
|
"ink": "^7.1.1",
|
|
58
58
|
"react": "^19.2.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/hast": "^3.0.4",
|
|
62
|
-
"@markii/runtime": "^0.17.0-next.
|
|
62
|
+
"@markii/runtime": "^0.17.0-next.2",
|
|
63
63
|
"@types/react": "^19.2.0"
|
|
64
64
|
}
|
|
65
65
|
}
|