@markii/ansi 0.17.0-next.0 → 0.17.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,6 +37,8 @@ export declare function emptyInlineTitle(name: string): string;
37
37
  export declare function invalidAttributeValueTitle(directive: string, attribute: string, value: string): string;
38
38
  /** The marker text for a component-built image whose `src` was refused as unsafe. Wording identical to `@markii/html`'s `unsafeImageSrcTitle`. */
39
39
  export declare function unsafeImageSrcTitle(directive: string): string;
40
+ /** The diagnostic text for a host `renderCode` callback that threw; the code block rendered as usual. Wording identical to `@markii/html`'s `codeRendererFailedTitle`. */
41
+ export declare function codeRendererFailedTitle(): string;
40
42
  /** The diagnostic text for a registered block component written as an inline directive. Wording identical to `@markii/html`'s `formMismatchTitle`. */
41
43
  export declare function formMismatchTitle(directive: string): string;
42
44
  /**
@@ -71,3 +73,14 @@ export declare const NO_ENGINE_VIEW_LABEL = "no terminal view for";
71
73
  * and is never printed in the note.
72
74
  */
73
75
  export declare function noEngineViewTitle(directive: string, pack: string, engines: readonly string[]): string;
76
+ /**
77
+ * The fallback label for a directive of a pack whose terminal view script
78
+ * could not be loaded. Followed by the directive name in backticks, like the
79
+ * other fallback labels. `pack` is stripped by the caller's sanitizer.
80
+ */
81
+ export declare function packLoadFailedLabel(pack: string): string;
82
+ /**
83
+ * The diagnostic text for the same case. The loader's own line (what threw)
84
+ * is on the diagnostics surface already; this one names the directive.
85
+ */
86
+ export declare function packLoadFailedTitle(directive: string, pack: string): string;
@@ -99,6 +99,10 @@ export function invalidAttributeValueTitle(directive, attribute, value) {
99
99
  export function unsafeImageSrcTitle(directive) {
100
100
  return `${directive}: image source was refused as unsafe and was not shown`;
101
101
  }
102
+ /** The diagnostic text for a host `renderCode` callback that threw; the code block rendered as usual. Wording identical to `@markii/html`'s `codeRendererFailedTitle`. */
103
+ export function codeRendererFailedTitle() {
104
+ return 'code block: the host code renderer failed, so the plain block was shown';
105
+ }
102
106
  /** The diagnostic text for a registered block component written as an inline directive. Wording identical to `@markii/html`'s `formMismatchTitle`. */
103
107
  export function formMismatchTitle(directive) {
104
108
  return `${directive}: block component was written inline and was not rendered`;
@@ -144,3 +148,18 @@ export function noEngineViewTitle(directive, pack, engines) {
144
148
  : 'it has no views for any engine';
145
149
  return `The ${pack} pack has no terminal view for this component; ${has}`;
146
150
  }
151
+ /**
152
+ * The fallback label for a directive of a pack whose terminal view script
153
+ * could not be loaded. Followed by the directive name in backticks, like the
154
+ * other fallback labels. `pack` is stripped by the caller's sanitizer.
155
+ */
156
+ export function packLoadFailedLabel(pack) {
157
+ return `the \`${pack}\` pack failed to load, no terminal view for`;
158
+ }
159
+ /**
160
+ * The diagnostic text for the same case. The loader's own line (what threw)
161
+ * is on the diagnostics surface already; this one names the directive.
162
+ */
163
+ export function packLoadFailedTitle(directive, pack) {
164
+ return `The ${pack} pack failed to load in the terminal, so ${directive} has no view`;
165
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { renderMarkToAnsi, renderMarkNodeToAnsi, renderMarkInlineToAnsi, buildMarkElement, type RenderMarkOptions, } from './render.js';
1
+ export { renderMarkToAnsi, renderMarkNodeToAnsi, renderMarkInlineToAnsi, buildMarkElement, type RenderCode, type RenderCodeBlock, type RenderMarkOptions, } from './render.js';
2
2
  export { fg, colorize, bold, dim, italic, underline, inverse, hyperlink, detectColorLevel, resolveColorOption, type ColorLevel, type ColorOption, type AnsiColor, } from './ansi.js';
3
3
  export { defaultAnsiTheme, type AnsiTheme, type Tier1Token } from './theme.js';
4
4
  export { BOX_STYLES, isBoxStyle, glyphsFor, type BoxStyle, type FrameEdges, type GlyphSet, } from './glyphs.js';
package/dist/render.d.ts CHANGED
@@ -14,12 +14,36 @@ export interface ElementTabPanel {
14
14
  label: string;
15
15
  body: ReactNode;
16
16
  }
17
+ /** A fenced code block as offered to `RenderMarkOptions.renderCode`. */
18
+ export interface RenderCodeBlock {
19
+ /** The info-string language, or `undefined` when the fence names none. */
20
+ readonly lang: string | undefined;
21
+ /** The code text, control characters removed and tabs expanded. */
22
+ readonly code: string;
23
+ }
24
+ /** The `renderCode` callback: a string for a code block, or `undefined` to draw it as usual. */
25
+ export type RenderCode = (code: RenderCodeBlock) => string | undefined;
17
26
  /**
18
27
  * Options for `renderMarkToAnsi`/`renderMarkNodeToAnsi`/`renderMarkInlineToAnsi`.
19
28
  * `store`/`vault` are offered both here and as positional parameters; when
20
29
  * both are given, the option wins.
21
30
  */
22
31
  export interface RenderMarkOptions {
32
+ /**
33
+ * Replaces the output for an ordinary fenced code block. Called with the
34
+ * info-string language (`undefined` when the fence names none) and the
35
+ * code text, already stripped of control characters and with tabs
36
+ * expanded (the engine's own sanitizer, so author escapes cannot reach
37
+ * the terminal through a host that echoes it). Return a string to use it,
38
+ * or `undefined` to draw the block as usual. The returned string is
39
+ * HOST-TRUSTED and inserted as-is, so the host may add its own SGR
40
+ * styling and owns keeping control characters out of anything it adds.
41
+ * Never called for script fences or inline code. A callback that throws
42
+ * draws the block as usual and reports a `'code-renderer-failed'` event
43
+ * through `onDiagnostic`. Element results are not supported: fenced blocks
44
+ * are drawn as strings in this engine.
45
+ */
46
+ readonly renderCode?: RenderCode;
23
47
  readonly width?: number;
24
48
  readonly color?: ColorOption;
25
49
  readonly theme?: AnsiTheme;
package/dist/render.js CHANGED
@@ -15,7 +15,7 @@ import { sanitizeBlockText, sanitizeUrlText, stripControlCharacters, } from './s
15
15
  import { readRegistryComponent, registryLayoutAxis, registrySelfLayout, resolveDirectiveAlias, } from './registry.js';
16
16
  import { applyLayout, resolveLayoutAttributes, } from './layout.js';
17
17
  import { resolveScopedPath } from './resolve.js';
18
- import { dataStateSuffix, emptyInlineTitle, failureToken, formMismatchTitle, NO_ENGINE_VIEW_LABEL, noEngineViewTitle, invalidAttributeValueLabel, invalidAttributeValueTitle, unsafeImageSrcLabel, unsafeImageSrcTitle, } from './failure-presentation.js';
18
+ import { dataStateSuffix, emptyInlineTitle, failureToken, codeRendererFailedTitle, formMismatchTitle, NO_ENGINE_VIEW_LABEL, noEngineViewTitle, packLoadFailedLabel, packLoadFailedTitle, invalidAttributeValueLabel, invalidAttributeValueTitle, unsafeImageSrcLabel, unsafeImageSrcTitle, } from './failure-presentation.js';
19
19
  import { createUnavailableComponentLookup, formatValue, getContract, reportDiagnostic, } from '@markii/stdlib';
20
20
  import { resolveImageAttribute, } from './image-resolve.js';
21
21
  import { resolveHrefAttribute } from './href-resolve.js';
@@ -208,8 +208,11 @@ function isRegisteredInline(entry) {
208
208
  }
209
209
  }
210
210
  /** The fallback label line, worded for the reason and the form the directive was written in. Plain text (no escapes). */
211
- function fallbackLabel(name, inline, reason) {
211
+ function fallbackLabel(name, inline, reason, pack) {
212
212
  const code = `\`${stripControlCharacters(name)}\``;
213
+ if (reason === 'pack-load-failed') {
214
+ return `${packLoadFailedLabel(stripControlCharacters(pack ?? ''))} ${code}`;
215
+ }
213
216
  if (reason === 'no-engine-view')
214
217
  return `${NO_ENGINE_VIEW_LABEL} ${code}`;
215
218
  if (reason === 'form-mismatch') {
@@ -220,8 +223,8 @@ function fallbackLabel(name, inline, reason) {
220
223
  return `unknown component ${code}`;
221
224
  }
222
225
  /** The unknown-directive fallback: dim inline text for a text directive, a dashed dim frame for a block directive. Wording matches `@markii/html`'s `fallbackLabel` exactly. */
223
- function unknownDirective(name, inline, childrenText, reason, width, color, glyphs) {
224
- const label = fallbackLabel(name, inline, reason);
226
+ function unknownDirective(name, inline, childrenText, reason, width, color, glyphs, pack) {
227
+ const label = fallbackLabel(name, inline, reason, pack);
225
228
  if (inline) {
226
229
  return childrenText === ''
227
230
  ? dim(label, color)
@@ -380,9 +383,11 @@ function renderDirectiveContentString(name, kind, isBlock, attributes, childrenT
380
383
  reportDiagnostic(wctx.onDiagnostic, {
381
384
  kind: 'no-engine-view',
382
385
  directive: name,
383
- message: noEngineViewTitle(name, missingView.pack, missingView.engines),
386
+ message: missingView.loadFailed
387
+ ? packLoadFailedTitle(name, missingView.pack)
388
+ : noEngineViewTitle(name, missingView.pack, missingView.engines),
384
389
  });
385
- return unknownDirective(name, inline, safeChildrenText(childrenText), 'no-engine-view', width, wctx.color, wctx.glyphs);
390
+ return unknownDirective(name, inline, safeChildrenText(childrenText), missingView.loadFailed ? 'pack-load-failed' : 'no-engine-view', width, wctx.color, wctx.glyphs, missingView.pack);
386
391
  }
387
392
  return unknownDirective(name || '(unnamed)', inline, safeChildrenText(childrenText), 'unregistered', width, wctx.color, wctx.glyphs);
388
393
  }
@@ -536,6 +541,9 @@ function renderBlockString(node, width, indent, wctx) {
536
541
  const marker = renderScriptMarker(node, wctx.color, wctx.glyphs);
537
542
  if (marker !== undefined)
538
543
  return marker;
544
+ const custom = applyRenderCode(node, wctx);
545
+ if (custom !== undefined)
546
+ return custom;
539
547
  return renderCodeBlock(node, wctx.color);
540
548
  }
541
549
  return wrapText(renderInlineChildrenString(node.children ?? [], width, indent, wctx), width).join('\n');
@@ -648,6 +656,35 @@ function renderScriptMarker(node, color, glyphs) {
648
656
  return undefined;
649
657
  }
650
658
  }
659
+ /**
660
+ * Offers one ordinary fenced code block to the host's `renderCode`. The
661
+ * text handed over is already run through `sanitizeBlockText` (control
662
+ * characters dropped, tabs expanded), so a host that echoes it cannot
663
+ * smuggle author escapes into the terminal. Returns the host's string, or
664
+ * `undefined` to draw the block as usual: no callback, a decline, a
665
+ * non-string result, or a throw (reported through `onDiagnostic`).
666
+ */
667
+ function applyRenderCode(node, wctx) {
668
+ if (!wctx.renderCode)
669
+ return undefined;
670
+ const codeNode = findChildElement(node, 'code');
671
+ if (!codeNode)
672
+ return undefined;
673
+ try {
674
+ const result = wctx.renderCode({
675
+ lang: getLanguage(codeNode) || undefined,
676
+ code: sanitizeBlockText(getCodeText(codeNode)),
677
+ });
678
+ return typeof result === 'string' ? result : undefined;
679
+ }
680
+ catch {
681
+ reportDiagnostic(wctx.onDiagnostic, {
682
+ kind: 'code-renderer-failed',
683
+ message: codeRendererFailedTitle(),
684
+ });
685
+ return undefined;
686
+ }
687
+ }
651
688
  function renderCodeBlock(node, color) {
652
689
  const codeNode = findChildElement(node, 'code');
653
690
  const language = getLanguage(codeNode);
@@ -1355,7 +1392,7 @@ function renderFailureFallback(error, color) {
1355
1392
  const message = error instanceof Error ? error.message : String(error);
1356
1393
  return `${dim('failed to render document', color)}\n${stripControlCharacters(message)}\n`;
1357
1394
  }
1358
- function buildWalkContext(registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable) {
1395
+ function buildWalkContext(registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, renderCode) {
1359
1396
  return {
1360
1397
  registry,
1361
1398
  scope,
@@ -1367,6 +1404,7 @@ function buildWalkContext(registry, scope, color, theme, glyphs, interactive, re
1367
1404
  resolveHref,
1368
1405
  onDiagnostic,
1369
1406
  unavailable,
1407
+ renderCode,
1370
1408
  focusCounter: { current: 0 },
1371
1409
  focusables: undefined,
1372
1410
  };
@@ -1374,13 +1412,13 @@ function buildWalkContext(registry, scope, color, theme, glyphs, interactive, re
1374
1412
  /** The columns assumed when a caller supplies no `width` option. */
1375
1413
  const DEFAULT_WIDTH = 80;
1376
1414
  /** Builds the root Ink element for `root`, given every resolved render input. Exported for `buildMarkElement` (the live-viewer entry point, phase 3) as well as this module's own three string-producing entry points. */
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);
1415
+ function buildRootElement(root, registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, width, renderCode) {
1416
+ const wctx = buildWalkContext(registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, renderCode);
1379
1417
  const body = renderBlocksElement(root.children, width, '', wctx);
1380
1418
  return (_jsx(Box, { width: width, flexDirection: "column", children: body ?? _jsx(Text, { children: " " }) }));
1381
1419
  }
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);
1420
+ async function renderRootToString(root, registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, width, renderCode) {
1421
+ const element = buildRootElement(root, registry, scope, color, theme, glyphs, interactive, resolveImageSrc, resolveHref, onDiagnostic, unavailable, width, renderCode);
1384
1422
  const raw = await renderInkToString(element, width);
1385
1423
  return finalizeOutput(raw);
1386
1424
  }
@@ -1405,7 +1443,7 @@ async function renderRootToString(root, registry, scope, color, theme, glyphs, i
1405
1443
  export async function renderMarkToAnsi(text, registry = defaultAnsiRegistry, store, vault, options) {
1406
1444
  const color = resolveColorOption(options?.color);
1407
1445
  try {
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);
1446
+ return await renderRootToString(toHast(text), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, color, options?.theme ?? defaultAnsiTheme, glyphsFor(options?.box), options?.interactive ?? false, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, createUnavailableComponentLookup(options?.unavailableComponents), options?.width ?? DEFAULT_WIDTH, options?.renderCode);
1409
1447
  }
1410
1448
  catch (error) {
1411
1449
  return renderFailureFallback(error, color);
@@ -1420,7 +1458,7 @@ export async function renderMarkToAnsi(text, registry = defaultAnsiRegistry, sto
1420
1458
  export async function renderMarkNodeToAnsi(node, registry = defaultAnsiRegistry, store, vault, options) {
1421
1459
  const color = resolveColorOption(options?.color);
1422
1460
  try {
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);
1461
+ return await renderRootToString(nodeOrRootToHast(node), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, color, options?.theme ?? defaultAnsiTheme, glyphsFor(options?.box), options?.interactive ?? false, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, createUnavailableComponentLookup(options?.unavailableComponents), options?.width ?? DEFAULT_WIDTH, options?.renderCode);
1424
1462
  }
1425
1463
  catch (error) {
1426
1464
  return renderFailureFallback(error, color);
@@ -1484,7 +1522,7 @@ export function buildMarkElement(document, registry = defaultAnsiRegistry, optio
1484
1522
  root = { type: 'root', children: [] };
1485
1523
  }
1486
1524
  const unavailable = createUnavailableComponentLookup(options?.unavailableComponents);
1487
- const content = buildRootElement(root, registry, scope, color, theme, glyphs, interactive, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, unavailable, width);
1525
+ const content = buildRootElement(root, registry, scope, color, theme, glyphs, interactive, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, unavailable, width, options?.renderCode);
1488
1526
  if (!interactive)
1489
1527
  return content;
1490
1528
  // The walk above already assigned every focusable component's `focusId`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markii/ansi",
3
- "version": "0.17.0-next.0",
3
+ "version": "0.17.0-next.1",
4
4
  "description": "The terminal renderer for Markii (.mk.md): a registry-driven hast-to-terminal engine built on Ink. Renders a note once as a string with optional color, for a pipe or a log, or as a live Ink tree a host can mount.",
5
5
  "keywords": [
6
6
  "markdown",
@@ -51,15 +51,15 @@
51
51
  "regenerate:fixtures": "npm run build:dist && node scripts/regenerate-ansi-fixtures.ts"
52
52
  },
53
53
  "dependencies": {
54
- "@markii/core": "^0.17.0-next.0",
55
- "@markii/pack": "^0.17.0-next.0",
56
- "@markii/stdlib": "^0.17.0-next.0",
54
+ "@markii/core": "^0.17.0-next.1",
55
+ "@markii/pack": "^0.17.0-next.1",
56
+ "@markii/stdlib": "^0.17.0-next.1",
57
57
  "ink": "^7.1.1",
58
58
  "react": "^19.2.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@types/hast": "^3.0.4",
62
- "@markii/runtime": "^0.17.0-next.0",
62
+ "@markii/runtime": "^0.17.0-next.1",
63
63
  "@types/react": "^19.2.0"
64
64
  }
65
65
  }