@markii/ansi 0.0.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +24 -13
- package/dist/components/callout.js +45 -21
- package/dist/components/card.d.ts +24 -2
- package/dist/components/card.js +41 -12
- 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 +9 -16
- package/dist/components/details-string.d.ts +26 -0
- package/dist/components/details-string.js +39 -0
- package/dist/components/details.d.ts +30 -10
- package/dist/components/details.js +27 -25
- package/dist/components/divider.js +9 -11
- 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 +6 -14
- package/dist/components/rating.d.ts +1 -2
- package/dist/components/rating.js +2 -3
- package/dist/components/row.d.ts +33 -20
- package/dist/components/row.js +33 -51
- package/dist/components/stat.d.ts +3 -8
- package/dist/components/stat.js +4 -9
- package/dist/components/tab.d.ts +9 -8
- package/dist/components/tab.js +12 -10
- package/dist/components/table-grid.js +1 -2
- package/dist/components/table.d.ts +5 -10
- package/dist/components/table.js +7 -12
- package/dist/components/tabs-string.d.ts +17 -0
- package/dist/components/tabs-string.js +30 -0
- package/dist/components/tabs.d.ts +29 -14
- package/dist/components/tabs.js +37 -30
- package/dist/index.d.ts +2 -4
- package/dist/index.js +6 -7
- package/dist/ink-string.d.ts +12 -0
- package/dist/ink-string.js +41 -0
- package/dist/interactive.d.ts +41 -0
- package/dist/interactive.js +64 -0
- package/dist/layout.js +1 -2
- package/dist/registry.d.ts +91 -100
- package/dist/registry.js +17 -9
- package/dist/render.d.ts +39 -24
- package/dist/render.js +676 -373
- package/dist/text-grid.d.ts +54 -0
- package/dist/text-grid.js +249 -0
- package/package.json +7 -4
- 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,9 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment } 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 { frameBlock, measureWidth, padText, ruleLine, wrapText, } from './text-grid.js';
|
|
9
|
+
import { drawTableGrid } from './components/table-grid.js';
|
|
10
|
+
import { resolveRowLayout } from './components/row.js';
|
|
11
|
+
import { resolveCardInnerWidth } from './components/card.js';
|
|
12
|
+
import { resolveCalloutInnerWidth } from './components/callout.js';
|
|
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';
|
|
@@ -13,28 +20,55 @@ import { 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 } from './ink-string.js';
|
|
24
|
+
import { InteractiveRoot } from './interactive.js';
|
|
25
|
+
import { InteractiveTabs } from './components/tabs.js';
|
|
26
|
+
import { InteractiveDetails } from './components/details.js';
|
|
17
27
|
/**
|
|
18
|
-
* The hast walk for the terminal engine
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
28
|
+
* The hast walk for the terminal engine (batch 10: the Ink rewrite). Every
|
|
29
|
+
* DECISION this walk makes is unchanged from the pre-Ink engine (alias
|
|
30
|
+
* resolution, layout resolution, data binding, form mismatch, invalid-enum
|
|
31
|
+
* notice, empty-inline notice, never-throw component containment, script-
|
|
32
|
+
* fence markers, link and image handling) — see AGENTS.md's architecture
|
|
33
|
+
* rules and the batch-10 brief. What changed is the OUTPUT shape: instead of
|
|
34
|
+
* building one big plain string, the walk builds a tree of Ink elements
|
|
35
|
+
* (`Box`/`Text`) and hands it to `ink-string.ts` (the render-once path) or
|
|
36
|
+
* returns it directly (the new live-viewer export, `buildMarkElement`).
|
|
37
|
+
*
|
|
38
|
+
* Two rendering "modes" coexist deliberately, and BOTH still exist in the
|
|
39
|
+
* pre-Ink engine's shape:
|
|
24
40
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
41
|
+
* - STRING mode (`render*String` functions below): the pre-Ink algorithm,
|
|
42
|
+
* almost verbatim, still building one flattened, pre-wrapped, pre-aligned
|
|
43
|
+
* string. This is what every standard component's OWN body still uses —
|
|
44
|
+
* `card` frames a string, `callout` bars a string, `details` indents a
|
|
45
|
+
* string — because these components draw FIXED-WIDTH GLYPHS that must be
|
|
46
|
+
* correct the moment the string is built; Ink lays a tree out AFTER it is
|
|
47
|
+
* built, so it cannot reflow a box-drawing frame or re-justify per-line
|
|
48
|
+
* text alignment the way `text=center` needs (Ink has no per-line
|
|
49
|
+
* paragraph-justify primitive). String mode is also the ONLY mode inline
|
|
50
|
+
* content ever uses, at any nesting depth: an inline directive's body is
|
|
51
|
+
* phrasing content by definition (docs/spec.md), never a layout the reader
|
|
52
|
+
* would want Ink to lay out as boxes.
|
|
53
|
+
* - ELEMENT mode (`render*Element` functions below): real Ink elements,
|
|
54
|
+
* used for the document's own block sequencing (so ordinary prose wraps
|
|
55
|
+
* through Ink's own layout instead of this engine's manual `wrap()`) and
|
|
56
|
+
* for the three block directives that genuinely need Ink's real layout or
|
|
57
|
+
* real component state: `row` (a flex `Box` row instead of the deleted
|
|
58
|
+
* `columns()` string helper), `tabs` and `details` (real React state, for
|
|
59
|
+
* `ctx.interactive`'s keyboard-driven switching/toggling). A `row`/`tabs`/
|
|
60
|
+
* `details` reached through a STRING-mode ancestor (nested inside `card`,
|
|
61
|
+
* `callout`, `figure`, a standalone `cell`, or a GFM table cell) falls
|
|
62
|
+
* back to a flattened STRING rendering of itself with no interactivity —
|
|
63
|
+
* an accepted, documented boundary: a self-drawing container's body is, by
|
|
64
|
+
* its own nature, a fixed string, so nothing nested inside one can be
|
|
65
|
+
* live. See this file's `renderRowString`/`renderTabsString` for that
|
|
66
|
+
* fallback.
|
|
32
67
|
*
|
|
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.
|
|
68
|
+
* Interactive components have no meaning for a NON-interactive render at
|
|
69
|
+
* all: `@markii/stdlib`'s `INTERACTIVE_ATTRIBUTE` marks an element a HOST
|
|
70
|
+
* should treat as a live control (a script-marker `<summary>`, say); this
|
|
71
|
+
* engine never reads that attribute and never emits a marker for it.
|
|
38
72
|
*/
|
|
39
73
|
/** The hast tag name `@markii/core` marks every directive with. */
|
|
40
74
|
const DIRECTIVE_TAG = 'mk-directive';
|
|
@@ -46,6 +80,10 @@ const VALUE_DIRECTIVE_NAME = 'value';
|
|
|
46
80
|
const DATA_ATTRIBUTE_KEY = 'data';
|
|
47
81
|
/** The attribute `@markii/core` preserves a code fence's raw `meta` string onto. */
|
|
48
82
|
const CODE_META_ATTR = 'data-mk-meta';
|
|
83
|
+
/** The three block directive names given real Ink layout/state (see this file's top comment). */
|
|
84
|
+
const ROW_NAME = 'row';
|
|
85
|
+
const TABS_NAME = 'tabs';
|
|
86
|
+
const DETAILS_NAME = 'details';
|
|
49
87
|
/** Block-level hast tags this engine recognizes; anything else inside a directive's children is treated as inline content. */
|
|
50
88
|
const BLOCK_TAGS = new Set([
|
|
51
89
|
'p',
|
|
@@ -74,6 +112,30 @@ function nodeOrRootToHast(node) {
|
|
|
74
112
|
}
|
|
75
113
|
return { type: 'root', children };
|
|
76
114
|
}
|
|
115
|
+
function nextFocusId(wctx) {
|
|
116
|
+
const id = wctx.focusCounter.current;
|
|
117
|
+
wctx.focusCounter.current += 1;
|
|
118
|
+
return id;
|
|
119
|
+
}
|
|
120
|
+
/** 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. */
|
|
121
|
+
function recordFocusable(wctx, descriptor) {
|
|
122
|
+
wctx.focusables?.push(descriptor);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The ONE place a STRING built by this file's string engine becomes an Ink
|
|
126
|
+
* `<Text>` leaf. Every such string passes through `ansi.js`'s
|
|
127
|
+
* `carrySgrAcrossLines` first — see that function's doc comment for why:
|
|
128
|
+
* Ink resets SGR state at every embedded line boundary within one `<Text>`
|
|
129
|
+
* value, so a multi-line styled string (a dim-wrapped frame, a wrapped
|
|
130
|
+
* bold heading, a paragraph whose bold span straddled a wrap point) must
|
|
131
|
+
* have every physical line already self-contained before Ink ever sees it.
|
|
132
|
+
* This is the SYSTEMATIC fix (a single choke point) rather than a per-
|
|
133
|
+
* call-site one: every `<Text>` in this file that renders a plain string
|
|
134
|
+
* uses this, never a bare `<Text>{str}</Text>`.
|
|
135
|
+
*/
|
|
136
|
+
function styledText(text) {
|
|
137
|
+
return _jsx(Text, { children: carrySgrAcrossLines(text) });
|
|
138
|
+
}
|
|
77
139
|
/** A directive property read back as a string, or `undefined` if absent or non-string. */
|
|
78
140
|
function stringProperty(element, name) {
|
|
79
141
|
const value = element.properties?.[name];
|
|
@@ -141,30 +203,9 @@ function fallbackLabel(name, inline, reason) {
|
|
|
141
203
|
}
|
|
142
204
|
return `unknown component ${code}`;
|
|
143
205
|
}
|
|
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
206
|
/** 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
207
|
function unknownDirective(name, inline, childrenText, reason, width, color) {
|
|
162
208
|
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
209
|
if (inline) {
|
|
169
210
|
return childrenText === ''
|
|
170
211
|
? dim(label, color)
|
|
@@ -174,24 +215,23 @@ function unknownDirective(name, inline, childrenText, reason, width, color) {
|
|
|
174
215
|
const wrappedChildren = childrenText
|
|
175
216
|
? childrenText
|
|
176
217
|
.split('\n')
|
|
177
|
-
.flatMap((line) =>
|
|
218
|
+
.flatMap((line) => wrapText(line, innerWidth))
|
|
178
219
|
.join('\n')
|
|
179
220
|
: '';
|
|
180
221
|
const body = wrappedChildren ? `${label}\n\n${wrappedChildren}` : label;
|
|
181
|
-
return dim(
|
|
222
|
+
return dim(frameBlock(body, { style: 'dashed', width }), color);
|
|
182
223
|
}
|
|
183
224
|
/** A registered component that threw while rendering, contained to a neutral box rather than failing the whole document. Never dumps the error text. */
|
|
184
225
|
function componentError(name, inline, childrenText, width, color) {
|
|
185
226
|
const label = `component \`${stripControlCharacters(name)}\` failed to render`;
|
|
186
|
-
// Same boundary rule as `unknownDirective`'s inline branch above.
|
|
187
227
|
if (inline) {
|
|
188
228
|
return childrenText === ''
|
|
189
229
|
? dim(label, color)
|
|
190
230
|
: `${dim(`${label}:`, color)} ${childrenText}`;
|
|
191
231
|
}
|
|
192
|
-
return dim(
|
|
232
|
+
return dim(frameBlock(label, { style: 'dashed', width }), color);
|
|
193
233
|
}
|
|
194
|
-
/** Splits a `data=<name>` attribute off `attributes` and resolves it against `scope`. Never throws.
|
|
234
|
+
/** Splits a `data=<name>` attribute off `attributes` and resolves it against `scope`. Never throws. */
|
|
195
235
|
function resolveDataAttribute(attributes, scope) {
|
|
196
236
|
if (!Object.hasOwn(attributes, DATA_ATTRIBUTE_KEY))
|
|
197
237
|
return { attributes };
|
|
@@ -207,7 +247,7 @@ function resolveDataAttribute(attributes, scope) {
|
|
|
207
247
|
dataFailureKind: resolved.status === 'error' ? resolved.failureKind : undefined,
|
|
208
248
|
};
|
|
209
249
|
}
|
|
210
|
-
/** Whether one of `name`'s `@markii/stdlib` contract attributes with a closed `enum` is present with a value outside it.
|
|
250
|
+
/** Whether one of `name`'s `@markii/stdlib` contract attributes with a closed `enum` is present with a value outside it. */
|
|
211
251
|
function invalidEnumAttribute(name, attributes) {
|
|
212
252
|
const contract = getContract(name);
|
|
213
253
|
if (!contract)
|
|
@@ -231,14 +271,7 @@ function resolveValue(scope, name) {
|
|
|
231
271
|
return { value: undefined, status: 'missing' };
|
|
232
272
|
return resolveScopedPath(scope, trimmed);
|
|
233
273
|
}
|
|
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
|
-
*/
|
|
274
|
+
/** Builds the missing/stale/resolved marker text for a resolved value name. */
|
|
242
275
|
function renderValueMarker(name, resolved, format, decimals, color, theme) {
|
|
243
276
|
if (resolved.status === 'missing' || resolved.status === 'error') {
|
|
244
277
|
const failureKind = resolved.status === 'error' ? resolved.failureKind : undefined;
|
|
@@ -256,24 +289,20 @@ function renderValueMarker(name, resolved, format, decimals, color, theme) {
|
|
|
256
289
|
return suffix ? `${styled}${dim(suffix, color)}` : styled;
|
|
257
290
|
}
|
|
258
291
|
/** Builds the `AnsiRenderContext` handed to one component invocation. */
|
|
259
|
-
function buildComponentContext(width, indent, wctx, binding, layout) {
|
|
292
|
+
function buildComponentContext(width, indent, wctx, binding, layout, focusId) {
|
|
260
293
|
const { color, theme, scope } = wctx;
|
|
261
294
|
const ctx = {
|
|
262
295
|
width,
|
|
263
296
|
indent,
|
|
264
297
|
color,
|
|
265
298
|
theme,
|
|
299
|
+
interactive: wctx.interactive,
|
|
266
300
|
style: (text, token) => style(text, token, theme, color),
|
|
267
301
|
bold: (text) => bold(text, color),
|
|
268
302
|
dim: (text) => dim(text, color),
|
|
269
303
|
italic: (text) => italic(text, color),
|
|
270
304
|
underline: (text) => underline(text, color),
|
|
271
305
|
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
306
|
text: (value) => stripControlCharacters(value),
|
|
278
307
|
resolve: (name) => resolveValue(scope, name),
|
|
279
308
|
valueMarker: (name, format, decimals) => {
|
|
@@ -287,6 +316,8 @@ function buildComponentContext(width, indent, wctx, binding, layout) {
|
|
|
287
316
|
resolveHref: wctx.resolveHref,
|
|
288
317
|
onDiagnostic: wctx.onDiagnostic,
|
|
289
318
|
};
|
|
319
|
+
if (focusId !== undefined)
|
|
320
|
+
ctx.focusId = focusId;
|
|
290
321
|
if ('data' in binding) {
|
|
291
322
|
ctx.data = binding.data;
|
|
292
323
|
ctx.dataStatus = binding.dataStatus;
|
|
@@ -297,42 +328,54 @@ function buildComponentContext(width, indent, wctx, binding, layout) {
|
|
|
297
328
|
ctx.layout = layout;
|
|
298
329
|
return ctx;
|
|
299
330
|
}
|
|
300
|
-
|
|
301
|
-
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// STRING MODE — the pre-Ink algorithm. Used for every inline position (at
|
|
333
|
+
// any depth) and for a directive's own body once it is not one of the three
|
|
334
|
+
// Ink-element-aware directives (`row`/`tabs`/`details`).
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
function safeChildrenText(text) {
|
|
337
|
+
return text ?? '';
|
|
338
|
+
}
|
|
339
|
+
function isBlockLevelChildren(children) {
|
|
340
|
+
for (const child of children) {
|
|
341
|
+
if (child.type !== 'element')
|
|
342
|
+
continue;
|
|
343
|
+
if (BLOCK_TAGS.has(child.tagName))
|
|
344
|
+
return true;
|
|
345
|
+
if (child.tagName === DIRECTIVE_TAG) {
|
|
346
|
+
const kind = stringProperty(child, 'data-mk-kind');
|
|
347
|
+
if (kind !== TEXT_DIRECTIVE_KIND)
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
function renderDirectiveContentString(name, kind, isBlock, attributes, childrenText, width, indent, wctx, layoutForComponent) {
|
|
302
354
|
const inline = !isBlock;
|
|
303
355
|
const entry = Object.hasOwn(wctx.registry, name)
|
|
304
356
|
? wctx.registry[name]
|
|
305
357
|
: undefined;
|
|
306
358
|
const component = readRegistryComponent(entry);
|
|
307
359
|
if (!entry || component == null) {
|
|
308
|
-
return unknownDirective(name || '(unnamed)', inline, safeChildrenText(
|
|
360
|
+
return unknownDirective(name || '(unnamed)', inline, safeChildrenText(childrenText), 'unregistered', width, wctx.color);
|
|
309
361
|
}
|
|
310
362
|
if (isFormMismatch(entry, kind)) {
|
|
311
|
-
return unknownDirective(name || '(unnamed)', inline, safeChildrenText(
|
|
363
|
+
return unknownDirective(name || '(unnamed)', inline, safeChildrenText(childrenText), 'form-mismatch', width, wctx.color);
|
|
312
364
|
}
|
|
313
365
|
const binding = resolveDataAttribute(attributes, wctx.scope);
|
|
314
|
-
const ctx = buildComponentContext(width, indent, wctx, binding, layoutForComponent);
|
|
366
|
+
const ctx = buildComponentContext(width, indent, wctx, binding, layoutForComponent, undefined);
|
|
315
367
|
let rendered;
|
|
316
368
|
try {
|
|
317
|
-
|
|
369
|
+
const result = component({
|
|
370
|
+
attributes: binding.attributes,
|
|
371
|
+
children: childrenText,
|
|
372
|
+
ctx,
|
|
373
|
+
});
|
|
374
|
+
rendered = typeof result === 'string' ? result : String(result ?? '');
|
|
318
375
|
}
|
|
319
376
|
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.
|
|
377
|
+
return componentError(name || '(unnamed)', inline, safeChildrenText(childrenText), width, wctx.color);
|
|
378
|
+
}
|
|
336
379
|
const markerSeparator = inline ? ' ' : '\n';
|
|
337
380
|
const invalidEnum = invalidEnumAttribute(name, binding.attributes);
|
|
338
381
|
if (invalidEnum) {
|
|
@@ -345,221 +388,129 @@ function renderDirectiveContent(name, kind, isBlock, attributes, children, width
|
|
|
345
388
|
const label = invalidAttributeValueLabel(name, invalidEnum.attribute);
|
|
346
389
|
rendered = `${rendered}${markerSeparator}${dim(`[${label}]`, wctx.color)}`;
|
|
347
390
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (isRegisteredInline(entry) && safeChildrenText(children).trim() === '') {
|
|
391
|
+
if (isRegisteredInline(entry) &&
|
|
392
|
+
safeChildrenText(childrenText).trim() === '') {
|
|
351
393
|
rendered = `${rendered}${dim(` (${emptyInlineTitle(name)})`, wctx.color)}`;
|
|
352
394
|
}
|
|
353
395
|
return rendered;
|
|
354
396
|
}
|
|
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) {
|
|
397
|
+
function renderDirectiveChildrenString(children, width, indent, wctx) {
|
|
371
398
|
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
|
-
});
|
|
399
|
+
return renderBlocksString(children, width, indent, wctx);
|
|
403
400
|
}
|
|
404
|
-
return
|
|
401
|
+
return renderInlineChildrenString(children, width, indent, wctx);
|
|
405
402
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
: [
|
|
422
|
-
{
|
|
423
|
-
render: (options) => renderInlineChildren(element.children, options?.width ?? width, options?.indent ?? indent, wctx),
|
|
424
|
-
},
|
|
425
|
-
],
|
|
426
|
-
});
|
|
427
|
-
return fn;
|
|
428
|
-
}
|
|
429
|
-
/** Turns one `<mk-directive>` element into its rendered text, including the layout adjustment for a block directive whose layout it does not own. */
|
|
430
|
-
function renderDirective(element, width, indent, wctx) {
|
|
431
|
-
const written = stringProperty(element, 'data-mk-name') ?? '';
|
|
432
|
-
const kind = stringProperty(element, 'data-mk-kind');
|
|
433
|
-
const rawAttributes = parseAttributes(stringProperty(element, 'data-mk-attrs'));
|
|
434
|
-
const isBlock = kind !== TEXT_DIRECTIVE_KIND;
|
|
435
|
-
if (written === VALUE_DIRECTIVE_NAME) {
|
|
436
|
-
const plainLabel = extractPlainText(element.children);
|
|
437
|
-
const resolved = resolveValue(wctx.scope, plainLabel);
|
|
438
|
-
return renderValueMarker(plainLabel.trim(), resolved, rawAttributes.format ?? undefined, rawAttributes.decimals ?? undefined, wctx.color, wctx.theme);
|
|
403
|
+
function renderRowString(element, attributes, width, indent, wctx) {
|
|
404
|
+
const cellNodes = element.children.filter((node) => !(node.type === 'text' && node.value.trim() === ''));
|
|
405
|
+
const layout = resolveRowLayout(attributes, cellNodes.length, width);
|
|
406
|
+
if (cellNodes.length === 0)
|
|
407
|
+
return '';
|
|
408
|
+
const place = (text, w) => layout.align === 'left'
|
|
409
|
+
? text
|
|
410
|
+
: text
|
|
411
|
+
.split('\n')
|
|
412
|
+
.map((line) => padText(line, w, layout.align))
|
|
413
|
+
.join('\n');
|
|
414
|
+
if (layout.stacked) {
|
|
415
|
+
return cellNodes
|
|
416
|
+
.map((node) => place(renderBlockString(node, width, indent, wctx), width))
|
|
417
|
+
.join('\n\n');
|
|
439
418
|
}
|
|
440
|
-
const
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
419
|
+
const placedCells = cellNodes.map((node) => place(renderBlockString(node, layout.colWidth, indent, wctx), layout.colWidth));
|
|
420
|
+
const gridRows = [];
|
|
421
|
+
for (let index = 0; index < placedCells.length; index += layout.columnCount) {
|
|
422
|
+
const rowCells = placedCells.slice(index, index + layout.columnCount);
|
|
423
|
+
const lineArrays = rowCells.map((cell) => cell.split('\n'));
|
|
424
|
+
const rowHeight = Math.max(0, ...lineArrays.map((lines) => lines.length));
|
|
425
|
+
const lines = [];
|
|
426
|
+
for (let line = 0; line < rowHeight; line += 1) {
|
|
427
|
+
lines.push(lineArrays
|
|
428
|
+
.map((cellLines) => padText(cellLines[line] ?? '', layout.colWidth, 'left'))
|
|
429
|
+
.join(' ')
|
|
430
|
+
.replace(/[ ]+$/, ''));
|
|
431
|
+
}
|
|
432
|
+
gridRows.push(lines.join('\n'));
|
|
453
433
|
}
|
|
454
|
-
return
|
|
434
|
+
return gridRows.join('\n\n');
|
|
455
435
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
436
|
+
const DEFAULT_TAB_LABEL = 'Tab';
|
|
437
|
+
/** 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. */
|
|
438
|
+
function extractTabPanels(element, width, indent, wctx) {
|
|
439
|
+
const panels = [];
|
|
440
|
+
for (const node of element.children) {
|
|
441
|
+
if (node.type === 'text' && node.value.trim() === '')
|
|
442
|
+
continue;
|
|
443
|
+
if (node.type !== 'element')
|
|
444
|
+
continue;
|
|
445
|
+
let label = DEFAULT_TAB_LABEL;
|
|
446
|
+
let isTab = false;
|
|
447
|
+
if (node.tagName === DIRECTIVE_TAG) {
|
|
448
|
+
const written = stringProperty(node, 'data-mk-name') ?? '';
|
|
449
|
+
const attrs = parseAttributes(stringProperty(node, 'data-mk-attrs'));
|
|
450
|
+
const { name } = resolveDirectiveAlias(wctx.registry, written, attrs);
|
|
451
|
+
if (name === 'tab') {
|
|
452
|
+
label = attrs.label ?? DEFAULT_TAB_LABEL;
|
|
453
|
+
isTab = true;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// A `tab` child's own label is drawn ONCE, by the heading `tabs`
|
|
457
|
+
// itself builds below — rendering the whole `tab` directive here (as
|
|
458
|
+
// for any other block) would ALSO invoke `tab.ts`'s own component,
|
|
459
|
+
// which draws its OWN bold heading from the same attribute, printing
|
|
460
|
+
// the label twice. Render only the tab's INNER children instead. A
|
|
461
|
+
// non-`tab` block (an unlabeled panel) still renders as a whole block,
|
|
462
|
+
// unchanged.
|
|
463
|
+
const body = isTab
|
|
464
|
+
? renderDirectiveChildrenString(node.children, width, indent, wctx)
|
|
465
|
+
: renderBlockString(node, width, indent, wctx);
|
|
466
|
+
panels.push({ label, body });
|
|
464
467
|
}
|
|
465
|
-
return
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* A link renders as an OSC 8 hyperlink ONLY when: the color level allows
|
|
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})`;
|
|
468
|
+
return panels;
|
|
492
469
|
}
|
|
493
|
-
|
|
494
|
-
|
|
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) {
|
|
470
|
+
const ACTIVE_MARKER = ' (active)';
|
|
471
|
+
function renderBlockString(node, width, indent, wctx) {
|
|
502
472
|
if (node.type === 'text')
|
|
503
473
|
return stripControlCharacters(node.value);
|
|
504
474
|
if (node.type !== 'element')
|
|
505
475
|
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);
|
|
476
|
+
if (node.tagName === DIRECTIVE_TAG) {
|
|
477
|
+
return renderDirectiveString(node, width, indent, wctx);
|
|
533
478
|
}
|
|
479
|
+
if (/^h[1-6]$/.test(node.tagName)) {
|
|
480
|
+
const level = Number(node.tagName.slice(1));
|
|
481
|
+
const text = renderInlineChildrenString(node.children, width, indent, wctx);
|
|
482
|
+
const styled = level === 1
|
|
483
|
+
? underline(bold(text, wctx.color), wctx.color)
|
|
484
|
+
: bold(text, wctx.color);
|
|
485
|
+
return wrapText(styled, width).join('\n');
|
|
486
|
+
}
|
|
487
|
+
if (node.tagName === 'p') {
|
|
488
|
+
return wrapText(renderInlineChildrenString(node.children, width, indent, wctx), width).join('\n');
|
|
489
|
+
}
|
|
490
|
+
if (node.tagName === 'blockquote') {
|
|
491
|
+
const innerWidth = Math.max(1, width - 2);
|
|
492
|
+
const inner = renderBlocksString(node.children, innerWidth, `${indent}│ `, wctx);
|
|
493
|
+
const prefix = dim('│ ', wctx.color);
|
|
494
|
+
return inner
|
|
495
|
+
.split('\n')
|
|
496
|
+
.map((line) => `${prefix}${line}`)
|
|
497
|
+
.join('\n');
|
|
498
|
+
}
|
|
499
|
+
if (node.tagName === 'ul' || node.tagName === 'ol') {
|
|
500
|
+
return renderListString(node, width, indent, wctx);
|
|
501
|
+
}
|
|
502
|
+
if (node.tagName === 'hr')
|
|
503
|
+
return dim(ruleLine(width), wctx.color);
|
|
504
|
+
if (node.tagName === 'table')
|
|
505
|
+
return renderGfmTableString(node, width, wctx);
|
|
506
|
+
if (node.tagName === 'pre') {
|
|
507
|
+
const marker = renderScriptMarker(node, wctx.color);
|
|
508
|
+
if (marker !== undefined)
|
|
509
|
+
return marker;
|
|
510
|
+
return renderCodeBlock(node, wctx.color);
|
|
511
|
+
}
|
|
512
|
+
return wrapText(renderInlineChildrenString(node.children ?? [], width, indent, wctx), width).join('\n');
|
|
534
513
|
}
|
|
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
514
|
function taskState(li) {
|
|
564
515
|
for (const child of li.children) {
|
|
565
516
|
if (child.type !== 'element')
|
|
@@ -567,13 +518,11 @@ function taskState(li) {
|
|
|
567
518
|
if (child.tagName === 'input') {
|
|
568
519
|
return { checked: Boolean(child.properties?.checked) };
|
|
569
520
|
}
|
|
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
521
|
return undefined;
|
|
573
522
|
}
|
|
574
523
|
return undefined;
|
|
575
524
|
}
|
|
576
|
-
function
|
|
525
|
+
function renderListItemString(li, marker, width, indent, wctx) {
|
|
577
526
|
const task = taskState(li);
|
|
578
527
|
const contentChildren = li.children.filter((child) => !(task && child.type === 'element' && child.tagName === 'input'));
|
|
579
528
|
const nestedLists = [];
|
|
@@ -588,11 +537,11 @@ function renderListItem(li, marker, width, indent, wctx) {
|
|
|
588
537
|
}
|
|
589
538
|
}
|
|
590
539
|
const prefix = `${marker}${task ? (task.checked ? '[x] ' : '[ ] ') : ''}`;
|
|
591
|
-
const prefixWidth =
|
|
540
|
+
const prefixWidth = measureWidth(prefix);
|
|
592
541
|
const bodyWidth = Math.max(1, width - prefixWidth);
|
|
593
542
|
const bodyText = isBlockLevelChildren(restChildren)
|
|
594
|
-
?
|
|
595
|
-
:
|
|
543
|
+
? renderBlocksString(restChildren, bodyWidth, `${indent}${' '.repeat(prefixWidth)}`, wctx)
|
|
544
|
+
: wrapText(renderInlineChildrenString(restChildren, bodyWidth, indent, wctx), bodyWidth).join('\n');
|
|
596
545
|
const bodyLines = bodyText === '' ? [''] : bodyText.split('\n');
|
|
597
546
|
const firstLine = `${prefix}${bodyLines[0] ?? ''}`;
|
|
598
547
|
const restLines = bodyLines
|
|
@@ -600,7 +549,7 @@ function renderListItem(li, marker, width, indent, wctx) {
|
|
|
600
549
|
.map((line) => `${' '.repeat(prefixWidth)}${line}`);
|
|
601
550
|
let result = [firstLine, ...restLines].join('\n');
|
|
602
551
|
for (const nested of nestedLists) {
|
|
603
|
-
const nestedText =
|
|
552
|
+
const nestedText = renderBlockString(nested, Math.max(1, width - 2), `${indent} `, wctx);
|
|
604
553
|
result += `\n${nestedText
|
|
605
554
|
.split('\n')
|
|
606
555
|
.map((line) => ` ${line}`)
|
|
@@ -608,7 +557,7 @@ function renderListItem(li, marker, width, indent, wctx) {
|
|
|
608
557
|
}
|
|
609
558
|
return result;
|
|
610
559
|
}
|
|
611
|
-
function
|
|
560
|
+
function renderListString(el, width, indent, wctx) {
|
|
612
561
|
const ordered = el.tagName === 'ol';
|
|
613
562
|
const startRaw = el.properties?.start;
|
|
614
563
|
const start = ordered && typeof startRaw === 'number' ? startRaw : 1;
|
|
@@ -616,7 +565,7 @@ function renderList(el, width, indent, wctx) {
|
|
|
616
565
|
return items
|
|
617
566
|
.map((li, index) => {
|
|
618
567
|
const marker = ordered ? `${start + index}. ` : '- ';
|
|
619
|
-
return
|
|
568
|
+
return renderListItemString(li, marker, width, indent, wctx);
|
|
620
569
|
})
|
|
621
570
|
.join('\n');
|
|
622
571
|
}
|
|
@@ -647,14 +596,6 @@ function getCodeText(codeNode) {
|
|
|
647
596
|
}
|
|
648
597
|
return text.endsWith('\n') ? text.slice(0, -1) : text;
|
|
649
598
|
}
|
|
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
599
|
function renderScriptMarker(node, color) {
|
|
659
600
|
try {
|
|
660
601
|
const codeNode = findChildElement(node, 'code');
|
|
@@ -687,18 +628,7 @@ function renderCodeBlock(node, color) {
|
|
|
687
628
|
lines.push(dim(` ${line}`, color));
|
|
688
629
|
return lines.join('\n');
|
|
689
630
|
}
|
|
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) {
|
|
631
|
+
function renderGfmTableString(tableEl, width, wctx) {
|
|
702
632
|
const headerCells = [];
|
|
703
633
|
const bodyRows = [];
|
|
704
634
|
let sawHeader = false;
|
|
@@ -708,7 +638,7 @@ function renderTable(tableEl, width, wctx) {
|
|
|
708
638
|
continue;
|
|
709
639
|
if (cell.tagName !== 'td' && cell.tagName !== 'th')
|
|
710
640
|
continue;
|
|
711
|
-
into.push(
|
|
641
|
+
into.push(renderInlineChildrenString(cell.children, width, '', wctx));
|
|
712
642
|
}
|
|
713
643
|
};
|
|
714
644
|
for (const section of tableEl.children) {
|
|
@@ -733,51 +663,373 @@ function renderTable(tableEl, width, wctx) {
|
|
|
733
663
|
}
|
|
734
664
|
return drawTableGrid(headerCells.length > 0 ? headerCells : undefined, bodyRows, width, (text) => bold(text, wctx.color));
|
|
735
665
|
}
|
|
736
|
-
function
|
|
666
|
+
function getPlainText(node) {
|
|
667
|
+
let text = '';
|
|
668
|
+
for (const child of node.children) {
|
|
669
|
+
if (child.type === 'text')
|
|
670
|
+
text += stripControlCharacters(child.value);
|
|
671
|
+
else if (child.type === 'element')
|
|
672
|
+
text += getPlainText(child);
|
|
673
|
+
}
|
|
674
|
+
return text;
|
|
675
|
+
}
|
|
676
|
+
function renderLink(node, width, indent, wctx) {
|
|
677
|
+
const hrefRaw = typeof node.properties?.href === 'string' ? node.properties.href : '';
|
|
678
|
+
const resolvedHref = resolveHrefAttribute(hrefRaw, wctx.resolveHref);
|
|
679
|
+
const text = renderInlineChildrenString(node.children, width, indent, wctx);
|
|
680
|
+
const sanitizedHref = stripControlCharacters(resolvedHref);
|
|
681
|
+
const canHyperlink = wctx.color !== 'none' &&
|
|
682
|
+
resolvedHref.trim() !== '' &&
|
|
683
|
+
isSafeUrl(resolvedHref) &&
|
|
684
|
+
sanitizeUrlText(resolvedHref) === resolvedHref;
|
|
685
|
+
if (canHyperlink)
|
|
686
|
+
return hyperlink(text, resolvedHref, wctx.color);
|
|
687
|
+
const plainText = text.replace(/\x1b\[[0-9;]*m|\x1b\]8;;[^\x07\x1b]*\x07/g, '');
|
|
688
|
+
if (plainText === sanitizedHref)
|
|
689
|
+
return sanitizedHref;
|
|
690
|
+
return `${text} (${sanitizedHref})`;
|
|
691
|
+
}
|
|
692
|
+
function renderImage(node, wctx) {
|
|
693
|
+
const altRaw = typeof node.properties?.alt === 'string' ? node.properties.alt : '';
|
|
694
|
+
const alt = stripControlCharacters(altRaw);
|
|
695
|
+
const srcRaw = typeof node.properties?.src === 'string' ? node.properties.src : '';
|
|
696
|
+
const resolvedSrc = resolveImageAttribute(srcRaw, wctx.resolveImageSrc);
|
|
697
|
+
const src = stripControlCharacters(resolvedSrc);
|
|
698
|
+
return `[${alt}] (${src})`;
|
|
699
|
+
}
|
|
700
|
+
function renderInlineNodeString(node, width, indent, wctx) {
|
|
737
701
|
if (node.type === 'text')
|
|
738
702
|
return stripControlCharacters(node.value);
|
|
739
703
|
if (node.type !== 'element')
|
|
740
704
|
return '';
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
705
|
+
switch (node.tagName) {
|
|
706
|
+
case DIRECTIVE_TAG:
|
|
707
|
+
return renderDirectiveString(node, width, indent, wctx);
|
|
708
|
+
case 'strong':
|
|
709
|
+
return bold(renderInlineChildrenString(node.children, width, indent, wctx), wctx.color);
|
|
710
|
+
case 'em':
|
|
711
|
+
return italic(renderInlineChildrenString(node.children, width, indent, wctx), wctx.color);
|
|
712
|
+
case 'del':
|
|
713
|
+
return dim(`~${renderInlineChildrenString(node.children, width, indent, wctx)}~`, wctx.color);
|
|
714
|
+
case 'code': {
|
|
715
|
+
const inner = getPlainText(node);
|
|
716
|
+
return wctx.color === 'none'
|
|
717
|
+
? dim(inner, wctx.color)
|
|
718
|
+
: inverse(inner, wctx.color);
|
|
719
|
+
}
|
|
720
|
+
case 'a':
|
|
721
|
+
return renderLink(node, width, indent, wctx);
|
|
722
|
+
case 'img':
|
|
723
|
+
return renderImage(node, wctx);
|
|
724
|
+
case 'br':
|
|
725
|
+
return '\n';
|
|
726
|
+
case 'input':
|
|
727
|
+
return '';
|
|
728
|
+
default:
|
|
729
|
+
return renderInlineChildrenString(node.children ?? [], width, indent, wctx);
|
|
760
730
|
}
|
|
761
|
-
return wrap(renderInlineChildren(node.children ?? [], width, indent, wctx), width).join('\n');
|
|
762
731
|
}
|
|
763
|
-
function
|
|
732
|
+
function renderInlineChildrenString(children, width, indent, wctx) {
|
|
733
|
+
let result = '';
|
|
734
|
+
for (const child of children)
|
|
735
|
+
result += renderInlineNodeString(child, width, indent, wctx);
|
|
736
|
+
return result;
|
|
737
|
+
}
|
|
738
|
+
function renderBlocksString(nodes, width, indent, wctx) {
|
|
764
739
|
const blocks = [];
|
|
765
740
|
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.
|
|
741
|
+
const rendered = renderBlockString(node, width, indent, wctx);
|
|
776
742
|
if (rendered.trim() !== '')
|
|
777
743
|
blocks.push(rendered);
|
|
778
744
|
}
|
|
779
745
|
return blocks.join('\n\n');
|
|
780
746
|
}
|
|
747
|
+
function renderDirectiveString(element, width, indent, wctx) {
|
|
748
|
+
const written = stringProperty(element, 'data-mk-name') ?? '';
|
|
749
|
+
const kind = stringProperty(element, 'data-mk-kind');
|
|
750
|
+
const rawAttributes = parseAttributes(stringProperty(element, 'data-mk-attrs'));
|
|
751
|
+
const isBlock = kind !== TEXT_DIRECTIVE_KIND;
|
|
752
|
+
if (written === VALUE_DIRECTIVE_NAME) {
|
|
753
|
+
const plainLabel = extractPlainText(element.children);
|
|
754
|
+
const resolved = resolveValue(wctx.scope, plainLabel);
|
|
755
|
+
return renderValueMarker(plainLabel.trim(), resolved, rawAttributes.format ?? undefined, rawAttributes.decimals ?? undefined, wctx.color, wctx.theme);
|
|
756
|
+
}
|
|
757
|
+
const { name, attributes: aliased } = resolveDirectiveAlias(wctx.registry, written, rawAttributes);
|
|
758
|
+
// A `row`/`tabs` reached through STRING mode (nested inside a self-drawing
|
|
759
|
+
// container's own body) falls back to a flattened rendering with no real
|
|
760
|
+
// flex layout / interactivity — see this file's top comment.
|
|
761
|
+
if (isBlock && name === ROW_NAME && hasRealComponent(wctx.registry, name)) {
|
|
762
|
+
return renderRowString(element, aliased, width, indent, wctx);
|
|
763
|
+
}
|
|
764
|
+
if (isBlock && name === TABS_NAME && hasRealComponent(wctx.registry, name)) {
|
|
765
|
+
const panels = extractTabPanels(element, width, indent, wctx);
|
|
766
|
+
return panels
|
|
767
|
+
.map((panel, index) => {
|
|
768
|
+
const heading = bold(`${panel.label}${index === 0 ? ACTIVE_MARKER : ''}`, wctx.color);
|
|
769
|
+
return panel.body ? `${heading}\n${panel.body}` : heading;
|
|
770
|
+
})
|
|
771
|
+
.join('\n\n');
|
|
772
|
+
}
|
|
773
|
+
const ownedAxis = registryLayoutAxis(wctx.registry, name);
|
|
774
|
+
const { attributes, resolved: layoutResolved } = resolveLayoutAttributes(aliased, isBlock ? ownedAxis : undefined);
|
|
775
|
+
const isLayoutScope = ownedAxis !== undefined && isBlock;
|
|
776
|
+
const isSelfLayout = isBlock && registrySelfLayout(wctx.registry, name);
|
|
777
|
+
const layoutForWidth = isLayoutScope || isSelfLayout ? layoutResolved : undefined;
|
|
778
|
+
// `card`/`callout`/`details` narrow their own body before drawing a frame
|
|
779
|
+
// or an indent around it. Each computes that inner width from ONLY its
|
|
780
|
+
// own attributes (never from the body), so the walk resolves it here,
|
|
781
|
+
// BEFORE building children, and builds children exactly ONCE at that
|
|
782
|
+
// final width — never at the outer width followed by a re-wrap. A
|
|
783
|
+
// re-wrap-afterward shipped as a real regression (post-phase-2 review):
|
|
784
|
+
// it corrupts anything self-drawing nested inside (a card's frame, a
|
|
785
|
+
// row's columns), since a word-based re-wrapper has no notion that a run
|
|
786
|
+
// of box-drawing characters is not prose. See `resolveCardInnerWidth`'s
|
|
787
|
+
// doc comment.
|
|
788
|
+
let childrenWidth = width;
|
|
789
|
+
if (isBlock && name === 'card') {
|
|
790
|
+
childrenWidth = resolveCardInnerWidth(attributes, layoutForWidth, width);
|
|
791
|
+
}
|
|
792
|
+
else if (isBlock && name === 'callout') {
|
|
793
|
+
childrenWidth = resolveCalloutInnerWidth(attributes, layoutForWidth, width);
|
|
794
|
+
}
|
|
795
|
+
else if (isBlock && name === DETAILS_NAME) {
|
|
796
|
+
childrenWidth = resolveDetailsInnerWidth(width);
|
|
797
|
+
}
|
|
798
|
+
const childrenText = renderDirectiveChildrenString(element.children, childrenWidth, indent, wctx);
|
|
799
|
+
const content = renderDirectiveContentString(name, kind, isBlock, attributes, childrenText, width, indent, wctx, layoutForWidth);
|
|
800
|
+
if (isBlock && layoutResolved && !isLayoutScope && !isSelfLayout) {
|
|
801
|
+
return applyLayout(content, layoutResolved, width);
|
|
802
|
+
}
|
|
803
|
+
return content;
|
|
804
|
+
}
|
|
805
|
+
function hasRealComponent(registry, name) {
|
|
806
|
+
return (Object.hasOwn(registry, name) &&
|
|
807
|
+
readRegistryComponent(registry[name]) != null);
|
|
808
|
+
}
|
|
809
|
+
// ---------------------------------------------------------------------------
|
|
810
|
+
// ELEMENT MODE — real Ink elements, used for block sequencing and for
|
|
811
|
+
// `row`/`tabs`/`details`. See this file's top comment.
|
|
812
|
+
// ---------------------------------------------------------------------------
|
|
813
|
+
/** 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. */
|
|
814
|
+
function applyLayoutElement(element, layout, width) {
|
|
815
|
+
if (!layout)
|
|
816
|
+
return element;
|
|
817
|
+
let target = width;
|
|
818
|
+
if (layout.width === 'narrow')
|
|
819
|
+
target = Math.max(20, Math.round(width / 2));
|
|
820
|
+
const align = layout.align;
|
|
821
|
+
const alignItems = align === 'center'
|
|
822
|
+
? 'center'
|
|
823
|
+
: align === 'right'
|
|
824
|
+
? 'flex-end'
|
|
825
|
+
: 'flex-start';
|
|
826
|
+
if (target === width && !align)
|
|
827
|
+
return element;
|
|
828
|
+
return (_jsx(Box, { width: width, flexDirection: "column", alignItems: alignItems, children: _jsx(Box, { width: target, flexDirection: "column", children: element }) }));
|
|
829
|
+
}
|
|
830
|
+
function invalidEnumMarkerElement(name, attributes, wctx) {
|
|
831
|
+
const invalidEnum = invalidEnumAttribute(name, attributes);
|
|
832
|
+
if (!invalidEnum)
|
|
833
|
+
return null;
|
|
834
|
+
reportDiagnostic(wctx.onDiagnostic, {
|
|
835
|
+
kind: 'invalid-attribute-value',
|
|
836
|
+
directive: name,
|
|
837
|
+
attribute: invalidEnum.attribute,
|
|
838
|
+
message: invalidAttributeValueTitle(name, invalidEnum.attribute, invalidEnum.value),
|
|
839
|
+
});
|
|
840
|
+
const label = invalidAttributeValueLabel(name, invalidEnum.attribute);
|
|
841
|
+
return styledText(dim(`[${label}]`, wctx.color));
|
|
842
|
+
}
|
|
843
|
+
function renderRowElement(element, attributes, width, indent, wctx) {
|
|
844
|
+
const cellNodes = element.children.filter((node) => !(node.type === 'text' && node.value.trim() === ''));
|
|
845
|
+
if (cellNodes.length === 0)
|
|
846
|
+
return null;
|
|
847
|
+
const layout = resolveRowLayout(attributes, cellNodes.length, width);
|
|
848
|
+
if (layout.stacked) {
|
|
849
|
+
const items = cellNodes
|
|
850
|
+
.map((node, index) => (_jsx(Box, { marginTop: index > 0 ? 1 : 0, flexDirection: "column", children: renderBlockElement(node, width, indent, wctx) }, index)))
|
|
851
|
+
.filter(Boolean);
|
|
852
|
+
return _jsx(Box, { flexDirection: "column", children: items });
|
|
853
|
+
}
|
|
854
|
+
const gutter = 1;
|
|
855
|
+
const rows = [];
|
|
856
|
+
for (let index = 0; index < cellNodes.length; index += layout.columnCount) {
|
|
857
|
+
const rowCells = cellNodes.slice(index, index + layout.columnCount);
|
|
858
|
+
rows.push(_jsx(Box, { flexDirection: "row", marginTop: index > 0 ? 1 : 0, children: rowCells.map((node, cellIndex) => (_jsx(Box, { width: layout.colWidth, marginRight: cellIndex < rowCells.length - 1 ? gutter : 0, flexDirection: "column", children: renderBlockElement(node, layout.colWidth, indent, wctx) }, cellIndex))) }, index));
|
|
859
|
+
}
|
|
860
|
+
return _jsx(Box, { flexDirection: "column", children: rows });
|
|
861
|
+
}
|
|
862
|
+
function renderTabsElement(element, width, indent, wctx) {
|
|
863
|
+
const panels = extractTabPanelsElement(element, width, indent, wctx);
|
|
864
|
+
if (panels.length === 0)
|
|
865
|
+
return null;
|
|
866
|
+
const focusId = wctx.interactive ? nextFocusId(wctx) : undefined;
|
|
867
|
+
if (focusId !== undefined) {
|
|
868
|
+
recordFocusable(wctx, {
|
|
869
|
+
kind: 'tabs',
|
|
870
|
+
label: panels[0]?.label ?? DEFAULT_TAB_LABEL,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
return (_jsx(InteractiveTabs, { panels: panels, color: wctx.color, interactive: wctx.interactive, focusId: focusId }));
|
|
874
|
+
}
|
|
875
|
+
function extractTabPanelsElement(element, width, indent, wctx) {
|
|
876
|
+
const panels = [];
|
|
877
|
+
for (const node of element.children) {
|
|
878
|
+
if (node.type === 'text' && node.value.trim() === '')
|
|
879
|
+
continue;
|
|
880
|
+
if (node.type !== 'element')
|
|
881
|
+
continue;
|
|
882
|
+
let label = DEFAULT_TAB_LABEL;
|
|
883
|
+
let isTab = false;
|
|
884
|
+
if (node.tagName === DIRECTIVE_TAG) {
|
|
885
|
+
const written = stringProperty(node, 'data-mk-name') ?? '';
|
|
886
|
+
const attrs = parseAttributes(stringProperty(node, 'data-mk-attrs'));
|
|
887
|
+
const { name } = resolveDirectiveAlias(wctx.registry, written, attrs);
|
|
888
|
+
if (name === 'tab') {
|
|
889
|
+
label = attrs.label ?? DEFAULT_TAB_LABEL;
|
|
890
|
+
isTab = true;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
// See `extractTabPanels`'s (string-mode) identical comment: rendering
|
|
894
|
+
// the whole `tab` directive here would ALSO invoke `tab.ts`'s own
|
|
895
|
+
// component, printing the label a second time.
|
|
896
|
+
const body = isTab
|
|
897
|
+
? renderBlocksElement(node.children, width, indent, wctx)
|
|
898
|
+
: renderBlockElement(node, width, indent, wctx);
|
|
899
|
+
panels.push({ label, body });
|
|
900
|
+
}
|
|
901
|
+
return panels;
|
|
902
|
+
}
|
|
903
|
+
function renderDetailsElement(element, attributes, width, indent, wctx) {
|
|
904
|
+
const title = attributes.title ?? 'Details';
|
|
905
|
+
const open = Object.hasOwn(attributes, 'open');
|
|
906
|
+
const innerWidth = resolveDetailsInnerWidth(width);
|
|
907
|
+
const body = renderBlocksElement(element.children, innerWidth, indent, wctx);
|
|
908
|
+
const focusId = wctx.interactive ? nextFocusId(wctx) : undefined;
|
|
909
|
+
if (focusId !== undefined) {
|
|
910
|
+
recordFocusable(wctx, {
|
|
911
|
+
kind: 'details',
|
|
912
|
+
label: stripControlCharacters(title),
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
return (_jsx(InteractiveDetails, { title: stripControlCharacters(title), body: body, defaultOpen: open, color: wctx.color, interactive: wctx.interactive, focusId: focusId }));
|
|
916
|
+
}
|
|
917
|
+
/** Renders one directive as a real Ink element (block position only). Dispatches `row`/`tabs`/`details` to their real builders; every other directive falls back to the string engine, wrapped in one `<Text>`. */
|
|
918
|
+
function renderDirectiveElement(element, width, indent, wctx) {
|
|
919
|
+
const written = stringProperty(element, 'data-mk-name') ?? '';
|
|
920
|
+
const kind = stringProperty(element, 'data-mk-kind');
|
|
921
|
+
const rawAttributes = parseAttributes(stringProperty(element, 'data-mk-attrs'));
|
|
922
|
+
const isBlock = kind !== TEXT_DIRECTIVE_KIND;
|
|
923
|
+
if (!isBlock) {
|
|
924
|
+
// A stray inline (text) directive at a block position (rare; hast
|
|
925
|
+
// structure keeps these inside a paragraph in practice) still renders,
|
|
926
|
+
// via the string engine.
|
|
927
|
+
return styledText(renderDirectiveString(element, width, indent, wctx));
|
|
928
|
+
}
|
|
929
|
+
const { name, attributes: aliased } = resolveDirectiveAlias(wctx.registry, written, rawAttributes);
|
|
930
|
+
const entry = Object.hasOwn(wctx.registry, name)
|
|
931
|
+
? wctx.registry[name]
|
|
932
|
+
: undefined;
|
|
933
|
+
const formMismatch = entry ? isFormMismatch(entry, kind) : false;
|
|
934
|
+
if (!formMismatch &&
|
|
935
|
+
(name === ROW_NAME || name === TABS_NAME || name === DETAILS_NAME) &&
|
|
936
|
+
hasRealComponent(wctx.registry, name)) {
|
|
937
|
+
// None of `row`/`tabs`/`details` own a layout axis (`registryLayoutAxis`
|
|
938
|
+
// is undefined for all three in the standard registry), so, unlike a
|
|
939
|
+
// layout-wrapper scope, a `width=`/`align=` the author wrote still needs
|
|
940
|
+
// the GENERIC post-render treatment — `applyLayoutElement` below, the
|
|
941
|
+
// JSX-based twin of `layout.ts`'s string `applyLayout`.
|
|
942
|
+
const { attributes, resolved: layoutResolved } = resolveLayoutAttributes(aliased, undefined);
|
|
943
|
+
try {
|
|
944
|
+
let el = null;
|
|
945
|
+
if (name === ROW_NAME) {
|
|
946
|
+
el = renderRowElement(element, attributes, width, indent, wctx);
|
|
947
|
+
}
|
|
948
|
+
else if (name === TABS_NAME) {
|
|
949
|
+
el = renderTabsElement(element, width, indent, wctx);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
el = renderDetailsElement(element, attributes, width, indent, wctx);
|
|
953
|
+
}
|
|
954
|
+
if (!el)
|
|
955
|
+
return null;
|
|
956
|
+
const marker = invalidEnumMarkerElement(name, attributes, wctx);
|
|
957
|
+
const withMarker = marker ? (_jsxs(Box, { flexDirection: "column", children: [el, marker] })) : (el);
|
|
958
|
+
return applyLayoutElement(withMarker, layoutResolved, width);
|
|
959
|
+
}
|
|
960
|
+
catch {
|
|
961
|
+
return styledText(componentError(name || '(unnamed)', false, '', width, wctx.color));
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return styledText(renderDirectiveString(element, width, indent, wctx));
|
|
965
|
+
}
|
|
966
|
+
/** Renders one block-level hast node as a real Ink element, or `null` for a node that carries no content (whitespace-only text). */
|
|
967
|
+
function renderBlockElement(node, width, indent, wctx) {
|
|
968
|
+
if (node.type === 'text') {
|
|
969
|
+
return node.value.trim() === ''
|
|
970
|
+
? null
|
|
971
|
+
: styledText(stripControlCharacters(node.value));
|
|
972
|
+
}
|
|
973
|
+
if (node.type !== 'element')
|
|
974
|
+
return null;
|
|
975
|
+
if (node.tagName === DIRECTIVE_TAG) {
|
|
976
|
+
return renderDirectiveElement(node, width, indent, wctx);
|
|
977
|
+
}
|
|
978
|
+
if (node.tagName === 'blockquote') {
|
|
979
|
+
const innerWidth = Math.max(1, width - 2);
|
|
980
|
+
const inner = renderBlocksElement(node.children, innerWidth, `${indent}│ `, wctx);
|
|
981
|
+
if (!inner)
|
|
982
|
+
return null;
|
|
983
|
+
return (_jsxs(Box, { width: width, children: [styledText(dim('│ ', wctx.color)), _jsx(Box, { flexDirection: "column", width: innerWidth, children: inner })] }));
|
|
984
|
+
}
|
|
985
|
+
if (node.tagName === 'ul' || node.tagName === 'ol') {
|
|
986
|
+
return renderListElement(node, width, indent, wctx);
|
|
987
|
+
}
|
|
988
|
+
const text = renderBlockString(node, width, indent, wctx);
|
|
989
|
+
if (text.trim() === '')
|
|
990
|
+
return null;
|
|
991
|
+
return styledText(text);
|
|
992
|
+
}
|
|
993
|
+
function renderListElement(el, width, indent, wctx) {
|
|
994
|
+
const ordered = el.tagName === 'ol';
|
|
995
|
+
const startRaw = el.properties?.start;
|
|
996
|
+
const start = ordered && typeof startRaw === 'number' ? startRaw : 1;
|
|
997
|
+
const items = el.children.filter((child) => child.type === 'element' && child.tagName === 'li');
|
|
998
|
+
if (items.length === 0)
|
|
999
|
+
return null;
|
|
1000
|
+
const items$ = items.map((li, index) => {
|
|
1001
|
+
const marker = ordered ? `${start + index}. ` : '- ';
|
|
1002
|
+
const task = taskState(li);
|
|
1003
|
+
const contentChildren = li.children.filter((child) => !(task && child.type === 'element' && child.tagName === 'input'));
|
|
1004
|
+
const prefix = `${marker}${task ? (task.checked ? '[x] ' : '[ ] ') : ''}`;
|
|
1005
|
+
const prefixWidth = measureWidth(prefix);
|
|
1006
|
+
const bodyWidth = Math.max(1, width - prefixWidth);
|
|
1007
|
+
const nestedLists = contentChildren.filter((child) => child.type === 'element' &&
|
|
1008
|
+
(child.tagName === 'ul' || child.tagName === 'ol'));
|
|
1009
|
+
const restChildren = contentChildren.filter((child) => !nestedLists.includes(child));
|
|
1010
|
+
const bodyIsBlock = isBlockLevelChildren(restChildren);
|
|
1011
|
+
const bodyText = bodyIsBlock
|
|
1012
|
+
? undefined
|
|
1013
|
+
: wrapText(renderInlineChildrenString(restChildren, bodyWidth, indent, wctx), bodyWidth).join('\n');
|
|
1014
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [styledText(prefix), _jsx(Box, { width: bodyWidth, flexDirection: "column", children: bodyIsBlock
|
|
1015
|
+
? renderBlocksElement(restChildren, bodyWidth, indent, wctx)
|
|
1016
|
+
: 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));
|
|
1017
|
+
});
|
|
1018
|
+
return _jsx(Box, { flexDirection: "column", children: items$ });
|
|
1019
|
+
}
|
|
1020
|
+
/** 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`. */
|
|
1021
|
+
function renderBlocksElement(nodes, width, indent, wctx) {
|
|
1022
|
+
const elements = [];
|
|
1023
|
+
for (const node of nodes) {
|
|
1024
|
+
const el = renderBlockElement(node, width, indent, wctx);
|
|
1025
|
+
if (el === null)
|
|
1026
|
+
continue;
|
|
1027
|
+
elements.push(_jsx(Box, { marginTop: elements.length > 0 ? 1 : 0, flexDirection: "column", children: el }, elements.length));
|
|
1028
|
+
}
|
|
1029
|
+
if (elements.length === 0)
|
|
1030
|
+
return null;
|
|
1031
|
+
return _jsx(_Fragment, { children: elements });
|
|
1032
|
+
}
|
|
781
1033
|
/** Collapses trailing blank lines to exactly one terminating `\n`, and leaves an empty document as `''`. */
|
|
782
1034
|
function finalizeOutput(text) {
|
|
783
1035
|
if (text === '')
|
|
@@ -789,34 +1041,43 @@ function renderFailureFallback(error, color) {
|
|
|
789
1041
|
const message = error instanceof Error ? error.message : String(error);
|
|
790
1042
|
return `${dim('failed to render document', color)}\n${stripControlCharacters(message)}\n`;
|
|
791
1043
|
}
|
|
792
|
-
function
|
|
793
|
-
|
|
1044
|
+
function buildWalkContext(registry, scope, color, theme, interactive, resolveImageSrc, resolveHref, onDiagnostic) {
|
|
1045
|
+
return {
|
|
794
1046
|
registry,
|
|
795
1047
|
scope,
|
|
796
1048
|
color,
|
|
797
1049
|
theme,
|
|
1050
|
+
interactive,
|
|
798
1051
|
resolveImageSrc,
|
|
799
1052
|
resolveHref,
|
|
800
1053
|
onDiagnostic,
|
|
1054
|
+
focusCounter: { current: 0 },
|
|
1055
|
+
focusables: undefined,
|
|
801
1056
|
};
|
|
802
|
-
return finalizeOutput(renderBlocks(root.children, width, '', wctx));
|
|
803
1057
|
}
|
|
804
1058
|
/** The columns assumed when a caller supplies no `width` option. */
|
|
805
1059
|
const DEFAULT_WIDTH = 80;
|
|
1060
|
+
/** Builds the root Ink element for `root`, given every resolved render input. Exported for `buildMarkElement` (the live-viewer entry point, phase 3) as well as this module's own three string-producing entry points. */
|
|
1061
|
+
function buildRootElement(root, registry, scope, color, theme, interactive, resolveImageSrc, resolveHref, onDiagnostic, width) {
|
|
1062
|
+
const wctx = buildWalkContext(registry, scope, color, theme, interactive, resolveImageSrc, resolveHref, onDiagnostic);
|
|
1063
|
+
const body = renderBlocksElement(root.children, width, '', wctx);
|
|
1064
|
+
return (_jsx(Box, { width: width, flexDirection: "column", children: body ?? _jsx(Text, { children: " " }) }));
|
|
1065
|
+
}
|
|
1066
|
+
async function renderRootToString(root, registry, scope, color, theme, interactive, resolveImageSrc, resolveHref, onDiagnostic, width) {
|
|
1067
|
+
const element = buildRootElement(root, registry, scope, color, theme, interactive, resolveImageSrc, resolveHref, onDiagnostic, width);
|
|
1068
|
+
const raw = await renderInkToString(element, width);
|
|
1069
|
+
return finalizeOutput(raw);
|
|
1070
|
+
}
|
|
806
1071
|
/**
|
|
807
1072
|
* Renders Markii text to a plain string (optionally carrying ANSI SGR/OSC 8
|
|
808
1073
|
* escapes) using `registry` to resolve directive names. `registry` is
|
|
809
1074
|
* OPTIONAL here, unlike the other two engines' required parameter, and
|
|
810
|
-
* defaults to `defaultAnsiRegistry
|
|
811
|
-
* therefore renders the unknown-component fallback).
|
|
1075
|
+
* defaults to `defaultAnsiRegistry`.
|
|
812
1076
|
*
|
|
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
|
|
1077
|
+
* `async` since the first call in a process pays Ink/Yoga's one-time module-
|
|
1078
|
+
* evaluation cost (`ink-string.ts`'s doc comment; `SPIKE-10-findings.md`'s
|
|
1079
|
+
* "Item 3"). Never throws: parsing is tolerant, unknown names always render
|
|
1080
|
+
* a fallback, and any unexpected internal error degrades to the "failed to
|
|
820
1081
|
* render document" line.
|
|
821
1082
|
*
|
|
822
1083
|
* `options.color` resolves via `./ansi.js`'s `resolveColorOption`: `'auto'`
|
|
@@ -824,14 +1085,11 @@ const DEFAULT_WIDTH = 80;
|
|
|
824
1085
|
* environment knowledge of its own — a caller that wants automatic
|
|
825
1086
|
* detection calls `detectColorLevel` itself and passes the result as
|
|
826
1087
|
* `'16'`/`'256'`/`'truecolor'`.
|
|
827
|
-
*
|
|
828
|
-
* `store`/`vault` and `resolveImageSrc`/`resolveHref`/`onDiagnostic` carry
|
|
829
|
-
* the same meaning as `@markii/html`'s identical options.
|
|
830
1088
|
*/
|
|
831
|
-
export function renderMarkToAnsi(text, registry = defaultAnsiRegistry, store, vault, options) {
|
|
1089
|
+
export async function renderMarkToAnsi(text, registry = defaultAnsiRegistry, store, vault, options) {
|
|
832
1090
|
const color = resolveColorOption(options?.color);
|
|
833
1091
|
try {
|
|
834
|
-
return
|
|
1092
|
+
return await renderRootToString(toHast(text), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, color, options?.theme ?? defaultAnsiTheme, options?.interactive ?? false, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, options?.width ?? DEFAULT_WIDTH);
|
|
835
1093
|
}
|
|
836
1094
|
catch (error) {
|
|
837
1095
|
return renderFailureFallback(error, color);
|
|
@@ -839,21 +1097,20 @@ export function renderMarkToAnsi(text, registry = defaultAnsiRegistry, store, va
|
|
|
839
1097
|
}
|
|
840
1098
|
/**
|
|
841
1099
|
* 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
|
|
1100
|
+
* mdast node OR a whole already-parsed mdast document instead of raw
|
|
1101
|
+
* document text. Same registry resolution, same fallbacks, same purity and
|
|
845
1102
|
* never-throw guarantees, same options.
|
|
846
1103
|
*/
|
|
847
|
-
export function renderMarkNodeToAnsi(node, registry = defaultAnsiRegistry, store, vault, options) {
|
|
1104
|
+
export async function renderMarkNodeToAnsi(node, registry = defaultAnsiRegistry, store, vault, options) {
|
|
848
1105
|
const color = resolveColorOption(options?.color);
|
|
849
1106
|
try {
|
|
850
|
-
return
|
|
1107
|
+
return await renderRootToString(nodeOrRootToHast(node), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, color, options?.theme ?? defaultAnsiTheme, options?.interactive ?? false, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, options?.width ?? DEFAULT_WIDTH);
|
|
851
1108
|
}
|
|
852
1109
|
catch (error) {
|
|
853
1110
|
return renderFailureFallback(error, color);
|
|
854
1111
|
}
|
|
855
1112
|
}
|
|
856
|
-
/** Whether `root` is exactly one paragraph holding exactly one text directive (`:name[...]`) and nothing else.
|
|
1113
|
+
/** Whether `root` is exactly one paragraph holding exactly one text directive (`:name[...]`) and nothing else. */
|
|
857
1114
|
function loneInlineDirective(root) {
|
|
858
1115
|
if (root.children.length !== 1)
|
|
859
1116
|
return undefined;
|
|
@@ -869,17 +1126,15 @@ function loneInlineDirective(root) {
|
|
|
869
1126
|
}
|
|
870
1127
|
/**
|
|
871
1128
|
* 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.
|
|
1129
|
+
* it is. Any other source falls back to the exact same rendering
|
|
1130
|
+
* `renderMarkToAnsi` would produce.
|
|
876
1131
|
*/
|
|
877
|
-
export function renderMarkInlineToAnsi(text, registry = defaultAnsiRegistry, store, vault, options) {
|
|
1132
|
+
export async function renderMarkInlineToAnsi(text, registry = defaultAnsiRegistry, store, vault, options) {
|
|
878
1133
|
try {
|
|
879
1134
|
const root = parse(text);
|
|
880
1135
|
const lone = loneInlineDirective(root);
|
|
881
1136
|
if (lone) {
|
|
882
|
-
return renderMarkNodeToAnsi(lone, registry, store, vault, options);
|
|
1137
|
+
return await renderMarkNodeToAnsi(lone, registry, store, vault, options);
|
|
883
1138
|
}
|
|
884
1139
|
}
|
|
885
1140
|
catch {
|
|
@@ -887,3 +1142,51 @@ export function renderMarkInlineToAnsi(text, registry = defaultAnsiRegistry, sto
|
|
|
887
1142
|
}
|
|
888
1143
|
return renderMarkToAnsi(text, registry, store, vault, options);
|
|
889
1144
|
}
|
|
1145
|
+
/**
|
|
1146
|
+
* The live-viewer entry point (phase 3 consumes this): builds the same Ink
|
|
1147
|
+
* element tree `renderMarkToAnsi` would render to a string, but returns the
|
|
1148
|
+
* element itself so a host (the CLI's `markii view`) can mount it directly
|
|
1149
|
+
* with Ink's own `render()` instead of re-rendering a string on every
|
|
1150
|
+
* keystroke/resize. `document` accepts raw text OR an already-parsed mdast
|
|
1151
|
+
* node/root, mirroring the three string entry points' flexibility.
|
|
1152
|
+
*/
|
|
1153
|
+
export function buildMarkElement(document, registry = defaultAnsiRegistry, options) {
|
|
1154
|
+
const color = resolveColorOption(options?.color);
|
|
1155
|
+
const theme = options?.theme ?? defaultAnsiTheme;
|
|
1156
|
+
const scope = { store: options?.store, vault: options?.vault };
|
|
1157
|
+
const width = options?.width ?? DEFAULT_WIDTH;
|
|
1158
|
+
const interactive = options?.interactive ?? true;
|
|
1159
|
+
let root;
|
|
1160
|
+
try {
|
|
1161
|
+
root =
|
|
1162
|
+
typeof document === 'string'
|
|
1163
|
+
? toHast(document)
|
|
1164
|
+
: nodeOrRootToHast(document);
|
|
1165
|
+
}
|
|
1166
|
+
catch {
|
|
1167
|
+
root = { type: 'root', children: [] };
|
|
1168
|
+
}
|
|
1169
|
+
const content = buildRootElement(root, registry, scope, color, theme, interactive, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, width);
|
|
1170
|
+
if (!interactive)
|
|
1171
|
+
return content;
|
|
1172
|
+
// The walk above already assigned every focusable component's `focusId`
|
|
1173
|
+
// (`WalkContext.focusCounter`) and, incidentally, discarded each one's
|
|
1174
|
+
// descriptor (`WalkContext.focusables` is left `undefined` on the real
|
|
1175
|
+
// render, since nothing there needs it). Rebuilding once more, this time
|
|
1176
|
+
// collecting descriptors, is wasteful only in the sense that the tree is
|
|
1177
|
+
// constructed twice; simpler and safer than threading mutable state out
|
|
1178
|
+
// of a pure element-builder, and this entry point is called once per
|
|
1179
|
+
// mount/resize, not per keystroke.
|
|
1180
|
+
const counter = { current: 0 };
|
|
1181
|
+
const focusables = [];
|
|
1182
|
+
const countingWctx = buildWalkContext(registry, scope, color, theme, true, options?.resolveImageSrc, options?.resolveHref,
|
|
1183
|
+
// No `onDiagnostic`: the real walk above already reported every notice
|
|
1184
|
+
// this document produces, and this second walk exists only to collect
|
|
1185
|
+
// focus descriptors. Passing the callback here would report each notice
|
|
1186
|
+
// twice on every mount and again on every resize.
|
|
1187
|
+
undefined);
|
|
1188
|
+
countingWctx.focusCounter = counter;
|
|
1189
|
+
countingWctx.focusables = focusables;
|
|
1190
|
+
renderBlocksElement(root.children, width, '', countingWctx);
|
|
1191
|
+
return (_jsx(InteractiveRoot, { focusables: focusables, onExit: options?.onExit, color: color, theme: theme, children: content }));
|
|
1192
|
+
}
|