@markii/ansi 0.15.0 → 0.17.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/ansi.d.ts +26 -0
- package/dist/ansi.js +51 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +5 -4
- package/dist/components/callout.d.ts +38 -13
- package/dist/components/callout.js +68 -31
- package/dist/components/card.d.ts +34 -2
- package/dist/components/card.js +55 -18
- package/dist/components/cell.d.ts +5 -11
- package/dist/components/cell.js +11 -15
- package/dist/components/chart.d.ts +4 -10
- package/dist/components/chart.js +16 -24
- package/dist/components/details-string.d.ts +26 -0
- package/dist/components/details-string.js +39 -0
- package/dist/components/details.d.ts +32 -10
- package/dist/components/details.js +28 -25
- package/dist/components/divider.js +12 -16
- package/dist/components/figure.d.ts +5 -16
- package/dist/components/figure.js +9 -19
- package/dist/components/index.d.ts +14 -10
- package/dist/components/index.js +19 -16
- package/dist/components/kbd.d.ts +1 -1
- package/dist/components/kbd.js +3 -2
- package/dist/components/layout-wrapper.d.ts +7 -22
- package/dist/components/layout-wrapper.js +9 -23
- package/dist/components/progress.d.ts +3 -11
- package/dist/components/progress.js +8 -17
- package/dist/components/rating.d.ts +1 -2
- package/dist/components/rating.js +3 -4
- package/dist/components/row.d.ts +35 -20
- package/dist/components/row.js +35 -51
- package/dist/components/stat.d.ts +3 -8
- package/dist/components/stat.js +5 -11
- package/dist/components/tab.d.ts +9 -8
- package/dist/components/tab.js +12 -10
- package/dist/components/table-grid.d.ts +2 -1
- package/dist/components/table-grid.js +14 -12
- package/dist/components/table.d.ts +5 -10
- package/dist/components/table.js +8 -13
- package/dist/components/tabs-string.d.ts +17 -0
- package/dist/components/tabs-string.js +30 -0
- package/dist/components/tabs.d.ts +31 -14
- package/dist/components/tabs.js +55 -30
- 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 +6 -4
- package/dist/index.js +10 -7
- package/dist/ink-string.d.ts +23 -0
- package/dist/ink-string.js +65 -0
- package/dist/interactive.d.ts +66 -0
- package/dist/interactive.js +207 -0
- package/dist/layout.js +1 -2
- 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 +106 -100
- package/dist/registry.js +17 -9
- package/dist/render.d.ts +63 -25
- package/dist/render.js +1001 -380
- package/dist/text-grid.d.ts +90 -0
- package/dist/text-grid.js +300 -0
- package/package.json +9 -5
- package/dist/box.d.ts +0 -47
- package/dist/box.js +0 -209
- package/dist/measure.d.ts +0 -25
- package/dist/measure.js +0 -108
package/dist/render.js
CHANGED
|
@@ -1,40 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, isValidElement } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
1
4
|
import { parse, toHast, nodeToHast, isSafeUrl, parseMetaAttributes, isValidScriptName, } from '@markii/core';
|
|
2
|
-
import { bold, dim, hyperlink, inverse, italic, resolveColorOption, underline, } from './ansi.js';
|
|
5
|
+
import { bold, carrySgrAcrossLines, dim, hyperlink, inverse, italic, resolveColorOption, underline, } from './ansi.js';
|
|
3
6
|
import { style } from './style.js';
|
|
4
7
|
import { defaultAnsiTheme } from './theme.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
8
|
+
import { columnsBlock, frameBlock, frameTopLine, inkBorderStyle, isTextAlign, measureWidth, padText, ruleLine, wrapText, } from './text-grid.js';
|
|
9
|
+
import { drawTableGrid } from './components/table-grid.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
|
+
import { resolveDetailsInnerWidth } from './components/details-string.js';
|
|
7
14
|
import { sanitizeBlockText, sanitizeUrlText, stripControlCharacters, } from './sanitize.js';
|
|
8
15
|
import { readRegistryComponent, registryLayoutAxis, registrySelfLayout, resolveDirectiveAlias, } from './registry.js';
|
|
9
16
|
import { applyLayout, resolveLayoutAttributes, } from './layout.js';
|
|
10
17
|
import { resolveScopedPath } from './resolve.js';
|
|
11
|
-
import { dataStateSuffix, emptyInlineTitle, failureToken, invalidAttributeValueLabel, invalidAttributeValueTitle, } from './failure-presentation.js';
|
|
12
|
-
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';
|
|
13
20
|
import { resolveImageAttribute, } from './image-resolve.js';
|
|
14
21
|
import { resolveHrefAttribute } from './href-resolve.js';
|
|
15
22
|
import { defaultAnsiRegistry } from './components/index.js';
|
|
16
|
-
import {
|
|
23
|
+
import { renderInkToString, renderInkToStringSync } from './ink-string.js';
|
|
24
|
+
import { InteractiveRoot } from './interactive.js';
|
|
25
|
+
import { glyphsFor } from './glyphs.js';
|
|
26
|
+
import { InteractiveTabs } from './components/tabs.js';
|
|
27
|
+
import { InteractiveDetails } from './components/details.js';
|
|
17
28
|
/**
|
|
18
|
-
* The hast walk for the terminal engine
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
29
|
+
* The hast walk for the terminal engine (batch 10: the Ink rewrite). Every
|
|
30
|
+
* DECISION this walk makes is unchanged from the pre-Ink engine (alias
|
|
31
|
+
* resolution, layout resolution, data binding, form mismatch, invalid-enum
|
|
32
|
+
* notice, empty-inline notice, never-throw component containment, script-
|
|
33
|
+
* fence markers, link and image handling) — see AGENTS.md's architecture
|
|
34
|
+
* rules and the batch-10 brief. What changed is the OUTPUT shape: instead of
|
|
35
|
+
* building one big plain string, the walk builds a tree of Ink elements
|
|
36
|
+
* (`Box`/`Text`) and hands it to `ink-string.ts` (the render-once path) or
|
|
37
|
+
* returns it directly (the new live-viewer export, `buildMarkElement`).
|
|
38
|
+
*
|
|
39
|
+
* Two rendering "modes" coexist deliberately, and BOTH still exist in the
|
|
40
|
+
* pre-Ink engine's shape:
|
|
24
41
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
42
|
+
* - STRING mode (`render*String` functions below): the pre-Ink algorithm,
|
|
43
|
+
* almost verbatim, still building one flattened, pre-wrapped, pre-aligned
|
|
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
|
|
48
|
+
* content ever uses, at any nesting depth: an inline directive's body is
|
|
49
|
+
* phrasing content by definition (docs/spec.md), never a layout the reader
|
|
50
|
+
* would want Ink to lay out as boxes.
|
|
51
|
+
* - ELEMENT mode (`render*Element` functions below): real Ink elements,
|
|
52
|
+
* used for the document's own block sequencing (so ordinary prose wraps
|
|
53
|
+
* through Ink's own layout instead of this engine's manual `wrap()`) and
|
|
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.
|
|
32
64
|
*
|
|
33
|
-
* Interactive components have no meaning
|
|
34
|
-
* `INTERACTIVE_ATTRIBUTE` marks an element a
|
|
35
|
-
* control (a script-marker `<summary>`, say); this
|
|
36
|
-
*
|
|
37
|
-
* a marker for it — there is nothing to degrade, unlike a genuine failure.
|
|
65
|
+
* Interactive components have no meaning for a NON-interactive render at
|
|
66
|
+
* all: `@markii/stdlib`'s `INTERACTIVE_ATTRIBUTE` marks an element a HOST
|
|
67
|
+
* should treat as a live control (a script-marker `<summary>`, say); this
|
|
68
|
+
* engine never reads that attribute and never emits a marker for it.
|
|
38
69
|
*/
|
|
39
70
|
/** The hast tag name `@markii/core` marks every directive with. */
|
|
40
71
|
const DIRECTIVE_TAG = 'mk-directive';
|
|
@@ -46,6 +77,27 @@ const VALUE_DIRECTIVE_NAME = 'value';
|
|
|
46
77
|
const DATA_ATTRIBUTE_KEY = 'data';
|
|
47
78
|
/** The attribute `@markii/core` preserves a code fence's raw `meta` string onto. */
|
|
48
79
|
const CODE_META_ATTR = 'data-mk-meta';
|
|
80
|
+
/** The block directive names given real Ink layout/state (see this file's top comment). */
|
|
81
|
+
const ROW_NAME = 'row';
|
|
82
|
+
const TABS_NAME = 'tabs';
|
|
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';
|
|
49
101
|
/** Block-level hast tags this engine recognizes; anything else inside a directive's children is treated as inline content. */
|
|
50
102
|
const BLOCK_TAGS = new Set([
|
|
51
103
|
'p',
|
|
@@ -74,6 +126,30 @@ function nodeOrRootToHast(node) {
|
|
|
74
126
|
}
|
|
75
127
|
return { type: 'root', children };
|
|
76
128
|
}
|
|
129
|
+
function nextFocusId(wctx) {
|
|
130
|
+
const id = wctx.focusCounter.current;
|
|
131
|
+
wctx.focusCounter.current += 1;
|
|
132
|
+
return id;
|
|
133
|
+
}
|
|
134
|
+
/** Records `descriptor` for `focusId` when `wctx` is collecting them (a no-op otherwise). Call sites push in the same order they call `nextFocusId`, so `focusId` always equals the descriptor's index. */
|
|
135
|
+
function recordFocusable(wctx, descriptor) {
|
|
136
|
+
wctx.focusables?.push(descriptor);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The ONE place a STRING built by this file's string engine becomes an Ink
|
|
140
|
+
* `<Text>` leaf. Every such string passes through `ansi.js`'s
|
|
141
|
+
* `carrySgrAcrossLines` first — see that function's doc comment for why:
|
|
142
|
+
* Ink resets SGR state at every embedded line boundary within one `<Text>`
|
|
143
|
+
* value, so a multi-line styled string (a dim-wrapped frame, a wrapped
|
|
144
|
+
* bold heading, a paragraph whose bold span straddled a wrap point) must
|
|
145
|
+
* have every physical line already self-contained before Ink ever sees it.
|
|
146
|
+
* This is the SYSTEMATIC fix (a single choke point) rather than a per-
|
|
147
|
+
* call-site one: every `<Text>` in this file that renders a plain string
|
|
148
|
+
* uses this, never a bare `<Text>{str}</Text>`.
|
|
149
|
+
*/
|
|
150
|
+
function styledText(text) {
|
|
151
|
+
return _jsx(Text, { children: carrySgrAcrossLines(text) });
|
|
152
|
+
}
|
|
77
153
|
/** A directive property read back as a string, or `undefined` if absent or non-string. */
|
|
78
154
|
function stringProperty(element, name) {
|
|
79
155
|
const value = element.properties?.[name];
|
|
@@ -134,6 +210,8 @@ function isRegisteredInline(entry) {
|
|
|
134
210
|
/** The fallback label line, worded for the reason and the form the directive was written in. Plain text (no escapes). */
|
|
135
211
|
function fallbackLabel(name, inline, reason) {
|
|
136
212
|
const code = `\`${stripControlCharacters(name)}\``;
|
|
213
|
+
if (reason === 'no-engine-view')
|
|
214
|
+
return `${NO_ENGINE_VIEW_LABEL} ${code}`;
|
|
137
215
|
if (reason === 'form-mismatch') {
|
|
138
216
|
return inline
|
|
139
217
|
? `block component ${code} written inline`
|
|
@@ -141,30 +219,9 @@ function fallbackLabel(name, inline, reason) {
|
|
|
141
219
|
}
|
|
142
220
|
return `unknown component ${code}`;
|
|
143
221
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Renders `children`'s default (whole-body) text, never throwing. Used only
|
|
146
|
-
* on already-exceptional paths (an unregistered directive, a form mismatch,
|
|
147
|
-
* a component that itself threw): the render walk underneath `children` is
|
|
148
|
-
* documented never-throw, but these three call sites are exactly the ones
|
|
149
|
-
* built to contain a failure, so they must not become a new way to produce
|
|
150
|
-
* one themselves.
|
|
151
|
-
*/
|
|
152
|
-
function safeChildrenText(children) {
|
|
153
|
-
try {
|
|
154
|
-
return children();
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
return '';
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
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. */
|
|
161
|
-
function unknownDirective(name, inline, childrenText, reason, width, color) {
|
|
223
|
+
function unknownDirective(name, inline, childrenText, reason, width, color, glyphs) {
|
|
162
224
|
const label = fallbackLabel(name, inline, reason);
|
|
163
|
-
// The label and the passed-through content need a visible boundary. The
|
|
164
|
-
// HTML and React engines get one from the fallback's own styling, which a
|
|
165
|
-
// terminal cannot rely on: with color off there is nothing at all between
|
|
166
|
-
// the two, so the label runs straight into the author's words. A colon is
|
|
167
|
-
// the separator, and only when there is content to separate.
|
|
168
225
|
if (inline) {
|
|
169
226
|
return childrenText === ''
|
|
170
227
|
? dim(label, color)
|
|
@@ -174,24 +231,23 @@ function unknownDirective(name, inline, childrenText, reason, width, color) {
|
|
|
174
231
|
const wrappedChildren = childrenText
|
|
175
232
|
? childrenText
|
|
176
233
|
.split('\n')
|
|
177
|
-
.flatMap((line) =>
|
|
234
|
+
.flatMap((line) => wrapText(line, innerWidth))
|
|
178
235
|
.join('\n')
|
|
179
236
|
: '';
|
|
180
237
|
const body = wrappedChildren ? `${label}\n\n${wrappedChildren}` : label;
|
|
181
|
-
return dim(
|
|
238
|
+
return dim(frameBlock(body, { style: 'dashed', width, glyphs }), color);
|
|
182
239
|
}
|
|
183
240
|
/** A registered component that threw while rendering, contained to a neutral box rather than failing the whole document. Never dumps the error text. */
|
|
184
|
-
function componentError(name, inline, childrenText, width, color) {
|
|
241
|
+
function componentError(name, inline, childrenText, width, color, glyphs) {
|
|
185
242
|
const label = `component \`${stripControlCharacters(name)}\` failed to render`;
|
|
186
|
-
// Same boundary rule as `unknownDirective`'s inline branch above.
|
|
187
243
|
if (inline) {
|
|
188
244
|
return childrenText === ''
|
|
189
245
|
? dim(label, color)
|
|
190
246
|
: `${dim(`${label}:`, color)} ${childrenText}`;
|
|
191
247
|
}
|
|
192
|
-
return dim(
|
|
248
|
+
return dim(frameBlock(label, { style: 'dashed', width, glyphs }), color);
|
|
193
249
|
}
|
|
194
|
-
/** Splits a `data=<name>` attribute off `attributes` and resolves it against `scope`. Never throws.
|
|
250
|
+
/** Splits a `data=<name>` attribute off `attributes` and resolves it against `scope`. Never throws. */
|
|
195
251
|
function resolveDataAttribute(attributes, scope) {
|
|
196
252
|
if (!Object.hasOwn(attributes, DATA_ATTRIBUTE_KEY))
|
|
197
253
|
return { attributes };
|
|
@@ -207,7 +263,7 @@ function resolveDataAttribute(attributes, scope) {
|
|
|
207
263
|
dataFailureKind: resolved.status === 'error' ? resolved.failureKind : undefined,
|
|
208
264
|
};
|
|
209
265
|
}
|
|
210
|
-
/** Whether one of `name`'s `@markii/stdlib` contract attributes with a closed `enum` is present with a value outside it.
|
|
266
|
+
/** Whether one of `name`'s `@markii/stdlib` contract attributes with a closed `enum` is present with a value outside it. */
|
|
211
267
|
function invalidEnumAttribute(name, attributes) {
|
|
212
268
|
const contract = getContract(name);
|
|
213
269
|
if (!contract)
|
|
@@ -231,14 +287,7 @@ function resolveValue(scope, name) {
|
|
|
231
287
|
return { value: undefined, status: 'missing' };
|
|
232
288
|
return resolveScopedPath(scope, trimmed);
|
|
233
289
|
}
|
|
234
|
-
/**
|
|
235
|
-
* Builds the missing/stale/resolved marker text for a resolved value name.
|
|
236
|
-
* A plain miss (no error attached) shows only the `{name}` placeholder —
|
|
237
|
-
* there is nothing more to say. An `'error'` status, or a stale value, gets
|
|
238
|
-
* `failure-presentation.ts`'s `dataStateSuffix` appended as visible trailing
|
|
239
|
-
* text: a terminal has no tooltip channel, so the reason has to reach the
|
|
240
|
-
* text itself (AGENTS.md's "clean is not silent").
|
|
241
|
-
*/
|
|
290
|
+
/** Builds the missing/stale/resolved marker text for a resolved value name. */
|
|
242
291
|
function renderValueMarker(name, resolved, format, decimals, color, theme) {
|
|
243
292
|
if (resolved.status === 'missing' || resolved.status === 'error') {
|
|
244
293
|
const failureKind = resolved.status === 'error' ? resolved.failureKind : undefined;
|
|
@@ -256,24 +305,21 @@ function renderValueMarker(name, resolved, format, decimals, color, theme) {
|
|
|
256
305
|
return suffix ? `${styled}${dim(suffix, color)}` : styled;
|
|
257
306
|
}
|
|
258
307
|
/** Builds the `AnsiRenderContext` handed to one component invocation. */
|
|
259
|
-
function buildComponentContext(width, indent, wctx, binding, layout) {
|
|
308
|
+
function buildComponentContext(width, indent, wctx, binding, layout, focusId) {
|
|
260
309
|
const { color, theme, scope } = wctx;
|
|
261
310
|
const ctx = {
|
|
262
311
|
width,
|
|
263
312
|
indent,
|
|
264
313
|
color,
|
|
265
314
|
theme,
|
|
315
|
+
glyphs: wctx.glyphs,
|
|
316
|
+
interactive: wctx.interactive,
|
|
266
317
|
style: (text, token) => style(text, token, theme, color),
|
|
267
318
|
bold: (text) => bold(text, color),
|
|
268
319
|
dim: (text) => dim(text, color),
|
|
269
320
|
italic: (text) => italic(text, color),
|
|
270
321
|
underline: (text) => underline(text, color),
|
|
271
322
|
inverse: (text) => inverse(text, color),
|
|
272
|
-
wrap: (text, w) => wrap(text, w),
|
|
273
|
-
pad: (text, w, align) => pad(text, w, align),
|
|
274
|
-
columns: (blocks, widths, gutter) => columns(blocks, widths, gutter),
|
|
275
|
-
frame: (block, options) => frame(block, options),
|
|
276
|
-
rule: (w, char) => rule(w, char),
|
|
277
323
|
text: (value) => stripControlCharacters(value),
|
|
278
324
|
resolve: (name) => resolveValue(scope, name),
|
|
279
325
|
valueMarker: (name, format, decimals) => {
|
|
@@ -287,52 +333,86 @@ function buildComponentContext(width, indent, wctx, binding, layout) {
|
|
|
287
333
|
resolveHref: wctx.resolveHref,
|
|
288
334
|
onDiagnostic: wctx.onDiagnostic,
|
|
289
335
|
};
|
|
336
|
+
if (focusId !== undefined)
|
|
337
|
+
ctx.focusId = focusId;
|
|
290
338
|
if ('data' in binding) {
|
|
291
339
|
ctx.data = binding.data;
|
|
292
340
|
ctx.dataStatus = binding.dataStatus;
|
|
293
341
|
ctx.dataError = binding.dataError;
|
|
294
342
|
ctx.dataFailureKind = binding.dataFailureKind;
|
|
343
|
+
ctx.dataSuffix = dataStateSuffix(binding.dataStatus, binding.dataFailureKind);
|
|
295
344
|
}
|
|
296
345
|
if (layout)
|
|
297
346
|
ctx.layout = layout;
|
|
298
347
|
return ctx;
|
|
299
348
|
}
|
|
300
|
-
|
|
301
|
-
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// STRING MODE — the pre-Ink algorithm. Used for every inline position (at
|
|
351
|
+
// any depth) and for a directive's own body once it is not one of the three
|
|
352
|
+
// Ink-element-aware directives (`row`/`tabs`/`details`).
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
function safeChildrenText(text) {
|
|
355
|
+
return text ?? '';
|
|
356
|
+
}
|
|
357
|
+
function isBlockLevelChildren(children) {
|
|
358
|
+
for (const child of children) {
|
|
359
|
+
if (child.type !== 'element')
|
|
360
|
+
continue;
|
|
361
|
+
if (BLOCK_TAGS.has(child.tagName))
|
|
362
|
+
return true;
|
|
363
|
+
if (child.tagName === DIRECTIVE_TAG) {
|
|
364
|
+
const kind = stringProperty(child, 'data-mk-kind');
|
|
365
|
+
if (kind !== TEXT_DIRECTIVE_KIND)
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
function renderDirectiveContentString(name, kind, isBlock, attributes, childrenText, width, indent, wctx, layoutForComponent) {
|
|
302
372
|
const inline = !isBlock;
|
|
303
373
|
const entry = Object.hasOwn(wctx.registry, name)
|
|
304
374
|
? wctx.registry[name]
|
|
305
375
|
: undefined;
|
|
306
376
|
const component = readRegistryComponent(entry);
|
|
307
377
|
if (!entry || component == null) {
|
|
308
|
-
|
|
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);
|
|
309
388
|
}
|
|
310
389
|
if (isFormMismatch(entry, kind)) {
|
|
311
|
-
|
|
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);
|
|
312
396
|
}
|
|
313
397
|
const binding = resolveDataAttribute(attributes, wctx.scope);
|
|
314
|
-
const ctx = buildComponentContext(width, indent, wctx, binding, layoutForComponent);
|
|
398
|
+
const ctx = buildComponentContext(width, indent, wctx, binding, layoutForComponent, undefined);
|
|
315
399
|
let rendered;
|
|
316
400
|
try {
|
|
317
|
-
|
|
401
|
+
const result = component({
|
|
402
|
+
attributes: binding.attributes,
|
|
403
|
+
children: childrenText,
|
|
404
|
+
ctx,
|
|
405
|
+
});
|
|
406
|
+
rendered =
|
|
407
|
+
typeof result === 'string'
|
|
408
|
+
? result
|
|
409
|
+
: isValidElement(result)
|
|
410
|
+
? renderInkToStringSync(result, width)
|
|
411
|
+
: String(result ?? '');
|
|
318
412
|
}
|
|
319
413
|
catch {
|
|
320
|
-
return componentError(name || '(unnamed)', inline, safeChildrenText(
|
|
321
|
-
}
|
|
322
|
-
// The silent-value-drop mechanism (AGENTS.md "clean is not silent"): a
|
|
323
|
-
// known attribute's value outside its closed enum still renders the
|
|
324
|
-
// component exactly as registered, with a short labeled marker appended
|
|
325
|
-
// (there is no tooltip channel in a terminal, so the full reason cannot
|
|
326
|
-
// live inline without becoming body text again). The full sentence is
|
|
327
|
-
// reported to `onDiagnostic` for a host's own diagnostics surface; a
|
|
328
|
-
// caller that passes no `onDiagnostic` simply gets the marker with no
|
|
329
|
-
// reason anywhere, exactly like the browser engines with no tooltip read.
|
|
330
|
-
//
|
|
331
|
-
// A block component's marker goes on its OWN line. Appended to the same
|
|
332
|
-
// line it would land against the bottom edge of whatever the component
|
|
333
|
-
// drew, turning a clean box into a box with a label stuck to its corner.
|
|
334
|
-
// Only an inline component, which is already part of a line of text,
|
|
335
|
-
// takes the marker inline.
|
|
414
|
+
return componentError(name || '(unnamed)', inline, safeChildrenText(childrenText), width, wctx.color, wctx.glyphs);
|
|
415
|
+
}
|
|
336
416
|
const markerSeparator = inline ? ' ' : '\n';
|
|
337
417
|
const invalidEnum = invalidEnumAttribute(name, binding.attributes);
|
|
338
418
|
if (invalidEnum) {
|
|
@@ -345,221 +425,121 @@ function renderDirectiveContent(name, kind, isBlock, attributes, children, width
|
|
|
345
425
|
const label = invalidAttributeValueLabel(name, invalidEnum.attribute);
|
|
346
426
|
rendered = `${rendered}${markerSeparator}${dim(`[${label}]`, wctx.color)}`;
|
|
347
427
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (isRegisteredInline(entry) && safeChildrenText(children).trim() === '') {
|
|
428
|
+
if (isRegisteredInline(entry) &&
|
|
429
|
+
safeChildrenText(childrenText).trim() === '') {
|
|
351
430
|
rendered = `${rendered}${dim(` (${emptyInlineTitle(name)})`, wctx.color)}`;
|
|
352
431
|
}
|
|
353
432
|
return rendered;
|
|
354
433
|
}
|
|
355
|
-
|
|
356
|
-
function isBlockLevelChildren(children) {
|
|
357
|
-
for (const child of children) {
|
|
358
|
-
if (child.type !== 'element')
|
|
359
|
-
continue;
|
|
360
|
-
if (BLOCK_TAGS.has(child.tagName))
|
|
361
|
-
return true;
|
|
362
|
-
if (child.tagName === DIRECTIVE_TAG) {
|
|
363
|
-
const kind = stringProperty(child, 'data-mk-kind');
|
|
364
|
-
if (kind !== TEXT_DIRECTIVE_KIND)
|
|
365
|
-
return true;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
return false;
|
|
369
|
-
}
|
|
370
|
-
function renderDirectiveChildren(children, width, indent, wctx) {
|
|
434
|
+
function renderDirectiveChildrenString(children, width, indent, wctx) {
|
|
371
435
|
if (isBlockLevelChildren(children)) {
|
|
372
|
-
return
|
|
373
|
-
}
|
|
374
|
-
return renderInlineChildren(children, width, indent, wctx);
|
|
375
|
-
}
|
|
376
|
-
/** The resolved directive name for a `<mk-directive>` element, after alias resolution — the name `AnsiChildPart.name` reports. Attributes play no part in identifying a child this way, so an empty attribute map is enough. */
|
|
377
|
-
function resolvedChildDirectiveName(element, registry) {
|
|
378
|
-
const written = stringProperty(element, 'data-mk-name') ?? '';
|
|
379
|
-
return resolveDirectiveAlias(registry, written, {}).name;
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Builds one `AnsiChildPart` per top-level child of a directive's BLOCK
|
|
383
|
-
* body (a pure whitespace-only text node between blocks, the same kind
|
|
384
|
-
* `renderBlocks` already treats as carrying no content, contributes no
|
|
385
|
-
* part). Each part renders lazily via the same per-node `renderBlock` the
|
|
386
|
-
* ordinary block walk uses, so a container that calls `part.render` at its
|
|
387
|
-
* own chosen width gets exactly what a document-level render at that width
|
|
388
|
-
* would have produced — no separate rendering path to drift from the
|
|
389
|
-
* ordinary one.
|
|
390
|
-
*/
|
|
391
|
-
function buildBlockChildParts(nodes, width, indent, wctx) {
|
|
392
|
-
const parts = [];
|
|
393
|
-
for (const node of nodes) {
|
|
394
|
-
if (node.type === 'text' && node.value.trim() === '')
|
|
395
|
-
continue;
|
|
396
|
-
const name = node.type === 'element' && node.tagName === DIRECTIVE_TAG
|
|
397
|
-
? resolvedChildDirectiveName(node, wctx.registry)
|
|
398
|
-
: undefined;
|
|
399
|
-
parts.push({
|
|
400
|
-
name,
|
|
401
|
-
render: (options) => renderBlock(node, options?.width ?? width, options?.indent ?? indent, wctx),
|
|
402
|
-
});
|
|
436
|
+
return renderBlocksString(children, width, indent, wctx);
|
|
403
437
|
}
|
|
404
|
-
return
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Builds the `AnsiChildren` handle a component receives (`registry.ts`'s
|
|
408
|
-
* doc comment on the type): calling it renders the WHOLE body, lazily, at
|
|
409
|
-
* an optionally narrower width/indent; `.parts` exposes each top-level
|
|
410
|
-
* child separately for a container (`row`) that must size each one before
|
|
411
|
-
* it is drawn. Nothing under `element` is rendered until a caller actually
|
|
412
|
-
* invokes one of these functions.
|
|
413
|
-
*/
|
|
414
|
-
function buildChildren(element, width, indent, wctx) {
|
|
415
|
-
const isBlock = isBlockLevelChildren(element.children);
|
|
416
|
-
const fn = ((options) => renderDirectiveChildren(element.children, options?.width ?? width, options?.indent ?? indent, wctx));
|
|
417
|
-
Object.defineProperty(fn, 'parts', {
|
|
418
|
-
enumerable: true,
|
|
419
|
-
get: () => isBlock
|
|
420
|
-
? buildBlockChildParts(element.children, width, indent, wctx)
|
|
421
|
-
: [
|
|
422
|
-
{
|
|
423
|
-
render: (options) => renderInlineChildren(element.children, options?.width ?? width, options?.indent ?? indent, wctx),
|
|
424
|
-
},
|
|
425
|
-
],
|
|
426
|
-
});
|
|
427
|
-
return fn;
|
|
438
|
+
return renderInlineChildrenString(children, width, indent, wctx);
|
|
428
439
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
440
|
+
function renderRowString(element, attributes, width, indent, wctx) {
|
|
441
|
+
const cellNodes = element.children.filter((node) => !(node.type === 'text' && node.value.trim() === ''));
|
|
442
|
+
const layout = resolveRowLayout(attributes, cellNodes.length, width);
|
|
443
|
+
if (cellNodes.length === 0)
|
|
444
|
+
return '';
|
|
445
|
+
const place = (text, w) => layout.align === 'left'
|
|
446
|
+
? text
|
|
447
|
+
: text
|
|
448
|
+
.split('\n')
|
|
449
|
+
.map((line) => padText(line, w, layout.align))
|
|
450
|
+
.join('\n');
|
|
451
|
+
if (layout.stacked) {
|
|
452
|
+
return cellNodes
|
|
453
|
+
.map((node) => place(renderBlockString(node, width, indent, wctx), width))
|
|
454
|
+
.join('\n\n');
|
|
439
455
|
}
|
|
440
|
-
const
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
// callout, table, chart) also owns applying its own width/align — see
|
|
446
|
-
// `registry.ts`'s `AnsiRegistryEntry.selfLayout` doc comment for why the
|
|
447
|
-
// generic post-render `applyLayout` below would corrupt its frame.
|
|
448
|
-
const isSelfLayout = isBlock && registrySelfLayout(wctx.registry, name);
|
|
449
|
-
const children = buildChildren(element, width, indent, wctx);
|
|
450
|
-
const content = renderDirectiveContent(name, kind, isBlock, attributes, children, width, indent, wctx, isLayoutScope || isSelfLayout ? layoutResolved : undefined);
|
|
451
|
-
if (isBlock && layoutResolved && !isLayoutScope && !isSelfLayout) {
|
|
452
|
-
return applyLayout(content, layoutResolved, width);
|
|
456
|
+
const placedCells = cellNodes.map((node) => place(renderBlockString(node, layout.colWidth, indent, wctx), layout.colWidth));
|
|
457
|
+
const gridRows = [];
|
|
458
|
+
for (let index = 0; index < placedCells.length; index += layout.columnCount) {
|
|
459
|
+
const rowCells = placedCells.slice(index, index + layout.columnCount);
|
|
460
|
+
gridRows.push(columnsBlock(rowCells, rowCells.map(() => layout.colWidth), ROW_GUTTER));
|
|
453
461
|
}
|
|
454
|
-
return
|
|
462
|
+
return gridRows.join('\n\n');
|
|
455
463
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
+
const DEFAULT_TAB_LABEL = 'Tab';
|
|
465
|
+
/** Extracts each top-level `tab` child's own label attribute and rendered body, used by BOTH the string fallback and the real interactive builder — a structural replacement for the pre-Ink engine's fragile "split the flattened string on a blank line" heuristic. */
|
|
466
|
+
function extractTabPanels(element, width, indent, wctx) {
|
|
467
|
+
const panels = [];
|
|
468
|
+
for (const node of element.children) {
|
|
469
|
+
if (node.type === 'text' && node.value.trim() === '')
|
|
470
|
+
continue;
|
|
471
|
+
if (node.type !== 'element')
|
|
472
|
+
continue;
|
|
473
|
+
let label = DEFAULT_TAB_LABEL;
|
|
474
|
+
let isTab = false;
|
|
475
|
+
if (node.tagName === DIRECTIVE_TAG) {
|
|
476
|
+
const written = stringProperty(node, 'data-mk-name') ?? '';
|
|
477
|
+
const attrs = parseAttributes(stringProperty(node, 'data-mk-attrs'));
|
|
478
|
+
const { name } = resolveDirectiveAlias(wctx.registry, written, attrs);
|
|
479
|
+
if (name === 'tab') {
|
|
480
|
+
label = attrs.label ?? DEFAULT_TAB_LABEL;
|
|
481
|
+
isTab = true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// A `tab` child's own label is drawn ONCE, by the heading `tabs`
|
|
485
|
+
// itself builds below — rendering the whole `tab` directive here (as
|
|
486
|
+
// for any other block) would ALSO invoke `tab.ts`'s own component,
|
|
487
|
+
// which draws its OWN bold heading from the same attribute, printing
|
|
488
|
+
// the label twice. Render only the tab's INNER children instead. A
|
|
489
|
+
// non-`tab` block (an unlabeled panel) still renders as a whole block,
|
|
490
|
+
// unchanged.
|
|
491
|
+
const body = isTab
|
|
492
|
+
? renderDirectiveChildrenString(node.children, width, indent, wctx)
|
|
493
|
+
: renderBlockString(node, width, indent, wctx);
|
|
494
|
+
panels.push({ label, body });
|
|
464
495
|
}
|
|
465
|
-
return
|
|
496
|
+
return panels;
|
|
466
497
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
* escapes at all, `@markii/core`'s `isSafeUrl` accepts the (resolved)
|
|
470
|
-
* href, AND `./sanitize.js`'s `sanitizeUrlText` leaves the href UNCHANGED —
|
|
471
|
-
* that last check is the one that refuses a href carrying a BEL, ESC, or
|
|
472
|
-
* space rather than silently embedding a sanitized-down version of it,
|
|
473
|
-
* matching this module's `hyperlink.probe.test.ts`. Anything else falls
|
|
474
|
-
* back to plain text: the URL once, if the link's own text already IS the
|
|
475
|
-
* URL, otherwise the text followed by `(url)`.
|
|
476
|
-
*/
|
|
477
|
-
function renderLink(node, width, indent, wctx) {
|
|
478
|
-
const hrefRaw = typeof node.properties?.href === 'string' ? node.properties.href : '';
|
|
479
|
-
const resolvedHref = resolveHrefAttribute(hrefRaw, wctx.resolveHref);
|
|
480
|
-
const text = renderInlineChildren(node.children, width, indent, wctx);
|
|
481
|
-
const sanitizedHref = stripControlCharacters(resolvedHref);
|
|
482
|
-
const canHyperlink = wctx.color !== 'none' &&
|
|
483
|
-
resolvedHref.trim() !== '' &&
|
|
484
|
-
isSafeUrl(resolvedHref) &&
|
|
485
|
-
sanitizeUrlText(resolvedHref) === resolvedHref;
|
|
486
|
-
if (canHyperlink)
|
|
487
|
-
return hyperlink(text, resolvedHref, wctx.color);
|
|
488
|
-
const plainText = text.replace(/\x1b\[[0-9;]*m|\x1b\]8;;[^\x07\x1b]*\x07/g, '');
|
|
489
|
-
if (plainText === sanitizedHref)
|
|
490
|
-
return sanitizedHref;
|
|
491
|
-
return `${text} (${sanitizedHref})`;
|
|
492
|
-
}
|
|
493
|
-
function renderImage(node, wctx) {
|
|
494
|
-
const altRaw = typeof node.properties?.alt === 'string' ? node.properties.alt : '';
|
|
495
|
-
const alt = stripControlCharacters(altRaw);
|
|
496
|
-
const srcRaw = typeof node.properties?.src === 'string' ? node.properties.src : '';
|
|
497
|
-
const resolvedSrc = resolveImageAttribute(srcRaw, wctx.resolveImageSrc);
|
|
498
|
-
const src = stripControlCharacters(resolvedSrc);
|
|
499
|
-
return `[${alt}] (${src})`;
|
|
500
|
-
}
|
|
501
|
-
function renderInlineNode(node, width, indent, wctx) {
|
|
498
|
+
const ACTIVE_MARKER = ' (active)';
|
|
499
|
+
function renderBlockString(node, width, indent, wctx) {
|
|
502
500
|
if (node.type === 'text')
|
|
503
501
|
return stripControlCharacters(node.value);
|
|
504
502
|
if (node.type !== 'element')
|
|
505
503
|
return '';
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
return renderDirective(node, width, indent, wctx);
|
|
509
|
-
case 'strong':
|
|
510
|
-
return bold(renderInlineChildren(node.children, width, indent, wctx), wctx.color);
|
|
511
|
-
case 'em':
|
|
512
|
-
return italic(renderInlineChildren(node.children, width, indent, wctx), wctx.color);
|
|
513
|
-
case 'del':
|
|
514
|
-
return dim(`~${renderInlineChildren(node.children, width, indent, wctx)}~`, wctx.color);
|
|
515
|
-
case 'code': {
|
|
516
|
-
const inner = getPlainText(node);
|
|
517
|
-
return wctx.color === 'none'
|
|
518
|
-
? dim(inner, wctx.color)
|
|
519
|
-
: inverse(inner, wctx.color);
|
|
520
|
-
}
|
|
521
|
-
case 'a':
|
|
522
|
-
return renderLink(node, width, indent, wctx);
|
|
523
|
-
case 'img':
|
|
524
|
-
return renderImage(node, wctx);
|
|
525
|
-
case 'br':
|
|
526
|
-
return '\n';
|
|
527
|
-
case 'input':
|
|
528
|
-
// Task-list checkboxes are consumed by `renderListItem`; a stray one
|
|
529
|
-
// (outside a list item) produces nothing rather than leaking markup.
|
|
530
|
-
return '';
|
|
531
|
-
default:
|
|
532
|
-
return renderInlineChildren(node.children ?? [], width, indent, wctx);
|
|
504
|
+
if (node.tagName === DIRECTIVE_TAG) {
|
|
505
|
+
return renderDirectiveString(node, width, indent, wctx);
|
|
533
506
|
}
|
|
507
|
+
if (/^h[1-6]$/.test(node.tagName)) {
|
|
508
|
+
const level = Number(node.tagName.slice(1));
|
|
509
|
+
const text = renderInlineChildrenString(node.children, width, indent, wctx);
|
|
510
|
+
const styled = level === 1
|
|
511
|
+
? underline(bold(text, wctx.color), wctx.color)
|
|
512
|
+
: bold(text, wctx.color);
|
|
513
|
+
return wrapText(styled, width).join('\n');
|
|
514
|
+
}
|
|
515
|
+
if (node.tagName === 'p') {
|
|
516
|
+
return wrapText(renderInlineChildrenString(node.children, width, indent, wctx), width).join('\n');
|
|
517
|
+
}
|
|
518
|
+
if (node.tagName === 'blockquote') {
|
|
519
|
+
const innerWidth = Math.max(1, width - 2);
|
|
520
|
+
const inner = renderBlocksString(node.children, innerWidth, `${indent}${wctx.glyphs.quoteBar}`, wctx);
|
|
521
|
+
const prefix = dim(wctx.glyphs.quoteBar, wctx.color);
|
|
522
|
+
return inner
|
|
523
|
+
.split('\n')
|
|
524
|
+
.map((line) => `${prefix}${line}`)
|
|
525
|
+
.join('\n');
|
|
526
|
+
}
|
|
527
|
+
if (node.tagName === 'ul' || node.tagName === 'ol') {
|
|
528
|
+
return renderListString(node, width, indent, wctx);
|
|
529
|
+
}
|
|
530
|
+
if (node.tagName === 'hr') {
|
|
531
|
+
return dim(ruleLine(width, wctx.glyphs.rule), wctx.color);
|
|
532
|
+
}
|
|
533
|
+
if (node.tagName === 'table')
|
|
534
|
+
return renderGfmTableString(node, width, wctx);
|
|
535
|
+
if (node.tagName === 'pre') {
|
|
536
|
+
const marker = renderScriptMarker(node, wctx.color, wctx.glyphs);
|
|
537
|
+
if (marker !== undefined)
|
|
538
|
+
return marker;
|
|
539
|
+
return renderCodeBlock(node, wctx.color);
|
|
540
|
+
}
|
|
541
|
+
return wrapText(renderInlineChildrenString(node.children ?? [], width, indent, wctx), width).join('\n');
|
|
534
542
|
}
|
|
535
|
-
function renderInlineChildren(children, width, indent, wctx) {
|
|
536
|
-
let result = '';
|
|
537
|
-
for (const child of children)
|
|
538
|
-
result += renderInlineNode(child, width, indent, wctx);
|
|
539
|
-
return result;
|
|
540
|
-
}
|
|
541
|
-
function renderHeading(el, width, indent, wctx) {
|
|
542
|
-
const level = Number(el.tagName.slice(1));
|
|
543
|
-
const text = renderInlineChildren(el.children, width, indent, wctx);
|
|
544
|
-
const styled = level === 1
|
|
545
|
-
? underline(bold(text, wctx.color), wctx.color)
|
|
546
|
-
: bold(text, wctx.color);
|
|
547
|
-
return wrap(styled, width).join('\n');
|
|
548
|
-
}
|
|
549
|
-
function renderParagraph(el, width, indent, wctx) {
|
|
550
|
-
const text = renderInlineChildren(el.children, width, indent, wctx);
|
|
551
|
-
return wrap(text, width).join('\n');
|
|
552
|
-
}
|
|
553
|
-
function renderBlockquote(el, width, indent, wctx) {
|
|
554
|
-
const innerWidth = Math.max(1, width - 2);
|
|
555
|
-
const inner = renderBlocks(el.children, innerWidth, `${indent}│ `, wctx);
|
|
556
|
-
const prefix = dim('│ ', wctx.color);
|
|
557
|
-
return inner
|
|
558
|
-
.split('\n')
|
|
559
|
-
.map((line) => `${prefix}${line}`)
|
|
560
|
-
.join('\n');
|
|
561
|
-
}
|
|
562
|
-
/** The task-checkbox state of a list item, or `undefined` for an ordinary item. */
|
|
563
543
|
function taskState(li) {
|
|
564
544
|
for (const child of li.children) {
|
|
565
545
|
if (child.type !== 'element')
|
|
@@ -567,13 +547,11 @@ function taskState(li) {
|
|
|
567
547
|
if (child.tagName === 'input') {
|
|
568
548
|
return { checked: Boolean(child.properties?.checked) };
|
|
569
549
|
}
|
|
570
|
-
// A task checkbox is always the first element in `remark-gfm`'s output;
|
|
571
|
-
// any other element first means this is not a task item.
|
|
572
550
|
return undefined;
|
|
573
551
|
}
|
|
574
552
|
return undefined;
|
|
575
553
|
}
|
|
576
|
-
function
|
|
554
|
+
function renderListItemString(li, marker, width, indent, wctx) {
|
|
577
555
|
const task = taskState(li);
|
|
578
556
|
const contentChildren = li.children.filter((child) => !(task && child.type === 'element' && child.tagName === 'input'));
|
|
579
557
|
const nestedLists = [];
|
|
@@ -588,11 +566,11 @@ function renderListItem(li, marker, width, indent, wctx) {
|
|
|
588
566
|
}
|
|
589
567
|
}
|
|
590
568
|
const prefix = `${marker}${task ? (task.checked ? '[x] ' : '[ ] ') : ''}`;
|
|
591
|
-
const prefixWidth =
|
|
569
|
+
const prefixWidth = measureWidth(prefix);
|
|
592
570
|
const bodyWidth = Math.max(1, width - prefixWidth);
|
|
593
571
|
const bodyText = isBlockLevelChildren(restChildren)
|
|
594
|
-
?
|
|
595
|
-
:
|
|
572
|
+
? renderBlocksString(restChildren, bodyWidth, `${indent}${' '.repeat(prefixWidth)}`, wctx)
|
|
573
|
+
: wrapText(renderInlineChildrenString(restChildren, bodyWidth, indent, wctx), bodyWidth).join('\n');
|
|
596
574
|
const bodyLines = bodyText === '' ? [''] : bodyText.split('\n');
|
|
597
575
|
const firstLine = `${prefix}${bodyLines[0] ?? ''}`;
|
|
598
576
|
const restLines = bodyLines
|
|
@@ -600,7 +578,7 @@ function renderListItem(li, marker, width, indent, wctx) {
|
|
|
600
578
|
.map((line) => `${' '.repeat(prefixWidth)}${line}`);
|
|
601
579
|
let result = [firstLine, ...restLines].join('\n');
|
|
602
580
|
for (const nested of nestedLists) {
|
|
603
|
-
const nestedText =
|
|
581
|
+
const nestedText = renderBlockString(nested, Math.max(1, width - 2), `${indent} `, wctx);
|
|
604
582
|
result += `\n${nestedText
|
|
605
583
|
.split('\n')
|
|
606
584
|
.map((line) => ` ${line}`)
|
|
@@ -608,7 +586,7 @@ function renderListItem(li, marker, width, indent, wctx) {
|
|
|
608
586
|
}
|
|
609
587
|
return result;
|
|
610
588
|
}
|
|
611
|
-
function
|
|
589
|
+
function renderListString(el, width, indent, wctx) {
|
|
612
590
|
const ordered = el.tagName === 'ol';
|
|
613
591
|
const startRaw = el.properties?.start;
|
|
614
592
|
const start = ordered && typeof startRaw === 'number' ? startRaw : 1;
|
|
@@ -616,7 +594,7 @@ function renderList(el, width, indent, wctx) {
|
|
|
616
594
|
return items
|
|
617
595
|
.map((li, index) => {
|
|
618
596
|
const marker = ordered ? `${start + index}. ` : '- ';
|
|
619
|
-
return
|
|
597
|
+
return renderListItemString(li, marker, width, indent, wctx);
|
|
620
598
|
})
|
|
621
599
|
.join('\n');
|
|
622
600
|
}
|
|
@@ -647,15 +625,7 @@ function getCodeText(codeNode) {
|
|
|
647
625
|
}
|
|
648
626
|
return text.endsWith('\n') ? text.slice(0, -1) : text;
|
|
649
627
|
}
|
|
650
|
-
|
|
651
|
-
* Folds a script code block into a one-line collapsed marker, matching
|
|
652
|
-
* `@markii/html`'s `renderScriptMarker` summary wording exactly (a
|
|
653
|
-
* terminal has no expand/collapse affordance, so the body is never shown —
|
|
654
|
-
* see this module's top comment on interactive components). Returns
|
|
655
|
-
* `undefined` for any other `<pre>` so the caller renders it as an
|
|
656
|
-
* ordinary code block.
|
|
657
|
-
*/
|
|
658
|
-
function renderScriptMarker(node, color) {
|
|
628
|
+
function renderScriptMarker(node, color, glyphs) {
|
|
659
629
|
try {
|
|
660
630
|
const codeNode = findChildElement(node, 'code');
|
|
661
631
|
const meta = codeNode
|
|
@@ -669,7 +639,9 @@ function renderScriptMarker(node, color) {
|
|
|
669
639
|
return undefined;
|
|
670
640
|
const src = attrs.src || undefined;
|
|
671
641
|
const detail = src ?? getLanguage(codeNode);
|
|
672
|
-
const summary = detail
|
|
642
|
+
const summary = detail
|
|
643
|
+
? `${glyphs.script} ${name} ${glyphs.scriptSeparator} ${detail}`
|
|
644
|
+
: `${glyphs.script} ${name}`;
|
|
673
645
|
return dim(summary, color);
|
|
674
646
|
}
|
|
675
647
|
catch {
|
|
@@ -687,18 +659,7 @@ function renderCodeBlock(node, color) {
|
|
|
687
659
|
lines.push(dim(` ${line}`, color));
|
|
688
660
|
return lines.join('\n');
|
|
689
661
|
}
|
|
690
|
-
|
|
691
|
-
* An ordinary GFM markdown table, box-drawn with a bold header row through
|
|
692
|
-
* `./components/table-grid.js`'s `drawTableGrid` — the SAME box-drawing and
|
|
693
|
-
* width-negotiation routine the data-bound `::table` component
|
|
694
|
-
* (`./components/table.ts`) draws with, so this engine has exactly one
|
|
695
|
-
* table-drawing implementation rather than two. Rendered at the directive's
|
|
696
|
-
* real available `width`, not a hardcoded budget: each cell's inline
|
|
697
|
-
* markdown is rendered once at that width so its own links/emphasis/code
|
|
698
|
-
* are preserved, and `drawTableGrid` then negotiates how much of `width`
|
|
699
|
-
* each column actually gets.
|
|
700
|
-
*/
|
|
701
|
-
function renderTable(tableEl, width, wctx) {
|
|
662
|
+
function renderGfmTableString(tableEl, width, wctx) {
|
|
702
663
|
const headerCells = [];
|
|
703
664
|
const bodyRows = [];
|
|
704
665
|
let sawHeader = false;
|
|
@@ -708,7 +669,7 @@ function renderTable(tableEl, width, wctx) {
|
|
|
708
669
|
continue;
|
|
709
670
|
if (cell.tagName !== 'td' && cell.tagName !== 'th')
|
|
710
671
|
continue;
|
|
711
|
-
into.push(
|
|
672
|
+
into.push(renderInlineChildrenString(cell.children, width, '', wctx));
|
|
712
673
|
}
|
|
713
674
|
};
|
|
714
675
|
for (const section of tableEl.children) {
|
|
@@ -731,53 +692,658 @@ function renderTable(tableEl, width, wctx) {
|
|
|
731
692
|
}
|
|
732
693
|
}
|
|
733
694
|
}
|
|
734
|
-
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));
|
|
735
696
|
}
|
|
736
|
-
function
|
|
697
|
+
function getPlainText(node) {
|
|
698
|
+
let text = '';
|
|
699
|
+
for (const child of node.children) {
|
|
700
|
+
if (child.type === 'text')
|
|
701
|
+
text += stripControlCharacters(child.value);
|
|
702
|
+
else if (child.type === 'element')
|
|
703
|
+
text += getPlainText(child);
|
|
704
|
+
}
|
|
705
|
+
return text;
|
|
706
|
+
}
|
|
707
|
+
function renderLink(node, width, indent, wctx) {
|
|
708
|
+
const hrefRaw = typeof node.properties?.href === 'string' ? node.properties.href : '';
|
|
709
|
+
const resolvedHref = resolveHrefAttribute(hrefRaw, wctx.resolveHref);
|
|
710
|
+
const text = renderInlineChildrenString(node.children, width, indent, wctx);
|
|
711
|
+
const sanitizedHref = stripControlCharacters(resolvedHref);
|
|
712
|
+
const canHyperlink = wctx.color !== 'none' &&
|
|
713
|
+
resolvedHref.trim() !== '' &&
|
|
714
|
+
isSafeUrl(resolvedHref) &&
|
|
715
|
+
sanitizeUrlText(resolvedHref) === resolvedHref;
|
|
716
|
+
if (canHyperlink)
|
|
717
|
+
return hyperlink(text, resolvedHref, wctx.color);
|
|
718
|
+
const plainText = text.replace(/\x1b\[[0-9;]*m|\x1b\]8;;[^\x07\x1b]*\x07/g, '');
|
|
719
|
+
if (plainText === sanitizedHref)
|
|
720
|
+
return sanitizedHref;
|
|
721
|
+
return `${text} (${sanitizedHref})`;
|
|
722
|
+
}
|
|
723
|
+
function renderImage(node, wctx) {
|
|
724
|
+
const altRaw = typeof node.properties?.alt === 'string' ? node.properties.alt : '';
|
|
725
|
+
const alt = stripControlCharacters(altRaw);
|
|
726
|
+
const srcRaw = typeof node.properties?.src === 'string' ? node.properties.src : '';
|
|
727
|
+
const resolvedSrc = resolveImageAttribute(srcRaw, wctx.resolveImageSrc);
|
|
728
|
+
const src = stripControlCharacters(resolvedSrc);
|
|
729
|
+
return `[${alt}] (${src})`;
|
|
730
|
+
}
|
|
731
|
+
function renderInlineNodeString(node, width, indent, wctx) {
|
|
737
732
|
if (node.type === 'text')
|
|
738
733
|
return stripControlCharacters(node.value);
|
|
739
734
|
if (node.type !== 'element')
|
|
740
735
|
return '';
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
736
|
+
switch (node.tagName) {
|
|
737
|
+
case DIRECTIVE_TAG:
|
|
738
|
+
return renderDirectiveString(node, width, indent, wctx);
|
|
739
|
+
case 'strong':
|
|
740
|
+
return bold(renderInlineChildrenString(node.children, width, indent, wctx), wctx.color);
|
|
741
|
+
case 'em':
|
|
742
|
+
return italic(renderInlineChildrenString(node.children, width, indent, wctx), wctx.color);
|
|
743
|
+
case 'del':
|
|
744
|
+
return dim(`~${renderInlineChildrenString(node.children, width, indent, wctx)}~`, wctx.color);
|
|
745
|
+
case 'code': {
|
|
746
|
+
const inner = getPlainText(node);
|
|
747
|
+
return wctx.color === 'none'
|
|
748
|
+
? dim(inner, wctx.color)
|
|
749
|
+
: inverse(inner, wctx.color);
|
|
750
|
+
}
|
|
751
|
+
case 'a':
|
|
752
|
+
return renderLink(node, width, indent, wctx);
|
|
753
|
+
case 'img':
|
|
754
|
+
return renderImage(node, wctx);
|
|
755
|
+
case 'br':
|
|
756
|
+
return '\n';
|
|
757
|
+
case 'input':
|
|
758
|
+
return '';
|
|
759
|
+
default:
|
|
760
|
+
return renderInlineChildrenString(node.children ?? [], width, indent, wctx);
|
|
760
761
|
}
|
|
761
|
-
return wrap(renderInlineChildren(node.children ?? [], width, indent, wctx), width).join('\n');
|
|
762
762
|
}
|
|
763
|
-
function
|
|
763
|
+
function renderInlineChildrenString(children, width, indent, wctx) {
|
|
764
|
+
let result = '';
|
|
765
|
+
for (const child of children)
|
|
766
|
+
result += renderInlineNodeString(child, width, indent, wctx);
|
|
767
|
+
return result;
|
|
768
|
+
}
|
|
769
|
+
function renderBlocksString(nodes, width, indent, wctx) {
|
|
764
770
|
const blocks = [];
|
|
765
771
|
for (const node of nodes) {
|
|
766
|
-
const rendered =
|
|
767
|
-
// Whitespace-only output is dropped, not just the empty string.
|
|
768
|
-
// remark-rehype puts a formatting `\n` TEXT node between every pair of
|
|
769
|
-
// block elements, and `stripControlCharacters` turns a line feed into a
|
|
770
|
-
// space rather than deleting it (see its doc comment: inline text reads
|
|
771
|
-
// better that way). Kept, each of those would become a block of its own
|
|
772
|
-
// holding one space, so every pair of real blocks would be separated by
|
|
773
|
-
// a blank line, a space line, and another blank line instead of a single
|
|
774
|
-
// blank line. Nothing a reader can see is ever lost by this test: a
|
|
775
|
-
// block whose entire rendered form is whitespace carries no content.
|
|
772
|
+
const rendered = renderBlockString(node, width, indent, wctx);
|
|
776
773
|
if (rendered.trim() !== '')
|
|
777
774
|
blocks.push(rendered);
|
|
778
775
|
}
|
|
779
776
|
return blocks.join('\n\n');
|
|
780
777
|
}
|
|
778
|
+
function renderDirectiveString(element, width, indent, wctx) {
|
|
779
|
+
const written = stringProperty(element, 'data-mk-name') ?? '';
|
|
780
|
+
const kind = stringProperty(element, 'data-mk-kind');
|
|
781
|
+
const rawAttributes = parseAttributes(stringProperty(element, 'data-mk-attrs'));
|
|
782
|
+
const isBlock = kind !== TEXT_DIRECTIVE_KIND;
|
|
783
|
+
if (written === VALUE_DIRECTIVE_NAME) {
|
|
784
|
+
const plainLabel = extractPlainText(element.children);
|
|
785
|
+
const resolved = resolveValue(wctx.scope, plainLabel);
|
|
786
|
+
return renderValueMarker(plainLabel.trim(), resolved, rawAttributes.format ?? undefined, rawAttributes.decimals ?? undefined, wctx.color, wctx.theme);
|
|
787
|
+
}
|
|
788
|
+
const { name, attributes: aliased } = resolveDirectiveAlias(wctx.registry, written, rawAttributes);
|
|
789
|
+
// A `row`/`tabs` reached through STRING mode (nested inside a self-drawing
|
|
790
|
+
// container's own body) falls back to a flattened rendering with no real
|
|
791
|
+
// flex layout / interactivity — see this file's top comment.
|
|
792
|
+
if (isBlock && name === ROW_NAME && hasRealComponent(wctx.registry, name)) {
|
|
793
|
+
return renderRowString(element, aliased, width, indent, wctx);
|
|
794
|
+
}
|
|
795
|
+
if (isBlock && name === TABS_NAME && hasRealComponent(wctx.registry, name)) {
|
|
796
|
+
const panels = extractTabPanels(element, width, indent, wctx);
|
|
797
|
+
return panels
|
|
798
|
+
.map((panel, index) => {
|
|
799
|
+
const heading = bold(`${panel.label}${index === 0 ? ACTIVE_MARKER : ''}`, wctx.color);
|
|
800
|
+
return panel.body ? `${heading}\n${panel.body}` : heading;
|
|
801
|
+
})
|
|
802
|
+
.join('\n\n');
|
|
803
|
+
}
|
|
804
|
+
const ownedAxis = registryLayoutAxis(wctx.registry, name);
|
|
805
|
+
const { attributes, resolved: layoutResolved } = resolveLayoutAttributes(aliased, isBlock ? ownedAxis : undefined);
|
|
806
|
+
const isLayoutScope = ownedAxis !== undefined && isBlock;
|
|
807
|
+
const isSelfLayout = isBlock && registrySelfLayout(wctx.registry, name);
|
|
808
|
+
const layoutForWidth = isLayoutScope || isSelfLayout ? layoutResolved : undefined;
|
|
809
|
+
// `card`/`callout`/`details` narrow their own body before drawing a frame
|
|
810
|
+
// or an indent around it. Each computes that inner width from ONLY its
|
|
811
|
+
// own attributes (never from the body), so the walk resolves it here,
|
|
812
|
+
// BEFORE building children, and builds children exactly ONCE at that
|
|
813
|
+
// final width — never at the outer width followed by a re-wrap. A
|
|
814
|
+
// re-wrap-afterward shipped as a real regression (post-phase-2 review):
|
|
815
|
+
// it corrupts anything self-drawing nested inside (a card's frame, a
|
|
816
|
+
// row's columns), since a word-based re-wrapper has no notion that a run
|
|
817
|
+
// of box-drawing characters is not prose. See `resolveCardInnerWidth`'s
|
|
818
|
+
// doc comment.
|
|
819
|
+
let childrenWidth = width;
|
|
820
|
+
if (isBlock && name === 'card') {
|
|
821
|
+
childrenWidth = resolveCardInnerWidth(attributes, layoutForWidth, width);
|
|
822
|
+
}
|
|
823
|
+
else if (isBlock && name === 'callout') {
|
|
824
|
+
childrenWidth = resolveCalloutInnerWidth(attributes, layoutForWidth, width, wctx.glyphs);
|
|
825
|
+
}
|
|
826
|
+
else if (isBlock && name === DETAILS_NAME) {
|
|
827
|
+
childrenWidth = resolveDetailsInnerWidth(width);
|
|
828
|
+
}
|
|
829
|
+
const childrenText = renderDirectiveChildrenString(element.children, childrenWidth, indent, wctx);
|
|
830
|
+
const content = renderDirectiveContentString(name, kind, isBlock, attributes, childrenText, width, indent, wctx, layoutForWidth);
|
|
831
|
+
if (isBlock && layoutResolved && !isLayoutScope && !isSelfLayout) {
|
|
832
|
+
return applyLayout(content, layoutResolved, width);
|
|
833
|
+
}
|
|
834
|
+
return content;
|
|
835
|
+
}
|
|
836
|
+
function hasRealComponent(registry, name) {
|
|
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;
|
|
848
|
+
}
|
|
849
|
+
// ---------------------------------------------------------------------------
|
|
850
|
+
// ELEMENT MODE — real Ink elements, used for block sequencing and for
|
|
851
|
+
// `row`/`tabs`/`details`. See this file's top comment.
|
|
852
|
+
// ---------------------------------------------------------------------------
|
|
853
|
+
/** Wraps `element` per `layout`'s resolved width/align, the JSX-based twin of `layout.ts`'s string-based `applyLayout`, for the three directives whose own output is a real Ink element rather than a string. */
|
|
854
|
+
function applyLayoutElement(element, layout, width) {
|
|
855
|
+
if (!layout)
|
|
856
|
+
return element;
|
|
857
|
+
let target = width;
|
|
858
|
+
if (layout.width === 'narrow')
|
|
859
|
+
target = Math.max(20, Math.round(width / 2));
|
|
860
|
+
const align = layout.align;
|
|
861
|
+
const alignItems = align === 'center'
|
|
862
|
+
? 'center'
|
|
863
|
+
: align === 'right'
|
|
864
|
+
? 'flex-end'
|
|
865
|
+
: 'flex-start';
|
|
866
|
+
if (target === width && !align)
|
|
867
|
+
return element;
|
|
868
|
+
return (_jsx(Box, { width: width, flexDirection: "column", alignItems: alignItems, children: _jsx(Box, { width: target, flexDirection: "column", children: element }) }));
|
|
869
|
+
}
|
|
870
|
+
function invalidEnumMarkerElement(name, attributes, wctx) {
|
|
871
|
+
const invalidEnum = invalidEnumAttribute(name, attributes);
|
|
872
|
+
if (!invalidEnum)
|
|
873
|
+
return null;
|
|
874
|
+
reportDiagnostic(wctx.onDiagnostic, {
|
|
875
|
+
kind: 'invalid-attribute-value',
|
|
876
|
+
directive: name,
|
|
877
|
+
attribute: invalidEnum.attribute,
|
|
878
|
+
message: invalidAttributeValueTitle(name, invalidEnum.attribute, invalidEnum.value),
|
|
879
|
+
});
|
|
880
|
+
const label = invalidAttributeValueLabel(name, invalidEnum.attribute);
|
|
881
|
+
return styledText(dim(`[${label}]`, wctx.color));
|
|
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
|
+
}
|
|
972
|
+
function renderRowElement(element, attributes, width, indent, wctx) {
|
|
973
|
+
const cellNodes = element.children.filter((node) => !(node.type === 'text' && node.value.trim() === ''));
|
|
974
|
+
if (cellNodes.length === 0)
|
|
975
|
+
return null;
|
|
976
|
+
const layout = resolveRowLayout(attributes, cellNodes.length, width);
|
|
977
|
+
if (layout.stacked) {
|
|
978
|
+
const items = cellNodes
|
|
979
|
+
.map((node, index) => (_jsx(Box, { marginTop: index > 0 ? 1 : 0, flexDirection: "column", children: renderRowCellElement(node, width, indent, wctx, layout.align) }, index)))
|
|
980
|
+
.filter(Boolean);
|
|
981
|
+
return _jsx(Box, { flexDirection: "column", children: items });
|
|
982
|
+
}
|
|
983
|
+
const rows = [];
|
|
984
|
+
for (let index = 0; index < cellNodes.length; index += layout.columnCount) {
|
|
985
|
+
const rowCells = cellNodes.slice(index, index + layout.columnCount);
|
|
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));
|
|
987
|
+
}
|
|
988
|
+
return _jsx(Box, { flexDirection: "column", children: rows });
|
|
989
|
+
}
|
|
990
|
+
function renderTabsElement(element, width, indent, wctx) {
|
|
991
|
+
const panels = extractTabPanelsElement(element, width, indent, wctx);
|
|
992
|
+
if (panels.length === 0)
|
|
993
|
+
return null;
|
|
994
|
+
const focusId = wctx.interactive ? nextFocusId(wctx) : undefined;
|
|
995
|
+
if (focusId !== undefined) {
|
|
996
|
+
recordFocusable(wctx, {
|
|
997
|
+
kind: 'tabs',
|
|
998
|
+
label: panels[0]?.label ?? DEFAULT_TAB_LABEL,
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
return (_jsx(InteractiveTabs, { panels: panels, color: wctx.color, glyphs: wctx.glyphs, interactive: wctx.interactive, focusId: focusId }));
|
|
1002
|
+
}
|
|
1003
|
+
function extractTabPanelsElement(element, width, indent, wctx) {
|
|
1004
|
+
const panels = [];
|
|
1005
|
+
for (const node of element.children) {
|
|
1006
|
+
if (node.type === 'text' && node.value.trim() === '')
|
|
1007
|
+
continue;
|
|
1008
|
+
if (node.type !== 'element')
|
|
1009
|
+
continue;
|
|
1010
|
+
let label = DEFAULT_TAB_LABEL;
|
|
1011
|
+
let isTab = false;
|
|
1012
|
+
if (node.tagName === DIRECTIVE_TAG) {
|
|
1013
|
+
const written = stringProperty(node, 'data-mk-name') ?? '';
|
|
1014
|
+
const attrs = parseAttributes(stringProperty(node, 'data-mk-attrs'));
|
|
1015
|
+
const { name } = resolveDirectiveAlias(wctx.registry, written, attrs);
|
|
1016
|
+
if (name === 'tab') {
|
|
1017
|
+
label = attrs.label ?? DEFAULT_TAB_LABEL;
|
|
1018
|
+
isTab = true;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
// See `extractTabPanels`'s (string-mode) identical comment: rendering
|
|
1022
|
+
// the whole `tab` directive here would ALSO invoke `tab.ts`'s own
|
|
1023
|
+
// component, printing the label a second time.
|
|
1024
|
+
const body = isTab
|
|
1025
|
+
? renderBlocksElement(node.children, width, indent, wctx)
|
|
1026
|
+
: renderBlockElement(node, width, indent, wctx);
|
|
1027
|
+
panels.push({ label, body });
|
|
1028
|
+
}
|
|
1029
|
+
return panels;
|
|
1030
|
+
}
|
|
1031
|
+
function renderDetailsElement(element, attributes, width, indent, wctx) {
|
|
1032
|
+
const title = attributes.title ?? 'Details';
|
|
1033
|
+
const open = Object.hasOwn(attributes, 'open');
|
|
1034
|
+
const innerWidth = resolveDetailsInnerWidth(width);
|
|
1035
|
+
const body = renderBlocksElement(element.children, innerWidth, indent, wctx);
|
|
1036
|
+
const focusId = wctx.interactive ? nextFocusId(wctx) : undefined;
|
|
1037
|
+
if (focusId !== undefined) {
|
|
1038
|
+
recordFocusable(wctx, {
|
|
1039
|
+
kind: 'details',
|
|
1040
|
+
label: stripControlCharacters(title),
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
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] }));
|
|
1205
|
+
}
|
|
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>`. */
|
|
1207
|
+
function renderDirectiveElement(element, width, indent, wctx) {
|
|
1208
|
+
const written = stringProperty(element, 'data-mk-name') ?? '';
|
|
1209
|
+
const kind = stringProperty(element, 'data-mk-kind');
|
|
1210
|
+
const rawAttributes = parseAttributes(stringProperty(element, 'data-mk-attrs'));
|
|
1211
|
+
const isBlock = kind !== TEXT_DIRECTIVE_KIND;
|
|
1212
|
+
if (!isBlock) {
|
|
1213
|
+
// A stray inline (text) directive at a block position (rare; hast
|
|
1214
|
+
// structure keeps these inside a paragraph in practice) still renders,
|
|
1215
|
+
// via the string engine.
|
|
1216
|
+
return styledText(renderDirectiveString(element, width, indent, wctx));
|
|
1217
|
+
}
|
|
1218
|
+
const { name, attributes: aliased } = resolveDirectiveAlias(wctx.registry, written, rawAttributes);
|
|
1219
|
+
const entry = Object.hasOwn(wctx.registry, name)
|
|
1220
|
+
? wctx.registry[name]
|
|
1221
|
+
: undefined;
|
|
1222
|
+
const formMismatch = entry ? isFormMismatch(entry, kind) : false;
|
|
1223
|
+
if (!formMismatch &&
|
|
1224
|
+
(name === ROW_NAME ||
|
|
1225
|
+
name === TABS_NAME ||
|
|
1226
|
+
name === DETAILS_NAME ||
|
|
1227
|
+
name === CARD_NAME ||
|
|
1228
|
+
name === CALLOUT_NAME ||
|
|
1229
|
+
name === FIGURE_NAME) &&
|
|
1230
|
+
hasRealComponent(wctx.registry, name)) {
|
|
1231
|
+
// None of these six own a layout axis (`registryLayoutAxis` is
|
|
1232
|
+
// undefined for all of them in the standard registry), so, unlike a
|
|
1233
|
+
// layout-wrapper scope, a `width=`/`align=` the author wrote still needs
|
|
1234
|
+
// resolving here rather than being handled by the directive's own name.
|
|
1235
|
+
const { attributes, resolved: layoutResolved } = resolveLayoutAttributes(aliased, undefined);
|
|
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
|
+
}
|
|
1255
|
+
let el = null;
|
|
1256
|
+
if (name === ROW_NAME) {
|
|
1257
|
+
el = renderRowElement(element, attributes, width, indent, wctx);
|
|
1258
|
+
}
|
|
1259
|
+
else if (name === TABS_NAME) {
|
|
1260
|
+
el = renderTabsElement(element, width, indent, wctx);
|
|
1261
|
+
}
|
|
1262
|
+
else if (name === DETAILS_NAME) {
|
|
1263
|
+
el = renderDetailsElement(element, attributes, width, indent, wctx);
|
|
1264
|
+
}
|
|
1265
|
+
else {
|
|
1266
|
+
el = renderFigureElement(element, attributes, width, indent, wctx);
|
|
1267
|
+
}
|
|
1268
|
+
if (!el)
|
|
1269
|
+
return null;
|
|
1270
|
+
const marker = invalidEnumMarkerElement(name, attributes, wctx);
|
|
1271
|
+
const withMarker = marker ? (_jsxs(Box, { flexDirection: "column", children: [el, marker] })) : (el);
|
|
1272
|
+
return applyLayoutElement(withMarker, layoutResolved, width);
|
|
1273
|
+
}
|
|
1274
|
+
catch {
|
|
1275
|
+
return styledText(componentError(name || '(unnamed)', false, '', width, wctx.color, wctx.glyphs));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return styledText(renderDirectiveString(element, width, indent, wctx));
|
|
1279
|
+
}
|
|
1280
|
+
/** Renders one block-level hast node as a real Ink element, or `null` for a node that carries no content (whitespace-only text). */
|
|
1281
|
+
function renderBlockElement(node, width, indent, wctx) {
|
|
1282
|
+
if (node.type === 'text') {
|
|
1283
|
+
return node.value.trim() === ''
|
|
1284
|
+
? null
|
|
1285
|
+
: styledText(stripControlCharacters(node.value));
|
|
1286
|
+
}
|
|
1287
|
+
if (node.type !== 'element')
|
|
1288
|
+
return null;
|
|
1289
|
+
if (node.tagName === DIRECTIVE_TAG) {
|
|
1290
|
+
return renderDirectiveElement(node, width, indent, wctx);
|
|
1291
|
+
}
|
|
1292
|
+
if (node.tagName === 'blockquote') {
|
|
1293
|
+
const innerWidth = Math.max(1, width - 2);
|
|
1294
|
+
const inner = renderBlocksElement(node.children, innerWidth, `${indent}${wctx.glyphs.quoteBar}`, wctx);
|
|
1295
|
+
if (!inner)
|
|
1296
|
+
return null;
|
|
1297
|
+
return (_jsxs(Box, { width: width, children: [styledText(dim(wctx.glyphs.quoteBar, wctx.color)), _jsx(Box, { flexDirection: "column", width: innerWidth, children: inner })] }));
|
|
1298
|
+
}
|
|
1299
|
+
if (node.tagName === 'ul' || node.tagName === 'ol') {
|
|
1300
|
+
return renderListElement(node, width, indent, wctx);
|
|
1301
|
+
}
|
|
1302
|
+
const text = renderBlockString(node, width, indent, wctx);
|
|
1303
|
+
if (text.trim() === '')
|
|
1304
|
+
return null;
|
|
1305
|
+
return styledText(text);
|
|
1306
|
+
}
|
|
1307
|
+
function renderListElement(el, width, indent, wctx) {
|
|
1308
|
+
const ordered = el.tagName === 'ol';
|
|
1309
|
+
const startRaw = el.properties?.start;
|
|
1310
|
+
const start = ordered && typeof startRaw === 'number' ? startRaw : 1;
|
|
1311
|
+
const items = el.children.filter((child) => child.type === 'element' && child.tagName === 'li');
|
|
1312
|
+
if (items.length === 0)
|
|
1313
|
+
return null;
|
|
1314
|
+
const items$ = items.map((li, index) => {
|
|
1315
|
+
const marker = ordered ? `${start + index}. ` : '- ';
|
|
1316
|
+
const task = taskState(li);
|
|
1317
|
+
const contentChildren = li.children.filter((child) => !(task && child.type === 'element' && child.tagName === 'input'));
|
|
1318
|
+
const prefix = `${marker}${task ? (task.checked ? '[x] ' : '[ ] ') : ''}`;
|
|
1319
|
+
const prefixWidth = measureWidth(prefix);
|
|
1320
|
+
const bodyWidth = Math.max(1, width - prefixWidth);
|
|
1321
|
+
const nestedLists = contentChildren.filter((child) => child.type === 'element' &&
|
|
1322
|
+
(child.tagName === 'ul' || child.tagName === 'ol'));
|
|
1323
|
+
const restChildren = contentChildren.filter((child) => !nestedLists.includes(child));
|
|
1324
|
+
const bodyIsBlock = isBlockLevelChildren(restChildren);
|
|
1325
|
+
const bodyText = bodyIsBlock
|
|
1326
|
+
? undefined
|
|
1327
|
+
: wrapText(renderInlineChildrenString(restChildren, bodyWidth, indent, wctx), bodyWidth).join('\n');
|
|
1328
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [styledText(prefix), _jsx(Box, { width: bodyWidth, flexDirection: "column", children: bodyIsBlock
|
|
1329
|
+
? renderBlocksElement(restChildren, bodyWidth, indent, wctx)
|
|
1330
|
+
: styledText(bodyText ?? '') })] }), nestedLists.length > 0 && (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: nestedLists.map((nested, nestedIndex) => (_jsx(Fragment, { children: renderBlockElement(nested, Math.max(1, width - 2), `${indent} `, wctx) }, nestedIndex))) }))] }, index));
|
|
1331
|
+
});
|
|
1332
|
+
return _jsx(Box, { flexDirection: "column", children: items$ });
|
|
1333
|
+
}
|
|
1334
|
+
/** Renders a sequence of block-level hast nodes as a real Ink element (a column `Box`), one blank row (`marginTop`) between consecutive non-empty blocks — the Ink-element twin of `renderBlocksString`'s `'\n\n'.join`. */
|
|
1335
|
+
function renderBlocksElement(nodes, width, indent, wctx) {
|
|
1336
|
+
const elements = [];
|
|
1337
|
+
for (const node of nodes) {
|
|
1338
|
+
const el = renderBlockElement(node, width, indent, wctx);
|
|
1339
|
+
if (el === null)
|
|
1340
|
+
continue;
|
|
1341
|
+
elements.push(_jsx(Box, { marginTop: elements.length > 0 ? 1 : 0, flexDirection: "column", children: el }, elements.length));
|
|
1342
|
+
}
|
|
1343
|
+
if (elements.length === 0)
|
|
1344
|
+
return null;
|
|
1345
|
+
return _jsx(_Fragment, { children: elements });
|
|
1346
|
+
}
|
|
781
1347
|
/** Collapses trailing blank lines to exactly one terminating `\n`, and leaves an empty document as `''`. */
|
|
782
1348
|
function finalizeOutput(text) {
|
|
783
1349
|
if (text === '')
|
|
@@ -789,34 +1355,45 @@ function renderFailureFallback(error, color) {
|
|
|
789
1355
|
const message = error instanceof Error ? error.message : String(error);
|
|
790
1356
|
return `${dim('failed to render document', color)}\n${stripControlCharacters(message)}\n`;
|
|
791
1357
|
}
|
|
792
|
-
function
|
|
793
|
-
|
|
1358
|
+
function buildWalkContext(registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable) {
|
|
1359
|
+
return {
|
|
794
1360
|
registry,
|
|
795
1361
|
scope,
|
|
796
1362
|
color,
|
|
797
1363
|
theme,
|
|
1364
|
+
glyphs,
|
|
1365
|
+
interactive,
|
|
798
1366
|
resolveImageSrc,
|
|
799
1367
|
resolveHref,
|
|
800
1368
|
onDiagnostic,
|
|
1369
|
+
unavailable,
|
|
1370
|
+
focusCounter: { current: 0 },
|
|
1371
|
+
focusables: undefined,
|
|
801
1372
|
};
|
|
802
|
-
return finalizeOutput(renderBlocks(root.children, width, '', wctx));
|
|
803
1373
|
}
|
|
804
1374
|
/** The columns assumed when a caller supplies no `width` option. */
|
|
805
1375
|
const DEFAULT_WIDTH = 80;
|
|
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. */
|
|
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);
|
|
1379
|
+
const body = renderBlocksElement(root.children, width, '', wctx);
|
|
1380
|
+
return (_jsx(Box, { width: width, flexDirection: "column", children: body ?? _jsx(Text, { children: " " }) }));
|
|
1381
|
+
}
|
|
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);
|
|
1384
|
+
const raw = await renderInkToString(element, width);
|
|
1385
|
+
return finalizeOutput(raw);
|
|
1386
|
+
}
|
|
806
1387
|
/**
|
|
807
1388
|
* Renders Markii text to a plain string (optionally carrying ANSI SGR/OSC 8
|
|
808
1389
|
* escapes) using `registry` to resolve directive names. `registry` is
|
|
809
1390
|
* OPTIONAL here, unlike the other two engines' required parameter, and
|
|
810
|
-
* defaults to `defaultAnsiRegistry
|
|
811
|
-
* therefore renders the unknown-component fallback).
|
|
1391
|
+
* defaults to `defaultAnsiRegistry`.
|
|
812
1392
|
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
* fallback
|
|
817
|
-
* paragraph, heading, and list item to `options.width` (default 80). Pure
|
|
818
|
-
* and never-throwing: parsing is tolerant, unknown names always render a
|
|
819
|
-
* fallback, and any unexpected internal error degrades to the "failed to
|
|
1393
|
+
* `async` since the first call in a process pays Ink/Yoga's one-time module-
|
|
1394
|
+
* evaluation cost (`ink-string.ts`'s doc comment; `SPIKE-10-findings.md`'s
|
|
1395
|
+
* "Item 3"). Never throws: parsing is tolerant, unknown names always render
|
|
1396
|
+
* a fallback, and any unexpected internal error degrades to the "failed to
|
|
820
1397
|
* render document" line.
|
|
821
1398
|
*
|
|
822
1399
|
* `options.color` resolves via `./ansi.js`'s `resolveColorOption`: `'auto'`
|
|
@@ -824,14 +1401,11 @@ const DEFAULT_WIDTH = 80;
|
|
|
824
1401
|
* environment knowledge of its own — a caller that wants automatic
|
|
825
1402
|
* detection calls `detectColorLevel` itself and passes the result as
|
|
826
1403
|
* `'16'`/`'256'`/`'truecolor'`.
|
|
827
|
-
*
|
|
828
|
-
* `store`/`vault` and `resolveImageSrc`/`resolveHref`/`onDiagnostic` carry
|
|
829
|
-
* the same meaning as `@markii/html`'s identical options.
|
|
830
1404
|
*/
|
|
831
|
-
export function renderMarkToAnsi(text, registry = defaultAnsiRegistry, store, vault, options) {
|
|
1405
|
+
export async function renderMarkToAnsi(text, registry = defaultAnsiRegistry, store, vault, options) {
|
|
832
1406
|
const color = resolveColorOption(options?.color);
|
|
833
1407
|
try {
|
|
834
|
-
return
|
|
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);
|
|
835
1409
|
}
|
|
836
1410
|
catch (error) {
|
|
837
1411
|
return renderFailureFallback(error, color);
|
|
@@ -839,21 +1413,20 @@ export function renderMarkToAnsi(text, registry = defaultAnsiRegistry, store, va
|
|
|
839
1413
|
}
|
|
840
1414
|
/**
|
|
841
1415
|
* The block-level twin of `renderMarkToAnsi`: renders one already-parsed
|
|
842
|
-
* mdast node OR a whole already-parsed mdast document
|
|
843
|
-
*
|
|
844
|
-
* text. Same registry resolution, same fallbacks, same purity and
|
|
1416
|
+
* mdast node OR a whole already-parsed mdast document instead of raw
|
|
1417
|
+
* document text. Same registry resolution, same fallbacks, same purity and
|
|
845
1418
|
* never-throw guarantees, same options.
|
|
846
1419
|
*/
|
|
847
|
-
export function renderMarkNodeToAnsi(node, registry = defaultAnsiRegistry, store, vault, options) {
|
|
1420
|
+
export async function renderMarkNodeToAnsi(node, registry = defaultAnsiRegistry, store, vault, options) {
|
|
848
1421
|
const color = resolveColorOption(options?.color);
|
|
849
1422
|
try {
|
|
850
|
-
return
|
|
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);
|
|
851
1424
|
}
|
|
852
1425
|
catch (error) {
|
|
853
1426
|
return renderFailureFallback(error, color);
|
|
854
1427
|
}
|
|
855
1428
|
}
|
|
856
|
-
/** Whether `root` is exactly one paragraph holding exactly one text directive (`:name[...]`) and nothing else.
|
|
1429
|
+
/** Whether `root` is exactly one paragraph holding exactly one text directive (`:name[...]`) and nothing else. */
|
|
857
1430
|
function loneInlineDirective(root) {
|
|
858
1431
|
if (root.children.length !== 1)
|
|
859
1432
|
return undefined;
|
|
@@ -869,17 +1442,15 @@ function loneInlineDirective(root) {
|
|
|
869
1442
|
}
|
|
870
1443
|
/**
|
|
871
1444
|
* Renders `text` as a single, standalone inline directive when that is all
|
|
872
|
-
* it is
|
|
873
|
-
*
|
|
874
|
-
* directive renders that directive alone. Any other source falls back to
|
|
875
|
-
* the exact same rendering `renderMarkToAnsi` would produce.
|
|
1445
|
+
* it is. Any other source falls back to the exact same rendering
|
|
1446
|
+
* `renderMarkToAnsi` would produce.
|
|
876
1447
|
*/
|
|
877
|
-
export function renderMarkInlineToAnsi(text, registry = defaultAnsiRegistry, store, vault, options) {
|
|
1448
|
+
export async function renderMarkInlineToAnsi(text, registry = defaultAnsiRegistry, store, vault, options) {
|
|
878
1449
|
try {
|
|
879
1450
|
const root = parse(text);
|
|
880
1451
|
const lone = loneInlineDirective(root);
|
|
881
1452
|
if (lone) {
|
|
882
|
-
return renderMarkNodeToAnsi(lone, registry, store, vault, options);
|
|
1453
|
+
return await renderMarkNodeToAnsi(lone, registry, store, vault, options);
|
|
883
1454
|
}
|
|
884
1455
|
}
|
|
885
1456
|
catch {
|
|
@@ -887,3 +1458,53 @@ export function renderMarkInlineToAnsi(text, registry = defaultAnsiRegistry, sto
|
|
|
887
1458
|
}
|
|
888
1459
|
return renderMarkToAnsi(text, registry, store, vault, options);
|
|
889
1460
|
}
|
|
1461
|
+
/**
|
|
1462
|
+
* The live-viewer entry point (phase 3 consumes this): builds the same Ink
|
|
1463
|
+
* element tree `renderMarkToAnsi` would render to a string, but returns the
|
|
1464
|
+
* element itself so a host (the CLI's `markii view`) can mount it directly
|
|
1465
|
+
* with Ink's own `render()` instead of re-rendering a string on every
|
|
1466
|
+
* keystroke/resize. `document` accepts raw text OR an already-parsed mdast
|
|
1467
|
+
* node/root, mirroring the three string entry points' flexibility.
|
|
1468
|
+
*/
|
|
1469
|
+
export function buildMarkElement(document, registry = defaultAnsiRegistry, options) {
|
|
1470
|
+
const color = resolveColorOption(options?.color);
|
|
1471
|
+
const theme = options?.theme ?? defaultAnsiTheme;
|
|
1472
|
+
const glyphs = glyphsFor(options?.box);
|
|
1473
|
+
const scope = { store: options?.store, vault: options?.vault };
|
|
1474
|
+
const width = options?.width ?? DEFAULT_WIDTH;
|
|
1475
|
+
const interactive = options?.interactive ?? true;
|
|
1476
|
+
let root;
|
|
1477
|
+
try {
|
|
1478
|
+
root =
|
|
1479
|
+
typeof document === 'string'
|
|
1480
|
+
? toHast(document)
|
|
1481
|
+
: nodeOrRootToHast(document);
|
|
1482
|
+
}
|
|
1483
|
+
catch {
|
|
1484
|
+
root = { type: 'root', children: [] };
|
|
1485
|
+
}
|
|
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);
|
|
1488
|
+
if (!interactive)
|
|
1489
|
+
return content;
|
|
1490
|
+
// The walk above already assigned every focusable component's `focusId`
|
|
1491
|
+
// (`WalkContext.focusCounter`) and, incidentally, discarded each one's
|
|
1492
|
+
// descriptor (`WalkContext.focusables` is left `undefined` on the real
|
|
1493
|
+
// render, since nothing there needs it). Rebuilding once more, this time
|
|
1494
|
+
// collecting descriptors, is wasteful only in the sense that the tree is
|
|
1495
|
+
// constructed twice; simpler and safer than threading mutable state out
|
|
1496
|
+
// of a pure element-builder, and this entry point is called once per
|
|
1497
|
+
// mount/resize, not per keystroke.
|
|
1498
|
+
const counter = { current: 0 };
|
|
1499
|
+
const focusables = [];
|
|
1500
|
+
const countingWctx = buildWalkContext(registry, scope, color, theme, glyphs, true, options?.resolveImageSrc, options?.resolveHref,
|
|
1501
|
+
// No `onDiagnostic`: the real walk above already reported every notice
|
|
1502
|
+
// this document produces, and this second walk exists only to collect
|
|
1503
|
+
// focus descriptors. Passing the callback here would report each notice
|
|
1504
|
+
// twice on every mount and again on every resize.
|
|
1505
|
+
undefined, unavailable);
|
|
1506
|
+
countingWctx.focusCounter = counter;
|
|
1507
|
+
countingWctx.focusables = focusables;
|
|
1508
|
+
renderBlocksElement(root.children, width, '', countingWctx);
|
|
1509
|
+
return (_jsx(InteractiveRoot, { focusables: focusables, onExit: options?.onExit, color: color, theme: theme, keyLegend: glyphs.keyLegend, viewportRows: options?.viewportRows, width: width, children: content }));
|
|
1510
|
+
}
|