@markii/ansi 0.17.0-next.3 → 0.17.0-next.5

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.
@@ -39,8 +39,6 @@ export declare function invalidAttributeValueTitle(directive: string, attribute:
39
39
  export declare function unsafeImageSrcTitle(directive: string): string;
40
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
41
  export declare function codeRendererFailedTitle(): string;
42
- /** The diagnostic text for a directive that names no registered component; the unknown-component box was shown. */
43
- export declare function unknownDirectiveTitle(directive: string): string;
44
42
  /** The diagnostic text for a registered block component written as an inline directive. Wording identical to `@markii/html`'s `formMismatchTitle`. */
45
43
  export declare function formMismatchTitle(directive: string): string;
46
44
  /**
@@ -103,10 +103,6 @@ export function unsafeImageSrcTitle(directive) {
103
103
  export function codeRendererFailedTitle() {
104
104
  return 'code block: the host code renderer failed, so the plain block was shown';
105
105
  }
106
- /** The diagnostic text for a directive that names no registered component; the unknown-component box was shown. */
107
- export function unknownDirectiveTitle(directive) {
108
- return `${directive}: unknown component, shown as a placeholder`;
109
- }
110
106
  /** The diagnostic text for a registered block component written as an inline directive. Wording identical to `@markii/html`'s `formMismatchTitle`. */
111
107
  export function formMismatchTitle(directive) {
112
108
  return `${directive}: block component was written inline and was not rendered`;
package/dist/glyphs.d.ts CHANGED
@@ -68,6 +68,8 @@ export interface GlyphSet {
68
68
  readonly emptyValue: string;
69
69
  /** The key legend on the live viewer's status line. */
70
70
  readonly keyLegend: string;
71
+ /** The same legend with the re-run key listed second, used only when the host supplies a re-run callback. */
72
+ readonly keyLegendRerun: string;
71
73
  }
72
74
  /** The glyph set for `style`; anything other than `'ascii'` gets the unicode set. */
73
75
  export declare function glyphsFor(style: BoxStyle | undefined): GlyphSet;
package/dist/glyphs.js CHANGED
@@ -46,6 +46,7 @@ const UNICODE = {
46
46
  starEmpty: '☆',
47
47
  emptyValue: '—',
48
48
  keyLegend: 'q quit ↑↓/jk focus ←→/tab switch enter open/close space/pgdn scroll g/G top/end',
49
+ keyLegendRerun: 'q quit r run ↑↓/jk focus ←→/tab switch enter open/close space/pgdn scroll g/G top/end',
49
50
  };
50
51
  const ASCII = {
51
52
  topLeft: '+',
@@ -77,6 +78,7 @@ const ASCII = {
77
78
  starEmpty: '.',
78
79
  emptyValue: '-',
79
80
  keyLegend: 'q quit up/down/jk focus left/right/tab switch enter open/close space/pgdn scroll g/G top/end',
81
+ keyLegendRerun: 'q quit r run up/down/jk focus left/right/tab switch enter open/close space/pgdn scroll g/G top/end',
80
82
  };
81
83
  const GLYPHS = {
82
84
  unicode: UNICODE,
@@ -55,6 +55,16 @@ export interface InteractiveRootProps {
55
55
  focusables: readonly FocusableDescriptor[];
56
56
  /** Called when the reader presses `q`. The engine owns no process state: it never calls `process.exit` itself. */
57
57
  onExit?: () => void;
58
+ /**
59
+ * Called when the reader presses `r`. The host runs whatever it runs and
60
+ * returns when it is done; the engine owns no run, process, or script
61
+ * state. While the returned promise is pending the status line reads
62
+ * "running" and further `r` presses are ignored. A rejection ends the
63
+ * wait like a resolution. Absent, `r` does nothing and the legend omits it.
64
+ */
65
+ onRerun?: () => void | Promise<void>;
66
+ /** The legend to show instead of `keyLegend` when `onRerun` is given: the render's `GlyphSet.keyLegendRerun`. */
67
+ rerunLegend?: string;
58
68
  color: ColorLevel;
59
69
  theme: AnsiTheme;
60
70
  /** The key legend on the status line, identical regardless of what is focused: the render's `GlyphSet.keyLegend`, so it follows the box style. */
@@ -81,4 +91,4 @@ export interface InteractiveRootProps {
81
91
  * default (uninteractive) value there, so a string render never sees a
82
92
  * focus marker, a glyph, or a status line.
83
93
  */
84
- export declare function InteractiveRoot({ focusables, onExit, color, theme, keyLegend, viewportRows, width, children, }: InteractiveRootProps): ReactNode;
94
+ export declare function InteractiveRoot({ focusables, onExit, onRerun, rerunLegend, color, theme, keyLegend, viewportRows, width, children, }: InteractiveRootProps): ReactNode;
@@ -86,7 +86,7 @@ export function useFocusAnchor(focusId) {
86
86
  * default (uninteractive) value there, so a string render never sees a
87
87
  * focus marker, a glyph, or a status line.
88
88
  */
89
- export function InteractiveRoot({ focusables, onExit, color, theme, keyLegend, viewportRows, width, children, }) {
89
+ export function InteractiveRoot({ focusables, onExit, onRerun, rerunLegend, color, theme, keyLegend, viewportRows, width, children, }) {
90
90
  const focusableCount = focusables.length;
91
91
  const [focusIndex, setFocusIndex] = useState(0);
92
92
  // `focusId -> the label its last active-panel report carried`. Only a
@@ -119,11 +119,42 @@ export function InteractiveRoot({ focusables, onExit, color, theme, keyLegend, v
119
119
  return;
120
120
  setOffset(target === 'top' ? 0 : maxOffset());
121
121
  };
122
+ const [running, setRunning] = useState(false);
123
+ // A ref, not the state: two presses in one tick must start one run.
124
+ const runningRef = useRef(false);
125
+ const mounted = useRef(true);
126
+ useEffect(() => {
127
+ mounted.current = true;
128
+ return () => {
129
+ mounted.current = false;
130
+ };
131
+ }, []);
132
+ const startRerun = () => {
133
+ if (onRerun === undefined || runningRef.current)
134
+ return;
135
+ runningRef.current = true;
136
+ setRunning(true);
137
+ const finish = () => {
138
+ runningRef.current = false;
139
+ if (mounted.current)
140
+ setRunning(false);
141
+ };
142
+ try {
143
+ Promise.resolve(onRerun()).then(finish, finish);
144
+ }
145
+ catch {
146
+ finish();
147
+ }
148
+ };
122
149
  useInput((input, key) => {
123
150
  if (input === 'q') {
124
151
  onExit?.();
125
152
  return;
126
153
  }
154
+ if (input === 'r') {
155
+ startRerun();
156
+ return;
157
+ }
127
158
  if (viewport !== undefined) {
128
159
  const page = Math.max(1, viewport - 1);
129
160
  if (key.pageDown || input === ' ' || (key.ctrl && input === 'f')) {
@@ -222,13 +253,18 @@ export function InteractiveRoot({ focusables, onExit, color, theme, keyLegend, v
222
253
  const focusedLabel = focused && focusedId !== undefined
223
254
  ? (activeLabels[focusedId] ?? focused.label)
224
255
  : undefined;
225
- const statusLabel = focused
226
- ? `${focused.kind} "${focusedLabel}"`
227
- : 'nothing focusable';
256
+ const statusLabel = running
257
+ ? 'running'
258
+ : focused
259
+ ? `${focused.kind} "${focusedLabel}"`
260
+ : 'nothing focusable';
261
+ const legend = onRerun !== undefined && rerunLegend !== undefined
262
+ ? rerunLegend
263
+ : keyLegend;
228
264
  // Cut to the width on plain text: the status line must stay one row (the
229
265
  // viewport math counts it as one), and a label or legend that wrapped
230
266
  // would push the document up.
231
- const statusLine = dim(cutToWidth(`${statusLabel} ${keyLegend}`, width), color);
267
+ const statusLine = dim(cutToWidth(`${statusLabel} ${legend}`, width), color);
232
268
  const status = _jsx(Text, { children: statusLine });
233
269
  if (viewport === undefined) {
234
270
  return (_jsx(FocusContext.Provider, { value: api, children: _jsxs(Box, { flexDirection: "column", children: [children, status] }) }));
package/dist/render.d.ts CHANGED
@@ -57,13 +57,6 @@ export interface RenderMarkOptions {
57
57
  readonly resolveImageSrc?: ResolveImageSrc;
58
58
  readonly resolveHref?: ResolveHref;
59
59
  readonly onDiagnostic?: OnDiagnostic;
60
- /**
61
- * Also report a directive that names no registered component as an
62
- * `'unknown-directive'` event. Off by default: the fallback box already
63
- * marks it in the output. A host that places findings in the note's
64
- * source (a language server) turns it on.
65
- */
66
- readonly reportUnknownDirectives?: boolean;
67
60
  /**
68
61
  * Components of installed packs that this render cannot draw, as plain
69
62
  * data from the host. A directive named here renders the usual fallback
@@ -133,5 +126,7 @@ export declare function renderMarkInlineToAnsi(text: string, registry?: AnsiRegi
133
126
  */
134
127
  export declare function buildMarkElement(document: string | MarkNode | MarkRoot, registry?: AnsiRegistry, options?: RenderMarkOptions & {
135
128
  readonly onExit?: () => void;
129
+ /** Called when the reader presses `r`; see `InteractiveRoot`'s `onRerun`. The legend lists the key only when this is given. */
130
+ readonly onRerun?: () => void | Promise<void>;
136
131
  }): ReactElement;
137
132
  export {};
package/dist/render.js CHANGED
@@ -15,8 +15,8 @@ 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, codeRendererFailedTitle, formMismatchTitle, NO_ENGINE_VIEW_LABEL, noEngineViewTitle, packLoadFailedLabel, packLoadFailedTitle, invalidAttributeValueLabel, invalidAttributeValueTitle, unsafeImageSrcLabel, unknownDirectiveTitle, unsafeImageSrcTitle, } from './failure-presentation.js';
19
- import { createUnavailableComponentLookup, formatValue, getContract, reportDiagnostic, } from '@markii/stdlib';
18
+ import { dataStateSuffix, emptyInlineTitle, failureToken, codeRendererFailedTitle, formMismatchTitle, NO_ENGINE_VIEW_LABEL, noEngineViewTitle, packLoadFailedLabel, packLoadFailedTitle, invalidAttributeValueLabel, invalidAttributeValueTitle, unsafeImageSrcLabel, unsafeImageSrcTitle, } from './failure-presentation.js';
19
+ import { createUnavailableComponentLookup, formatValue, invalidEnumAttribute, reportDiagnostic, } from '@markii/stdlib';
20
20
  import { resolveImageAttribute, } from './image-resolve.js';
21
21
  import { resolveHrefAttribute } from './href-resolve.js';
22
22
  import { defaultAnsiRegistry } from './components/index.js';
@@ -289,23 +289,6 @@ function resolveDataAttribute(attributes, scope) {
289
289
  dataFailureKind: resolved.status === 'error' ? resolved.failureKind : undefined,
290
290
  };
291
291
  }
292
- /** Whether one of `name`'s `@markii/stdlib` contract attributes with a closed `enum` is present with a value outside it. */
293
- function invalidEnumAttribute(name, attributes) {
294
- const contract = getContract(name);
295
- if (!contract)
296
- return undefined;
297
- for (const [attribute, schema] of Object.entries(contract.attributes)) {
298
- const enumValues = schema.enum;
299
- if (!enumValues)
300
- continue;
301
- const value = attributes[attribute];
302
- if (value === undefined || value === null || value === '')
303
- continue;
304
- if (!enumValues.includes(value))
305
- return { attribute, value };
306
- }
307
- return undefined;
308
- }
309
292
  /** Resolves a `data=`/`:value[]` name against `scope`. Never throws. */
310
293
  function resolveValue(scope, name) {
311
294
  const trimmed = name.trim();
@@ -412,11 +395,6 @@ function renderDirectiveContentString(name, kind, isBlock, attributes, childrenT
412
395
  });
413
396
  return unknownDirective(name, inline, safeChildrenText(childrenText), missingView.loadFailed ? 'pack-load-failed' : 'no-engine-view', width, wctx.color, wctx.glyphs, missingView.pack);
414
397
  }
415
- reportDiagnostic(wctx.onDiagnostic, {
416
- kind: 'unknown-directive',
417
- directive: name || '(unnamed)',
418
- message: unknownDirectiveTitle(name || '(unnamed)'),
419
- });
420
398
  return unknownDirective(name || '(unnamed)', inline, safeChildrenText(childrenText), 'unregistered', width, wctx.color, wctx.glyphs);
421
399
  }
422
400
  if (isFormMismatch(entry, kind)) {
@@ -460,6 +438,11 @@ function renderDirectiveContentString(name, kind, isBlock, attributes, childrenT
460
438
  }
461
439
  if (isRegisteredInline(entry) &&
462
440
  safeChildrenText(childrenText).trim() === '') {
441
+ reportDiagnostic(wctx.onDiagnostic, {
442
+ kind: 'empty-inline',
443
+ directive: name,
444
+ message: emptyInlineTitle(name),
445
+ });
463
446
  rendered = `${rendered}${dim(` (${emptyInlineTitle(name)})`, wctx.color)}`;
464
447
  }
465
448
  return rendered;
@@ -1475,23 +1458,6 @@ function buildWalkContext(registry, scope, color, theme, glyphs, interactive, re
1475
1458
  focusables: undefined,
1476
1459
  };
1477
1460
  }
1478
- /**
1479
- * The `onDiagnostic` a render hands its walk: the caller's own, minus
1480
- * `'unknown-directive'` events unless `reportUnknownDirectives` asked for
1481
- * them. The fallback box already marks an unknown directive in the output,
1482
- * so a host that does not place findings in a source (the viewer, an
1483
- * export) keeps the quiet diagnostics surface it always had.
1484
- */
1485
- function diagnosticsFor(options) {
1486
- const downstream = options?.onDiagnostic;
1487
- if (downstream === undefined || options?.reportUnknownDirectives === true) {
1488
- return downstream;
1489
- }
1490
- return (event) => {
1491
- if (event.kind !== 'unknown-directive')
1492
- downstream(event);
1493
- };
1494
- }
1495
1461
  /** The columns assumed when a caller supplies no `width` option. */
1496
1462
  const DEFAULT_WIDTH = 80;
1497
1463
  /** Builds the root Ink element for `root`, given every resolved render input. Exported for `buildMarkElement` (the live-viewer entry point) as well as this module's own three string-producing entry points. */
@@ -1525,7 +1491,7 @@ async function renderRootToString(root, registry, scope, color, theme, glyphs, i
1525
1491
  export async function renderMarkToAnsi(text, registry = defaultAnsiRegistry, store, vault, options) {
1526
1492
  const color = resolveColorOption(options?.color);
1527
1493
  try {
1528
- 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, diagnosticsFor(options), createUnavailableComponentLookup(options?.unavailableComponents), options?.width ?? DEFAULT_WIDTH, options?.renderCode);
1494
+ 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);
1529
1495
  }
1530
1496
  catch (error) {
1531
1497
  return renderFailureFallback(error, color);
@@ -1540,7 +1506,7 @@ export async function renderMarkToAnsi(text, registry = defaultAnsiRegistry, sto
1540
1506
  export async function renderMarkNodeToAnsi(node, registry = defaultAnsiRegistry, store, vault, options) {
1541
1507
  const color = resolveColorOption(options?.color);
1542
1508
  try {
1543
- 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, diagnosticsFor(options), createUnavailableComponentLookup(options?.unavailableComponents), options?.width ?? DEFAULT_WIDTH, options?.renderCode);
1509
+ 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);
1544
1510
  }
1545
1511
  catch (error) {
1546
1512
  return renderFailureFallback(error, color);
@@ -1604,7 +1570,7 @@ export function buildMarkElement(document, registry = defaultAnsiRegistry, optio
1604
1570
  root = { type: 'root', children: [] };
1605
1571
  }
1606
1572
  const unavailable = createUnavailableComponentLookup(options?.unavailableComponents);
1607
- const content = buildRootElement(root, registry, scope, color, theme, glyphs, interactive, options?.resolveImageSrc, options?.resolveHref, diagnosticsFor(options), unavailable, width, options?.renderCode);
1573
+ const content = buildRootElement(root, registry, scope, color, theme, glyphs, interactive, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, unavailable, width, options?.renderCode);
1608
1574
  if (!interactive)
1609
1575
  return content;
1610
1576
  // The walk above already assigned every focusable component's `focusId`
@@ -1626,5 +1592,5 @@ export function buildMarkElement(document, registry = defaultAnsiRegistry, optio
1626
1592
  countingWctx.focusCounter = counter;
1627
1593
  countingWctx.focusables = focusables;
1628
1594
  renderBlocksElement(root.children, width, '', countingWctx);
1629
- return (_jsx(InteractiveRoot, { focusables: focusables, onExit: options?.onExit, color: color, theme: theme, keyLegend: glyphs.keyLegend, viewportRows: options?.viewportRows, width: width, children: content }));
1595
+ return (_jsx(InteractiveRoot, { focusables: focusables, onExit: options?.onExit, onRerun: options?.onRerun, rerunLegend: glyphs.keyLegendRerun, color: color, theme: theme, keyLegend: glyphs.keyLegend, viewportRows: options?.viewportRows, width: width, children: content }));
1630
1596
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markii/ansi",
3
- "version": "0.17.0-next.3",
3
+ "version": "0.17.0-next.5",
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.3",
55
- "@markii/pack": "^0.17.0-next.3",
56
- "@markii/stdlib": "^0.17.0-next.3",
54
+ "@markii/core": "^0.17.0-next.5",
55
+ "@markii/pack": "^0.17.0-next.5",
56
+ "@markii/stdlib": "^0.17.0-next.5",
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.3",
62
+ "@markii/runtime": "^0.17.0-next.5",
63
63
  "@types/react": "^19.2.0"
64
64
  }
65
65
  }