@markii/ansi 0.16.0 → 0.17.0-next.1
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 +28 -0
- package/dist/failure-presentation.js +41 -0
- package/dist/glyphs.d.ts +73 -0
- package/dist/glyphs.js +88 -0
- package/dist/index.d.ts +5 -1
- 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 +48 -1
- package/dist/render.js +441 -85
- 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, codeRendererFailedTitle, formMismatchTitle, NO_ENGINE_VIEW_LABEL, noEngineViewTitle, packLoadFailedLabel, packLoadFailedTitle, 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',
|
|
@@ -194,8 +208,13 @@ function isRegisteredInline(entry) {
|
|
|
194
208
|
}
|
|
195
209
|
}
|
|
196
210
|
/** The fallback label line, worded for the reason and the form the directive was written in. Plain text (no escapes). */
|
|
197
|
-
function fallbackLabel(name, inline, reason) {
|
|
211
|
+
function fallbackLabel(name, inline, reason, pack) {
|
|
198
212
|
const code = `\`${stripControlCharacters(name)}\``;
|
|
213
|
+
if (reason === 'pack-load-failed') {
|
|
214
|
+
return `${packLoadFailedLabel(stripControlCharacters(pack ?? ''))} ${code}`;
|
|
215
|
+
}
|
|
216
|
+
if (reason === 'no-engine-view')
|
|
217
|
+
return `${NO_ENGINE_VIEW_LABEL} ${code}`;
|
|
199
218
|
if (reason === 'form-mismatch') {
|
|
200
219
|
return inline
|
|
201
220
|
? `block component ${code} written inline`
|
|
@@ -204,8 +223,8 @@ function fallbackLabel(name, inline, reason) {
|
|
|
204
223
|
return `unknown component ${code}`;
|
|
205
224
|
}
|
|
206
225
|
/** 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) {
|
|
208
|
-
const label = fallbackLabel(name, inline, reason);
|
|
226
|
+
function unknownDirective(name, inline, childrenText, reason, width, color, glyphs, pack) {
|
|
227
|
+
const label = fallbackLabel(name, inline, reason, pack);
|
|
209
228
|
if (inline) {
|
|
210
229
|
return childrenText === ''
|
|
211
230
|
? dim(label, color)
|
|
@@ -219,17 +238,17 @@ function unknownDirective(name, inline, childrenText, reason, width, color) {
|
|
|
219
238
|
.join('\n')
|
|
220
239
|
: '';
|
|
221
240
|
const body = wrappedChildren ? `${label}\n\n${wrappedChildren}` : label;
|
|
222
|
-
return dim(frameBlock(body, { style: 'dashed', width }), color);
|
|
241
|
+
return dim(frameBlock(body, { style: 'dashed', width, glyphs }), color);
|
|
223
242
|
}
|
|
224
243
|
/** 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) {
|
|
244
|
+
function componentError(name, inline, childrenText, width, color, glyphs) {
|
|
226
245
|
const label = `component \`${stripControlCharacters(name)}\` failed to render`;
|
|
227
246
|
if (inline) {
|
|
228
247
|
return childrenText === ''
|
|
229
248
|
? dim(label, color)
|
|
230
249
|
: `${dim(`${label}:`, color)} ${childrenText}`;
|
|
231
250
|
}
|
|
232
|
-
return dim(frameBlock(label, { style: 'dashed', width }), color);
|
|
251
|
+
return dim(frameBlock(label, { style: 'dashed', width, glyphs }), color);
|
|
233
252
|
}
|
|
234
253
|
/** Splits a `data=<name>` attribute off `attributes` and resolves it against `scope`. Never throws. */
|
|
235
254
|
function resolveDataAttribute(attributes, scope) {
|
|
@@ -296,6 +315,7 @@ function buildComponentContext(width, indent, wctx, binding, layout, focusId) {
|
|
|
296
315
|
indent,
|
|
297
316
|
color,
|
|
298
317
|
theme,
|
|
318
|
+
glyphs: wctx.glyphs,
|
|
299
319
|
interactive: wctx.interactive,
|
|
300
320
|
style: (text, token) => style(text, token, theme, color),
|
|
301
321
|
bold: (text) => bold(text, color),
|
|
@@ -323,6 +343,7 @@ function buildComponentContext(width, indent, wctx, binding, layout, focusId) {
|
|
|
323
343
|
ctx.dataStatus = binding.dataStatus;
|
|
324
344
|
ctx.dataError = binding.dataError;
|
|
325
345
|
ctx.dataFailureKind = binding.dataFailureKind;
|
|
346
|
+
ctx.dataSuffix = dataStateSuffix(binding.dataStatus, binding.dataFailureKind);
|
|
326
347
|
}
|
|
327
348
|
if (layout)
|
|
328
349
|
ctx.layout = layout;
|
|
@@ -357,10 +378,26 @@ function renderDirectiveContentString(name, kind, isBlock, attributes, childrenT
|
|
|
357
378
|
: undefined;
|
|
358
379
|
const component = readRegistryComponent(entry);
|
|
359
380
|
if (!entry || component == null) {
|
|
360
|
-
|
|
381
|
+
const missingView = wctx.unavailable?.(name);
|
|
382
|
+
if (missingView !== undefined) {
|
|
383
|
+
reportDiagnostic(wctx.onDiagnostic, {
|
|
384
|
+
kind: 'no-engine-view',
|
|
385
|
+
directive: name,
|
|
386
|
+
message: missingView.loadFailed
|
|
387
|
+
? packLoadFailedTitle(name, missingView.pack)
|
|
388
|
+
: noEngineViewTitle(name, missingView.pack, missingView.engines),
|
|
389
|
+
});
|
|
390
|
+
return unknownDirective(name, inline, safeChildrenText(childrenText), missingView.loadFailed ? 'pack-load-failed' : 'no-engine-view', width, wctx.color, wctx.glyphs, missingView.pack);
|
|
391
|
+
}
|
|
392
|
+
return unknownDirective(name || '(unnamed)', inline, safeChildrenText(childrenText), 'unregistered', width, wctx.color, wctx.glyphs);
|
|
361
393
|
}
|
|
362
394
|
if (isFormMismatch(entry, kind)) {
|
|
363
|
-
|
|
395
|
+
reportDiagnostic(wctx.onDiagnostic, {
|
|
396
|
+
kind: 'form-mismatch',
|
|
397
|
+
directive: name,
|
|
398
|
+
message: formMismatchTitle(name),
|
|
399
|
+
});
|
|
400
|
+
return unknownDirective(name || '(unnamed)', inline, safeChildrenText(childrenText), 'form-mismatch', width, wctx.color, wctx.glyphs);
|
|
364
401
|
}
|
|
365
402
|
const binding = resolveDataAttribute(attributes, wctx.scope);
|
|
366
403
|
const ctx = buildComponentContext(width, indent, wctx, binding, layoutForComponent, undefined);
|
|
@@ -371,10 +408,15 @@ function renderDirectiveContentString(name, kind, isBlock, attributes, childrenT
|
|
|
371
408
|
children: childrenText,
|
|
372
409
|
ctx,
|
|
373
410
|
});
|
|
374
|
-
rendered =
|
|
411
|
+
rendered =
|
|
412
|
+
typeof result === 'string'
|
|
413
|
+
? result
|
|
414
|
+
: isValidElement(result)
|
|
415
|
+
? renderInkToStringSync(result, width)
|
|
416
|
+
: String(result ?? '');
|
|
375
417
|
}
|
|
376
418
|
catch {
|
|
377
|
-
return componentError(name || '(unnamed)', inline, safeChildrenText(childrenText), width, wctx.color);
|
|
419
|
+
return componentError(name || '(unnamed)', inline, safeChildrenText(childrenText), width, wctx.color, wctx.glyphs);
|
|
378
420
|
}
|
|
379
421
|
const markerSeparator = inline ? ' ' : '\n';
|
|
380
422
|
const invalidEnum = invalidEnumAttribute(name, binding.attributes);
|
|
@@ -420,16 +462,7 @@ function renderRowString(element, attributes, width, indent, wctx) {
|
|
|
420
462
|
const gridRows = [];
|
|
421
463
|
for (let index = 0; index < placedCells.length; index += layout.columnCount) {
|
|
422
464
|
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'));
|
|
465
|
+
gridRows.push(columnsBlock(rowCells, rowCells.map(() => layout.colWidth), ROW_GUTTER));
|
|
433
466
|
}
|
|
434
467
|
return gridRows.join('\n\n');
|
|
435
468
|
}
|
|
@@ -489,8 +522,8 @@ function renderBlockString(node, width, indent, wctx) {
|
|
|
489
522
|
}
|
|
490
523
|
if (node.tagName === 'blockquote') {
|
|
491
524
|
const innerWidth = Math.max(1, width - 2);
|
|
492
|
-
const inner = renderBlocksString(node.children, innerWidth, `${indent}
|
|
493
|
-
const prefix = dim(
|
|
525
|
+
const inner = renderBlocksString(node.children, innerWidth, `${indent}${wctx.glyphs.quoteBar}`, wctx);
|
|
526
|
+
const prefix = dim(wctx.glyphs.quoteBar, wctx.color);
|
|
494
527
|
return inner
|
|
495
528
|
.split('\n')
|
|
496
529
|
.map((line) => `${prefix}${line}`)
|
|
@@ -499,14 +532,18 @@ function renderBlockString(node, width, indent, wctx) {
|
|
|
499
532
|
if (node.tagName === 'ul' || node.tagName === 'ol') {
|
|
500
533
|
return renderListString(node, width, indent, wctx);
|
|
501
534
|
}
|
|
502
|
-
if (node.tagName === 'hr')
|
|
503
|
-
return dim(ruleLine(width), wctx.color);
|
|
535
|
+
if (node.tagName === 'hr') {
|
|
536
|
+
return dim(ruleLine(width, wctx.glyphs.rule), wctx.color);
|
|
537
|
+
}
|
|
504
538
|
if (node.tagName === 'table')
|
|
505
539
|
return renderGfmTableString(node, width, wctx);
|
|
506
540
|
if (node.tagName === 'pre') {
|
|
507
|
-
const marker = renderScriptMarker(node, wctx.color);
|
|
541
|
+
const marker = renderScriptMarker(node, wctx.color, wctx.glyphs);
|
|
508
542
|
if (marker !== undefined)
|
|
509
543
|
return marker;
|
|
544
|
+
const custom = applyRenderCode(node, wctx);
|
|
545
|
+
if (custom !== undefined)
|
|
546
|
+
return custom;
|
|
510
547
|
return renderCodeBlock(node, wctx.color);
|
|
511
548
|
}
|
|
512
549
|
return wrapText(renderInlineChildrenString(node.children ?? [], width, indent, wctx), width).join('\n');
|
|
@@ -596,7 +633,7 @@ function getCodeText(codeNode) {
|
|
|
596
633
|
}
|
|
597
634
|
return text.endsWith('\n') ? text.slice(0, -1) : text;
|
|
598
635
|
}
|
|
599
|
-
function renderScriptMarker(node, color) {
|
|
636
|
+
function renderScriptMarker(node, color, glyphs) {
|
|
600
637
|
try {
|
|
601
638
|
const codeNode = findChildElement(node, 'code');
|
|
602
639
|
const meta = codeNode
|
|
@@ -610,13 +647,44 @@ function renderScriptMarker(node, color) {
|
|
|
610
647
|
return undefined;
|
|
611
648
|
const src = attrs.src || undefined;
|
|
612
649
|
const detail = src ?? getLanguage(codeNode);
|
|
613
|
-
const summary = detail
|
|
650
|
+
const summary = detail
|
|
651
|
+
? `${glyphs.script} ${name} ${glyphs.scriptSeparator} ${detail}`
|
|
652
|
+
: `${glyphs.script} ${name}`;
|
|
614
653
|
return dim(summary, color);
|
|
615
654
|
}
|
|
616
655
|
catch {
|
|
617
656
|
return undefined;
|
|
618
657
|
}
|
|
619
658
|
}
|
|
659
|
+
/**
|
|
660
|
+
* Offers one ordinary fenced code block to the host's `renderCode`. The
|
|
661
|
+
* text handed over is already run through `sanitizeBlockText` (control
|
|
662
|
+
* characters dropped, tabs expanded), so a host that echoes it cannot
|
|
663
|
+
* smuggle author escapes into the terminal. Returns the host's string, or
|
|
664
|
+
* `undefined` to draw the block as usual: no callback, a decline, a
|
|
665
|
+
* non-string result, or a throw (reported through `onDiagnostic`).
|
|
666
|
+
*/
|
|
667
|
+
function applyRenderCode(node, wctx) {
|
|
668
|
+
if (!wctx.renderCode)
|
|
669
|
+
return undefined;
|
|
670
|
+
const codeNode = findChildElement(node, 'code');
|
|
671
|
+
if (!codeNode)
|
|
672
|
+
return undefined;
|
|
673
|
+
try {
|
|
674
|
+
const result = wctx.renderCode({
|
|
675
|
+
lang: getLanguage(codeNode) || undefined,
|
|
676
|
+
code: sanitizeBlockText(getCodeText(codeNode)),
|
|
677
|
+
});
|
|
678
|
+
return typeof result === 'string' ? result : undefined;
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
reportDiagnostic(wctx.onDiagnostic, {
|
|
682
|
+
kind: 'code-renderer-failed',
|
|
683
|
+
message: codeRendererFailedTitle(),
|
|
684
|
+
});
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
620
688
|
function renderCodeBlock(node, color) {
|
|
621
689
|
const codeNode = findChildElement(node, 'code');
|
|
622
690
|
const language = getLanguage(codeNode);
|
|
@@ -661,7 +729,7 @@ function renderGfmTableString(tableEl, width, wctx) {
|
|
|
661
729
|
}
|
|
662
730
|
}
|
|
663
731
|
}
|
|
664
|
-
return drawTableGrid(headerCells.length > 0 ? headerCells : undefined, bodyRows, width, (text) => bold(text, wctx.color));
|
|
732
|
+
return drawTableGrid(headerCells.length > 0 ? headerCells : undefined, bodyRows, width, wctx.glyphs, (text) => bold(text, wctx.color));
|
|
665
733
|
}
|
|
666
734
|
function getPlainText(node) {
|
|
667
735
|
let text = '';
|
|
@@ -790,7 +858,7 @@ function renderDirectiveString(element, width, indent, wctx) {
|
|
|
790
858
|
childrenWidth = resolveCardInnerWidth(attributes, layoutForWidth, width);
|
|
791
859
|
}
|
|
792
860
|
else if (isBlock && name === 'callout') {
|
|
793
|
-
childrenWidth = resolveCalloutInnerWidth(attributes, layoutForWidth, width);
|
|
861
|
+
childrenWidth = resolveCalloutInnerWidth(attributes, layoutForWidth, width, wctx.glyphs);
|
|
794
862
|
}
|
|
795
863
|
else if (isBlock && name === DETAILS_NAME) {
|
|
796
864
|
childrenWidth = resolveDetailsInnerWidth(width);
|
|
@@ -803,8 +871,17 @@ function renderDirectiveString(element, width, indent, wctx) {
|
|
|
803
871
|
return content;
|
|
804
872
|
}
|
|
805
873
|
function hasRealComponent(registry, name) {
|
|
806
|
-
|
|
807
|
-
|
|
874
|
+
if (!Object.hasOwn(registry, name))
|
|
875
|
+
return false;
|
|
876
|
+
const component = readRegistryComponent(registry[name]);
|
|
877
|
+
if (component == null)
|
|
878
|
+
return false;
|
|
879
|
+
// The element form of a callout replaces the standard component's string
|
|
880
|
+
// drawing. A host that registered its own `callout` keeps its own output,
|
|
881
|
+
// so only the standard one takes the element path.
|
|
882
|
+
if (name === CALLOUT_NAME)
|
|
883
|
+
return component === Callout;
|
|
884
|
+
return true;
|
|
808
885
|
}
|
|
809
886
|
// ---------------------------------------------------------------------------
|
|
810
887
|
// ELEMENT MODE — real Ink elements, used for block sequencing and for
|
|
@@ -840,6 +917,95 @@ function invalidEnumMarkerElement(name, attributes, wctx) {
|
|
|
840
917
|
const label = invalidAttributeValueLabel(name, invalidEnum.attribute);
|
|
841
918
|
return styledText(dim(`[${label}]`, wctx.color));
|
|
842
919
|
}
|
|
920
|
+
/**
|
|
921
|
+
* Whether `node` is a block directive whose element form can hold real
|
|
922
|
+
* state or focus (`row`, `tabs`, `details`, `card`, `callout`, `figure`).
|
|
923
|
+
* Used only to decide which children of a justified body stay Ink subtrees.
|
|
924
|
+
*/
|
|
925
|
+
function isInteractiveCapableBlock(node, wctx) {
|
|
926
|
+
if (node.type !== 'element' || node.tagName !== DIRECTIVE_TAG)
|
|
927
|
+
return false;
|
|
928
|
+
if (stringProperty(node, 'data-mk-kind') === TEXT_DIRECTIVE_KIND) {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
const { name } = resolveDirectiveAlias(wctx.registry, stringProperty(node, 'data-mk-name') ?? '', parseAttributes(stringProperty(node, 'data-mk-attrs')));
|
|
932
|
+
return ((name === ROW_NAME ||
|
|
933
|
+
name === TABS_NAME ||
|
|
934
|
+
name === DETAILS_NAME ||
|
|
935
|
+
name === CARD_NAME ||
|
|
936
|
+
name === CALLOUT_NAME ||
|
|
937
|
+
name === FIGURE_NAME) &&
|
|
938
|
+
hasRealComponent(wctx.registry, name));
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* The body of a self-drawing container (`card`, `callout`, a `row` cell)
|
|
942
|
+
* as one Ink element, honoring its `text=` alignment.
|
|
943
|
+
*
|
|
944
|
+
* `left` keeps the whole body a real Ink subtree. `center`/`right` justify
|
|
945
|
+
* each block with `text-grid.ts`'s `padText`, the one justification
|
|
946
|
+
* implementation, which needs a string. In the render-once path that means
|
|
947
|
+
* the whole body is one flattened, justified string, exactly as the
|
|
948
|
+
* pre-Ink engine drew it (the committed fixtures depend on it). In the
|
|
949
|
+
* live viewer a block that can hold focus (a `tabs`, a `details`, a nested
|
|
950
|
+
* container) stays an Ink subtree and is placed at the left of the body,
|
|
951
|
+
* because a subtree cannot be justified line by line; every other block is
|
|
952
|
+
* still justified. So the live view trades alignment of those blocks'
|
|
953
|
+
* headings for being usable.
|
|
954
|
+
*/
|
|
955
|
+
function renderAlignedBodyElement(nodes, width, indent, wctx, align) {
|
|
956
|
+
if (align === 'left')
|
|
957
|
+
return renderBlocksElement(nodes, width, indent, wctx);
|
|
958
|
+
const justify = (text) => text
|
|
959
|
+
.split('\n')
|
|
960
|
+
.map((line) => padText(line, width, align))
|
|
961
|
+
.join('\n');
|
|
962
|
+
if (!wctx.interactive) {
|
|
963
|
+
const text = renderDirectiveChildrenString(nodes, width, indent, wctx);
|
|
964
|
+
return text === '' ? null : styledText(justify(text));
|
|
965
|
+
}
|
|
966
|
+
const elements = [];
|
|
967
|
+
for (const node of nodes) {
|
|
968
|
+
let el;
|
|
969
|
+
if (isInteractiveCapableBlock(node, wctx)) {
|
|
970
|
+
el = renderBlockElement(node, width, indent, wctx);
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
const text = renderBlockString(node, width, indent, wctx);
|
|
974
|
+
el = text.trim() === '' ? null : styledText(justify(text));
|
|
975
|
+
}
|
|
976
|
+
if (el === null)
|
|
977
|
+
continue;
|
|
978
|
+
elements.push(_jsx(Box, { marginTop: elements.length > 0 ? 1 : 0, flexDirection: "column", children: el }, elements.length));
|
|
979
|
+
}
|
|
980
|
+
return elements.length === 0 ? null : _jsx(_Fragment, { children: elements });
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Renders one `row` cell at `cellWidth`. `text=left` (the default) keeps
|
|
984
|
+
* the cell a real Ink element tree (`renderBlockElement`), so anything
|
|
985
|
+
* focusable inside it stays focusable. `text=center`/`text=right` instead
|
|
986
|
+
* falls back to a flattened, pre-justified STRING — batch 11's justification
|
|
987
|
+
* rule: exactly ONE implementation, `text-grid.ts`'s `padText`, never a
|
|
988
|
+
* second Ink-`alignItems`-based mechanism. Before this batch, `row`'s
|
|
989
|
+
* ELEMENT-mode grid silently ignored `text=` altogether (only the STRING-
|
|
990
|
+
* mode fallback, reached when a `row` is nested inside a self-drawing
|
|
991
|
+
* container, honored it); this closes that gap using the SAME helper the
|
|
992
|
+
* STRING mode already uses, rather than inventing a second mechanism.
|
|
993
|
+
*/
|
|
994
|
+
function renderRowCellElement(node, cellWidth, indent, wctx, align) {
|
|
995
|
+
if (align === 'left')
|
|
996
|
+
return renderBlockElement(node, cellWidth, indent, wctx);
|
|
997
|
+
if (wctx.interactive) {
|
|
998
|
+
return renderAlignedBodyElement([node], cellWidth, indent, wctx, align);
|
|
999
|
+
}
|
|
1000
|
+
const text = renderBlockString(node, cellWidth, indent, wctx);
|
|
1001
|
+
if (text.trim() === '')
|
|
1002
|
+
return null;
|
|
1003
|
+
const justified = text
|
|
1004
|
+
.split('\n')
|
|
1005
|
+
.map((line) => padText(line, cellWidth, align))
|
|
1006
|
+
.join('\n');
|
|
1007
|
+
return styledText(justified);
|
|
1008
|
+
}
|
|
843
1009
|
function renderRowElement(element, attributes, width, indent, wctx) {
|
|
844
1010
|
const cellNodes = element.children.filter((node) => !(node.type === 'text' && node.value.trim() === ''));
|
|
845
1011
|
if (cellNodes.length === 0)
|
|
@@ -847,15 +1013,14 @@ function renderRowElement(element, attributes, width, indent, wctx) {
|
|
|
847
1013
|
const layout = resolveRowLayout(attributes, cellNodes.length, width);
|
|
848
1014
|
if (layout.stacked) {
|
|
849
1015
|
const items = cellNodes
|
|
850
|
-
.map((node, index) => (_jsx(Box, { marginTop: index > 0 ? 1 : 0, flexDirection: "column", children:
|
|
1016
|
+
.map((node, index) => (_jsx(Box, { marginTop: index > 0 ? 1 : 0, flexDirection: "column", children: renderRowCellElement(node, width, indent, wctx, layout.align) }, index)))
|
|
851
1017
|
.filter(Boolean);
|
|
852
1018
|
return _jsx(Box, { flexDirection: "column", children: items });
|
|
853
1019
|
}
|
|
854
|
-
const gutter = 1;
|
|
855
1020
|
const rows = [];
|
|
856
1021
|
for (let index = 0; index < cellNodes.length; index += layout.columnCount) {
|
|
857
1022
|
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 ?
|
|
1023
|
+
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
1024
|
}
|
|
860
1025
|
return _jsx(Box, { flexDirection: "column", children: rows });
|
|
861
1026
|
}
|
|
@@ -870,7 +1035,7 @@ function renderTabsElement(element, width, indent, wctx) {
|
|
|
870
1035
|
label: panels[0]?.label ?? DEFAULT_TAB_LABEL,
|
|
871
1036
|
});
|
|
872
1037
|
}
|
|
873
|
-
return (_jsx(InteractiveTabs, { panels: panels, color: wctx.color, interactive: wctx.interactive, focusId: focusId }));
|
|
1038
|
+
return (_jsx(InteractiveTabs, { panels: panels, color: wctx.color, glyphs: wctx.glyphs, interactive: wctx.interactive, focusId: focusId }));
|
|
874
1039
|
}
|
|
875
1040
|
function extractTabPanelsElement(element, width, indent, wctx) {
|
|
876
1041
|
const panels = [];
|
|
@@ -912,9 +1077,170 @@ function renderDetailsElement(element, attributes, width, indent, wctx) {
|
|
|
912
1077
|
label: stripControlCharacters(title),
|
|
913
1078
|
});
|
|
914
1079
|
}
|
|
915
|
-
return (_jsx(InteractiveDetails, { title: stripControlCharacters(title), body: body, defaultOpen: open, color: wctx.color, interactive: wctx.interactive, focusId: focusId }));
|
|
1080
|
+
return (_jsx(InteractiveDetails, { title: stripControlCharacters(title), body: body, defaultOpen: open, color: wctx.color, glyphs: wctx.glyphs, interactive: wctx.interactive, focusId: focusId }));
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Places a self-sized element (a `card`'s own bordered `Box`, already drawn
|
|
1084
|
+
* at `boxWidth`) within the full `width` per the resolved `align` preset —
|
|
1085
|
+
* the ELEMENT-mode twin of `layout.ts`'s string-based `selfLayoutAlign`,
|
|
1086
|
+
* kept separate from `applyLayoutElement` below because a self-drawing
|
|
1087
|
+
* component (unlike `row`/`tabs`/`details`) has ALREADY resolved its own
|
|
1088
|
+
* width (including `fit`, which `applyLayoutElement` does not implement)
|
|
1089
|
+
* and must never be narrowed a second time.
|
|
1090
|
+
*/
|
|
1091
|
+
function selfLayoutAlignElement(element, boxWidth, layout, width) {
|
|
1092
|
+
if (!layout?.align)
|
|
1093
|
+
return element;
|
|
1094
|
+
const alignItems = layout.align === 'center'
|
|
1095
|
+
? 'center'
|
|
1096
|
+
: layout.align === 'right'
|
|
1097
|
+
? 'flex-end'
|
|
1098
|
+
: 'flex-start';
|
|
1099
|
+
return (_jsx(Box, { width: width, flexDirection: "column", alignItems: alignItems, children: _jsx(Box, { width: boxWidth, flexDirection: "column", children: element }) }));
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* `:::card{title="..." text=left|center|right} ... :::` as a real Ink
|
|
1103
|
+
* element (batch 11: card moves off `frameBlock`'s hand-drawn string frame
|
|
1104
|
+
* onto Ink's own `Box` border, so a `tabs`/`details` nested in its body can
|
|
1105
|
+
* become a real focusable — this function's `body` is built through
|
|
1106
|
+
* `renderBlocksElement`, ELEMENT mode, whenever that is possible).
|
|
1107
|
+
*
|
|
1108
|
+
* Ink's border has no primitive for weaving text into an edge, so a titled
|
|
1109
|
+
* card still draws its OWN top line by hand (`frameTopLine`, the exact same
|
|
1110
|
+
* string `frameBlock` would have produced) and tells Ink not to draw a top
|
|
1111
|
+
* border at all; an untitled card lets Ink draw all four sides, which
|
|
1112
|
+
* `frameGlyphs`'s character set makes byte-identical to `frameBlock`'s own
|
|
1113
|
+
* untitled output (proven against the committed fixtures).
|
|
1114
|
+
*
|
|
1115
|
+
* `text=center`/`text=right` (batch 11's justification rule: exactly ONE
|
|
1116
|
+
* implementation, `text-grid.ts`'s `padText`, pre-justifying a STRING before
|
|
1117
|
+
* Ink ever sees it, never a second Ink-`alignItems`-based mechanism) falls
|
|
1118
|
+
* back to building the body as one flattened, pre-justified string, exactly
|
|
1119
|
+
* like the pre-Ink engine did — an accepted, documented trade: a `tabs`/
|
|
1120
|
+
* `details` nested inside a CENTERED or RIGHT-aligned card body is not
|
|
1121
|
+
* focusable, the same "self-drawing container's own body is a fixed string"
|
|
1122
|
+
* boundary this engine already draws elsewhere (this file's top comment).
|
|
1123
|
+
* `text=left` (the default, and by far the common case) keeps full nested
|
|
1124
|
+
* interactivity.
|
|
1125
|
+
*/
|
|
1126
|
+
function renderCardElement(element, attributes, width, indent, wctx, layout) {
|
|
1127
|
+
const title = attributes.title ?? null;
|
|
1128
|
+
const titleText = title ? stripControlCharacters(title) : undefined;
|
|
1129
|
+
const rawTextAlign = attributes.text;
|
|
1130
|
+
const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
|
|
1131
|
+
const boxWidth = resolveCardBoxWidth({ ...attributes, title: titleText ?? null }, layout, width);
|
|
1132
|
+
const innerWidth = Math.max(1, boxWidth - CARD_FRAME_OVERHEAD);
|
|
1133
|
+
const bodyElement = renderAlignedBodyElement(element.children, innerWidth, indent, wctx, align);
|
|
1134
|
+
const bordered = (_jsx(Box, { width: boxWidth, flexDirection: "column", borderStyle: inkBorderStyle('solid', wctx.glyphs), borderTop: !titleText, children: bodyElement ?? _jsx(Text, { children: " " }) }));
|
|
1135
|
+
const framed = titleText ? (_jsxs(Box, { flexDirection: "column", width: boxWidth, children: [styledText(frameTopLine({
|
|
1136
|
+
style: 'solid',
|
|
1137
|
+
title: titleText,
|
|
1138
|
+
width: boxWidth,
|
|
1139
|
+
glyphs: wctx.glyphs,
|
|
1140
|
+
})), bordered] })) : (bordered);
|
|
1141
|
+
const positioned = selfLayoutAlignElement(framed, boxWidth, layout, width);
|
|
1142
|
+
// The invalid-`text=`-value marker is appended AFTER alignment, on its
|
|
1143
|
+
// own unindented line, exactly like the STRING engine's own
|
|
1144
|
+
// `renderDirectiveContentString` (the marker is never itself aligned or
|
|
1145
|
+
// framed) — see `invalidEnumMarkerElement`'s call sites elsewhere in this
|
|
1146
|
+
// file for the same shape.
|
|
1147
|
+
const marker = invalidEnumMarkerElement(CARD_NAME, attributes, wctx);
|
|
1148
|
+
if (!marker)
|
|
1149
|
+
return positioned;
|
|
1150
|
+
return (_jsxs(Box, { flexDirection: "column", children: [positioned, marker] }));
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* `:::callout{type=info|warning|danger title="..." text=left|center|right}`
|
|
1154
|
+
* as a real Ink element, so a `tabs`/`details` in its body can be focused.
|
|
1155
|
+
*
|
|
1156
|
+
* The colored bar is the left border of an Ink `Box`, one column wide, whose
|
|
1157
|
+
* border glyph is the bar already styled by this engine's own SGR helpers
|
|
1158
|
+
* (never an Ink color prop). Ink repeats that glyph down the full height of
|
|
1159
|
+
* the body, so the bar needs no knowledge of the body's line count. The
|
|
1160
|
+
* bar glyph's trailing space becomes the box's left padding, so the layout
|
|
1161
|
+
* matches the string form column for column.
|
|
1162
|
+
*
|
|
1163
|
+
* The icon/label line and the title line are justified per `text=` with
|
|
1164
|
+
* `padText`; the body goes through `renderAlignedBodyElement`.
|
|
1165
|
+
*/
|
|
1166
|
+
function renderCalloutElement(element, attributes, width, indent, wctx, layout) {
|
|
1167
|
+
const type = resolveCalloutType(attributes);
|
|
1168
|
+
const token = CALLOUT_TOKENS[type];
|
|
1169
|
+
const titleText = attributes.title
|
|
1170
|
+
? stripControlCharacters(attributes.title)
|
|
1171
|
+
: undefined;
|
|
1172
|
+
const rawTextAlign = attributes.text;
|
|
1173
|
+
const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
|
|
1174
|
+
const { glyphs, color, theme } = wctx;
|
|
1175
|
+
const barWidth = measureWidth(glyphs.calloutBar);
|
|
1176
|
+
const boxWidth = resolveCalloutBoxWidth(type, titleText ?? null, layout, width, glyphs);
|
|
1177
|
+
const innerWidth = Math.max(1, boxWidth - barWidth);
|
|
1178
|
+
const placeLine = (line) => align === 'left' ? line : padText(line, innerWidth, align);
|
|
1179
|
+
const barGlyph = glyphs.calloutBar.trimEnd();
|
|
1180
|
+
const border = {
|
|
1181
|
+
topLeft: '',
|
|
1182
|
+
top: '',
|
|
1183
|
+
topRight: '',
|
|
1184
|
+
right: '',
|
|
1185
|
+
bottomRight: '',
|
|
1186
|
+
bottom: '',
|
|
1187
|
+
bottomLeft: '',
|
|
1188
|
+
left: style(barGlyph, token, theme, color),
|
|
1189
|
+
};
|
|
1190
|
+
const body = renderAlignedBodyElement(element.children, innerWidth, indent, wctx, align);
|
|
1191
|
+
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] }));
|
|
1192
|
+
const positioned = selfLayoutAlignElement(box, boxWidth, layout, width);
|
|
1193
|
+
const marker = invalidEnumMarkerElement(CALLOUT_NAME, attributes, wctx);
|
|
1194
|
+
if (!marker)
|
|
1195
|
+
return positioned;
|
|
1196
|
+
return (_jsxs(Box, { flexDirection: "column", children: [positioned, marker] }));
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* `:::figure{src="..." alt="..."} caption markdown :::` as a real Ink
|
|
1200
|
+
* element (batch 11: `figure` never drew a hand-drawn frame — it is a plain
|
|
1201
|
+
* dim alt/src line followed by its caption body — but its body still only
|
|
1202
|
+
* ever reached STRING mode before this change, so a `tabs`/`details` in its
|
|
1203
|
+
* caption could never become focusable. `body` here is built through
|
|
1204
|
+
* `renderBlocksElement`, ELEMENT mode, exactly mirroring `figure.ts`'s own
|
|
1205
|
+
* STRING-mode component line for line: the header lines (alt/src/refused)
|
|
1206
|
+
* stack directly above the caption with NO blank row between them, matching
|
|
1207
|
+
* `Figure`'s own `lines.join('\n')` (a single line break, not a blank
|
|
1208
|
+
* paragraph gap) — `Box`'s default zero margin between column children
|
|
1209
|
+
* reproduces that exactly.
|
|
1210
|
+
*/
|
|
1211
|
+
const FIGURE_DIRECTIVE_NAME = 'figure';
|
|
1212
|
+
function renderFigureElement(element, attributes, width, indent, wctx) {
|
|
1213
|
+
const rawSrc = attributes.src ?? null;
|
|
1214
|
+
const alt = attributes.alt ?? '';
|
|
1215
|
+
const refused = Boolean(rawSrc) && !isSafeUrl(rawSrc);
|
|
1216
|
+
const safeSrc = rawSrc && !refused ? rawSrc : null;
|
|
1217
|
+
const src = safeSrc
|
|
1218
|
+
? stripControlCharacters(resolveImageAttribute(safeSrc, wctx.resolveImageSrc))
|
|
1219
|
+
: null;
|
|
1220
|
+
const headerLines = [];
|
|
1221
|
+
if (rawSrc) {
|
|
1222
|
+
if (alt) {
|
|
1223
|
+
headerLines.push(dim(`alt: ${stripControlCharacters(alt)}`, wctx.color));
|
|
1224
|
+
}
|
|
1225
|
+
if (refused) {
|
|
1226
|
+
reportDiagnostic(wctx.onDiagnostic, {
|
|
1227
|
+
kind: 'unsafe-image-src',
|
|
1228
|
+
directive: FIGURE_DIRECTIVE_NAME,
|
|
1229
|
+
message: unsafeImageSrcTitle(FIGURE_DIRECTIVE_NAME),
|
|
1230
|
+
});
|
|
1231
|
+
headerLines.push(dim(`[${unsafeImageSrcLabel(FIGURE_DIRECTIVE_NAME)}]`, wctx.color));
|
|
1232
|
+
}
|
|
1233
|
+
else if (src) {
|
|
1234
|
+
headerLines.push(dim(src, wctx.color));
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const headerElement = headerLines.length > 0 ? styledText(headerLines.join('\n')) : null;
|
|
1238
|
+
const body = renderBlocksElement(element.children, width, indent, wctx);
|
|
1239
|
+
if (!headerElement && !body)
|
|
1240
|
+
return null;
|
|
1241
|
+
return (_jsxs(Box, { flexDirection: "column", children: [headerElement, body] }));
|
|
916
1242
|
}
|
|
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>`. */
|
|
1243
|
+
/** 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
1244
|
function renderDirectiveElement(element, width, indent, wctx) {
|
|
919
1245
|
const written = stringProperty(element, 'data-mk-name') ?? '';
|
|
920
1246
|
const kind = stringProperty(element, 'data-mk-kind');
|
|
@@ -932,15 +1258,37 @@ function renderDirectiveElement(element, width, indent, wctx) {
|
|
|
932
1258
|
: undefined;
|
|
933
1259
|
const formMismatch = entry ? isFormMismatch(entry, kind) : false;
|
|
934
1260
|
if (!formMismatch &&
|
|
935
|
-
(name === ROW_NAME ||
|
|
1261
|
+
(name === ROW_NAME ||
|
|
1262
|
+
name === TABS_NAME ||
|
|
1263
|
+
name === DETAILS_NAME ||
|
|
1264
|
+
name === CARD_NAME ||
|
|
1265
|
+
name === CALLOUT_NAME ||
|
|
1266
|
+
name === FIGURE_NAME) &&
|
|
936
1267
|
hasRealComponent(wctx.registry, name)) {
|
|
937
|
-
// None of
|
|
938
|
-
//
|
|
1268
|
+
// None of these six own a layout axis (`registryLayoutAxis` is
|
|
1269
|
+
// undefined for all of them in the standard registry), so, unlike a
|
|
939
1270
|
// layout-wrapper scope, a `width=`/`align=` the author wrote still needs
|
|
940
|
-
//
|
|
941
|
-
// JSX-based twin of `layout.ts`'s string `applyLayout`.
|
|
1271
|
+
// resolving here rather than being handled by the directive's own name.
|
|
942
1272
|
const { attributes, resolved: layoutResolved } = resolveLayoutAttributes(aliased, undefined);
|
|
943
1273
|
try {
|
|
1274
|
+
if (name === CARD_NAME) {
|
|
1275
|
+
// `card` resolves its OWN width (including `fit`, which the generic
|
|
1276
|
+
// `applyLayoutElement` below does not implement) and aligns itself
|
|
1277
|
+
// with `selfLayoutAlignElement` — see `renderCardElement`'s doc
|
|
1278
|
+
// comment. It never goes through the generic post-render wrap.
|
|
1279
|
+
return renderCardElement(element, attributes, width, indent, wctx, layoutResolved);
|
|
1280
|
+
}
|
|
1281
|
+
if (name === CALLOUT_NAME && !wctx.interactive && layoutResolved?.align) {
|
|
1282
|
+
// The string form places each line of a callout on its own, so
|
|
1283
|
+
// `align=` leaves the bars ragged; the element form places the block
|
|
1284
|
+
// as a unit. The render-once output stays the string form's, byte for
|
|
1285
|
+
// byte, and only the live viewer places the callout as a unit.
|
|
1286
|
+
return styledText(renderDirectiveString(element, width, indent, wctx));
|
|
1287
|
+
}
|
|
1288
|
+
if (name === CALLOUT_NAME) {
|
|
1289
|
+
// Self-sized and self-aligned like `card`: see `renderCalloutElement`.
|
|
1290
|
+
return renderCalloutElement(element, attributes, width, indent, wctx, layoutResolved);
|
|
1291
|
+
}
|
|
944
1292
|
let el = null;
|
|
945
1293
|
if (name === ROW_NAME) {
|
|
946
1294
|
el = renderRowElement(element, attributes, width, indent, wctx);
|
|
@@ -948,9 +1296,12 @@ function renderDirectiveElement(element, width, indent, wctx) {
|
|
|
948
1296
|
else if (name === TABS_NAME) {
|
|
949
1297
|
el = renderTabsElement(element, width, indent, wctx);
|
|
950
1298
|
}
|
|
951
|
-
else {
|
|
1299
|
+
else if (name === DETAILS_NAME) {
|
|
952
1300
|
el = renderDetailsElement(element, attributes, width, indent, wctx);
|
|
953
1301
|
}
|
|
1302
|
+
else {
|
|
1303
|
+
el = renderFigureElement(element, attributes, width, indent, wctx);
|
|
1304
|
+
}
|
|
954
1305
|
if (!el)
|
|
955
1306
|
return null;
|
|
956
1307
|
const marker = invalidEnumMarkerElement(name, attributes, wctx);
|
|
@@ -958,7 +1309,7 @@ function renderDirectiveElement(element, width, indent, wctx) {
|
|
|
958
1309
|
return applyLayoutElement(withMarker, layoutResolved, width);
|
|
959
1310
|
}
|
|
960
1311
|
catch {
|
|
961
|
-
return styledText(componentError(name || '(unnamed)', false, '', width, wctx.color));
|
|
1312
|
+
return styledText(componentError(name || '(unnamed)', false, '', width, wctx.color, wctx.glyphs));
|
|
962
1313
|
}
|
|
963
1314
|
}
|
|
964
1315
|
return styledText(renderDirectiveString(element, width, indent, wctx));
|
|
@@ -977,10 +1328,10 @@ function renderBlockElement(node, width, indent, wctx) {
|
|
|
977
1328
|
}
|
|
978
1329
|
if (node.tagName === 'blockquote') {
|
|
979
1330
|
const innerWidth = Math.max(1, width - 2);
|
|
980
|
-
const inner = renderBlocksElement(node.children, innerWidth, `${indent}
|
|
1331
|
+
const inner = renderBlocksElement(node.children, innerWidth, `${indent}${wctx.glyphs.quoteBar}`, wctx);
|
|
981
1332
|
if (!inner)
|
|
982
1333
|
return null;
|
|
983
|
-
return (_jsxs(Box, { width: width, children: [styledText(dim(
|
|
1334
|
+
return (_jsxs(Box, { width: width, children: [styledText(dim(wctx.glyphs.quoteBar, wctx.color)), _jsx(Box, { flexDirection: "column", width: innerWidth, children: inner })] }));
|
|
984
1335
|
}
|
|
985
1336
|
if (node.tagName === 'ul' || node.tagName === 'ol') {
|
|
986
1337
|
return renderListElement(node, width, indent, wctx);
|
|
@@ -1041,16 +1392,19 @@ function renderFailureFallback(error, color) {
|
|
|
1041
1392
|
const message = error instanceof Error ? error.message : String(error);
|
|
1042
1393
|
return `${dim('failed to render document', color)}\n${stripControlCharacters(message)}\n`;
|
|
1043
1394
|
}
|
|
1044
|
-
function buildWalkContext(registry, scope, color, theme, interactive, resolveImageSrc, resolveHref, onDiagnostic) {
|
|
1395
|
+
function buildWalkContext(registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, renderCode) {
|
|
1045
1396
|
return {
|
|
1046
1397
|
registry,
|
|
1047
1398
|
scope,
|
|
1048
1399
|
color,
|
|
1049
1400
|
theme,
|
|
1401
|
+
glyphs,
|
|
1050
1402
|
interactive,
|
|
1051
1403
|
resolveImageSrc,
|
|
1052
1404
|
resolveHref,
|
|
1053
1405
|
onDiagnostic,
|
|
1406
|
+
unavailable,
|
|
1407
|
+
renderCode,
|
|
1054
1408
|
focusCounter: { current: 0 },
|
|
1055
1409
|
focusables: undefined,
|
|
1056
1410
|
};
|
|
@@ -1058,13 +1412,13 @@ function buildWalkContext(registry, scope, color, theme, interactive, resolveIma
|
|
|
1058
1412
|
/** The columns assumed when a caller supplies no `width` option. */
|
|
1059
1413
|
const DEFAULT_WIDTH = 80;
|
|
1060
1414
|
/** 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);
|
|
1415
|
+
function buildRootElement(root, registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, width, renderCode) {
|
|
1416
|
+
const wctx = buildWalkContext(registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, renderCode);
|
|
1063
1417
|
const body = renderBlocksElement(root.children, width, '', wctx);
|
|
1064
1418
|
return (_jsx(Box, { width: width, flexDirection: "column", children: body ?? _jsx(Text, { children: " " }) }));
|
|
1065
1419
|
}
|
|
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);
|
|
1420
|
+
async function renderRootToString(root, registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, width, renderCode) {
|
|
1421
|
+
const element = buildRootElement(root, registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, width, renderCode);
|
|
1068
1422
|
const raw = await renderInkToString(element, width);
|
|
1069
1423
|
return finalizeOutput(raw);
|
|
1070
1424
|
}
|
|
@@ -1089,7 +1443,7 @@ async function renderRootToString(root, registry, scope, color, theme, interacti
|
|
|
1089
1443
|
export async function renderMarkToAnsi(text, registry = defaultAnsiRegistry, store, vault, options) {
|
|
1090
1444
|
const color = resolveColorOption(options?.color);
|
|
1091
1445
|
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);
|
|
1446
|
+
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, options?.renderCode);
|
|
1093
1447
|
}
|
|
1094
1448
|
catch (error) {
|
|
1095
1449
|
return renderFailureFallback(error, color);
|
|
@@ -1104,7 +1458,7 @@ export async function renderMarkToAnsi(text, registry = defaultAnsiRegistry, sto
|
|
|
1104
1458
|
export async function renderMarkNodeToAnsi(node, registry = defaultAnsiRegistry, store, vault, options) {
|
|
1105
1459
|
const color = resolveColorOption(options?.color);
|
|
1106
1460
|
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);
|
|
1461
|
+
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, options?.renderCode);
|
|
1108
1462
|
}
|
|
1109
1463
|
catch (error) {
|
|
1110
1464
|
return renderFailureFallback(error, color);
|
|
@@ -1153,6 +1507,7 @@ export async function renderMarkInlineToAnsi(text, registry = defaultAnsiRegistr
|
|
|
1153
1507
|
export function buildMarkElement(document, registry = defaultAnsiRegistry, options) {
|
|
1154
1508
|
const color = resolveColorOption(options?.color);
|
|
1155
1509
|
const theme = options?.theme ?? defaultAnsiTheme;
|
|
1510
|
+
const glyphs = glyphsFor(options?.box);
|
|
1156
1511
|
const scope = { store: options?.store, vault: options?.vault };
|
|
1157
1512
|
const width = options?.width ?? DEFAULT_WIDTH;
|
|
1158
1513
|
const interactive = options?.interactive ?? true;
|
|
@@ -1166,7 +1521,8 @@ export function buildMarkElement(document, registry = defaultAnsiRegistry, optio
|
|
|
1166
1521
|
catch {
|
|
1167
1522
|
root = { type: 'root', children: [] };
|
|
1168
1523
|
}
|
|
1169
|
-
const
|
|
1524
|
+
const unavailable = createUnavailableComponentLookup(options?.unavailableComponents);
|
|
1525
|
+
const content = buildRootElement(root, registry, scope, color, theme, glyphs, interactive, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, unavailable, width, options?.renderCode);
|
|
1170
1526
|
if (!interactive)
|
|
1171
1527
|
return content;
|
|
1172
1528
|
// The walk above already assigned every focusable component's `focusId`
|
|
@@ -1179,14 +1535,14 @@ export function buildMarkElement(document, registry = defaultAnsiRegistry, optio
|
|
|
1179
1535
|
// mount/resize, not per keystroke.
|
|
1180
1536
|
const counter = { current: 0 };
|
|
1181
1537
|
const focusables = [];
|
|
1182
|
-
const countingWctx = buildWalkContext(registry, scope, color, theme, true, options?.resolveImageSrc, options?.resolveHref,
|
|
1538
|
+
const countingWctx = buildWalkContext(registry, scope, color, theme, glyphs, true, options?.resolveImageSrc, options?.resolveHref,
|
|
1183
1539
|
// No `onDiagnostic`: the real walk above already reported every notice
|
|
1184
1540
|
// this document produces, and this second walk exists only to collect
|
|
1185
1541
|
// focus descriptors. Passing the callback here would report each notice
|
|
1186
1542
|
// twice on every mount and again on every resize.
|
|
1187
|
-
undefined);
|
|
1543
|
+
undefined, unavailable);
|
|
1188
1544
|
countingWctx.focusCounter = counter;
|
|
1189
1545
|
countingWctx.focusables = focusables;
|
|
1190
1546
|
renderBlocksElement(root.children, width, '', countingWctx);
|
|
1191
|
-
return (_jsx(InteractiveRoot, { focusables: focusables, onExit: options?.onExit, color: color, theme: theme, children: content }));
|
|
1547
|
+
return (_jsx(InteractiveRoot, { focusables: focusables, onExit: options?.onExit, color: color, theme: theme, keyLegend: glyphs.keyLegend, viewportRows: options?.viewportRows, width: width, children: content }));
|
|
1192
1548
|
}
|