@markii/ansi 0.0.1 → 0.16.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 (60) hide show
  1. package/dist/ansi.d.ts +26 -0
  2. package/dist/ansi.js +51 -0
  3. package/dist/components/badge.d.ts +1 -1
  4. package/dist/components/badge.js +5 -4
  5. package/dist/components/callout.d.ts +24 -13
  6. package/dist/components/callout.js +45 -21
  7. package/dist/components/card.d.ts +24 -2
  8. package/dist/components/card.js +41 -12
  9. package/dist/components/cell.d.ts +5 -11
  10. package/dist/components/cell.js +11 -15
  11. package/dist/components/chart.d.ts +4 -10
  12. package/dist/components/chart.js +9 -16
  13. package/dist/components/details-string.d.ts +26 -0
  14. package/dist/components/details-string.js +39 -0
  15. package/dist/components/details.d.ts +30 -10
  16. package/dist/components/details.js +27 -25
  17. package/dist/components/divider.js +9 -11
  18. package/dist/components/figure.d.ts +5 -16
  19. package/dist/components/figure.js +9 -19
  20. package/dist/components/index.d.ts +14 -10
  21. package/dist/components/index.js +19 -16
  22. package/dist/components/kbd.d.ts +1 -1
  23. package/dist/components/kbd.js +3 -2
  24. package/dist/components/layout-wrapper.d.ts +7 -22
  25. package/dist/components/layout-wrapper.js +9 -23
  26. package/dist/components/progress.d.ts +3 -11
  27. package/dist/components/progress.js +6 -14
  28. package/dist/components/rating.d.ts +1 -2
  29. package/dist/components/rating.js +2 -3
  30. package/dist/components/row.d.ts +33 -20
  31. package/dist/components/row.js +33 -51
  32. package/dist/components/stat.d.ts +3 -8
  33. package/dist/components/stat.js +4 -9
  34. package/dist/components/tab.d.ts +9 -8
  35. package/dist/components/tab.js +12 -10
  36. package/dist/components/table-grid.js +1 -2
  37. package/dist/components/table.d.ts +5 -10
  38. package/dist/components/table.js +7 -12
  39. package/dist/components/tabs-string.d.ts +17 -0
  40. package/dist/components/tabs-string.js +30 -0
  41. package/dist/components/tabs.d.ts +29 -14
  42. package/dist/components/tabs.js +37 -30
  43. package/dist/index.d.ts +2 -4
  44. package/dist/index.js +6 -7
  45. package/dist/ink-string.d.ts +12 -0
  46. package/dist/ink-string.js +41 -0
  47. package/dist/interactive.d.ts +41 -0
  48. package/dist/interactive.js +64 -0
  49. package/dist/layout.js +1 -2
  50. package/dist/registry.d.ts +91 -100
  51. package/dist/registry.js +17 -9
  52. package/dist/render.d.ts +39 -24
  53. package/dist/render.js +676 -373
  54. package/dist/text-grid.d.ts +54 -0
  55. package/dist/text-grid.js +249 -0
  56. package/package.json +7 -4
  57. package/dist/box.d.ts +0 -47
  58. package/dist/box.js +0 -209
  59. package/dist/measure.d.ts +0 -25
  60. package/dist/measure.js +0 -108
@@ -0,0 +1,41 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { AnsiTheme } from './theme.js';
3
+ import type { ColorLevel } from './ansi.js';
4
+ /** 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. */
5
+ export declare function useIsFocused(focusId: number | undefined): boolean;
6
+ /** Wraps `text` in the accent-colored focus marker when interactive AND `focusId` is the focused one; otherwise returns `text` unchanged. */
7
+ export declare function useFocusMarker(text: string, focusId: number | undefined): string;
8
+ /**
9
+ * One focusable component, in the same document order `render.tsx`'s
10
+ * counting walk assigns `focusId`s in — `focusables[focusId]` is always
11
+ * that component's descriptor. `label` is a stable identity: for `tabs`,
12
+ * the FIRST panel's label, never the currently active one. A `tabs` block's
13
+ * active panel is its own component state, so naming it on the status line
14
+ * would need a report channel back up to this root on every switch; the
15
+ * first label identifies the block just as well and cannot loop.
16
+ */
17
+ export interface FocusableDescriptor {
18
+ kind: 'tabs' | 'details';
19
+ label: string;
20
+ }
21
+ export interface InteractiveRootProps {
22
+ /** Every focusable component the walk assigned a `focusId` to, in `focusId` order (empty for none). */
23
+ focusables: readonly FocusableDescriptor[];
24
+ /** Called when the reader presses `q`. The engine owns no process state: it never calls `process.exit` itself (AGENTS.md, batch-10 brief). */
25
+ onExit?: () => void;
26
+ color: ColorLevel;
27
+ theme: AnsiTheme;
28
+ children: ReactNode;
29
+ }
30
+ /**
31
+ * The root of an interactive render: owns which `focusId` is focused, moves
32
+ * it on up/down/j/k, calls `onExit` on `q`, and renders the one-line status
33
+ * bar (batch 10.1) naming the focused block and the key legend. Rendered
34
+ * only when the caller actually wants a live, keyboard-driven view:
35
+ * `buildMarkElement` mounts this when `interactive` is true, but the
36
+ * render-ONCE string path (`ink-string.ts`, `renderRootToString`) never
37
+ * reaches it at all — `tabs`/`details` still fall back to `FocusContext`'s
38
+ * default (uninteractive) value there, so a string render never sees a
39
+ * focus marker, a glyph, or a status line.
40
+ */
41
+ export declare function InteractiveRoot({ focusables, onExit, color, theme, children, }: InteractiveRootProps): ReactNode;
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo, useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { dim } from './ansi.js';
5
+ import { style } from './style.js';
6
+ const FocusContext = createContext({
7
+ interactive: false,
8
+ focusedId: undefined,
9
+ focusMarker: (text) => text,
10
+ });
11
+ /** 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. */
12
+ export function useIsFocused(focusId) {
13
+ const api = useContext(FocusContext);
14
+ return focusId !== undefined && api.interactive && api.focusedId === focusId;
15
+ }
16
+ /** Wraps `text` in the accent-colored focus marker when interactive AND `focusId` is the focused one; otherwise returns `text` unchanged. */
17
+ export function useFocusMarker(text, focusId) {
18
+ const api = useContext(FocusContext);
19
+ const focused = focusId !== undefined && api.interactive && api.focusedId === focusId;
20
+ return focused ? api.focusMarker(text) : text;
21
+ }
22
+ /** The key legend shown on the status line, identical regardless of what is focused. */
23
+ const KEY_LEGEND = '↑↓/jk focus ←→/tab switch enter open/close q quit';
24
+ /**
25
+ * The root of an interactive render: owns which `focusId` is focused, moves
26
+ * it on up/down/j/k, calls `onExit` on `q`, and renders the one-line status
27
+ * bar (batch 10.1) naming the focused block and the key legend. Rendered
28
+ * only when the caller actually wants a live, keyboard-driven view:
29
+ * `buildMarkElement` mounts this when `interactive` is true, but the
30
+ * render-ONCE string path (`ink-string.ts`, `renderRootToString`) never
31
+ * reaches it at all — `tabs`/`details` still fall back to `FocusContext`'s
32
+ * default (uninteractive) value there, so a string render never sees a
33
+ * focus marker, a glyph, or a status line.
34
+ */
35
+ export function InteractiveRoot({ focusables, onExit, color, theme, children, }) {
36
+ const focusableCount = focusables.length;
37
+ const [focusIndex, setFocusIndex] = useState(0);
38
+ useInput((input, key) => {
39
+ if (input === 'q') {
40
+ onExit?.();
41
+ return;
42
+ }
43
+ if (focusableCount === 0)
44
+ return;
45
+ if (key.downArrow || input === 'j') {
46
+ setFocusIndex((current) => (current + 1) % focusableCount);
47
+ }
48
+ else if (key.upArrow || input === 'k') {
49
+ setFocusIndex((current) => (current - 1 + focusableCount) % focusableCount);
50
+ }
51
+ }, { isActive: true });
52
+ const focusedId = focusableCount > 0 ? focusIndex % focusableCount : undefined;
53
+ const api = useMemo(() => ({
54
+ interactive: true,
55
+ focusedId,
56
+ focusMarker: (text) => style(text, '--mk-accent', theme, color),
57
+ }), [focusedId, color, theme]);
58
+ const focused = focusedId !== undefined ? focusables[focusedId] : undefined;
59
+ const statusLabel = focused
60
+ ? `${focused.kind} "${focused.label}"`
61
+ : 'nothing focusable';
62
+ const statusLine = dim(`${statusLabel} ${KEY_LEGEND}`, color);
63
+ return (_jsx(FocusContext.Provider, { value: api, children: _jsxs(Box, { flexDirection: "column", children: [children, _jsx(Text, { children: statusLine })] }) }));
64
+ }
package/dist/layout.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { ALIGN_PRESETS, LAYOUT_ATTRIBUTE_KEYS, WIDTH_PRESETS, } from '@markii/stdlib';
2
- import { pad, wrap } from './box.js';
3
- import { measure } from './measure.js';
2
+ import { padText as pad, wrapText as wrap, measureWidth as measure, } from './text-grid.js';
4
3
  /**
5
4
  * The terminal counterpart of `@markii/html`'s `layout.ts`: the same closed
6
5
  * `width`/`align` vocabulary from `@markii/stdlib`, but resolved to COLUMN
@@ -1,12 +1,4 @@
1
- /**
2
- * The registry contract for the terminal engine: the string-emitting twin of
3
- * `@markii/html`'s `registry.ts`, itself the string-emitting twin of
4
- * `@markii/react`'s `registry.ts`. A component here is a plain function from
5
- * attributes and already-rendered children TEXT to an output string — no
6
- * hast, no React, no HTML. The alias, merge, and hostile-configuration rules
7
- * are kept identical to both existing renderers, so a note resolves the same
8
- * way in all three.
9
- */
1
+ import type { FC, ReactNode } from 'react';
10
2
  import type { FailureKind, ValueStatus } from './value-types.js';
11
3
  import type { LayoutAxis, OnDiagnostic } from '@markii/stdlib';
12
4
  import type { ColorLevel } from './ansi.js';
@@ -14,6 +6,23 @@ import type { AnsiTheme, Tier1Token } from './theme.js';
14
6
  import type { ResolvedLayoutAttributes } from './layout.js';
15
7
  import type { ResolveImageSrc } from './image-resolve.js';
16
8
  import type { ResolveHref } from './href-resolve.js';
9
+ /**
10
+ * The registry contract for the terminal engine, batch-10 (the Ink
11
+ * rewrite): a component is a React function component receiving
12
+ * `{ attributes, children, ctx }`, where `children` is ALREADY a built Ink
13
+ * element tree (a `ReactNode`) rather than the lazy string-producing
14
+ * `AnsiChildren` handle the pre-Ink engine used. Building children eagerly
15
+ * is the batch-10 brief's own instruction (`AGENTS.md`'s architecture
16
+ * section is silent on this; the brief is explicit: "Delete... the lazy-
17
+ * children machinery"): Ink/Yoga lays a tree out AFTER it is built, exactly
18
+ * like a browser reflows HTML, so a component no longer needs to ask for a
19
+ * narrower re-render of its own body the way the old string engine did —
20
+ * the one standard component that still needs a per-cell WIDTH NUMBER before
21
+ * it draws (`row`, whose cells may hold a self-drawing box like `card`) gets
22
+ * that from `render.tsx`'s own walk, which still threads `width` top-down
23
+ * exactly as before; see `render.tsx`'s `renderRow` for why that one case is
24
+ * handled at the walk level rather than through a registry-facing API.
25
+ */
17
26
  /**
18
27
  * Attributes parsed off a directive, e.g. `{type=warning title="Careful"}`. A
19
28
  * bare attribute (present but valueless, e.g. `{collapsed}`) arrives as
@@ -32,28 +41,47 @@ export interface ValueResolution {
32
41
  failureKind?: FailureKind;
33
42
  }
34
43
  /**
35
- * The render context handed to every component. Mirrors
36
- * `@markii/html`'s `HtmlRenderContext` field for field, with the HTML-only
37
- * `esc` swapped for `text` (this engine's control-character sanitizer, see
38
- * `./sanitize.js`'s `stripControlCharacters`) and a set of terminal-only
39
- * fields added: `width`/`indent` (the layout budget this directive's own
40
- * output has to work with), `color`/`theme` (what this render resolved),
41
- * and the box/style helpers pre-bound to both.
44
+ * The render context handed to every component. Mirrors the pre-Ink
45
+ * engine's `AnsiRenderContext` for every field EXCEPT the string-layout
46
+ * helpers (`wrap`/`pad`/`columns`/`frame`/`rule`), which are dropped:
47
+ * ordinary prose now wraps through Ink's own layout, and the handful of
48
+ * self-drawing standard components (`card`, `callout`, `divider`, `chart`,
49
+ * `table`) that still need exact box-drawing glyphs use `./text-grid.js`'s
50
+ * private helpers directly rather than through the context.
42
51
  */
43
52
  export interface AnsiRenderContext {
44
- /** The columns available to this directive's own output. A component wraps its own text to this width. */
53
+ /** The columns available to this directive's own output. A self-drawing component sizes its own frame to this width (or to `layout`'s resolved preset of it). */
45
54
  width: number;
46
55
  /**
47
56
  * The prefix an enclosing block (a blockquote, a nested list) has already
48
- * applied to every line above this directive. Informational only: a
49
- * component emits its own block UNINDENTED, and the walk applies the
50
- * prefix afterward, exactly as `width` is already the post-indent budget.
57
+ * applied to every line above this directive, informational only: a
58
+ * component draws its own content at `width` and the walk applies the
59
+ * enclosing indent as an Ink `Box` wrapper, not as a string prefix.
51
60
  */
52
61
  indent: string;
53
62
  /** The resolved color depth for this render. */
54
63
  color: ColorLevel;
55
64
  /** The resolved theme for this render. */
56
65
  theme: AnsiTheme;
66
+ /**
67
+ * Whether this render is in INTERACTIVE mode (a live viewer), as opposed
68
+ * to the default render-once mode. NAME COLLISION, documented at both
69
+ * declarations: this is UNRELATED to Ink's own `render()` option also
70
+ * spelled `interactive` (`ink-string.ts`'s doc comment) — that one
71
+ * controls which escape sequences Ink emits around a frame; this one
72
+ * controls which CONTENT a component renders (`tabs` shows only the
73
+ * active panel, `details` starts closed unless `open` is present) and
74
+ * whether it registers a keyboard handler at all.
75
+ */
76
+ interactive: boolean;
77
+ /**
78
+ * A stable, document-order identity for a FOCUSABLE component (currently
79
+ * `tabs` and `details`) when `interactive` is true; `undefined` when not
80
+ * interactive, or for any other component. Assigned once by `render.tsx`
81
+ * as it walks the tree (see `render.tsx`'s `WalkContext.nextFocusId`), so
82
+ * a component never has to compute its own position in document order.
83
+ */
84
+ focusId?: number;
57
85
  /** Applies `theme`'s color for `token` at this render's `color` level. Unchanged at `'none'` or for a `null` theme entry. */
58
86
  style(text: string, token: Tier1Token): string;
59
87
  /** SGR bold, pre-bound to this render's color level. */
@@ -66,26 +94,11 @@ export interface AnsiRenderContext {
66
94
  underline(text: string): string;
67
95
  /** SGR inverse/reverse video, pre-bound to this render's color level. */
68
96
  inverse(text: string): string;
69
- /** Greedy word wrap, pre-bound to nothing (width is explicit here since a component may wrap narrower than its own `ctx.width`, e.g. inside its own frame). */
70
- wrap(text: string, width: number): string[];
71
- /** Pads `text` to `width` columns, aligned `left`/`center`/`right`. */
72
- pad(text: string, width: number, align: 'left' | 'center' | 'right'): string;
73
- /** Places blocks side by side; see `./box.js`'s `columns`. */
74
- columns(blocks: readonly string[], widths: readonly number[], gutter: number): string;
75
- /** Draws a box around `block`; see `./box.js`'s `frame`. */
76
- frame(block: string, options: {
77
- style: 'solid' | 'dashed';
78
- title?: string;
79
- width: number;
80
- }): string;
81
- /** A full-width horizontal rule; see `./box.js`'s `rule`. */
82
- rule(width: number, char?: string): string;
83
97
  /**
84
98
  * The sanitizer a component MUST run any author-supplied string through
85
- * before printing it (this engine's counterpart to the HTML context's
86
- * `esc`): strips every control character an untrusted note could use to
87
- * move the cursor or smuggle an escape sequence (`./sanitize.js`'s
88
- * `stripControlCharacters`).
99
+ * before printing it: strips every control character an untrusted note
100
+ * could use to move the cursor or smuggle an escape sequence
101
+ * (`./sanitize.js`'s `stripControlCharacters`).
89
102
  */
90
103
  text(value: string): string;
91
104
  /** Resolves a `data=`/`:value[...]` name against the current render's store/vault. Never throws. */
@@ -108,76 +121,39 @@ export interface AnsiRenderContext {
108
121
  onDiagnostic?: OnDiagnostic;
109
122
  }
110
123
  /**
111
- * How a component wants ONE render of its children to happen: at a
112
- * narrower `width` than the directive itself was rendered at (a self-layout
113
- * box sizing its own inner budget), and/or a different `indent`. Omitting a
114
- * field inherits the value the enclosing directive was rendered with.
115
- */
116
- export interface AnsiChildrenOptions {
117
- width?: number;
118
- indent?: string;
119
- }
120
- /**
121
- * One top-level child of a directive's own body, independently renderable
122
- * at its own width — the piece `row` needs and no other standard component
123
- * does: it must decide each cell's column width BEFORE that cell's content
124
- * (including a self-drawing box like `card`) is rendered, not re-wrap an
125
- * already-drawn frame afterward. `name` is the child's own resolved
126
- * directive name (e.g. `'cell'`) when the child is itself a directive, and
127
- * `undefined` for a plain block (a paragraph, a list) or a directive with a
128
- * different name — enough for a container to recognize a semantically
129
- * tagged child without ever seeing a hast node.
124
+ * One registry component: a React function component. `attributes` are the
125
+ * directive's raw string attributes (bare attributes as `null`); `children`
126
+ * is the directive's already-rendered inner markdown, an Ink element tree;
127
+ * `ctx` is `AnsiRenderContext` above. Attribute parsing, validation, and
128
+ * defaulting are the component's own job, exactly as in the other two
129
+ * engines' contracts. A component MAY use React hooks (`useState`,
130
+ * `useInput`, ...) since `render.tsx` always invokes it as a JSX element,
131
+ * never as a plain function call — `tabs` and `details` need this for
132
+ * interactive mode.
130
133
  */
131
- export interface AnsiChildPart {
132
- readonly name?: string;
133
- render(options?: AnsiChildrenOptions): string;
134
+ export interface AnsiComponentProps {
135
+ attributes: DirectiveAttributes;
136
+ children: ReactNode;
137
+ ctx: AnsiRenderContext;
134
138
  }
135
- /**
136
- * The children a component receives. Calling it directly renders the
137
- * directive's WHOLE body as one flattened string — what every component
138
- * except `row` wants, and what used to be eagerly computed as a plain
139
- * string before the component ever ran. `options` narrows the width/indent
140
- * for that one call (a self-layout box's inner budget); omitted, it renders
141
- * at the width/indent the directive itself was given.
142
- *
143
- * `.parts` exposes each of the directive's own top-level children
144
- * separately, in document order, each independently renderable at its own
145
- * width via its own `render(options)` — see `AnsiChildPart`.
146
- *
147
- * Laziness is the point: nothing under this directive is rendered until a
148
- * component actually calls `children()` or one of `.parts`' `render()`
149
- * functions, so a container can decide sizes top-down before anything below
150
- * it draws a single character.
151
- */
152
- export type AnsiChildren = ((options?: AnsiChildrenOptions) => string) & {
153
- readonly parts: readonly AnsiChildPart[];
154
- };
155
- /**
156
- * One registry component: receives the directive's raw string attributes
157
- * (bare attributes as `null`), a lazy handle onto its inner markdown
158
- * (`AnsiChildren`, see above), and the render context, and returns the text
159
- * to emit. Attribute parsing, validation, and defaulting are the
160
- * component's own job, exactly as in the other two engines' contracts.
161
- */
162
- export type AnsiComponent = (attributes: DirectiveAttributes, children: AnsiChildren, ctx: AnsiRenderContext) => string;
139
+ export type AnsiComponent = FC<AnsiComponentProps>;
163
140
  /** One registry entry: the component plus whether it is meant to be used inline vs as a block, and whether it is a layout scope. Mirrors `@markii/html`'s `HtmlRegistryEntry`. */
164
141
  export interface AnsiRegistryEntry {
165
142
  component: AnsiComponent;
166
143
  inline?: boolean;
167
144
  layout?: LayoutAxis;
168
145
  /**
169
- * Marks a component that draws its OWN box (a frame, a table grid) rather
170
- * than plain wrapped text — `card`, `callout`, `table`, `chart` in the
171
- * standard set. `applyLayout`'s generic re-wrap/pad would corrupt a
172
- * pre-drawn frame's box-drawing characters if `render.ts` narrowed it
173
- * AFTER the component already drew it at the full width. A `selfLayout`
174
- * component instead receives the resolved `width`/`align` presets as
175
- * `ctx.layout` (the same field a layout-WRAPPER scope receives) and is
176
- * trusted to size its own frame correctly; `render.ts` then skips its
177
- * usual post-render `applyLayout` call for it, exactly as it already does
178
- * for a layout-wrapper scope. Unlike `layout: LayoutAxis`, this is not
179
- * "this directive's name sets an axis" — it is "this directive draws pixels
180
- * that a generic wrap/pad would break."
146
+ * Marks a component that draws its OWN fixed-width block (box-drawing
147
+ * glyphs, a rule, a sparkline) rather than plain wrapped prose — `card`,
148
+ * `callout`, `divider`, `table`, `chart` in the standard set. A generic
149
+ * post-render narrow/align pass would corrupt a pre-drawn frame's
150
+ * box-drawing characters, so a `selfLayout` component instead receives the
151
+ * resolved `width`/`align` presets as `ctx.layout` and is trusted to size
152
+ * its own frame correctly; `render.tsx` skips its usual post-render
153
+ * `applyLayout` wrapping for it. Kept from the pre-Ink engine: Ink's own
154
+ * layout does not solve this, because the content in question is a
155
+ * pre-built STRING (see `text-grid.ts`'s module doc comment), not
156
+ * Ink primitives Yoga can reflow.
181
157
  */
182
158
  selfLayout?: boolean;
183
159
  }
@@ -225,3 +201,18 @@ export interface ResolvedDirective {
225
201
  * throws.
226
202
  */
227
203
  export declare function resolveDirectiveAlias(registry: AnsiRegistry, name: string, attributes: DirectiveAttributes): ResolvedDirective;
204
+ /**
205
+ * Reads a component's `children` prop back as plain text. `render.tsx`
206
+ * always hands most standard components (every one except `row`, `tabs`,
207
+ * `details`) an already-flattened STRING as `children` — see `render.tsx`'s
208
+ * top comment on why those three are the only ones that get a real Ink
209
+ * element tree instead. `children` is still typed `ReactNode` on
210
+ * `AnsiComponentProps` (the honest public contract: "an Ink element tree",
211
+ * per the batch-10 brief, and a string IS a valid `ReactNode`), so a
212
+ * component that needs its body AS TEXT (to measure it, frame it, indent
213
+ * it, or check whether it is empty) reads it through this helper rather
214
+ * than assuming the type; a non-string `children` degrades to `''` rather
215
+ * than throwing, matching this package's never-throw posture everywhere
216
+ * else.
217
+ */
218
+ export declare function childrenText(children: ReactNode): string;
package/dist/registry.js CHANGED
@@ -1,12 +1,3 @@
1
- /**
2
- * The registry contract for the terminal engine: the string-emitting twin of
3
- * `@markii/html`'s `registry.ts`, itself the string-emitting twin of
4
- * `@markii/react`'s `registry.ts`. A component here is a plain function from
5
- * attributes and already-rendered children TEXT to an output string — no
6
- * hast, no React, no HTML. The alias, merge, and hostile-configuration rules
7
- * are kept identical to both existing renderers, so a note resolves the same
8
- * way in all three.
9
- */
10
1
  /** The symbol an alias table hangs off a registry under. Mirrors `@markii/html`'s `REGISTRY_ALIASES`. */
11
2
  export const REGISTRY_ALIASES = Symbol('markii.ansi.registry.aliases');
12
3
  /** Reads a registry's alias table, or `undefined` if it has none. Returned as-is; treat as read-only. */
@@ -119,3 +110,20 @@ export function resolveDirectiveAlias(registry, name, attributes) {
119
110
  attributes: mergeAliasAttributes(alias.attributes, attributes),
120
111
  };
121
112
  }
113
+ /**
114
+ * Reads a component's `children` prop back as plain text. `render.tsx`
115
+ * always hands most standard components (every one except `row`, `tabs`,
116
+ * `details`) an already-flattened STRING as `children` — see `render.tsx`'s
117
+ * top comment on why those three are the only ones that get a real Ink
118
+ * element tree instead. `children` is still typed `ReactNode` on
119
+ * `AnsiComponentProps` (the honest public contract: "an Ink element tree",
120
+ * per the batch-10 brief, and a string IS a valid `ReactNode`), so a
121
+ * component that needs its body AS TEXT (to measure it, frame it, indent
122
+ * it, or check whether it is empty) reads it through this helper rather
123
+ * than assuming the type; a non-string `children` degrades to `''` rather
124
+ * than throwing, matching this package's never-throw posture everywhere
125
+ * else.
126
+ */
127
+ export function childrenText(children) {
128
+ return typeof children === 'string' ? children : '';
129
+ }
package/dist/render.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ReactElement, ReactNode } from 'react';
1
2
  import { parse } from '@markii/core';
2
3
  import type { MarkNode } from '@markii/core';
3
4
  import { type ColorOption } from './ansi.js';
@@ -8,11 +9,14 @@ import { type ResolveImageSrc } from './image-resolve.js';
8
9
  import { type ResolveHref } from './href-resolve.js';
9
10
  import type { AnsiValueStore, AnsiVaultStore } from './value-types.js';
10
11
  type MarkRoot = ReturnType<typeof parse>;
12
+ export interface ElementTabPanel {
13
+ label: string;
14
+ body: ReactNode;
15
+ }
11
16
  /**
12
17
  * Options for `renderMarkToAnsi`/`renderMarkNodeToAnsi`/`renderMarkInlineToAnsi`.
13
18
  * `store`/`vault` are offered both here and as positional parameters; when
14
- * both are given, the option wins (matching `@markii/html`'s
15
- * `RenderMarkOptions`).
19
+ * both are given, the option wins.
16
20
  */
17
21
  export interface RenderMarkOptions {
18
22
  readonly width?: number;
@@ -23,21 +27,27 @@ export interface RenderMarkOptions {
23
27
  readonly onDiagnostic?: OnDiagnostic;
24
28
  readonly store?: AnsiValueStore;
25
29
  readonly vault?: AnsiVaultStore;
30
+ /**
31
+ * Renders the document's CONTENT in interactive shape (tabs show only
32
+ * their active panel, details start closed unless `open`) without
33
+ * actually mounting a live keyboard loop — this render-once path always
34
+ * captures exactly one frame (`ink-string.ts`'s own `interactive: false`
35
+ * Ink render option is a SEPARATE thing; see its doc comment for the
36
+ * name collision). Defaults to `false`: unset, `tabs`/`details` render
37
+ * every panel/always-open exactly as the pre-Ink engine did.
38
+ */
39
+ readonly interactive?: boolean;
26
40
  }
27
41
  /**
28
42
  * Renders Markii text to a plain string (optionally carrying ANSI SGR/OSC 8
29
43
  * escapes) using `registry` to resolve directive names. `registry` is
30
44
  * OPTIONAL here, unlike the other two engines' required parameter, and
31
- * defaults to `defaultAnsiRegistry` (empty in this phase; every directive
32
- * therefore renders the unknown-component fallback).
45
+ * defaults to `defaultAnsiRegistry`.
33
46
  *
34
- * Pipeline: `@markii/core`'s `toHast` (parse -> tag directive nodes ->
35
- * remark-rehype -> sanitize URLs) -> a width-aware hast walk that swaps
36
- * directive elements for registry components (or the unknown-directive
37
- * fallback), folds script fences into a one-line marker, and wraps every
38
- * paragraph, heading, and list item to `options.width` (default 80). Pure
39
- * and never-throwing: parsing is tolerant, unknown names always render a
40
- * fallback, and any unexpected internal error degrades to the "failed to
47
+ * `async` since the first call in a process pays Ink/Yoga's one-time module-
48
+ * evaluation cost (`ink-string.ts`'s doc comment; `SPIKE-10-findings.md`'s
49
+ * "Item 3"). Never throws: parsing is tolerant, unknown names always render
50
+ * a fallback, and any unexpected internal error degrades to the "failed to
41
51
  * render document" line.
42
52
  *
43
53
  * `options.color` resolves via `./ansi.js`'s `resolveColorOption`: `'auto'`
@@ -45,25 +55,30 @@ export interface RenderMarkOptions {
45
55
  * environment knowledge of its own — a caller that wants automatic
46
56
  * detection calls `detectColorLevel` itself and passes the result as
47
57
  * `'16'`/`'256'`/`'truecolor'`.
48
- *
49
- * `store`/`vault` and `resolveImageSrc`/`resolveHref`/`onDiagnostic` carry
50
- * the same meaning as `@markii/html`'s identical options.
51
58
  */
52
- export declare function renderMarkToAnsi(text: string, registry?: AnsiRegistry, store?: AnsiValueStore, vault?: AnsiVaultStore, options?: RenderMarkOptions): string;
59
+ export declare function renderMarkToAnsi(text: string, registry?: AnsiRegistry, store?: AnsiValueStore, vault?: AnsiVaultStore, options?: RenderMarkOptions): Promise<string>;
53
60
  /**
54
61
  * The block-level twin of `renderMarkToAnsi`: renders one already-parsed
55
- * mdast node OR a whole already-parsed mdast document (`@markii/core`'s
56
- * `MarkNode`, or the `Root` `parse` itself returns) instead of raw document
57
- * text. Same registry resolution, same fallbacks, same purity and
62
+ * mdast node OR a whole already-parsed mdast document instead of raw
63
+ * document text. Same registry resolution, same fallbacks, same purity and
58
64
  * never-throw guarantees, same options.
59
65
  */
60
- export declare function renderMarkNodeToAnsi(node: MarkNode | MarkRoot, registry?: AnsiRegistry, store?: AnsiValueStore, vault?: AnsiVaultStore, options?: RenderMarkOptions): string;
66
+ export declare function renderMarkNodeToAnsi(node: MarkNode | MarkRoot, registry?: AnsiRegistry, store?: AnsiValueStore, vault?: AnsiVaultStore, options?: RenderMarkOptions): Promise<string>;
61
67
  /**
62
68
  * Renders `text` as a single, standalone inline directive when that is all
63
- * it is, mirroring `@markii/html`'s identical `renderMarkInlineToHtml`: a
64
- * source whose only parsed block is a paragraph holding exactly one text
65
- * directive renders that directive alone. Any other source falls back to
66
- * the exact same rendering `renderMarkToAnsi` would produce.
69
+ * it is. Any other source falls back to the exact same rendering
70
+ * `renderMarkToAnsi` would produce.
71
+ */
72
+ export declare function renderMarkInlineToAnsi(text: string, registry?: AnsiRegistry, store?: AnsiValueStore, vault?: AnsiVaultStore, options?: RenderMarkOptions): Promise<string>;
73
+ /**
74
+ * The live-viewer entry point (phase 3 consumes this): builds the same Ink
75
+ * element tree `renderMarkToAnsi` would render to a string, but returns the
76
+ * element itself so a host (the CLI's `markii view`) can mount it directly
77
+ * with Ink's own `render()` instead of re-rendering a string on every
78
+ * keystroke/resize. `document` accepts raw text OR an already-parsed mdast
79
+ * node/root, mirroring the three string entry points' flexibility.
67
80
  */
68
- export declare function renderMarkInlineToAnsi(text: string, registry?: AnsiRegistry, store?: AnsiValueStore, vault?: AnsiVaultStore, options?: RenderMarkOptions): string;
81
+ export declare function buildMarkElement(document: string | MarkNode | MarkRoot, registry?: AnsiRegistry, options?: RenderMarkOptions & {
82
+ readonly onExit?: () => void;
83
+ }): ReactElement;
69
84
  export {};