@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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/dist/ansi.d.ts +26 -0
  3. package/dist/ansi.js +51 -0
  4. package/dist/components/badge.d.ts +1 -1
  5. package/dist/components/badge.js +5 -4
  6. package/dist/components/callout.d.ts +38 -13
  7. package/dist/components/callout.js +68 -31
  8. package/dist/components/card.d.ts +34 -2
  9. package/dist/components/card.js +55 -18
  10. package/dist/components/cell.d.ts +5 -11
  11. package/dist/components/cell.js +11 -15
  12. package/dist/components/chart.d.ts +4 -10
  13. package/dist/components/chart.js +16 -24
  14. package/dist/components/details-string.d.ts +26 -0
  15. package/dist/components/details-string.js +39 -0
  16. package/dist/components/details.d.ts +32 -10
  17. package/dist/components/details.js +28 -25
  18. package/dist/components/divider.js +12 -16
  19. package/dist/components/figure.d.ts +5 -16
  20. package/dist/components/figure.js +9 -19
  21. package/dist/components/index.d.ts +14 -10
  22. package/dist/components/index.js +19 -16
  23. package/dist/components/kbd.d.ts +1 -1
  24. package/dist/components/kbd.js +3 -2
  25. package/dist/components/layout-wrapper.d.ts +7 -22
  26. package/dist/components/layout-wrapper.js +9 -23
  27. package/dist/components/progress.d.ts +3 -11
  28. package/dist/components/progress.js +8 -17
  29. package/dist/components/rating.d.ts +1 -2
  30. package/dist/components/rating.js +3 -4
  31. package/dist/components/row.d.ts +35 -20
  32. package/dist/components/row.js +35 -51
  33. package/dist/components/stat.d.ts +3 -8
  34. package/dist/components/stat.js +5 -11
  35. package/dist/components/tab.d.ts +9 -8
  36. package/dist/components/tab.js +12 -10
  37. package/dist/components/table-grid.d.ts +2 -1
  38. package/dist/components/table-grid.js +14 -12
  39. package/dist/components/table.d.ts +5 -10
  40. package/dist/components/table.js +8 -13
  41. package/dist/components/tabs-string.d.ts +17 -0
  42. package/dist/components/tabs-string.js +30 -0
  43. package/dist/components/tabs.d.ts +31 -14
  44. package/dist/components/tabs.js +55 -30
  45. package/dist/failure-presentation.d.ts +15 -0
  46. package/dist/failure-presentation.js +22 -0
  47. package/dist/glyphs.d.ts +73 -0
  48. package/dist/glyphs.js +88 -0
  49. package/dist/index.d.ts +6 -4
  50. package/dist/index.js +10 -7
  51. package/dist/ink-string.d.ts +23 -0
  52. package/dist/ink-string.js +65 -0
  53. package/dist/interactive.d.ts +66 -0
  54. package/dist/interactive.js +207 -0
  55. package/dist/layout.js +1 -2
  56. package/dist/pack-loader.d.ts +64 -0
  57. package/dist/pack-loader.js +172 -0
  58. package/dist/pack-script.d.ts +44 -0
  59. package/dist/pack-script.js +135 -0
  60. package/dist/registry.d.ts +106 -100
  61. package/dist/registry.js +17 -9
  62. package/dist/render.d.ts +63 -25
  63. package/dist/render.js +1001 -380
  64. package/dist/text-grid.d.ts +90 -0
  65. package/dist/text-grid.js +300 -0
  66. package/package.json +9 -5
  67. package/dist/box.d.ts +0 -47
  68. package/dist/box.js +0 -209
  69. package/dist/measure.d.ts +0 -25
  70. 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 { AnsiComponent } from '../registry.js';
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. A terminal has no click-driven tab switching (see
5
- * `render.ts`'s top comment on interactive components), so every panel is
6
- * shown, stacked, in document order — matching `@markii/html`'s `Tabs`
7
- * faithfulness limitation of showing every panel rather than picking one.
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
- * "The active tab marked": since every panel is always visible here, this
10
- * engine treats the FIRST tab (document order) as the one a live host would
11
- * show before any interaction, and marks only its heading line — every
12
- * `tab` child's block is joined into `childrenText` with a blank line
13
- * between them (`render.ts`'s `renderBlocks`), the same separator this
14
- * engine uses between any two sibling blocks, so splitting on the first
15
- * blank line reliably finds the boundary between the first tab's block and
16
- * the rest without needing any information `tabs` does not have.
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 declare const Tabs: AnsiComponent;
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;
@@ -1,30 +1,55 @@
1
- /** The visible marker appended to whichever tab is treated as "active" (see this module's doc comment for how that is decided). */
2
- const ACTIVE_MARKER = ' (active)';
3
- /**
4
- * `::::tabs :::tab{label="..."} ... ::: :::tab{label="..."} ... ::: ::::` —
5
- * a tabbed panel switcher. A terminal has no click-driven tab switching (see
6
- * `render.ts`'s top comment on interactive components), so every panel is
7
- * shown, stacked, in document order — matching `@markii/html`'s `Tabs`
8
- * faithfulness limitation of showing every panel rather than picking one.
9
- *
10
- * "The active tab marked": since every panel is always visible here, this
11
- * engine treats the FIRST tab (document order) as the one a live host would
12
- * show before any interaction, and marks only its heading line — every
13
- * `tab` child's block is joined into `childrenText` with a blank line
14
- * between them (`render.ts`'s `renderBlocks`), the same separator this
15
- * engine uses between any two sibling blocks, so splitting on the first
16
- * blank line reliably finds the boundary between the first tab's block and
17
- * the rest without needing any information `tabs` does not have.
18
- */
19
- export const Tabs = (_attributes, children) => {
20
- const childrenText = children();
21
- if (!childrenText.trim())
22
- return '';
23
- const blocks = childrenText.split('\n\n');
24
- const [first, ...rest] = blocks;
25
- if (first === undefined)
26
- return childrenText;
27
- const lines = first.split('\n');
28
- lines[0] = `${lines[0] ?? ''}${ACTIVE_MARKER}`;
29
- return [lines.join('\n'), ...rest].join('\n\n');
30
- };
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
+ }
@@ -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 AnsiChildren, type AnsiChildPart, type AnsiChildrenOptions, type AnsiRenderContext, type RegistryAlias, type RegistryAliases, type ResolvedDirective, type ValueResolution, } from './registry.js';
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 framework-free terminal renderer for Markii documents. It
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 and no React runtime. It is a third platform
5
- // renderer alongside @markii/html and @markii/react.
6
- export { renderMarkToAnsi, renderMarkNodeToAnsi, renderMarkInlineToAnsi, } from './render.js';
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;