@markii/ansi 0.15.0 → 0.17.0-next.0
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/ansi.d.ts +26 -0
- package/dist/ansi.js +51 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +5 -4
- package/dist/components/callout.d.ts +38 -13
- package/dist/components/callout.js +68 -31
- package/dist/components/card.d.ts +34 -2
- package/dist/components/card.js +55 -18
- package/dist/components/cell.d.ts +5 -11
- package/dist/components/cell.js +11 -15
- package/dist/components/chart.d.ts +4 -10
- package/dist/components/chart.js +16 -24
- package/dist/components/details-string.d.ts +26 -0
- package/dist/components/details-string.js +39 -0
- package/dist/components/details.d.ts +32 -10
- package/dist/components/details.js +28 -25
- package/dist/components/divider.js +12 -16
- package/dist/components/figure.d.ts +5 -16
- package/dist/components/figure.js +9 -19
- package/dist/components/index.d.ts +14 -10
- package/dist/components/index.js +19 -16
- package/dist/components/kbd.d.ts +1 -1
- package/dist/components/kbd.js +3 -2
- package/dist/components/layout-wrapper.d.ts +7 -22
- package/dist/components/layout-wrapper.js +9 -23
- package/dist/components/progress.d.ts +3 -11
- package/dist/components/progress.js +8 -17
- package/dist/components/rating.d.ts +1 -2
- package/dist/components/rating.js +3 -4
- package/dist/components/row.d.ts +35 -20
- package/dist/components/row.js +35 -51
- package/dist/components/stat.d.ts +3 -8
- package/dist/components/stat.js +5 -11
- package/dist/components/tab.d.ts +9 -8
- package/dist/components/tab.js +12 -10
- package/dist/components/table-grid.d.ts +2 -1
- package/dist/components/table-grid.js +14 -12
- package/dist/components/table.d.ts +5 -10
- package/dist/components/table.js +8 -13
- package/dist/components/tabs-string.d.ts +17 -0
- package/dist/components/tabs-string.js +30 -0
- package/dist/components/tabs.d.ts +31 -14
- package/dist/components/tabs.js +55 -30
- package/dist/failure-presentation.d.ts +15 -0
- package/dist/failure-presentation.js +22 -0
- package/dist/glyphs.d.ts +73 -0
- package/dist/glyphs.js +88 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +10 -7
- package/dist/ink-string.d.ts +23 -0
- package/dist/ink-string.js +65 -0
- package/dist/interactive.d.ts +66 -0
- package/dist/interactive.js +207 -0
- package/dist/layout.js +1 -2
- 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 +106 -100
- package/dist/registry.js +17 -9
- package/dist/render.d.ts +63 -25
- package/dist/render.js +1001 -380
- package/dist/text-grid.d.ts +90 -0
- package/dist/text-grid.js +300 -0
- package/package.json +9 -5
- package/dist/box.d.ts +0 -47
- package/dist/box.js +0 -209
- package/dist/measure.d.ts +0 -25
- package/dist/measure.js +0 -108
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { childrenText } from '../registry.js';
|
|
2
|
+
/** The visible marker appended to whichever tab is treated as "active". */
|
|
3
|
+
const ACTIVE_MARKER = ' (active)';
|
|
4
|
+
/**
|
|
5
|
+
* The STRING-mode `tabs` registration — the fallback rendering used only
|
|
6
|
+
* when a `tabs` directive is reached through `render.tsx`'s STRING mode
|
|
7
|
+
* (nested inside a self-drawing container) AND, unlike `row`, not
|
|
8
|
+
* reachable through the panel-extraction path there either (both of
|
|
9
|
+
* `render.tsx`'s STRING and ELEMENT dispatchers for `tabs` build panels
|
|
10
|
+
* directly from the hast tree via `extractTabPanels`/`extractTabPanelsElement`
|
|
11
|
+
* rather than calling this component, since only the hast tree — not an
|
|
12
|
+
* already-flattened string — can tell each `tab` child's own `label` apart
|
|
13
|
+
* from its neighbor without a fragile heuristic). This registration exists
|
|
14
|
+
* so the registry always has a real `tabs` component (alias resolution,
|
|
15
|
+
* the contract-drift coverage test, and a caller invoking it directly all
|
|
16
|
+
* see one), using the same blank-line-boundary heuristic the pre-Ink engine
|
|
17
|
+
* used when it has nothing but a flattened string to work with.
|
|
18
|
+
*/
|
|
19
|
+
export const Tabs = ({ children }) => {
|
|
20
|
+
const text = childrenText(children);
|
|
21
|
+
if (!text.trim())
|
|
22
|
+
return '';
|
|
23
|
+
const blocks = text.split('\n\n');
|
|
24
|
+
const [first, ...rest] = blocks;
|
|
25
|
+
if (first === undefined)
|
|
26
|
+
return text;
|
|
27
|
+
const lines = first.split('\n');
|
|
28
|
+
lines[0] = `${lines[0] ?? ''}${ACTIVE_MARKER}`;
|
|
29
|
+
return [lines.join('\n'), ...rest].join('\n\n');
|
|
30
|
+
};
|
|
@@ -1,18 +1,35 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { GlyphSet } from '../glyphs.js';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import type { ColorLevel } from '../ansi.js';
|
|
4
|
+
import type { ElementTabPanel } from '../render.js';
|
|
2
5
|
/**
|
|
3
6
|
* `::::tabs :::tab{label="..."} ... ::: :::tab{label="..."} ... ::: ::::` —
|
|
4
|
-
* a tabbed panel switcher
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* a tabbed panel switcher, built as a real Ink element (`render.tsx`'s
|
|
8
|
+
* `renderTabsElement` calls this directly rather than going through the
|
|
9
|
+
* ordinary registry-component invocation, so it can hand it real per-panel
|
|
10
|
+
* subtrees — see `registry.ts`'s and `render.tsx`'s doc comments).
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* the
|
|
12
|
+
* Non-interactive (the default, and the render-once path): every panel is
|
|
13
|
+
* shown, stacked, in document order, the first one's heading marked
|
|
14
|
+
* `(active)` — matching the pre-Ink engine's faithfulness limitation of
|
|
15
|
+
* showing every panel rather than picking one, since a static render has no
|
|
16
|
+
* way to ask which panel a reader wants.
|
|
17
|
+
*
|
|
18
|
+
* Interactive: only the active panel's body is shown; the header row lists
|
|
19
|
+
* every label, the active one bracketed; left/right or Tab switches it when
|
|
20
|
+
* this `tabs` instance is focused (`useIsFocused`). When this `tabs`
|
|
21
|
+
* instance itself is the FOCUSED component (as opposed to merely being
|
|
22
|
+
* interactive), the whole header strip is drawn in the accent color and
|
|
23
|
+
* prefixed with a `› ` glyph (`useFocusMarker`), so focus is visible even
|
|
24
|
+
* on a theme with no perceptible accent color; an unfocused header strip
|
|
25
|
+
* gets two leading spaces instead, so nothing shifts horizontally when
|
|
26
|
+
* focus moves onto or off of this block.
|
|
17
27
|
*/
|
|
18
|
-
export
|
|
28
|
+
export interface InteractiveTabsProps {
|
|
29
|
+
panels: readonly ElementTabPanel[];
|
|
30
|
+
color: ColorLevel;
|
|
31
|
+
glyphs: GlyphSet;
|
|
32
|
+
interactive: boolean;
|
|
33
|
+
focusId: number | undefined;
|
|
34
|
+
}
|
|
35
|
+
export declare function InteractiveTabs({ panels, color, glyphs, interactive, focusId, }: InteractiveTabsProps): ReactNode;
|
package/dist/components/tabs.js
CHANGED
|
@@ -1,30 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { bold, dim } from '../ansi.js';
|
|
5
|
+
import { useFocusAnchor, useFocusMarker, useIsFocused, useReportActiveLabel, } from '../interactive.js';
|
|
6
|
+
export function InteractiveTabs({ panels, color, glyphs, interactive, focusId, }) {
|
|
7
|
+
const [active, setActive] = useState(0);
|
|
8
|
+
const focused = useIsFocused(focusId);
|
|
9
|
+
const reportActiveLabel = useReportActiveLabel();
|
|
10
|
+
const anchor = useFocusAnchor(focusId);
|
|
11
|
+
useInput((_input, key) => {
|
|
12
|
+
if (panels.length === 0)
|
|
13
|
+
return;
|
|
14
|
+
let nextIndex;
|
|
15
|
+
if (key.leftArrow) {
|
|
16
|
+
nextIndex = (active - 1 + panels.length) % panels.length;
|
|
17
|
+
}
|
|
18
|
+
else if (key.rightArrow || key.tab) {
|
|
19
|
+
nextIndex = (active + 1) % panels.length;
|
|
20
|
+
}
|
|
21
|
+
if (nextIndex === undefined)
|
|
22
|
+
return;
|
|
23
|
+
setActive(nextIndex);
|
|
24
|
+
}, { isActive: interactive && focused });
|
|
25
|
+
// Every hook this component calls runs BEFORE the non-interactive early
|
|
26
|
+
// return below, so the hook order is identical in both modes. The header
|
|
27
|
+
// strip is cheap to build and unused in the non-interactive branch; that
|
|
28
|
+
// waste is the price of never making a hook call conditional.
|
|
29
|
+
const activeIndex = Math.min(active, Math.max(0, panels.length - 1));
|
|
30
|
+
const activePanel = panels[activeIndex];
|
|
31
|
+
const activeLabel = activePanel?.label ?? '';
|
|
32
|
+
// Tells the status line which panel is showing. Reported from an effect
|
|
33
|
+
// on the active label itself, so it also follows a re-parse (`--watch`)
|
|
34
|
+
// that changes the panels under a mounted block. The root ignores a
|
|
35
|
+
// report that repeats what it already has, so this cannot loop.
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (interactive && focusId !== undefined) {
|
|
38
|
+
reportActiveLabel(focusId, activeLabel);
|
|
39
|
+
}
|
|
40
|
+
}, [interactive, focusId, activeLabel, reportActiveLabel]);
|
|
41
|
+
const strip = panels
|
|
42
|
+
.map((panel, index) => {
|
|
43
|
+
const rendered = index === activeIndex
|
|
44
|
+
? bold(`[${panel.label}]`, color)
|
|
45
|
+
: dim(panel.label, color);
|
|
46
|
+
return index > 0 ? ` ${rendered}` : rendered;
|
|
47
|
+
})
|
|
48
|
+
.join('');
|
|
49
|
+
const focusGlyph = focused ? glyphs.focus : ' ';
|
|
50
|
+
const headingLine = useFocusMarker(`${focusGlyph}${strip}`, focusId);
|
|
51
|
+
if (!interactive) {
|
|
52
|
+
return (_jsx(Box, { flexDirection: "column", children: panels.map((panel, index) => (_jsxs(Box, { marginTop: index > 0 ? 1 : 0, flexDirection: "column", children: [_jsx(Text, { children: bold(`${panel.label}${index === 0 ? ' (active)' : ''}`, color) }), panel.body] }, index))) }));
|
|
53
|
+
}
|
|
54
|
+
return (_jsxs(Box, { ref: anchor, flexDirection: "column", children: [_jsx(Text, { children: headingLine }), activePanel?.body] }));
|
|
55
|
+
}
|
|
@@ -37,6 +37,8 @@ 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 registered block component written as an inline directive. Wording identical to `@markii/html`'s `formMismatchTitle`. */
|
|
41
|
+
export declare function formMismatchTitle(directive: string): string;
|
|
40
42
|
/**
|
|
41
43
|
* The INLINE marker text for a known attribute's value outside its closed
|
|
42
44
|
* enum, this engine's own departure from `@markii/html`'s identical wording:
|
|
@@ -56,3 +58,16 @@ export declare function invalidAttributeValueLabel(directive: string, attribute:
|
|
|
56
58
|
* only.
|
|
57
59
|
*/
|
|
58
60
|
export declare function unsafeImageSrcLabel(directive: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* The words of a fallback label for a directive that names a component of
|
|
63
|
+
* an installed pack with no view in this engine. Followed by the directive
|
|
64
|
+
* name in backticks, like the other fallback labels.
|
|
65
|
+
*/
|
|
66
|
+
export declare const NO_ENGINE_VIEW_LABEL = "no terminal view for";
|
|
67
|
+
/**
|
|
68
|
+
* The diagnostic text for the same case: the pack, and the engines the
|
|
69
|
+
* component does ship a view for. A terminal has no tooltip, so this
|
|
70
|
+
* sentence reaches the host's diagnostics surface through `onDiagnostic`
|
|
71
|
+
* and is never printed in the note.
|
|
72
|
+
*/
|
|
73
|
+
export declare function noEngineViewTitle(directive: string, pack: string, engines: readonly string[]): string;
|
|
@@ -99,6 +99,10 @@ 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 registered block component written as an inline directive. Wording identical to `@markii/html`'s `formMismatchTitle`. */
|
|
103
|
+
export function formMismatchTitle(directive) {
|
|
104
|
+
return `${directive}: block component was written inline and was not rendered`;
|
|
105
|
+
}
|
|
102
106
|
/**
|
|
103
107
|
* The INLINE marker text for a known attribute's value outside its closed
|
|
104
108
|
* enum, this engine's own departure from `@markii/html`'s identical wording:
|
|
@@ -122,3 +126,21 @@ export function invalidAttributeValueLabel(directive, attribute) {
|
|
|
122
126
|
export function unsafeImageSrcLabel(directive) {
|
|
123
127
|
return `${directive}: image not shown`;
|
|
124
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* The words of a fallback label for a directive that names a component of
|
|
131
|
+
* an installed pack with no view in this engine. Followed by the directive
|
|
132
|
+
* name in backticks, like the other fallback labels.
|
|
133
|
+
*/
|
|
134
|
+
export const NO_ENGINE_VIEW_LABEL = 'no terminal view for';
|
|
135
|
+
/**
|
|
136
|
+
* The diagnostic text for the same case: the pack, and the engines the
|
|
137
|
+
* component does ship a view for. A terminal has no tooltip, so this
|
|
138
|
+
* sentence reaches the host's diagnostics surface through `onDiagnostic`
|
|
139
|
+
* and is never printed in the note.
|
|
140
|
+
*/
|
|
141
|
+
export function noEngineViewTitle(directive, pack, engines) {
|
|
142
|
+
const has = engines.length > 0
|
|
143
|
+
? `it has views for: ${engines.join(', ')}`
|
|
144
|
+
: 'it has no views for any engine';
|
|
145
|
+
return `The ${pack} pack has no terminal view for this component; ${has}`;
|
|
146
|
+
}
|
package/dist/glyphs.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every glyph this engine draws on its own, as one table per box style.
|
|
3
|
+
* `unicode` is the default; `ascii` is a 7-bit form for a terminal that
|
|
4
|
+
* cannot show box drawing or symbols. Every component, frame, rule, and
|
|
5
|
+
* status-line string reads its glyphs from the set the render was given,
|
|
6
|
+
* so the two styles cannot drift apart: `glyphs.test.ts` renders the whole
|
|
7
|
+
* render corpus in `ascii` and fails on any byte above 0x7f that did not
|
|
8
|
+
* come from the note itself.
|
|
9
|
+
*
|
|
10
|
+
* Each entry occupies the same number of terminal columns in both sets
|
|
11
|
+
* where layout depends on it (frames, rules, bars, focus and fold markers),
|
|
12
|
+
* so switching style never changes how a note wraps. The engine never
|
|
13
|
+
* picks a style from the environment; the host decides.
|
|
14
|
+
*/
|
|
15
|
+
export declare const BOX_STYLES: readonly ["unicode", "ascii"];
|
|
16
|
+
export type BoxStyle = (typeof BOX_STYLES)[number];
|
|
17
|
+
export declare function isBoxStyle(value: string): value is BoxStyle;
|
|
18
|
+
/** One frame edge set: the solid frame of a card, or the dashed one of a fallback box. */
|
|
19
|
+
export interface FrameEdges {
|
|
20
|
+
readonly horizontal: string;
|
|
21
|
+
readonly vertical: string;
|
|
22
|
+
}
|
|
23
|
+
export interface GlyphSet {
|
|
24
|
+
/** Corners shared by every frame and by a table's outer edge. */
|
|
25
|
+
readonly topLeft: string;
|
|
26
|
+
readonly topRight: string;
|
|
27
|
+
readonly bottomLeft: string;
|
|
28
|
+
readonly bottomRight: string;
|
|
29
|
+
readonly solid: FrameEdges;
|
|
30
|
+
readonly dashed: FrameEdges;
|
|
31
|
+
/** A table's inner junctions. */
|
|
32
|
+
readonly teeDown: string;
|
|
33
|
+
readonly teeUp: string;
|
|
34
|
+
readonly teeRight: string;
|
|
35
|
+
readonly teeLeft: string;
|
|
36
|
+
readonly cross: string;
|
|
37
|
+
/** A thematic break and a `divider` drawn as a line. */
|
|
38
|
+
readonly rule: string;
|
|
39
|
+
/** A `divider` drawn as dots. */
|
|
40
|
+
readonly ruleDots: string;
|
|
41
|
+
/** A `divider{style=ornament}` centerpiece. */
|
|
42
|
+
readonly ornament: string;
|
|
43
|
+
/** The bar a blockquote's lines start with, including its trailing space. */
|
|
44
|
+
readonly quoteBar: string;
|
|
45
|
+
/** The bar a callout's lines start with, including its trailing space. */
|
|
46
|
+
readonly calloutBar: string;
|
|
47
|
+
readonly calloutIcons: {
|
|
48
|
+
readonly info: string;
|
|
49
|
+
readonly warning: string;
|
|
50
|
+
readonly danger: string;
|
|
51
|
+
};
|
|
52
|
+
/** The marker before a focused block's heading, including its trailing space. */
|
|
53
|
+
readonly focus: string;
|
|
54
|
+
/** A details block's fold marker. */
|
|
55
|
+
readonly foldOpen: string;
|
|
56
|
+
readonly foldClosed: string;
|
|
57
|
+
/** The collapsed script marker: its leading symbol and the separator before its detail. */
|
|
58
|
+
readonly script: string;
|
|
59
|
+
readonly scriptSeparator: string;
|
|
60
|
+
/** Eight steps, lowest first, that a line chart's sparkline is built from. */
|
|
61
|
+
readonly sparkline: string;
|
|
62
|
+
/** A filled bar cell (chart bars, a progress bar's done part) and an empty one. */
|
|
63
|
+
readonly barFilled: string;
|
|
64
|
+
readonly barEmpty: string;
|
|
65
|
+
readonly starFilled: string;
|
|
66
|
+
readonly starEmpty: string;
|
|
67
|
+
/** What a `stat` shows when its value is empty. */
|
|
68
|
+
readonly emptyValue: string;
|
|
69
|
+
/** The key legend on the live viewer's status line. */
|
|
70
|
+
readonly keyLegend: string;
|
|
71
|
+
}
|
|
72
|
+
/** The glyph set for `style`; anything other than `'ascii'` gets the unicode set. */
|
|
73
|
+
export declare function glyphsFor(style: BoxStyle | undefined): GlyphSet;
|
package/dist/glyphs.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every glyph this engine draws on its own, as one table per box style.
|
|
3
|
+
* `unicode` is the default; `ascii` is a 7-bit form for a terminal that
|
|
4
|
+
* cannot show box drawing or symbols. Every component, frame, rule, and
|
|
5
|
+
* status-line string reads its glyphs from the set the render was given,
|
|
6
|
+
* so the two styles cannot drift apart: `glyphs.test.ts` renders the whole
|
|
7
|
+
* render corpus in `ascii` and fails on any byte above 0x7f that did not
|
|
8
|
+
* come from the note itself.
|
|
9
|
+
*
|
|
10
|
+
* Each entry occupies the same number of terminal columns in both sets
|
|
11
|
+
* where layout depends on it (frames, rules, bars, focus and fold markers),
|
|
12
|
+
* so switching style never changes how a note wraps. The engine never
|
|
13
|
+
* picks a style from the environment; the host decides.
|
|
14
|
+
*/
|
|
15
|
+
export const BOX_STYLES = ['unicode', 'ascii'];
|
|
16
|
+
export function isBoxStyle(value) {
|
|
17
|
+
return BOX_STYLES.includes(value);
|
|
18
|
+
}
|
|
19
|
+
const UNICODE = {
|
|
20
|
+
topLeft: '┌',
|
|
21
|
+
topRight: '┐',
|
|
22
|
+
bottomLeft: '└',
|
|
23
|
+
bottomRight: '┘',
|
|
24
|
+
solid: { horizontal: '─', vertical: '│' },
|
|
25
|
+
dashed: { horizontal: '╌', vertical: '┆' },
|
|
26
|
+
teeDown: '┬',
|
|
27
|
+
teeUp: '┴',
|
|
28
|
+
teeRight: '├',
|
|
29
|
+
teeLeft: '┤',
|
|
30
|
+
cross: '┼',
|
|
31
|
+
rule: '─',
|
|
32
|
+
ruleDots: '·',
|
|
33
|
+
ornament: '❖',
|
|
34
|
+
quoteBar: '│ ',
|
|
35
|
+
calloutBar: '▌ ',
|
|
36
|
+
calloutIcons: { info: 'ℹ', warning: '▲', danger: '✕' },
|
|
37
|
+
focus: '› ',
|
|
38
|
+
foldOpen: '▾',
|
|
39
|
+
foldClosed: '▸',
|
|
40
|
+
script: '⚙',
|
|
41
|
+
scriptSeparator: '·',
|
|
42
|
+
sparkline: '▁▂▃▄▅▆▇█',
|
|
43
|
+
barFilled: '█',
|
|
44
|
+
barEmpty: '░',
|
|
45
|
+
starFilled: '★',
|
|
46
|
+
starEmpty: '☆',
|
|
47
|
+
emptyValue: '—',
|
|
48
|
+
keyLegend: 'q quit ↑↓/jk focus ←→/tab switch enter open/close space/pgdn scroll g/G top/end',
|
|
49
|
+
};
|
|
50
|
+
const ASCII = {
|
|
51
|
+
topLeft: '+',
|
|
52
|
+
topRight: '+',
|
|
53
|
+
bottomLeft: '+',
|
|
54
|
+
bottomRight: '+',
|
|
55
|
+
solid: { horizontal: '-', vertical: '|' },
|
|
56
|
+
dashed: { horizontal: '.', vertical: ':' },
|
|
57
|
+
teeDown: '+',
|
|
58
|
+
teeUp: '+',
|
|
59
|
+
teeRight: '+',
|
|
60
|
+
teeLeft: '+',
|
|
61
|
+
cross: '+',
|
|
62
|
+
rule: '-',
|
|
63
|
+
ruleDots: '.',
|
|
64
|
+
ornament: '*',
|
|
65
|
+
quoteBar: '| ',
|
|
66
|
+
calloutBar: '| ',
|
|
67
|
+
calloutIcons: { info: 'i', warning: '!', danger: 'x' },
|
|
68
|
+
focus: '> ',
|
|
69
|
+
foldOpen: '-',
|
|
70
|
+
foldClosed: '+',
|
|
71
|
+
script: '#',
|
|
72
|
+
scriptSeparator: '-',
|
|
73
|
+
sparkline: '_.-:=+*#',
|
|
74
|
+
barFilled: '#',
|
|
75
|
+
barEmpty: '.',
|
|
76
|
+
starFilled: '*',
|
|
77
|
+
starEmpty: '.',
|
|
78
|
+
emptyValue: '-',
|
|
79
|
+
keyLegend: 'q quit up/down/jk focus left/right/tab switch enter open/close space/pgdn scroll g/G top/end',
|
|
80
|
+
};
|
|
81
|
+
const GLYPHS = {
|
|
82
|
+
unicode: UNICODE,
|
|
83
|
+
ascii: ASCII,
|
|
84
|
+
};
|
|
85
|
+
/** The glyph set for `style`; anything other than `'ascii'` gets the unicode set. */
|
|
86
|
+
export function glyphsFor(style) {
|
|
87
|
+
return style === 'ascii' ? GLYPHS.ascii : GLYPHS.unicode;
|
|
88
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
export { renderMarkToAnsi, renderMarkNodeToAnsi, renderMarkInlineToAnsi, type RenderMarkOptions, } from './render.js';
|
|
1
|
+
export { renderMarkToAnsi, renderMarkNodeToAnsi, renderMarkInlineToAnsi, buildMarkElement, 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
|
+
export { BOX_STYLES, isBoxStyle, glyphsFor, type BoxStyle, type FrameEdges, type GlyphSet, } from './glyphs.js';
|
|
4
5
|
export { style } from './style.js';
|
|
5
|
-
export { createAnsiRegistry, mergeAnsiRegistries, registryAliases, readRegistryComponent, resolveDirectiveAlias, REGISTRY_ALIASES, type DirectiveAttributes, type AnsiRegistry, type AnsiRegistryEntry, type AnsiComponent, type
|
|
6
|
+
export { createAnsiRegistry, mergeAnsiRegistries, registryAliases, readRegistryComponent, resolveDirectiveAlias, childrenText, REGISTRY_ALIASES, type DirectiveAttributes, type AnsiRegistry, type AnsiRegistryEntry, type AnsiComponent, type AnsiComponentProps, type AnsiRenderContext, type RegistryAlias, type RegistryAliases, type ResolvedDirective, type ValueResolution, } from './registry.js';
|
|
6
7
|
export { resolveStorePath, resolveScopedPath, VAULT_NAME_PREFIX, type StorePathResolution, type ValueScope, } from './resolve.js';
|
|
7
8
|
export { failurePhrase, failureTitle, failureToken, dataStateSuffix, emptyInlineTitle, invalidAttributeValueLabel, invalidAttributeValueTitle, unsafeImageSrcLabel, unsafeImageSrcTitle, } from './failure-presentation.js';
|
|
8
9
|
export { stringifyStoredValue } from './value-format.js';
|
|
9
|
-
export { measure, stripAnsi } from './measure.js';
|
|
10
10
|
export { stripControlCharacters, sanitizeBlockText, sanitizeUrlText, BLOCK_TAB_WIDTH, } from './sanitize.js';
|
|
11
|
-
export { wrap, pad, indentBlock, columns, frame, rule, type FrameOptions, } from './box.js';
|
|
12
11
|
export { resolveLayoutAttributes, applyLayout, LAYOUT_ATTRIBUTE_KEYS, type ResolvedLayoutAttributes, type ResolvedLayoutPresets, type WidthPreset, type AlignPreset, } from './layout.js';
|
|
13
12
|
export { type ResolveImageSrc } from './image-resolve.js';
|
|
14
13
|
export { type ResolveHref } from './href-resolve.js';
|
|
15
14
|
export type { AnsiValueStore, AnsiVaultStore, FailureKind, StoredValue, ValueStatus, } from './value-types.js';
|
|
15
|
+
export { defaultAnsiRegistry } from './components/index.js';
|
|
16
|
+
export { ANSI_ENGINE_ID, loadAnsiPack, installAnsiPacks, sanitizeValueForPack, type AnsiPackComponentModules, type AnsiPackToInstall, type LoadAnsiPackResult, type InstallAnsiPacksResult, } from './pack-loader.js';
|
|
17
|
+
export { loadTerminalPackScript, installTerminalPackGlobals, TERMINAL_REGISTER_GLOBAL, TERMINAL_REACT_GLOBAL, TERMINAL_INK_GLOBAL, type LoadTerminalPackScriptResult, } from './pack-script.js';
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
// @markii/ansi: a
|
|
1
|
+
// @markii/ansi: a terminal renderer for Markii documents, built on Ink. It
|
|
2
2
|
// consumes @markii/core's sanitized hast and emits a plain string (optionally
|
|
3
3
|
// carrying ANSI SGR/OSC 8 escapes) for a terminal, a pipe, or any host that
|
|
4
|
-
// wants text with no HTML
|
|
5
|
-
//
|
|
6
|
-
|
|
4
|
+
// wants text with no HTML runtime — plus, for a live viewer, the Ink element
|
|
5
|
+
// tree itself (`buildMarkElement`). It is a third platform renderer alongside
|
|
6
|
+
// @markii/html and @markii/react.
|
|
7
|
+
export { renderMarkToAnsi, renderMarkNodeToAnsi, renderMarkInlineToAnsi, buildMarkElement, } from './render.js';
|
|
7
8
|
export { fg, colorize, bold, dim, italic, underline, inverse, hyperlink, detectColorLevel, resolveColorOption, } from './ansi.js';
|
|
8
9
|
export { defaultAnsiTheme } from './theme.js';
|
|
10
|
+
export { BOX_STYLES, isBoxStyle, glyphsFor, } from './glyphs.js';
|
|
9
11
|
export { style } from './style.js';
|
|
10
|
-
export { createAnsiRegistry, mergeAnsiRegistries, registryAliases, readRegistryComponent, resolveDirectiveAlias, REGISTRY_ALIASES, } from './registry.js';
|
|
12
|
+
export { createAnsiRegistry, mergeAnsiRegistries, registryAliases, readRegistryComponent, resolveDirectiveAlias, childrenText, REGISTRY_ALIASES, } from './registry.js';
|
|
11
13
|
export { resolveStorePath, resolveScopedPath, VAULT_NAME_PREFIX, } from './resolve.js';
|
|
12
14
|
export { failurePhrase, failureTitle, failureToken, dataStateSuffix, emptyInlineTitle, invalidAttributeValueLabel, invalidAttributeValueTitle, unsafeImageSrcLabel, unsafeImageSrcTitle, } from './failure-presentation.js';
|
|
13
15
|
export { stringifyStoredValue } from './value-format.js';
|
|
14
|
-
export { measure, stripAnsi } from './measure.js';
|
|
15
16
|
export { stripControlCharacters, sanitizeBlockText, sanitizeUrlText, BLOCK_TAB_WIDTH, } from './sanitize.js';
|
|
16
|
-
export { wrap, pad, indentBlock, columns, frame, rule, } from './box.js';
|
|
17
17
|
export { resolveLayoutAttributes, applyLayout, LAYOUT_ATTRIBUTE_KEYS, } from './layout.js';
|
|
18
|
+
export { defaultAnsiRegistry } from './components/index.js';
|
|
19
|
+
export { ANSI_ENGINE_ID, loadAnsiPack, installAnsiPacks, sanitizeValueForPack, } from './pack-loader.js';
|
|
20
|
+
export { loadTerminalPackScript, installTerminalPackGlobals, TERMINAL_REGISTER_GLOBAL, TERMINAL_REACT_GLOBAL, TERMINAL_INK_GLOBAL, } from './pack-script.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Mounts `element` with Ink at `columns` wide, captures the first (and only)
|
|
4
|
+
* frame as a plain string, and unmounts. `ink` is imported dynamically
|
|
5
|
+
* because `yoga-layout` (Ink's layout engine) resolves its WASM module via a
|
|
6
|
+
* top-level `await` at ES-module-evaluation time (`SPIKE-10-findings.md`'s
|
|
7
|
+
* "Item 3"): the first call to this function pays that one-time cost, and
|
|
8
|
+
* every exported entry point that reaches here is therefore `async`. A
|
|
9
|
+
* later call in the same process reuses the already-evaluated module graph,
|
|
10
|
+
* the ordinary ESM module-cache guarantee.
|
|
11
|
+
*/
|
|
12
|
+
export declare function renderInkToString(element: ReactElement, columns: number): Promise<string>;
|
|
13
|
+
/**
|
|
14
|
+
* The same capture as `renderInkToString`, synchronously. Valid only once
|
|
15
|
+
* Ink's module has finished evaluating, which is always true inside a walk
|
|
16
|
+
* (`render.tsx` imports Ink statically). Used for a registry component that
|
|
17
|
+
* returns an Ink element instead of a string (a terminal pack's view): the
|
|
18
|
+
* element is laid out at the width the directive was given and captured as
|
|
19
|
+
* text, so it composes with the string-built blocks around it. The result
|
|
20
|
+
* is a snapshot, so a component rendered this way has no live state.
|
|
21
|
+
* Trailing newlines Ink adds are removed.
|
|
22
|
+
*/
|
|
23
|
+
export declare function renderInkToStringSync(element: ReactElement, columns: number): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
import { render as inkRender } from 'ink';
|
|
3
|
+
function makeCollectingStream(columns) {
|
|
4
|
+
const chunks = [];
|
|
5
|
+
const stream = new Writable({
|
|
6
|
+
write(chunk, _encoding, callback) {
|
|
7
|
+
chunks.push(chunk.toString('utf8'));
|
|
8
|
+
callback();
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
stream.columns = columns;
|
|
12
|
+
stream.rows = 40;
|
|
13
|
+
// Ink's static/measured layout paths expect a TTY-shaped stream.
|
|
14
|
+
stream.isTTY = true;
|
|
15
|
+
stream.getColorDepth = () => 8;
|
|
16
|
+
stream.hasColors = () => true;
|
|
17
|
+
return { stream, getOutput: () => chunks.join('') };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Mounts `element` with Ink at `columns` wide, captures the first (and only)
|
|
21
|
+
* frame as a plain string, and unmounts. `ink` is imported dynamically
|
|
22
|
+
* because `yoga-layout` (Ink's layout engine) resolves its WASM module via a
|
|
23
|
+
* top-level `await` at ES-module-evaluation time (`SPIKE-10-findings.md`'s
|
|
24
|
+
* "Item 3"): the first call to this function pays that one-time cost, and
|
|
25
|
+
* every exported entry point that reaches here is therefore `async`. A
|
|
26
|
+
* later call in the same process reuses the already-evaluated module graph,
|
|
27
|
+
* the ordinary ESM module-cache guarantee.
|
|
28
|
+
*/
|
|
29
|
+
export async function renderInkToString(element, columns) {
|
|
30
|
+
const { render } = await import('ink');
|
|
31
|
+
const { stream, getOutput } = makeCollectingStream(columns);
|
|
32
|
+
const instance = render(element, {
|
|
33
|
+
stdout: stream,
|
|
34
|
+
stdin: process.stdin,
|
|
35
|
+
exitOnCtrlC: false,
|
|
36
|
+
patchConsole: false,
|
|
37
|
+
debug: false,
|
|
38
|
+
interactive: false,
|
|
39
|
+
});
|
|
40
|
+
instance.unmount();
|
|
41
|
+
return getOutput();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The same capture as `renderInkToString`, synchronously. Valid only once
|
|
45
|
+
* Ink's module has finished evaluating, which is always true inside a walk
|
|
46
|
+
* (`render.tsx` imports Ink statically). Used for a registry component that
|
|
47
|
+
* returns an Ink element instead of a string (a terminal pack's view): the
|
|
48
|
+
* element is laid out at the width the directive was given and captured as
|
|
49
|
+
* text, so it composes with the string-built blocks around it. The result
|
|
50
|
+
* is a snapshot, so a component rendered this way has no live state.
|
|
51
|
+
* Trailing newlines Ink adds are removed.
|
|
52
|
+
*/
|
|
53
|
+
export function renderInkToStringSync(element, columns) {
|
|
54
|
+
const { stream, getOutput } = makeCollectingStream(columns);
|
|
55
|
+
const instance = inkRender(element, {
|
|
56
|
+
stdout: stream,
|
|
57
|
+
stdin: process.stdin,
|
|
58
|
+
exitOnCtrlC: false,
|
|
59
|
+
patchConsole: false,
|
|
60
|
+
debug: false,
|
|
61
|
+
interactive: false,
|
|
62
|
+
});
|
|
63
|
+
instance.unmount();
|
|
64
|
+
return getOutput().replace(/\n+$/, '');
|
|
65
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { DOMElement } from 'ink';
|
|
3
|
+
import type { AnsiTheme } from './theme.js';
|
|
4
|
+
import type { ColorLevel } from './ansi.js';
|
|
5
|
+
/** 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
|
+
export declare function useIsFocused(focusId: number | undefined): boolean;
|
|
7
|
+
/** Wraps `text` in the accent-colored focus marker when interactive AND `focusId` is the focused one; otherwise returns `text` unchanged. */
|
|
8
|
+
export declare function useFocusMarker(text: string, focusId: number | undefined): string;
|
|
9
|
+
/**
|
|
10
|
+
* Lets a `tabs` instance report its currently active panel's label to
|
|
11
|
+
* `InteractiveRoot`, so the status line names the panel showing rather than
|
|
12
|
+
* the block's first one. Call it from an effect, never during render (see
|
|
13
|
+
* `FocusApi.reportActiveLabel`).
|
|
14
|
+
*/
|
|
15
|
+
export declare function useReportActiveLabel(): (focusId: number, label: string) => void;
|
|
16
|
+
/**
|
|
17
|
+
* The ref callback a focusable component puts on its outermost `Box`, so
|
|
18
|
+
* the live viewer can scroll the focused block into view. A no-op without a
|
|
19
|
+
* `focusId` or outside an interactive render.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useFocusAnchor(focusId: number | undefined): (node: DOMElement | null) => void;
|
|
22
|
+
/**
|
|
23
|
+
* One focusable component, in the same document order `render.tsx`'s
|
|
24
|
+
* counting walk assigns `focusId`s in — `focusables[focusId]` is always
|
|
25
|
+
* that component's descriptor. `label` is the descriptor's STABLE identity
|
|
26
|
+
* (for `tabs`, the FIRST panel's label): it is used on the status line only
|
|
27
|
+
* until a `tabs` instance reports its actual active panel via
|
|
28
|
+
* `reportActiveLabel`, and as the fallback before any report has happened
|
|
29
|
+
* yet (mount, or a reader who has not switched panels).
|
|
30
|
+
*/
|
|
31
|
+
export interface FocusableDescriptor {
|
|
32
|
+
kind: 'tabs' | 'details';
|
|
33
|
+
label: string;
|
|
34
|
+
}
|
|
35
|
+
export interface InteractiveRootProps {
|
|
36
|
+
/** Every focusable component the walk assigned a `focusId` to, in `focusId` order (empty for none). */
|
|
37
|
+
focusables: readonly FocusableDescriptor[];
|
|
38
|
+
/** Called when the reader presses `q`. The engine owns no process state: it never calls `process.exit` itself (AGENTS.md, batch-10 brief). */
|
|
39
|
+
onExit?: () => void;
|
|
40
|
+
color: ColorLevel;
|
|
41
|
+
theme: AnsiTheme;
|
|
42
|
+
/** The key legend on the status line, identical regardless of what is focused: the render's `GlyphSet.keyLegend`, so it follows the box style. */
|
|
43
|
+
keyLegend: string;
|
|
44
|
+
/**
|
|
45
|
+
* The terminal's row count. When given, the document gets a viewport of
|
|
46
|
+
* this many rows minus the one-row status line, scrolled with the keys in
|
|
47
|
+
* the legend, and the status line stays pinned at the bottom. Absent, the
|
|
48
|
+
* document is laid out at its full height with the status line after it.
|
|
49
|
+
*/
|
|
50
|
+
viewportRows?: number;
|
|
51
|
+
/** The columns the status line is cut to, so it stays one row. */
|
|
52
|
+
width: number;
|
|
53
|
+
children: ReactNode;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The root of an interactive render: owns which `focusId` is focused, moves
|
|
57
|
+
* it on up/down/j/k, calls `onExit` on `q`, and renders the one-line status
|
|
58
|
+
* bar (batch 10.1) naming the focused block and the key legend. Rendered
|
|
59
|
+
* only when the caller actually wants a live, keyboard-driven view:
|
|
60
|
+
* `buildMarkElement` mounts this when `interactive` is true, but the
|
|
61
|
+
* render-ONCE string path (`ink-string.ts`, `renderRootToString`) never
|
|
62
|
+
* reaches it at all — `tabs`/`details` still fall back to `FocusContext`'s
|
|
63
|
+
* default (uninteractive) value there, so a string render never sees a
|
|
64
|
+
* focus marker, a glyph, or a status line.
|
|
65
|
+
*/
|
|
66
|
+
export declare function InteractiveRoot({ focusables, onExit, color, theme, keyLegend, viewportRows, width, children, }: InteractiveRootProps): ReactNode;
|