@markii/html 0.16.0 → 0.17.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sadigaxund
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -34,3 +34,26 @@ export declare const NOTICE_ATTRIBUTE = "data-mk-notice";
34
34
  export declare function invalidAttributeValueTitle(directive: string, attribute: string, value: string): string;
35
35
  /** The `title` tooltip for `NOTICE_ATTRIBUTE` on a `figure` with a refused `src`. Ported verbatim from `@markii/react`. */
36
36
  export declare function unsafeImageSrcTitle(directive: string): string;
37
+ /** The diagnostic text for a host `renderCode` callback that threw; the code block rendered as usual. Identical across the engines. */
38
+ export declare function codeRendererFailedTitle(): string;
39
+ /** The diagnostic text for a registered block component written as an inline directive, which renders as the inline fallback instead. */
40
+ export declare function formMismatchTitle(directive: string): string;
41
+ /**
42
+ * The words of a fallback label for a directive that names a component of
43
+ * an installed pack with no view in this engine. Followed by the directive
44
+ * name in a `<code>` element. A static HTML file is what this engine makes,
45
+ * so that is what the label calls the missing view.
46
+ */
47
+ export declare const NO_ENGINE_VIEW_LABEL = "no HTML view for";
48
+ /**
49
+ * The diagnostic text for the same case (also the fallback box's tooltip):
50
+ * the pack, and the engines the component does ship a view for.
51
+ */
52
+ export declare function noEngineViewTitle(directive: string, pack: string, engines: readonly string[]): string;
53
+ /**
54
+ * The visible separator between an inline fallback's label and its content,
55
+ * emitted as real text (`mk-unknown__separator`) so a copy or a plain-text
56
+ * export reads the same as `@markii/ansi`'s output. An inline fallback with
57
+ * no content shows the bare label and no separator.
58
+ */
59
+ export declare const INLINE_FALLBACK_SEPARATOR = ": ";
@@ -82,3 +82,35 @@ export function invalidAttributeValueTitle(directive, attribute, value) {
82
82
  export function unsafeImageSrcTitle(directive) {
83
83
  return `${directive}: image source was refused as unsafe and was not shown`;
84
84
  }
85
+ /** The diagnostic text for a host `renderCode` callback that threw; the code block rendered as usual. Identical across the engines. */
86
+ export function codeRendererFailedTitle() {
87
+ return 'code block: the host code renderer failed, so the plain block was shown';
88
+ }
89
+ /** The diagnostic text for a registered block component written as an inline directive, which renders as the inline fallback instead. */
90
+ export function formMismatchTitle(directive) {
91
+ return `${directive}: block component was written inline and was not rendered`;
92
+ }
93
+ /**
94
+ * The words of a fallback label for a directive that names a component of
95
+ * an installed pack with no view in this engine. Followed by the directive
96
+ * name in a `<code>` element. A static HTML file is what this engine makes,
97
+ * so that is what the label calls the missing view.
98
+ */
99
+ export const NO_ENGINE_VIEW_LABEL = 'no HTML view for';
100
+ /**
101
+ * The diagnostic text for the same case (also the fallback box's tooltip):
102
+ * the pack, and the engines the component does ship a view for.
103
+ */
104
+ export function noEngineViewTitle(directive, pack, engines) {
105
+ const has = engines.length > 0
106
+ ? `it has views for: ${engines.join(', ')}`
107
+ : 'it has no views for any engine';
108
+ return `The ${pack} pack has no HTML view for this component; ${has}`;
109
+ }
110
+ /**
111
+ * The visible separator between an inline fallback's label and its content,
112
+ * emitted as real text (`mk-unknown__separator`) so a copy or a plain-text
113
+ * export reads the same as `@markii/ansi`'s output. An inline fallback with
114
+ * no content shows the bare label and no separator.
115
+ */
116
+ export const INLINE_FALLBACK_SEPARATOR = ': ';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { renderMarkToHtml, renderMarkNodeToHtml, renderMarkInlineToHtml, type RenderMarkOptions, } from './render.js';
1
+ export { renderMarkToHtml, renderMarkNodeToHtml, renderMarkInlineToHtml, type RenderCode, type RenderCodeBlock, type RenderMarkOptions, } from './render.js';
2
2
  export { type ResolveImageSrc } from './image-resolve.js';
3
3
  export { type ResolveHref } from './href-resolve.js';
4
4
  export { escapeHtml } from './escape.js';
package/dist/render.d.ts CHANGED
@@ -2,7 +2,7 @@ import { parse } from '@markii/core';
2
2
  import type { MarkNode } from '@markii/core';
3
3
  import type { ValueStore, VaultStore } from '@markii/runtime';
4
4
  import type { HtmlRegistry } from './registry.js';
5
- import type { OnDiagnostic } from '@markii/stdlib';
5
+ import type { OnDiagnostic, UnavailableComponent } from '@markii/stdlib';
6
6
  import type { ResolveImageSrc } from './image-resolve.js';
7
7
  import type { ResolveHref } from './href-resolve.js';
8
8
  /**
@@ -12,6 +12,15 @@ import type { ResolveHref } from './href-resolve.js';
12
12
  * `@markii/react`'s identical `MarkRoot` type alias.
13
13
  */
14
14
  type MarkRoot = ReturnType<typeof parse>;
15
+ /** A fenced code block as offered to `RenderMarkOptions.renderCode`. */
16
+ export interface RenderCodeBlock {
17
+ /** The info-string language, or `undefined` when the fence names none. */
18
+ readonly lang: string | undefined;
19
+ /** The raw code text, without the fence lines. */
20
+ readonly code: string;
21
+ }
22
+ /** The `renderCode` callback: host HTML for a code block, or `undefined` to render it as usual. */
23
+ export type RenderCode = (code: RenderCodeBlock) => string | undefined;
15
24
  /**
16
25
  * Renders Markii text to a static HTML string using `registry` to resolve
17
26
  * directive names. Pipeline: `@markii/core`'s `toHast` (parse -> tag directive
@@ -65,9 +74,31 @@ type MarkRoot = ReturnType<typeof parse>;
65
74
  * `RenderMarkOptions`, so the two engines cannot diverge.
66
75
  */
67
76
  export interface RenderMarkOptions {
77
+ /**
78
+ * Replaces the output for an ordinary fenced code block. Called with the
79
+ * info-string language (`undefined` when the fence names none) and the
80
+ * raw code text. Return an HTML string to use it, or `undefined` to render
81
+ * the block as usual. Never called for script fences (the collapsed
82
+ * `{name=...}` markers) or for inline code.
83
+ *
84
+ * The returned string is HOST-TRUSTED output and is inserted as-is, with
85
+ * no escaping and no sanitizing: the host owns the escaping. Escape the
86
+ * code text yourself if you wrap it in markup. A callback that throws
87
+ * renders the block as usual and reports a `'code-renderer-failed'`
88
+ * event through `onDiagnostic`.
89
+ */
90
+ readonly renderCode?: RenderCode;
68
91
  readonly resolveImageSrc?: ResolveImageSrc;
69
92
  readonly resolveHref?: ResolveHref;
70
93
  readonly onDiagnostic?: OnDiagnostic;
94
+ /**
95
+ * Components of installed packs that this render cannot draw, as plain
96
+ * data from the host. A directive named here renders the usual fallback
97
+ * box labeled "no HTML view for" instead of "unknown component", and
98
+ * `onDiagnostic` gets a `'no-engine-view'` event. Omitted, the render is
99
+ * unchanged.
100
+ */
101
+ readonly unavailableComponents?: readonly UnavailableComponent[];
71
102
  /**
72
103
  * The note's own last-run values, the same thing the third positional
73
104
  * parameter takes. Offered here as well so a caller that already builds
package/dist/render.js CHANGED
@@ -4,8 +4,8 @@ import { readRegistryComponent, registryLayoutAxis, resolveDirectiveAlias, } fro
4
4
  import { resolveLayoutAttributes } from './layout.js';
5
5
  import { escapeHtml } from './escape.js';
6
6
  import { resolveScopedPath } from './resolve.js';
7
- import { failureKindClass, failureTitle, EMPTY_INLINE_MARKER_CLASS, emptyInlineTitle, invalidAttributeValueTitle, } from './failure-presentation.js';
8
- import { formatValue, getContract, reportDiagnostic, INTERACTIVE_ATTRIBUTE, } from '@markii/stdlib';
7
+ import { failureKindClass, failureTitle, EMPTY_INLINE_MARKER_CLASS, emptyInlineTitle, codeRendererFailedTitle, formMismatchTitle, INLINE_FALLBACK_SEPARATOR, invalidAttributeValueTitle, NO_ENGINE_VIEW_LABEL, noEngineViewTitle, } from './failure-presentation.js';
8
+ import { createUnavailableComponentLookup, formatValue, getContract, reportDiagnostic, INTERACTIVE_ATTRIBUTE, } from '@markii/stdlib';
9
9
  import { resolveImageAttribute } from './image-resolve.js';
10
10
  import { resolveHrefAttribute } from './href-resolve.js';
11
11
  function isMarkRoot(node) {
@@ -213,6 +213,8 @@ function isRegisteredInline(entry) {
213
213
  /** The fallback label line, worded for the reason and the form the directive was written in. Returns HTML. */
214
214
  function fallbackLabel(name, inline, reason) {
215
215
  const code = `<code>${escapeHtml(name)}</code>`;
216
+ if (reason === 'no-engine-view')
217
+ return `${NO_ENGINE_VIEW_LABEL} ${code}`;
216
218
  if (reason === 'form-mismatch') {
217
219
  return inline
218
220
  ? `block component ${code} written inline`
@@ -220,24 +222,39 @@ function fallbackLabel(name, inline, reason) {
220
222
  }
221
223
  return `unknown component ${code}`;
222
224
  }
225
+ /**
226
+ * The separator an inline fallback's label ends with when the fallback has
227
+ * content to show after it, matching `@markii/ansi`'s `label: content`.
228
+ * Nested inside the label span so it takes the label's styling.
229
+ */
230
+ function inlineSeparator(childrenHtml) {
231
+ return childrenHtml.trim() === ''
232
+ ? ''
233
+ : `<span class="mk-unknown__separator">${INLINE_FALLBACK_SEPARATOR}</span>`;
234
+ }
223
235
  /**
224
236
  * The unknown-directive fallback, matching `@markii/react`'s `UnknownDirective`
225
237
  * markup and class names byte-for-byte so one stylesheet covers both renderers.
226
238
  * An inline directive is built from `<span>`s (it lives inside a paragraph); a
227
239
  * block directive gets the dashed `<div>` box with its inner content preserved.
228
240
  */
229
- function unknownDirective(name, inline, childrenHtml, reason) {
230
- const reasonClass = reason === 'form-mismatch' ? ' mk-unknown--mismatch' : '';
241
+ function unknownDirective(name, inline, childrenHtml, reason, title) {
242
+ const reasonClass = reason === 'form-mismatch'
243
+ ? ' mk-unknown--mismatch'
244
+ : reason === 'no-engine-view'
245
+ ? ' mk-unknown--no-view'
246
+ : '';
247
+ const titleAttr = title === undefined ? '' : ` title="${escapeHtml(title)}"`;
231
248
  const label = fallbackLabel(name, inline, reason);
232
249
  if (inline) {
233
- return (`<span class="mk-unknown mk-unknown--inline${reasonClass}">` +
234
- `<span class="mk-unknown__label">${label}</span>` +
250
+ return (`<span class="mk-unknown mk-unknown--inline${reasonClass}"${titleAttr}>` +
251
+ `<span class="mk-unknown__label">${label}${inlineSeparator(childrenHtml)}</span>` +
235
252
  `${childrenHtml}</span>`);
236
253
  }
237
254
  const content = childrenHtml
238
255
  ? `<div class="mk-unknown__content">${childrenHtml}</div>`
239
256
  : '';
240
- return (`<div class="mk-unknown mk-unknown--block${reasonClass}">` +
257
+ return (`<div class="mk-unknown mk-unknown--block${reasonClass}"${titleAttr}>` +
241
258
  `<p class="mk-unknown__label">${label}</p>${content}</div>`);
242
259
  }
243
260
  /**
@@ -251,7 +268,7 @@ function componentError(name, inline, childrenHtml) {
251
268
  const code = `<code>${escapeHtml(name)}</code>`;
252
269
  if (inline) {
253
270
  return (`<span class="mk-unknown mk-unknown--inline mk-unknown--error">` +
254
- `<span class="mk-unknown__label">component ${code} failed to render</span>` +
271
+ `<span class="mk-unknown__label">component ${code} failed to render${inlineSeparator(childrenHtml)}</span>` +
255
272
  `${childrenHtml}</span>`);
256
273
  }
257
274
  const content = childrenHtml
@@ -365,7 +382,7 @@ function invalidEnumAttribute(name, attributes) {
365
382
  return undefined;
366
383
  }
367
384
  /** Resolves one directive (registry component, `:value[...]`, or the fallback) given its layout-stripped attributes. Never throws. */
368
- function renderDirectiveContent(name, kind, attributes, childrenHtml, plainLabel, registry, ctx, scope, layoutClassName) {
385
+ function renderDirectiveContent(name, kind, attributes, childrenHtml, plainLabel, registry, ctx, scope, layoutClassName, unavailable) {
369
386
  if (name === VALUE_DIRECTIVE_NAME) {
370
387
  return ctx.valueMarker(plainLabel, attributes.format ?? undefined, attributes.decimals ?? undefined);
371
388
  }
@@ -373,9 +390,24 @@ function renderDirectiveContent(name, kind, attributes, childrenHtml, plainLabel
373
390
  const entry = Object.hasOwn(registry, name) ? registry[name] : undefined;
374
391
  const component = readRegistryComponent(entry);
375
392
  if (!entry || component == null) {
393
+ const missingView = unavailable?.(name);
394
+ if (missingView !== undefined) {
395
+ const title = noEngineViewTitle(name, missingView.pack, missingView.engines);
396
+ reportDiagnostic(ctx.onDiagnostic, {
397
+ kind: 'no-engine-view',
398
+ directive: name,
399
+ message: title,
400
+ });
401
+ return unknownDirective(name, inline, childrenHtml, 'no-engine-view', title);
402
+ }
376
403
  return unknownDirective(name || '(unnamed)', inline, childrenHtml, 'unregistered');
377
404
  }
378
405
  if (isFormMismatch(entry, kind)) {
406
+ reportDiagnostic(ctx.onDiagnostic, {
407
+ kind: 'form-mismatch',
408
+ directive: name,
409
+ message: formMismatchTitle(name),
410
+ });
379
411
  return unknownDirective(name || '(unnamed)', inline, childrenHtml, 'form-mismatch');
380
412
  }
381
413
  const binding = resolveDataAttribute(attributes, scope);
@@ -417,7 +449,7 @@ function renderDirectiveContent(name, kind, attributes, childrenHtml, plainLabel
417
449
  return rendered;
418
450
  }
419
451
  /** Turns one `<mk-directive>` element (children already transformed) into its HTML string, including the layout wrapper for block directives. */
420
- function renderDirective(element, registry, ctx, scope) {
452
+ function renderDirective(element, registry, ctx, scope, unavailable) {
421
453
  const written = stringProperty(element, 'data-mk-name') ?? '';
422
454
  const kind = stringProperty(element, 'data-mk-kind');
423
455
  const rawAttributes = parseAttributes(stringProperty(element, 'data-mk-attrs'));
@@ -436,7 +468,7 @@ function renderDirective(element, registry, ctx, scope) {
436
468
  const ownedAxis = registryLayoutAxis(registry, name);
437
469
  const { attributes, className } = resolveLayoutAttributes(aliased, ownedAxis);
438
470
  const isLayoutScope = ownedAxis !== undefined && isBlock;
439
- const content = renderDirectiveContent(name, kind, attributes, childrenHtml, plainLabel, registry, ctx, scope, isLayoutScope ? className : undefined);
471
+ const content = renderDirectiveContent(name, kind, attributes, childrenHtml, plainLabel, registry, ctx, scope, isLayoutScope ? className : undefined, unavailable);
440
472
  return isBlock && className && !isLayoutScope
441
473
  ? `<div class="${escapeHtml(className)}">${content}</div>`
442
474
  : content;
@@ -475,17 +507,48 @@ function applyHrefResolver(node, resolveHref) {
475
507
  node.properties.href = resolveHrefAttribute(href, resolveHref);
476
508
  }
477
509
  }
478
- function makeTransform(registry, ctx, scope, resolveHref) {
510
+ /**
511
+ * Offers one ordinary fenced code block (a `<pre>` holding a `<code>`) to the
512
+ * host's `renderCode`. Returns the host's HTML, or `undefined` to render the
513
+ * block as usual: no callback, a callback that declines, or one that throws
514
+ * (reported through `onDiagnostic`, never breaking the render). A callback
515
+ * result that is not a string is treated as declining.
516
+ */
517
+ function applyRenderCode(node, renderCode, onDiagnostic) {
518
+ if (!renderCode)
519
+ return undefined;
520
+ const codeNode = findChildElement(node, 'code');
521
+ if (!codeNode)
522
+ return undefined;
523
+ try {
524
+ const result = renderCode({
525
+ lang: getLanguage(codeNode) || undefined,
526
+ code: getCodeText(codeNode),
527
+ });
528
+ return typeof result === 'string' ? result : undefined;
529
+ }
530
+ catch {
531
+ reportDiagnostic(onDiagnostic, {
532
+ kind: 'code-renderer-failed',
533
+ message: codeRendererFailedTitle(),
534
+ });
535
+ return undefined;
536
+ }
537
+ }
538
+ function makeTransform(registry, ctx, scope, resolveHref, unavailable, renderCode) {
479
539
  function transform(node) {
480
540
  if (node.type !== 'element')
481
541
  return node;
482
542
  node.children = node.children.map((child) => transform(child));
483
543
  if (node.tagName === DIRECTIVE_TAG)
484
- return raw(renderDirective(node, registry, ctx, scope));
544
+ return raw(renderDirective(node, registry, ctx, scope, unavailable));
485
545
  if (node.tagName === 'pre') {
486
546
  const marker = renderScriptMarker(node);
487
547
  if (marker !== undefined)
488
548
  return raw(marker);
549
+ const custom = applyRenderCode(node, renderCode, ctx.onDiagnostic);
550
+ if (custom !== undefined)
551
+ return raw(custom);
489
552
  }
490
553
  if (node.tagName === 'img') {
491
554
  applyImageResolver(node, ctx.resolveImageSrc);
@@ -504,15 +567,15 @@ function renderFailureFallback(error) {
504
567
  `<p class="mk-unknown__label">failed to render document</p>` +
505
568
  `<pre class="mk-unknown__content">${escapeHtml(message)}</pre></div>`);
506
569
  }
507
- function renderRoot(root, registry, scope, resolveImageSrc, resolveHref, onDiagnostic) {
570
+ function renderRoot(root, registry, scope, resolveImageSrc, resolveHref, onDiagnostic, unavailable, renderCode) {
508
571
  const ctx = createBaseContext(scope, resolveImageSrc, onDiagnostic);
509
- const transform = makeTransform(registry, ctx, scope, resolveHref);
572
+ const transform = makeTransform(registry, ctx, scope, resolveHref, unavailable, renderCode);
510
573
  root.children = root.children.map(transform);
511
574
  return serialize(root.children);
512
575
  }
513
576
  export function renderMarkToHtml(text, registry, store, vault, options) {
514
577
  try {
515
- return renderRoot(toHast(text), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic);
578
+ return renderRoot(toHast(text), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, createUnavailableComponentLookup(options?.unavailableComponents), options?.renderCode);
516
579
  }
517
580
  catch (error) {
518
581
  return renderFailureFallback(error);
@@ -533,7 +596,7 @@ export function renderMarkToHtml(text, registry, store, vault, options) {
533
596
  */
534
597
  export function renderMarkNodeToHtml(node, registry, store, vault, options) {
535
598
  try {
536
- return renderRoot(nodeOrRootToHast(node), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic);
599
+ return renderRoot(nodeOrRootToHast(node), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, createUnavailableComponentLookup(options?.unavailableComponents), options?.renderCode);
537
600
  }
538
601
  catch (error) {
539
602
  return renderFailureFallback(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markii/html",
3
- "version": "0.16.0",
3
+ "version": "0.17.0-next.1",
4
4
  "description": "A framework-free static HTML renderer for Markii (.mk.md): a registry-driven hast-to-HTML string engine. Zero React; for stopped-changing documents (publish, CI, email, archive).",
5
5
  "keywords": [
6
6
  "markdown",
@@ -54,9 +54,9 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "hast-util-to-html": "^9.0.0",
57
- "@markii/core": "^0.16.0",
58
- "@markii/runtime": "^0.16.0",
59
- "@markii/stdlib": "^0.16.0"
57
+ "@markii/core": "^0.17.0-next.1",
58
+ "@markii/runtime": "^0.17.0-next.1",
59
+ "@markii/stdlib": "^0.17.0-next.1"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/hast": "^3.0.4"