@markii/html 0.16.0 → 0.17.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE 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,24 @@ 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 registered block component written as an inline directive, which renders as the inline fallback instead. */
38
+ export declare function formMismatchTitle(directive: string): string;
39
+ /**
40
+ * The words of a fallback label for a directive that names a component of
41
+ * an installed pack with no view in this engine. Followed by the directive
42
+ * name in a `<code>` element. A static HTML file is what this engine makes,
43
+ * so that is what the label calls the missing view.
44
+ */
45
+ export declare const NO_ENGINE_VIEW_LABEL = "no HTML view for";
46
+ /**
47
+ * The diagnostic text for the same case (also the fallback box's tooltip):
48
+ * the pack, and the engines the component does ship a view for.
49
+ */
50
+ export declare function noEngineViewTitle(directive: string, pack: string, engines: readonly string[]): string;
51
+ /**
52
+ * The visible separator between an inline fallback's label and its content,
53
+ * emitted as real text (`mk-unknown__separator`) so a copy or a plain-text
54
+ * export reads the same as `@markii/ansi`'s output. An inline fallback with
55
+ * no content shows the bare label and no separator.
56
+ */
57
+ export declare const INLINE_FALLBACK_SEPARATOR = ": ";
@@ -82,3 +82,31 @@ 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 registered block component written as an inline directive, which renders as the inline fallback instead. */
86
+ export function formMismatchTitle(directive) {
87
+ return `${directive}: block component was written inline and was not rendered`;
88
+ }
89
+ /**
90
+ * The words of a fallback label for a directive that names a component of
91
+ * an installed pack with no view in this engine. Followed by the directive
92
+ * name in a `<code>` element. A static HTML file is what this engine makes,
93
+ * so that is what the label calls the missing view.
94
+ */
95
+ export const NO_ENGINE_VIEW_LABEL = 'no HTML view for';
96
+ /**
97
+ * The diagnostic text for the same case (also the fallback box's tooltip):
98
+ * the pack, and the engines the component does ship a view for.
99
+ */
100
+ export function noEngineViewTitle(directive, pack, engines) {
101
+ const has = engines.length > 0
102
+ ? `it has views for: ${engines.join(', ')}`
103
+ : 'it has no views for any engine';
104
+ return `The ${pack} pack has no HTML view for this component; ${has}`;
105
+ }
106
+ /**
107
+ * The visible separator between an inline fallback's label and its content,
108
+ * emitted as real text (`mk-unknown__separator`) so a copy or a plain-text
109
+ * export reads the same as `@markii/ansi`'s output. An inline fallback with
110
+ * no content shows the bare label and no separator.
111
+ */
112
+ export const INLINE_FALLBACK_SEPARATOR = ': ';
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
  /**
@@ -68,6 +68,14 @@ export interface RenderMarkOptions {
68
68
  readonly resolveImageSrc?: ResolveImageSrc;
69
69
  readonly resolveHref?: ResolveHref;
70
70
  readonly onDiagnostic?: OnDiagnostic;
71
+ /**
72
+ * Components of installed packs that this render cannot draw, as plain
73
+ * data from the host. A directive named here renders the usual fallback
74
+ * box labeled "no HTML view for" instead of "unknown component", and
75
+ * `onDiagnostic` gets a `'no-engine-view'` event. Omitted, the render is
76
+ * unchanged.
77
+ */
78
+ readonly unavailableComponents?: readonly UnavailableComponent[];
71
79
  /**
72
80
  * The note's own last-run values, the same thing the third positional
73
81
  * 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, 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,13 +507,13 @@ function applyHrefResolver(node, resolveHref) {
475
507
  node.properties.href = resolveHrefAttribute(href, resolveHref);
476
508
  }
477
509
  }
478
- function makeTransform(registry, ctx, scope, resolveHref) {
510
+ function makeTransform(registry, ctx, scope, resolveHref, unavailable) {
479
511
  function transform(node) {
480
512
  if (node.type !== 'element')
481
513
  return node;
482
514
  node.children = node.children.map((child) => transform(child));
483
515
  if (node.tagName === DIRECTIVE_TAG)
484
- return raw(renderDirective(node, registry, ctx, scope));
516
+ return raw(renderDirective(node, registry, ctx, scope, unavailable));
485
517
  if (node.tagName === 'pre') {
486
518
  const marker = renderScriptMarker(node);
487
519
  if (marker !== undefined)
@@ -504,15 +536,15 @@ function renderFailureFallback(error) {
504
536
  `<p class="mk-unknown__label">failed to render document</p>` +
505
537
  `<pre class="mk-unknown__content">${escapeHtml(message)}</pre></div>`);
506
538
  }
507
- function renderRoot(root, registry, scope, resolveImageSrc, resolveHref, onDiagnostic) {
539
+ function renderRoot(root, registry, scope, resolveImageSrc, resolveHref, onDiagnostic, unavailable) {
508
540
  const ctx = createBaseContext(scope, resolveImageSrc, onDiagnostic);
509
- const transform = makeTransform(registry, ctx, scope, resolveHref);
541
+ const transform = makeTransform(registry, ctx, scope, resolveHref, unavailable);
510
542
  root.children = root.children.map(transform);
511
543
  return serialize(root.children);
512
544
  }
513
545
  export function renderMarkToHtml(text, registry, store, vault, options) {
514
546
  try {
515
- return renderRoot(toHast(text), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic);
547
+ return renderRoot(toHast(text), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, createUnavailableComponentLookup(options?.unavailableComponents));
516
548
  }
517
549
  catch (error) {
518
550
  return renderFailureFallback(error);
@@ -533,7 +565,7 @@ export function renderMarkToHtml(text, registry, store, vault, options) {
533
565
  */
534
566
  export function renderMarkNodeToHtml(node, registry, store, vault, options) {
535
567
  try {
536
- return renderRoot(nodeOrRootToHast(node), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic);
568
+ return renderRoot(nodeOrRootToHast(node), registry, { store: options?.store ?? store, vault: options?.vault ?? vault }, options?.resolveImageSrc, options?.resolveHref, options?.onDiagnostic, createUnavailableComponentLookup(options?.unavailableComponents));
537
569
  }
538
570
  catch (error) {
539
571
  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.0",
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.0",
58
+ "@markii/runtime": "^0.17.0-next.0",
59
+ "@markii/stdlib": "^0.17.0-next.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/hast": "^3.0.4"