@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.
- package/LICENSE +21 -0
- package/dist/components/callout.d.ts +15 -1
- package/dist/components/callout.js +32 -19
- package/dist/components/card.d.ts +10 -0
- package/dist/components/card.js +21 -13
- package/dist/components/chart.d.ts +1 -1
- package/dist/components/chart.js +8 -9
- package/dist/components/details-string.js +1 -1
- package/dist/components/details.d.ts +3 -1
- package/dist/components/details.js +7 -6
- package/dist/components/divider.js +3 -5
- package/dist/components/progress.js +2 -3
- package/dist/components/rating.js +2 -2
- package/dist/components/row.d.ts +2 -0
- package/dist/components/row.js +3 -1
- package/dist/components/stat.js +1 -2
- package/dist/components/table-grid.d.ts +2 -1
- package/dist/components/table-grid.js +13 -10
- package/dist/components/table.js +1 -1
- package/dist/components/tabs.d.ts +3 -1
- package/dist/components/tabs.js +25 -7
- package/dist/failure-presentation.d.ts +15 -0
- package/dist/failure-presentation.js +22 -0
- package/dist/glyphs.d.ts +73 -0
- package/dist/glyphs.js +88 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/ink-string.d.ts +11 -0
- package/dist/ink-string.js +24 -0
- package/dist/interactive.d.ts +31 -6
- package/dist/interactive.js +155 -12
- package/dist/pack-loader.d.ts +64 -0
- package/dist/pack-loader.js +172 -0
- package/dist/pack-script.d.ts +44 -0
- package/dist/pack-script.js +135 -0
- package/dist/registry.d.ts +15 -0
- package/dist/render.d.ts +24 -1
- package/dist/render.js +401 -83
- package/dist/text-grid.d.ts +43 -7
- package/dist/text-grid.js +69 -18
- package/package.json +6 -5
package/dist/render.js
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
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
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
|
57
|
-
* real component state: `row` (a flex `Box` row
|
|
58
|
-
*
|
|
59
|
-
* `
|
|
60
|
-
*
|
|
61
|
-
* `
|
|
62
|
-
* back to a flattened STRING rendering of
|
|
63
|
-
* an accepted
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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}
|
|
493
|
-
const prefix = dim(
|
|
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
|
|
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
|
-
|
|
807
|
-
|
|
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:
|
|
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 ?
|
|
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 ||
|
|
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
|
|
938
|
-
//
|
|
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
|
-
//
|
|
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}
|
|
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(
|
|
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
|
|
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
|
}
|