@markii/ansi 0.16.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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/dist/components/callout.d.ts +15 -1
  3. package/dist/components/callout.js +32 -19
  4. package/dist/components/card.d.ts +10 -0
  5. package/dist/components/card.js +21 -13
  6. package/dist/components/chart.d.ts +1 -1
  7. package/dist/components/chart.js +8 -9
  8. package/dist/components/details-string.js +1 -1
  9. package/dist/components/details.d.ts +3 -1
  10. package/dist/components/details.js +7 -6
  11. package/dist/components/divider.js +3 -5
  12. package/dist/components/progress.js +2 -3
  13. package/dist/components/rating.js +2 -2
  14. package/dist/components/row.d.ts +2 -0
  15. package/dist/components/row.js +3 -1
  16. package/dist/components/stat.js +1 -2
  17. package/dist/components/table-grid.d.ts +2 -1
  18. package/dist/components/table-grid.js +13 -10
  19. package/dist/components/table.js +1 -1
  20. package/dist/components/tabs.d.ts +3 -1
  21. package/dist/components/tabs.js +25 -7
  22. package/dist/failure-presentation.d.ts +15 -0
  23. package/dist/failure-presentation.js +22 -0
  24. package/dist/glyphs.d.ts +73 -0
  25. package/dist/glyphs.js +88 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.js +4 -0
  28. package/dist/ink-string.d.ts +11 -0
  29. package/dist/ink-string.js +24 -0
  30. package/dist/interactive.d.ts +31 -6
  31. package/dist/interactive.js +155 -12
  32. package/dist/pack-loader.d.ts +64 -0
  33. package/dist/pack-loader.js +172 -0
  34. package/dist/pack-script.d.ts +44 -0
  35. package/dist/pack-script.js +135 -0
  36. package/dist/registry.d.ts +15 -0
  37. package/dist/render.d.ts +24 -1
  38. package/dist/render.js +401 -83
  39. package/dist/text-grid.d.ts +43 -7
  40. package/dist/text-grid.js +69 -18
  41. package/package.json +6 -5
package/dist/render.js CHANGED
@@ -1,27 +1,28 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Fragment } from 'react';
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment, isValidElement } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { parse, toHast, nodeToHast, isSafeUrl, parseMetaAttributes, isValidScriptName, } from '@markii/core';
5
5
  import { bold, carrySgrAcrossLines, dim, hyperlink, inverse, italic, resolveColorOption, underline, } from './ansi.js';
6
6
  import { style } from './style.js';
7
7
  import { defaultAnsiTheme } from './theme.js';
8
- import { frameBlock, measureWidth, padText, ruleLine, wrapText, } from './text-grid.js';
8
+ import { columnsBlock, frameBlock, frameTopLine, inkBorderStyle, isTextAlign, measureWidth, padText, ruleLine, wrapText, } from './text-grid.js';
9
9
  import { drawTableGrid } from './components/table-grid.js';
10
- import { resolveRowLayout } from './components/row.js';
11
- import { resolveCardInnerWidth } from './components/card.js';
12
- import { resolveCalloutInnerWidth } from './components/callout.js';
10
+ import { ROW_GUTTER, resolveRowLayout, } from './components/row.js';
11
+ import { CARD_FRAME_OVERHEAD, resolveCardBoxWidth, resolveCardInnerWidth, } from './components/card.js';
12
+ import { Callout, CALLOUT_TOKENS, headerTextFor, resolveCalloutBoxWidth, resolveCalloutInnerWidth, resolveCalloutType, } from './components/callout.js';
13
13
  import { resolveDetailsInnerWidth } from './components/details-string.js';
14
14
  import { sanitizeBlockText, sanitizeUrlText, stripControlCharacters, } from './sanitize.js';
15
15
  import { readRegistryComponent, registryLayoutAxis, registrySelfLayout, resolveDirectiveAlias, } from './registry.js';
16
16
  import { applyLayout, resolveLayoutAttributes, } from './layout.js';
17
17
  import { resolveScopedPath } from './resolve.js';
18
- import { dataStateSuffix, emptyInlineTitle, failureToken, invalidAttributeValueLabel, invalidAttributeValueTitle, } from './failure-presentation.js';
19
- import { formatValue, getContract, reportDiagnostic, } from '@markii/stdlib';
18
+ import { dataStateSuffix, emptyInlineTitle, failureToken, formMismatchTitle, NO_ENGINE_VIEW_LABEL, noEngineViewTitle, invalidAttributeValueLabel, invalidAttributeValueTitle, unsafeImageSrcLabel, unsafeImageSrcTitle, } from './failure-presentation.js';
19
+ import { createUnavailableComponentLookup, formatValue, getContract, reportDiagnostic, } from '@markii/stdlib';
20
20
  import { resolveImageAttribute, } from './image-resolve.js';
21
21
  import { resolveHrefAttribute } from './href-resolve.js';
22
22
  import { defaultAnsiRegistry } from './components/index.js';
23
- import { renderInkToString } from './ink-string.js';
23
+ import { renderInkToString, renderInkToStringSync } from './ink-string.js';
24
24
  import { InteractiveRoot } from './interactive.js';
25
+ import { glyphsFor } from './glyphs.js';
25
26
  import { InteractiveTabs } from './components/tabs.js';
26
27
  import { InteractiveDetails } from './components/details.js';
27
28
  /**
@@ -40,30 +41,26 @@ import { InteractiveDetails } from './components/details.js';
40
41
  *
41
42
  * - STRING mode (`render*String` functions below): the pre-Ink algorithm,
42
43
  * almost verbatim, still building one flattened, pre-wrapped, pre-aligned
43
- * string. This is what every standard component's OWN body still uses —
44
- * `card` frames a string, `callout` bars a string, `details` indents a
45
- * string — because these components draw FIXED-WIDTH GLYPHS that must be
46
- * correct the moment the string is built; Ink lays a tree out AFTER it is
47
- * built, so it cannot reflow a box-drawing frame or re-justify per-line
48
- * text alignment the way `text=center` needs (Ink has no per-line
49
- * paragraph-justify primitive). String mode is also the ONLY mode inline
44
+ * string. This is what a self-drawing component's body uses when it sits
45
+ * inside a string-drawn ancestor (a standalone `cell`, a GFM table cell),
46
+ * and what a non-left `text=` body uses in the render-once path, because
47
+ * Ink has no per-line paragraph-justify primitive. String mode is also the ONLY mode inline
50
48
  * content ever uses, at any nesting depth: an inline directive's body is
51
49
  * phrasing content by definition (docs/spec.md), never a layout the reader
52
50
  * would want Ink to lay out as boxes.
53
51
  * - ELEMENT mode (`render*Element` functions below): real Ink elements,
54
52
  * used for the document's own block sequencing (so ordinary prose wraps
55
53
  * through Ink's own layout instead of this engine's manual `wrap()`) and
56
- * for the three block directives that genuinely need Ink's real layout or
57
- * real component state: `row` (a flex `Box` row instead of the deleted
58
- * `columns()` string helper), `tabs` and `details` (real React state, for
59
- * `ctx.interactive`'s keyboard-driven switching/toggling). A `row`/`tabs`/
60
- * `details` reached through a STRING-mode ancestor (nested inside `card`,
61
- * `callout`, `figure`, a standalone `cell`, or a GFM table cell) falls
62
- * back to a flattened STRING rendering of itself with no interactivity —
63
- * an accepted, documented boundary: a self-drawing container's body is, by
64
- * its own nature, a fixed string, so nothing nested inside one can be
65
- * live. See this file's `renderRowString`/`renderTabsString` for that
66
- * fallback.
54
+ * for the block directives that genuinely need Ink's real layout or
55
+ * real component state: `row` (a flex `Box` row), `tabs` and `details`
56
+ * (real React state, for `ctx.interactive`'s keyboard-driven switching/
57
+ * toggling), and the containers `card`, `callout` and `figure`, whose
58
+ * bodies are Ink subtrees so those can sit inside them. A `row`/`tabs`/
59
+ * `details` reached through a STRING-mode ancestor (a standalone `cell`,
60
+ * or a GFM table cell) falls back to a flattened STRING rendering of
61
+ * itself with no interactivity, an accepted boundary: a body drawn as a
62
+ * string cannot hold live state. See `renderRowString` and
63
+ * `renderDirectiveString` for that fallback.
67
64
  *
68
65
  * Interactive components have no meaning for a NON-interactive render at
69
66
  * all: `@markii/stdlib`'s `INTERACTIVE_ATTRIBUTE` marks an element a HOST
@@ -80,10 +77,27 @@ const VALUE_DIRECTIVE_NAME = 'value';
80
77
  const DATA_ATTRIBUTE_KEY = 'data';
81
78
  /** The attribute `@markii/core` preserves a code fence's raw `meta` string onto. */
82
79
  const CODE_META_ATTR = 'data-mk-meta';
83
- /** The three block directive names given real Ink layout/state (see this file's top comment). */
80
+ /** The block directive names given real Ink layout/state (see this file's top comment). */
84
81
  const ROW_NAME = 'row';
85
82
  const TABS_NAME = 'tabs';
86
83
  const DETAILS_NAME = 'details';
84
+ /**
85
+ * `card` and `figure` join this list (batch 11's Ink-frames phase): both are
86
+ * containers whose body previously only ever reached STRING mode, so a
87
+ * `tabs`/`details` nested inside one could never become focusable. `card`
88
+ * draws its frame with an Ink `Box` border (`renderCardElement`); `figure`
89
+ * never drew a frame at all (it is a caption + optional image line), so it
90
+ * gains no border, only ELEMENT-mode children.
91
+ *
92
+ * `callout` joins them too. Its colored bar is a one-column left border
93
+ * on an Ink `Box` whose border glyph is the already-styled bar string: Ink
94
+ * repeats that glyph down the body's whole height, whatever the height turns
95
+ * out to be, and the color still comes from this engine's own SGR helpers,
96
+ * never an Ink color prop.
97
+ */
98
+ const CARD_NAME = 'card';
99
+ const CALLOUT_NAME = 'callout';
100
+ const FIGURE_NAME = 'figure';
87
101
  /** Block-level hast tags this engine recognizes; anything else inside a directive's children is treated as inline content. */
88
102
  const BLOCK_TAGS = new Set([
89
103
  'p',
@@ -196,6 +210,8 @@ function isRegisteredInline(entry) {
196
210
  /** The fallback label line, worded for the reason and the form the directive was written in. Plain text (no escapes). */
197
211
  function fallbackLabel(name, inline, reason) {
198
212
  const code = `\`${stripControlCharacters(name)}\``;
213
+ if (reason === 'no-engine-view')
214
+ return `${NO_ENGINE_VIEW_LABEL} ${code}`;
199
215
  if (reason === 'form-mismatch') {
200
216
  return inline
201
217
  ? `block component ${code} written inline`
@@ -204,7 +220,7 @@ function fallbackLabel(name, inline, reason) {
204
220
  return `unknown component ${code}`;
205
221
  }
206
222
  /** The unknown-directive fallback: dim inline text for a text directive, a dashed dim frame for a block directive. Wording matches `@markii/html`'s `fallbackLabel` exactly. */
207
- function unknownDirective(name, inline, childrenText, reason, width, color) {
223
+ function unknownDirective(name, inline, childrenText, reason, width, color, glyphs) {
208
224
  const label = fallbackLabel(name, inline, reason);
209
225
  if (inline) {
210
226
  return childrenText === ''
@@ -219,17 +235,17 @@ function unknownDirective(name, inline, childrenText, reason, width, color) {
219
235
  .join('\n')
220
236
  : '';
221
237
  const body = wrappedChildren ? `${label}\n\n${wrappedChildren}` : label;
222
- return dim(frameBlock(body, { style: 'dashed', width }), color);
238
+ return dim(frameBlock(body, { style: 'dashed', width, glyphs }), color);
223
239
  }
224
240
  /** A registered component that threw while rendering, contained to a neutral box rather than failing the whole document. Never dumps the error text. */
225
- function componentError(name, inline, childrenText, width, color) {
241
+ function componentError(name, inline, childrenText, width, color, glyphs) {
226
242
  const label = `component \`${stripControlCharacters(name)}\` failed to render`;
227
243
  if (inline) {
228
244
  return childrenText === ''
229
245
  ? dim(label, color)
230
246
  : `${dim(`${label}:`, color)} ${childrenText}`;
231
247
  }
232
- return dim(frameBlock(label, { style: 'dashed', width }), color);
248
+ return dim(frameBlock(label, { style: 'dashed', width, glyphs }), color);
233
249
  }
234
250
  /** Splits a `data=<name>` attribute off `attributes` and resolves it against `scope`. Never throws. */
235
251
  function resolveDataAttribute(attributes, scope) {
@@ -296,6 +312,7 @@ function buildComponentContext(width, indent, wctx, binding, layout, focusId) {
296
312
  indent,
297
313
  color,
298
314
  theme,
315
+ glyphs: wctx.glyphs,
299
316
  interactive: wctx.interactive,
300
317
  style: (text, token) => style(text, token, theme, color),
301
318
  bold: (text) => bold(text, color),
@@ -323,6 +340,7 @@ function buildComponentContext(width, indent, wctx, binding, layout, focusId) {
323
340
  ctx.dataStatus = binding.dataStatus;
324
341
  ctx.dataError = binding.dataError;
325
342
  ctx.dataFailureKind = binding.dataFailureKind;
343
+ ctx.dataSuffix = dataStateSuffix(binding.dataStatus, binding.dataFailureKind);
326
344
  }
327
345
  if (layout)
328
346
  ctx.layout = layout;
@@ -357,10 +375,24 @@ function renderDirectiveContentString(name, kind, isBlock, attributes, childrenT
357
375
  : undefined;
358
376
  const component = readRegistryComponent(entry);
359
377
  if (!entry || component == null) {
360
- return unknownDirective(name || '(unnamed)', inline, safeChildrenText(childrenText), 'unregistered', width, wctx.color);
378
+ const missingView = wctx.unavailable?.(name);
379
+ if (missingView !== undefined) {
380
+ reportDiagnostic(wctx.onDiagnostic, {
381
+ kind: 'no-engine-view',
382
+ directive: name,
383
+ message: noEngineViewTitle(name, missingView.pack, missingView.engines),
384
+ });
385
+ return unknownDirective(name, inline, safeChildrenText(childrenText), 'no-engine-view', width, wctx.color, wctx.glyphs);
386
+ }
387
+ return unknownDirective(name || '(unnamed)', inline, safeChildrenText(childrenText), 'unregistered', width, wctx.color, wctx.glyphs);
361
388
  }
362
389
  if (isFormMismatch(entry, kind)) {
363
- return unknownDirective(name || '(unnamed)', inline, safeChildrenText(childrenText), 'form-mismatch', width, wctx.color);
390
+ reportDiagnostic(wctx.onDiagnostic, {
391
+ kind: 'form-mismatch',
392
+ directive: name,
393
+ message: formMismatchTitle(name),
394
+ });
395
+ return unknownDirective(name || '(unnamed)', inline, safeChildrenText(childrenText), 'form-mismatch', width, wctx.color, wctx.glyphs);
364
396
  }
365
397
  const binding = resolveDataAttribute(attributes, wctx.scope);
366
398
  const ctx = buildComponentContext(width, indent, wctx, binding, layoutForComponent, undefined);
@@ -371,10 +403,15 @@ function renderDirectiveContentString(name, kind, isBlock, attributes, childrenT
371
403
  children: childrenText,
372
404
  ctx,
373
405
  });
374
- rendered = typeof result === 'string' ? result : String(result ?? '');
406
+ rendered =
407
+ typeof result === 'string'
408
+ ? result
409
+ : isValidElement(result)
410
+ ? renderInkToStringSync(result, width)
411
+ : String(result ?? '');
375
412
  }
376
413
  catch {
377
- return componentError(name || '(unnamed)', inline, safeChildrenText(childrenText), width, wctx.color);
414
+ return componentError(name || '(unnamed)', inline, safeChildrenText(childrenText), width, wctx.color, wctx.glyphs);
378
415
  }
379
416
  const markerSeparator = inline ? ' ' : '\n';
380
417
  const invalidEnum = invalidEnumAttribute(name, binding.attributes);
@@ -420,16 +457,7 @@ function renderRowString(element, attributes, width, indent, wctx) {
420
457
  const gridRows = [];
421
458
  for (let index = 0; index < placedCells.length; index += layout.columnCount) {
422
459
  const rowCells = placedCells.slice(index, index + layout.columnCount);
423
- const lineArrays = rowCells.map((cell) => cell.split('\n'));
424
- const rowHeight = Math.max(0, ...lineArrays.map((lines) => lines.length));
425
- const lines = [];
426
- for (let line = 0; line < rowHeight; line += 1) {
427
- lines.push(lineArrays
428
- .map((cellLines) => padText(cellLines[line] ?? '', layout.colWidth, 'left'))
429
- .join(' ')
430
- .replace(/[ ]+$/, ''));
431
- }
432
- gridRows.push(lines.join('\n'));
460
+ gridRows.push(columnsBlock(rowCells, rowCells.map(() => layout.colWidth), ROW_GUTTER));
433
461
  }
434
462
  return gridRows.join('\n\n');
435
463
  }
@@ -489,8 +517,8 @@ function renderBlockString(node, width, indent, wctx) {
489
517
  }
490
518
  if (node.tagName === 'blockquote') {
491
519
  const innerWidth = Math.max(1, width - 2);
492
- const inner = renderBlocksString(node.children, innerWidth, `${indent}│ `, wctx);
493
- const prefix = dim('│ ', wctx.color);
520
+ const inner = renderBlocksString(node.children, innerWidth, `${indent}${wctx.glyphs.quoteBar}`, wctx);
521
+ const prefix = dim(wctx.glyphs.quoteBar, wctx.color);
494
522
  return inner
495
523
  .split('\n')
496
524
  .map((line) => `${prefix}${line}`)
@@ -499,12 +527,13 @@ function renderBlockString(node, width, indent, wctx) {
499
527
  if (node.tagName === 'ul' || node.tagName === 'ol') {
500
528
  return renderListString(node, width, indent, wctx);
501
529
  }
502
- if (node.tagName === 'hr')
503
- return dim(ruleLine(width), wctx.color);
530
+ if (node.tagName === 'hr') {
531
+ return dim(ruleLine(width, wctx.glyphs.rule), wctx.color);
532
+ }
504
533
  if (node.tagName === 'table')
505
534
  return renderGfmTableString(node, width, wctx);
506
535
  if (node.tagName === 'pre') {
507
- const marker = renderScriptMarker(node, wctx.color);
536
+ const marker = renderScriptMarker(node, wctx.color, wctx.glyphs);
508
537
  if (marker !== undefined)
509
538
  return marker;
510
539
  return renderCodeBlock(node, wctx.color);
@@ -596,7 +625,7 @@ function getCodeText(codeNode) {
596
625
  }
597
626
  return text.endsWith('\n') ? text.slice(0, -1) : text;
598
627
  }
599
- function renderScriptMarker(node, color) {
628
+ function renderScriptMarker(node, color, glyphs) {
600
629
  try {
601
630
  const codeNode = findChildElement(node, 'code');
602
631
  const meta = codeNode
@@ -610,7 +639,9 @@ function renderScriptMarker(node, color) {
610
639
  return undefined;
611
640
  const src = attrs.src || undefined;
612
641
  const detail = src ?? getLanguage(codeNode);
613
- const summary = detail ? `⚙ ${name} · ${detail}` : `⚙ ${name}`;
642
+ const summary = detail
643
+ ? `${glyphs.script} ${name} ${glyphs.scriptSeparator} ${detail}`
644
+ : `${glyphs.script} ${name}`;
614
645
  return dim(summary, color);
615
646
  }
616
647
  catch {
@@ -661,7 +692,7 @@ function renderGfmTableString(tableEl, width, wctx) {
661
692
  }
662
693
  }
663
694
  }
664
- return drawTableGrid(headerCells.length > 0 ? headerCells : undefined, bodyRows, width, (text) => bold(text, wctx.color));
695
+ return drawTableGrid(headerCells.length > 0 ? headerCells : undefined, bodyRows, width, wctx.glyphs, (text) => bold(text, wctx.color));
665
696
  }
666
697
  function getPlainText(node) {
667
698
  let text = '';
@@ -790,7 +821,7 @@ function renderDirectiveString(element, width, indent, wctx) {
790
821
  childrenWidth = resolveCardInnerWidth(attributes, layoutForWidth, width);
791
822
  }
792
823
  else if (isBlock && name === 'callout') {
793
- childrenWidth = resolveCalloutInnerWidth(attributes, layoutForWidth, width);
824
+ childrenWidth = resolveCalloutInnerWidth(attributes, layoutForWidth, width, wctx.glyphs);
794
825
  }
795
826
  else if (isBlock && name === DETAILS_NAME) {
796
827
  childrenWidth = resolveDetailsInnerWidth(width);
@@ -803,8 +834,17 @@ function renderDirectiveString(element, width, indent, wctx) {
803
834
  return content;
804
835
  }
805
836
  function hasRealComponent(registry, name) {
806
- return (Object.hasOwn(registry, name) &&
807
- readRegistryComponent(registry[name]) != null);
837
+ if (!Object.hasOwn(registry, name))
838
+ return false;
839
+ const component = readRegistryComponent(registry[name]);
840
+ if (component == null)
841
+ return false;
842
+ // The element form of a callout replaces the standard component's string
843
+ // drawing. A host that registered its own `callout` keeps its own output,
844
+ // so only the standard one takes the element path.
845
+ if (name === CALLOUT_NAME)
846
+ return component === Callout;
847
+ return true;
808
848
  }
809
849
  // ---------------------------------------------------------------------------
810
850
  // ELEMENT MODE — real Ink elements, used for block sequencing and for
@@ -840,6 +880,95 @@ function invalidEnumMarkerElement(name, attributes, wctx) {
840
880
  const label = invalidAttributeValueLabel(name, invalidEnum.attribute);
841
881
  return styledText(dim(`[${label}]`, wctx.color));
842
882
  }
883
+ /**
884
+ * Whether `node` is a block directive whose element form can hold real
885
+ * state or focus (`row`, `tabs`, `details`, `card`, `callout`, `figure`).
886
+ * Used only to decide which children of a justified body stay Ink subtrees.
887
+ */
888
+ function isInteractiveCapableBlock(node, wctx) {
889
+ if (node.type !== 'element' || node.tagName !== DIRECTIVE_TAG)
890
+ return false;
891
+ if (stringProperty(node, 'data-mk-kind') === TEXT_DIRECTIVE_KIND) {
892
+ return false;
893
+ }
894
+ const { name } = resolveDirectiveAlias(wctx.registry, stringProperty(node, 'data-mk-name') ?? '', parseAttributes(stringProperty(node, 'data-mk-attrs')));
895
+ return ((name === ROW_NAME ||
896
+ name === TABS_NAME ||
897
+ name === DETAILS_NAME ||
898
+ name === CARD_NAME ||
899
+ name === CALLOUT_NAME ||
900
+ name === FIGURE_NAME) &&
901
+ hasRealComponent(wctx.registry, name));
902
+ }
903
+ /**
904
+ * The body of a self-drawing container (`card`, `callout`, a `row` cell)
905
+ * as one Ink element, honoring its `text=` alignment.
906
+ *
907
+ * `left` keeps the whole body a real Ink subtree. `center`/`right` justify
908
+ * each block with `text-grid.ts`'s `padText`, the one justification
909
+ * implementation, which needs a string. In the render-once path that means
910
+ * the whole body is one flattened, justified string, exactly as the
911
+ * pre-Ink engine drew it (the committed fixtures depend on it). In the
912
+ * live viewer a block that can hold focus (a `tabs`, a `details`, a nested
913
+ * container) stays an Ink subtree and is placed at the left of the body,
914
+ * because a subtree cannot be justified line by line; every other block is
915
+ * still justified. So the live view trades alignment of those blocks'
916
+ * headings for being usable.
917
+ */
918
+ function renderAlignedBodyElement(nodes, width, indent, wctx, align) {
919
+ if (align === 'left')
920
+ return renderBlocksElement(nodes, width, indent, wctx);
921
+ const justify = (text) => text
922
+ .split('\n')
923
+ .map((line) => padText(line, width, align))
924
+ .join('\n');
925
+ if (!wctx.interactive) {
926
+ const text = renderDirectiveChildrenString(nodes, width, indent, wctx);
927
+ return text === '' ? null : styledText(justify(text));
928
+ }
929
+ const elements = [];
930
+ for (const node of nodes) {
931
+ let el;
932
+ if (isInteractiveCapableBlock(node, wctx)) {
933
+ el = renderBlockElement(node, width, indent, wctx);
934
+ }
935
+ else {
936
+ const text = renderBlockString(node, width, indent, wctx);
937
+ el = text.trim() === '' ? null : styledText(justify(text));
938
+ }
939
+ if (el === null)
940
+ continue;
941
+ elements.push(_jsx(Box, { marginTop: elements.length > 0 ? 1 : 0, flexDirection: "column", children: el }, elements.length));
942
+ }
943
+ return elements.length === 0 ? null : _jsx(_Fragment, { children: elements });
944
+ }
945
+ /**
946
+ * Renders one `row` cell at `cellWidth`. `text=left` (the default) keeps
947
+ * the cell a real Ink element tree (`renderBlockElement`), so anything
948
+ * focusable inside it stays focusable. `text=center`/`text=right` instead
949
+ * falls back to a flattened, pre-justified STRING — batch 11's justification
950
+ * rule: exactly ONE implementation, `text-grid.ts`'s `padText`, never a
951
+ * second Ink-`alignItems`-based mechanism. Before this batch, `row`'s
952
+ * ELEMENT-mode grid silently ignored `text=` altogether (only the STRING-
953
+ * mode fallback, reached when a `row` is nested inside a self-drawing
954
+ * container, honored it); this closes that gap using the SAME helper the
955
+ * STRING mode already uses, rather than inventing a second mechanism.
956
+ */
957
+ function renderRowCellElement(node, cellWidth, indent, wctx, align) {
958
+ if (align === 'left')
959
+ return renderBlockElement(node, cellWidth, indent, wctx);
960
+ if (wctx.interactive) {
961
+ return renderAlignedBodyElement([node], cellWidth, indent, wctx, align);
962
+ }
963
+ const text = renderBlockString(node, cellWidth, indent, wctx);
964
+ if (text.trim() === '')
965
+ return null;
966
+ const justified = text
967
+ .split('\n')
968
+ .map((line) => padText(line, cellWidth, align))
969
+ .join('\n');
970
+ return styledText(justified);
971
+ }
843
972
  function renderRowElement(element, attributes, width, indent, wctx) {
844
973
  const cellNodes = element.children.filter((node) => !(node.type === 'text' && node.value.trim() === ''));
845
974
  if (cellNodes.length === 0)
@@ -847,15 +976,14 @@ function renderRowElement(element, attributes, width, indent, wctx) {
847
976
  const layout = resolveRowLayout(attributes, cellNodes.length, width);
848
977
  if (layout.stacked) {
849
978
  const items = cellNodes
850
- .map((node, index) => (_jsx(Box, { marginTop: index > 0 ? 1 : 0, flexDirection: "column", children: renderBlockElement(node, width, indent, wctx) }, index)))
979
+ .map((node, index) => (_jsx(Box, { marginTop: index > 0 ? 1 : 0, flexDirection: "column", children: renderRowCellElement(node, width, indent, wctx, layout.align) }, index)))
851
980
  .filter(Boolean);
852
981
  return _jsx(Box, { flexDirection: "column", children: items });
853
982
  }
854
- const gutter = 1;
855
983
  const rows = [];
856
984
  for (let index = 0; index < cellNodes.length; index += layout.columnCount) {
857
985
  const rowCells = cellNodes.slice(index, index + layout.columnCount);
858
- rows.push(_jsx(Box, { flexDirection: "row", marginTop: index > 0 ? 1 : 0, children: rowCells.map((node, cellIndex) => (_jsx(Box, { width: layout.colWidth, marginRight: cellIndex < rowCells.length - 1 ? gutter : 0, flexDirection: "column", children: renderBlockElement(node, layout.colWidth, indent, wctx) }, cellIndex))) }, index));
986
+ rows.push(_jsx(Box, { flexDirection: "row", marginTop: index > 0 ? 1 : 0, children: rowCells.map((node, cellIndex) => (_jsx(Box, { width: layout.colWidth, marginRight: cellIndex < rowCells.length - 1 ? ROW_GUTTER : 0, flexDirection: "column", children: renderRowCellElement(node, layout.colWidth, indent, wctx, layout.align) }, cellIndex))) }, index));
859
987
  }
860
988
  return _jsx(Box, { flexDirection: "column", children: rows });
861
989
  }
@@ -870,7 +998,7 @@ function renderTabsElement(element, width, indent, wctx) {
870
998
  label: panels[0]?.label ?? DEFAULT_TAB_LABEL,
871
999
  });
872
1000
  }
873
- return (_jsx(InteractiveTabs, { panels: panels, color: wctx.color, interactive: wctx.interactive, focusId: focusId }));
1001
+ return (_jsx(InteractiveTabs, { panels: panels, color: wctx.color, glyphs: wctx.glyphs, interactive: wctx.interactive, focusId: focusId }));
874
1002
  }
875
1003
  function extractTabPanelsElement(element, width, indent, wctx) {
876
1004
  const panels = [];
@@ -912,9 +1040,170 @@ function renderDetailsElement(element, attributes, width, indent, wctx) {
912
1040
  label: stripControlCharacters(title),
913
1041
  });
914
1042
  }
915
- return (_jsx(InteractiveDetails, { title: stripControlCharacters(title), body: body, defaultOpen: open, color: wctx.color, interactive: wctx.interactive, focusId: focusId }));
1043
+ return (_jsx(InteractiveDetails, { title: stripControlCharacters(title), body: body, defaultOpen: open, color: wctx.color, glyphs: wctx.glyphs, interactive: wctx.interactive, focusId: focusId }));
1044
+ }
1045
+ /**
1046
+ * Places a self-sized element (a `card`'s own bordered `Box`, already drawn
1047
+ * at `boxWidth`) within the full `width` per the resolved `align` preset —
1048
+ * the ELEMENT-mode twin of `layout.ts`'s string-based `selfLayoutAlign`,
1049
+ * kept separate from `applyLayoutElement` below because a self-drawing
1050
+ * component (unlike `row`/`tabs`/`details`) has ALREADY resolved its own
1051
+ * width (including `fit`, which `applyLayoutElement` does not implement)
1052
+ * and must never be narrowed a second time.
1053
+ */
1054
+ function selfLayoutAlignElement(element, boxWidth, layout, width) {
1055
+ if (!layout?.align)
1056
+ return element;
1057
+ const alignItems = layout.align === 'center'
1058
+ ? 'center'
1059
+ : layout.align === 'right'
1060
+ ? 'flex-end'
1061
+ : 'flex-start';
1062
+ return (_jsx(Box, { width: width, flexDirection: "column", alignItems: alignItems, children: _jsx(Box, { width: boxWidth, flexDirection: "column", children: element }) }));
1063
+ }
1064
+ /**
1065
+ * `:::card{title="..." text=left|center|right} ... :::` as a real Ink
1066
+ * element (batch 11: card moves off `frameBlock`'s hand-drawn string frame
1067
+ * onto Ink's own `Box` border, so a `tabs`/`details` nested in its body can
1068
+ * become a real focusable — this function's `body` is built through
1069
+ * `renderBlocksElement`, ELEMENT mode, whenever that is possible).
1070
+ *
1071
+ * Ink's border has no primitive for weaving text into an edge, so a titled
1072
+ * card still draws its OWN top line by hand (`frameTopLine`, the exact same
1073
+ * string `frameBlock` would have produced) and tells Ink not to draw a top
1074
+ * border at all; an untitled card lets Ink draw all four sides, which
1075
+ * `frameGlyphs`'s character set makes byte-identical to `frameBlock`'s own
1076
+ * untitled output (proven against the committed fixtures).
1077
+ *
1078
+ * `text=center`/`text=right` (batch 11's justification rule: exactly ONE
1079
+ * implementation, `text-grid.ts`'s `padText`, pre-justifying a STRING before
1080
+ * Ink ever sees it, never a second Ink-`alignItems`-based mechanism) falls
1081
+ * back to building the body as one flattened, pre-justified string, exactly
1082
+ * like the pre-Ink engine did — an accepted, documented trade: a `tabs`/
1083
+ * `details` nested inside a CENTERED or RIGHT-aligned card body is not
1084
+ * focusable, the same "self-drawing container's own body is a fixed string"
1085
+ * boundary this engine already draws elsewhere (this file's top comment).
1086
+ * `text=left` (the default, and by far the common case) keeps full nested
1087
+ * interactivity.
1088
+ */
1089
+ function renderCardElement(element, attributes, width, indent, wctx, layout) {
1090
+ const title = attributes.title ?? null;
1091
+ const titleText = title ? stripControlCharacters(title) : undefined;
1092
+ const rawTextAlign = attributes.text;
1093
+ const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
1094
+ const boxWidth = resolveCardBoxWidth({ ...attributes, title: titleText ?? null }, layout, width);
1095
+ const innerWidth = Math.max(1, boxWidth - CARD_FRAME_OVERHEAD);
1096
+ const bodyElement = renderAlignedBodyElement(element.children, innerWidth, indent, wctx, align);
1097
+ const bordered = (_jsx(Box, { width: boxWidth, flexDirection: "column", borderStyle: inkBorderStyle('solid', wctx.glyphs), borderTop: !titleText, children: bodyElement ?? _jsx(Text, { children: " " }) }));
1098
+ const framed = titleText ? (_jsxs(Box, { flexDirection: "column", width: boxWidth, children: [styledText(frameTopLine({
1099
+ style: 'solid',
1100
+ title: titleText,
1101
+ width: boxWidth,
1102
+ glyphs: wctx.glyphs,
1103
+ })), bordered] })) : (bordered);
1104
+ const positioned = selfLayoutAlignElement(framed, boxWidth, layout, width);
1105
+ // The invalid-`text=`-value marker is appended AFTER alignment, on its
1106
+ // own unindented line, exactly like the STRING engine's own
1107
+ // `renderDirectiveContentString` (the marker is never itself aligned or
1108
+ // framed) — see `invalidEnumMarkerElement`'s call sites elsewhere in this
1109
+ // file for the same shape.
1110
+ const marker = invalidEnumMarkerElement(CARD_NAME, attributes, wctx);
1111
+ if (!marker)
1112
+ return positioned;
1113
+ return (_jsxs(Box, { flexDirection: "column", children: [positioned, marker] }));
1114
+ }
1115
+ /**
1116
+ * `:::callout{type=info|warning|danger title="..." text=left|center|right}`
1117
+ * as a real Ink element, so a `tabs`/`details` in its body can be focused.
1118
+ *
1119
+ * The colored bar is the left border of an Ink `Box`, one column wide, whose
1120
+ * border glyph is the bar already styled by this engine's own SGR helpers
1121
+ * (never an Ink color prop). Ink repeats that glyph down the full height of
1122
+ * the body, so the bar needs no knowledge of the body's line count. The
1123
+ * bar glyph's trailing space becomes the box's left padding, so the layout
1124
+ * matches the string form column for column.
1125
+ *
1126
+ * The icon/label line and the title line are justified per `text=` with
1127
+ * `padText`; the body goes through `renderAlignedBodyElement`.
1128
+ */
1129
+ function renderCalloutElement(element, attributes, width, indent, wctx, layout) {
1130
+ const type = resolveCalloutType(attributes);
1131
+ const token = CALLOUT_TOKENS[type];
1132
+ const titleText = attributes.title
1133
+ ? stripControlCharacters(attributes.title)
1134
+ : undefined;
1135
+ const rawTextAlign = attributes.text;
1136
+ const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
1137
+ const { glyphs, color, theme } = wctx;
1138
+ const barWidth = measureWidth(glyphs.calloutBar);
1139
+ const boxWidth = resolveCalloutBoxWidth(type, titleText ?? null, layout, width, glyphs);
1140
+ const innerWidth = Math.max(1, boxWidth - barWidth);
1141
+ const placeLine = (line) => align === 'left' ? line : padText(line, innerWidth, align);
1142
+ const barGlyph = glyphs.calloutBar.trimEnd();
1143
+ const border = {
1144
+ topLeft: '',
1145
+ top: '',
1146
+ topRight: '',
1147
+ right: '',
1148
+ bottomRight: '',
1149
+ bottom: '',
1150
+ bottomLeft: '',
1151
+ left: style(barGlyph, token, theme, color),
1152
+ };
1153
+ const body = renderAlignedBodyElement(element.children, innerWidth, indent, wctx, align);
1154
+ const box = (_jsxs(Box, { width: boxWidth, flexDirection: "column", borderStyle: border, borderTop: false, borderRight: false, borderBottom: false, paddingLeft: barWidth - measureWidth(barGlyph), children: [styledText(placeLine(style(headerTextFor(type, glyphs), token, theme, color))), titleText ? styledText(placeLine(bold(titleText, color))) : null, body] }));
1155
+ const positioned = selfLayoutAlignElement(box, boxWidth, layout, width);
1156
+ const marker = invalidEnumMarkerElement(CALLOUT_NAME, attributes, wctx);
1157
+ if (!marker)
1158
+ return positioned;
1159
+ return (_jsxs(Box, { flexDirection: "column", children: [positioned, marker] }));
1160
+ }
1161
+ /**
1162
+ * `:::figure{src="..." alt="..."} caption markdown :::` as a real Ink
1163
+ * element (batch 11: `figure` never drew a hand-drawn frame — it is a plain
1164
+ * dim alt/src line followed by its caption body — but its body still only
1165
+ * ever reached STRING mode before this change, so a `tabs`/`details` in its
1166
+ * caption could never become focusable. `body` here is built through
1167
+ * `renderBlocksElement`, ELEMENT mode, exactly mirroring `figure.ts`'s own
1168
+ * STRING-mode component line for line: the header lines (alt/src/refused)
1169
+ * stack directly above the caption with NO blank row between them, matching
1170
+ * `Figure`'s own `lines.join('\n')` (a single line break, not a blank
1171
+ * paragraph gap) — `Box`'s default zero margin between column children
1172
+ * reproduces that exactly.
1173
+ */
1174
+ const FIGURE_DIRECTIVE_NAME = 'figure';
1175
+ function renderFigureElement(element, attributes, width, indent, wctx) {
1176
+ const rawSrc = attributes.src ?? null;
1177
+ const alt = attributes.alt ?? '';
1178
+ const refused = Boolean(rawSrc) && !isSafeUrl(rawSrc);
1179
+ const safeSrc = rawSrc && !refused ? rawSrc : null;
1180
+ const src = safeSrc
1181
+ ? stripControlCharacters(resolveImageAttribute(safeSrc, wctx.resolveImageSrc))
1182
+ : null;
1183
+ const headerLines = [];
1184
+ if (rawSrc) {
1185
+ if (alt) {
1186
+ headerLines.push(dim(`alt: ${stripControlCharacters(alt)}`, wctx.color));
1187
+ }
1188
+ if (refused) {
1189
+ reportDiagnostic(wctx.onDiagnostic, {
1190
+ kind: 'unsafe-image-src',
1191
+ directive: FIGURE_DIRECTIVE_NAME,
1192
+ message: unsafeImageSrcTitle(FIGURE_DIRECTIVE_NAME),
1193
+ });
1194
+ headerLines.push(dim(`[${unsafeImageSrcLabel(FIGURE_DIRECTIVE_NAME)}]`, wctx.color));
1195
+ }
1196
+ else if (src) {
1197
+ headerLines.push(dim(src, wctx.color));
1198
+ }
1199
+ }
1200
+ const headerElement = headerLines.length > 0 ? styledText(headerLines.join('\n')) : null;
1201
+ const body = renderBlocksElement(element.children, width, indent, wctx);
1202
+ if (!headerElement && !body)
1203
+ return null;
1204
+ return (_jsxs(Box, { flexDirection: "column", children: [headerElement, body] }));
916
1205
  }
917
- /** Renders one directive as a real Ink element (block position only). Dispatches `row`/`tabs`/`details` to their real builders; every other directive falls back to the string engine, wrapped in one `<Text>`. */
1206
+ /** Renders one directive as a real Ink element (block position only). Dispatches `row`/`tabs`/`details`/`card`/`figure` to their real builders; every other directive falls back to the string engine, wrapped in one `<Text>`. */
918
1207
  function renderDirectiveElement(element, width, indent, wctx) {
919
1208
  const written = stringProperty(element, 'data-mk-name') ?? '';
920
1209
  const kind = stringProperty(element, 'data-mk-kind');
@@ -932,15 +1221,37 @@ function renderDirectiveElement(element, width, indent, wctx) {
932
1221
  : undefined;
933
1222
  const formMismatch = entry ? isFormMismatch(entry, kind) : false;
934
1223
  if (!formMismatch &&
935
- (name === ROW_NAME || name === TABS_NAME || name === DETAILS_NAME) &&
1224
+ (name === ROW_NAME ||
1225
+ name === TABS_NAME ||
1226
+ name === DETAILS_NAME ||
1227
+ name === CARD_NAME ||
1228
+ name === CALLOUT_NAME ||
1229
+ name === FIGURE_NAME) &&
936
1230
  hasRealComponent(wctx.registry, name)) {
937
- // None of `row`/`tabs`/`details` own a layout axis (`registryLayoutAxis`
938
- // is undefined for all three in the standard registry), so, unlike a
1231
+ // None of these six own a layout axis (`registryLayoutAxis` is
1232
+ // undefined for all of them in the standard registry), so, unlike a
939
1233
  // layout-wrapper scope, a `width=`/`align=` the author wrote still needs
940
- // the GENERIC post-render treatment — `applyLayoutElement` below, the
941
- // JSX-based twin of `layout.ts`'s string `applyLayout`.
1234
+ // resolving here rather than being handled by the directive's own name.
942
1235
  const { attributes, resolved: layoutResolved } = resolveLayoutAttributes(aliased, undefined);
943
1236
  try {
1237
+ if (name === CARD_NAME) {
1238
+ // `card` resolves its OWN width (including `fit`, which the generic
1239
+ // `applyLayoutElement` below does not implement) and aligns itself
1240
+ // with `selfLayoutAlignElement` — see `renderCardElement`'s doc
1241
+ // comment. It never goes through the generic post-render wrap.
1242
+ return renderCardElement(element, attributes, width, indent, wctx, layoutResolved);
1243
+ }
1244
+ if (name === CALLOUT_NAME && !wctx.interactive && layoutResolved?.align) {
1245
+ // The string form places each line of a callout on its own, so
1246
+ // `align=` leaves the bars ragged; the element form places the block
1247
+ // as a unit. The render-once output stays the string form's, byte for
1248
+ // byte, and only the live viewer places the callout as a unit.
1249
+ return styledText(renderDirectiveString(element, width, indent, wctx));
1250
+ }
1251
+ if (name === CALLOUT_NAME) {
1252
+ // Self-sized and self-aligned like `card`: see `renderCalloutElement`.
1253
+ return renderCalloutElement(element, attributes, width, indent, wctx, layoutResolved);
1254
+ }
944
1255
  let el = null;
945
1256
  if (name === ROW_NAME) {
946
1257
  el = renderRowElement(element, attributes, width, indent, wctx);
@@ -948,9 +1259,12 @@ function renderDirectiveElement(element, width, indent, wctx) {
948
1259
  else if (name === TABS_NAME) {
949
1260
  el = renderTabsElement(element, width, indent, wctx);
950
1261
  }
951
- else {
1262
+ else if (name === DETAILS_NAME) {
952
1263
  el = renderDetailsElement(element, attributes, width, indent, wctx);
953
1264
  }
1265
+ else {
1266
+ el = renderFigureElement(element, attributes, width, indent, wctx);
1267
+ }
954
1268
  if (!el)
955
1269
  return null;
956
1270
  const marker = invalidEnumMarkerElement(name, attributes, wctx);
@@ -958,7 +1272,7 @@ function renderDirectiveElement(element, width, indent, wctx) {
958
1272
  return applyLayoutElement(withMarker, layoutResolved, width);
959
1273
  }
960
1274
  catch {
961
- return styledText(componentError(name || '(unnamed)', false, '', width, wctx.color));
1275
+ return styledText(componentError(name || '(unnamed)', false, '', width, wctx.color, wctx.glyphs));
962
1276
  }
963
1277
  }
964
1278
  return styledText(renderDirectiveString(element, width, indent, wctx));
@@ -977,10 +1291,10 @@ function renderBlockElement(node, width, indent, wctx) {
977
1291
  }
978
1292
  if (node.tagName === 'blockquote') {
979
1293
  const innerWidth = Math.max(1, width - 2);
980
- const inner = renderBlocksElement(node.children, innerWidth, `${indent}│ `, wctx);
1294
+ const inner = renderBlocksElement(node.children, innerWidth, `${indent}${wctx.glyphs.quoteBar}`, wctx);
981
1295
  if (!inner)
982
1296
  return null;
983
- return (_jsxs(Box, { width: width, children: [styledText(dim('│ ', wctx.color)), _jsx(Box, { flexDirection: "column", width: innerWidth, children: inner })] }));
1297
+ return (_jsxs(Box, { width: width, children: [styledText(dim(wctx.glyphs.quoteBar, wctx.color)), _jsx(Box, { flexDirection: "column", width: innerWidth, children: inner })] }));
984
1298
  }
985
1299
  if (node.tagName === 'ul' || node.tagName === 'ol') {
986
1300
  return renderListElement(node, width, indent, wctx);
@@ -1041,16 +1355,18 @@ function renderFailureFallback(error, color) {
1041
1355
  const message = error instanceof Error ? error.message : String(error);
1042
1356
  return `${dim('failed to render document', color)}\n${stripControlCharacters(message)}\n`;
1043
1357
  }
1044
- function buildWalkContext(registry, scope, color, theme, interactive, resolveImageSrc, resolveHref, onDiagnostic) {
1358
+ function buildWalkContext(registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable) {
1045
1359
  return {
1046
1360
  registry,
1047
1361
  scope,
1048
1362
  color,
1049
1363
  theme,
1364
+ glyphs,
1050
1365
  interactive,
1051
1366
  resolveImageSrc,
1052
1367
  resolveHref,
1053
1368
  onDiagnostic,
1369
+ unavailable,
1054
1370
  focusCounter: { current: 0 },
1055
1371
  focusables: undefined,
1056
1372
  };
@@ -1058,13 +1374,13 @@ function buildWalkContext(registry, scope, color, theme, interactive, resolveIma
1058
1374
  /** The columns assumed when a caller supplies no `width` option. */
1059
1375
  const DEFAULT_WIDTH = 80;
1060
1376
  /** Builds the root Ink element for `root`, given every resolved render input. Exported for `buildMarkElement` (the live-viewer entry point, phase 3) as well as this module's own three string-producing entry points. */
1061
- function buildRootElement(root, registry, scope, color, theme, interactive, resolveImageSrc, resolveHref, onDiagnostic, width) {
1062
- const wctx = buildWalkContext(registry, scope, color, theme, interactive, resolveImageSrc, resolveHref, onDiagnostic);
1377
+ function buildRootElement(root, registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, width) {
1378
+ const wctx = buildWalkContext(registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable);
1063
1379
  const body = renderBlocksElement(root.children, width, '', wctx);
1064
1380
  return (_jsx(Box, { width: width, flexDirection: "column", children: body ?? _jsx(Text, { children: " " }) }));
1065
1381
  }
1066
- async function renderRootToString(root, registry, scope, color, theme, interactive, resolveImageSrc, resolveHref, onDiagnostic, width) {
1067
- const element = buildRootElement(root, registry, scope, color, theme, interactive, resolveImageSrc, resolveHref, onDiagnostic, width);
1382
+ async function renderRootToString(root, registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, width) {
1383
+ const element = buildRootElement(root, registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, width);
1068
1384
  const raw = await renderInkToString(element, width);
1069
1385
  return finalizeOutput(raw);
1070
1386
  }
@@ -1089,7 +1405,7 @@ async function renderRootToString(root, registry, scope, color, theme, interacti
1089
1405
  export async function renderMarkToAnsi(text, registry = defaultAnsiRegistry, store, vault, options) {
1090
1406
  const color = resolveColorOption(options?.color);
1091
1407
  try {
1092
- return await renderRootToString(toHast(text), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, color, options?.theme ?? defaultAnsiTheme, options?.interactive ?? false, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, options?.width ?? DEFAULT_WIDTH);
1408
+ return await renderRootToString(toHast(text), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, color, options?.theme ?? defaultAnsiTheme, glyphsFor(options?.box), options?.interactive ?? false, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, createUnavailableComponentLookup(options?.unavailableComponents), options?.width ?? DEFAULT_WIDTH);
1093
1409
  }
1094
1410
  catch (error) {
1095
1411
  return renderFailureFallback(error, color);
@@ -1104,7 +1420,7 @@ export async function renderMarkToAnsi(text, registry = defaultAnsiRegistry, sto
1104
1420
  export async function renderMarkNodeToAnsi(node, registry = defaultAnsiRegistry, store, vault, options) {
1105
1421
  const color = resolveColorOption(options?.color);
1106
1422
  try {
1107
- return await renderRootToString(nodeOrRootToHast(node), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, color, options?.theme ?? defaultAnsiTheme, options?.interactive ?? false, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, options?.width ?? DEFAULT_WIDTH);
1423
+ return await renderRootToString(nodeOrRootToHast(node), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, color, options?.theme ?? defaultAnsiTheme, glyphsFor(options?.box), options?.interactive ?? false, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, createUnavailableComponentLookup(options?.unavailableComponents), options?.width ?? DEFAULT_WIDTH);
1108
1424
  }
1109
1425
  catch (error) {
1110
1426
  return renderFailureFallback(error, color);
@@ -1153,6 +1469,7 @@ export async function renderMarkInlineToAnsi(text, registry = defaultAnsiRegistr
1153
1469
  export function buildMarkElement(document, registry = defaultAnsiRegistry, options) {
1154
1470
  const color = resolveColorOption(options?.color);
1155
1471
  const theme = options?.theme ?? defaultAnsiTheme;
1472
+ const glyphs = glyphsFor(options?.box);
1156
1473
  const scope = { store: options?.store, vault: options?.vault };
1157
1474
  const width = options?.width ?? DEFAULT_WIDTH;
1158
1475
  const interactive = options?.interactive ?? true;
@@ -1166,7 +1483,8 @@ export function buildMarkElement(document, registry = defaultAnsiRegistry, optio
1166
1483
  catch {
1167
1484
  root = { type: 'root', children: [] };
1168
1485
  }
1169
- const content = buildRootElement(root, registry, scope, color, theme, interactive, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, width);
1486
+ const unavailable = createUnavailableComponentLookup(options?.unavailableComponents);
1487
+ const content = buildRootElement(root, registry, scope, color, theme, glyphs, interactive, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, unavailable, width);
1170
1488
  if (!interactive)
1171
1489
  return content;
1172
1490
  // The walk above already assigned every focusable component's `focusId`
@@ -1179,14 +1497,14 @@ export function buildMarkElement(document, registry = defaultAnsiRegistry, optio
1179
1497
  // mount/resize, not per keystroke.
1180
1498
  const counter = { current: 0 };
1181
1499
  const focusables = [];
1182
- const countingWctx = buildWalkContext(registry, scope, color, theme, true, options?.resolveImageSrc, options?.resolveHref,
1500
+ const countingWctx = buildWalkContext(registry, scope, color, theme, glyphs, true, options?.resolveImageSrc, options?.resolveHref,
1183
1501
  // No `onDiagnostic`: the real walk above already reported every notice
1184
1502
  // this document produces, and this second walk exists only to collect
1185
1503
  // focus descriptors. Passing the callback here would report each notice
1186
1504
  // twice on every mount and again on every resize.
1187
- undefined);
1505
+ undefined, unavailable);
1188
1506
  countingWctx.focusCounter = counter;
1189
1507
  countingWctx.focusables = focusables;
1190
1508
  renderBlocksElement(root.children, width, '', countingWctx);
1191
- return (_jsx(InteractiveRoot, { focusables: focusables, onExit: options?.onExit, color: color, theme: theme, children: content }));
1509
+ return (_jsx(InteractiveRoot, { focusables: focusables, onExit: options?.onExit, color: color, theme: theme, keyLegend: glyphs.keyLegend, viewportRows: options?.viewportRows, width: width, children: content }));
1192
1510
  }