@markii/html 0.6.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.
Files changed (55) hide show
  1. package/dist/components/badge.d.ts +9 -0
  2. package/dist/components/badge.js +24 -0
  3. package/dist/components/callout.d.ts +10 -0
  4. package/dist/components/callout.js +29 -0
  5. package/dist/components/card.d.ts +9 -0
  6. package/dist/components/card.js +15 -0
  7. package/dist/components/cell.d.ts +9 -0
  8. package/dist/components/cell.js +10 -0
  9. package/dist/components/chart.d.ts +15 -0
  10. package/dist/components/chart.js +158 -0
  11. package/dist/components/details.d.ts +10 -0
  12. package/dist/components/details.js +16 -0
  13. package/dist/components/figure.d.ts +18 -0
  14. package/dist/components/figure.js +28 -0
  15. package/dist/components/index.d.ts +27 -0
  16. package/dist/components/index.js +91 -0
  17. package/dist/components/kbd.d.ts +7 -0
  18. package/dist/components/kbd.js +8 -0
  19. package/dist/components/layout-wrapper.d.ts +23 -0
  20. package/dist/components/layout-wrapper.js +45 -0
  21. package/dist/components/progress.d.ts +13 -0
  22. package/dist/components/progress.js +79 -0
  23. package/dist/components/rating.d.ts +9 -0
  24. package/dist/components/rating.js +33 -0
  25. package/dist/components/row.d.ts +9 -0
  26. package/dist/components/row.js +19 -0
  27. package/dist/components/stat.d.ts +18 -0
  28. package/dist/components/stat.js +81 -0
  29. package/dist/components/tab.d.ts +18 -0
  30. package/dist/components/tab.js +19 -0
  31. package/dist/components/tabs.d.ts +30 -0
  32. package/dist/components/tabs.js +33 -0
  33. package/dist/doc-css.generated.d.ts +2 -0
  34. package/dist/doc-css.generated.js +5 -0
  35. package/dist/document.d.ts +42 -0
  36. package/dist/document.js +44 -0
  37. package/dist/escape.d.ts +7 -0
  38. package/dist/escape.js +23 -0
  39. package/dist/failure-presentation.d.ts +14 -0
  40. package/dist/failure-presentation.js +56 -0
  41. package/dist/index.d.ts +8 -0
  42. package/dist/index.js +13 -0
  43. package/dist/layout.d.ts +31 -0
  44. package/dist/layout.js +75 -0
  45. package/dist/registry.d.ts +139 -0
  46. package/dist/registry.js +118 -0
  47. package/dist/render.d.ts +34 -0
  48. package/dist/render.js +417 -0
  49. package/dist/resolve.d.ts +59 -0
  50. package/dist/resolve.js +154 -0
  51. package/dist/test/html-context.d.ts +9 -0
  52. package/dist/test/html-context.js +21 -0
  53. package/dist/value-format.d.ts +12 -0
  54. package/dist/value-format.js +41 -0
  55. package/package.json +64 -0
@@ -0,0 +1,31 @@
1
+ import type { DirectiveAttributes } from './registry.js';
2
+ /**
3
+ * The closed set of layout-preset attributes (docs/format.md): a small,
4
+ * non-freeform-CSS vocabulary any block directive can carry regardless of
5
+ * which component renders it. These two keys are reserved: always intercepted
6
+ * before a component sees its `attributes`, whether or not their value turns
7
+ * out to be valid. This mirrors `@markii/react`'s `layout.ts` exactly, so the
8
+ * two renderers strip and classify the same keys the same way.
9
+ */
10
+ export declare const LAYOUT_ATTRIBUTE_KEYS: readonly ["width", "align"];
11
+ export interface ResolvedLayoutAttributes {
12
+ /** `attributes` with every reserved layout key (present, valid or not) removed. */
13
+ attributes: DirectiveAttributes;
14
+ /** Space-joined layout classes, or `undefined` if none applied: never an empty string. */
15
+ className?: string;
16
+ }
17
+ /**
18
+ * Splits docs/format.md's closed layout-attribute set (`width`, `align`) off
19
+ * `attributes`, returning the remaining attributes untouched plus the combined
20
+ * class string those two attributes resolve to, if any.
21
+ *
22
+ * Both keys are stripped whenever the key is present on the input, regardless
23
+ * of whether its value is valid; they are reserved layout attributes, so
24
+ * interception wins over a same-named attribute a component might otherwise
25
+ * read itself. Presence is checked with `Object.hasOwn`, so a directive
26
+ * attribute literally named `constructor` or `__proto__` is never mistaken for
27
+ * a real key via the prototype chain. An invalid or hostile value never
28
+ * produces a class: it is dropped silently, exactly like an absent attribute.
29
+ * Never throws.
30
+ */
31
+ export declare function resolveLayoutAttributes(attributes: DirectiveAttributes): ResolvedLayoutAttributes;
package/dist/layout.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The closed set of layout-preset attributes (docs/format.md): a small,
3
+ * non-freeform-CSS vocabulary any block directive can carry regardless of
4
+ * which component renders it. These two keys are reserved: always intercepted
5
+ * before a component sees its `attributes`, whether or not their value turns
6
+ * out to be valid. This mirrors `@markii/react`'s `layout.ts` exactly, so the
7
+ * two renderers strip and classify the same keys the same way.
8
+ */
9
+ export const LAYOUT_ATTRIBUTE_KEYS = ['width', 'align'];
10
+ /** `width=normal` is the explicit default: it maps to no class, same as an absent `width`. */
11
+ const NORMAL_WIDTH = 'normal';
12
+ /**
13
+ * `width` value -> class. Null-prototype so a hostile value like `__proto__`
14
+ * or `constructor` cannot resolve through the prototype chain to an inherited
15
+ * `Object.prototype` member; it simply misses the lookup, same as any other
16
+ * unrecognized value.
17
+ */
18
+ const WIDTH_CLASSES = Object.assign(Object.create(null), {
19
+ narrow: 'mk-width-narrow',
20
+ wide: 'mk-width-wide',
21
+ full: 'mk-width-full',
22
+ });
23
+ /** `align` value -> class. Same null-prototype defense as `WIDTH_CLASSES`. */
24
+ const ALIGN_CLASSES = Object.assign(Object.create(null), {
25
+ left: 'mk-align-left',
26
+ center: 'mk-align-center',
27
+ right: 'mk-align-right',
28
+ });
29
+ function widthClassFor(value) {
30
+ if (value === null || value === undefined || value === '')
31
+ return undefined;
32
+ if (value === NORMAL_WIDTH)
33
+ return undefined;
34
+ return WIDTH_CLASSES[value];
35
+ }
36
+ function alignClassFor(value) {
37
+ if (value === null || value === undefined || value === '')
38
+ return undefined;
39
+ return ALIGN_CLASSES[value];
40
+ }
41
+ /**
42
+ * Splits docs/format.md's closed layout-attribute set (`width`, `align`) off
43
+ * `attributes`, returning the remaining attributes untouched plus the combined
44
+ * class string those two attributes resolve to, if any.
45
+ *
46
+ * Both keys are stripped whenever the key is present on the input, regardless
47
+ * of whether its value is valid; they are reserved layout attributes, so
48
+ * interception wins over a same-named attribute a component might otherwise
49
+ * read itself. Presence is checked with `Object.hasOwn`, so a directive
50
+ * attribute literally named `constructor` or `__proto__` is never mistaken for
51
+ * a real key via the prototype chain. An invalid or hostile value never
52
+ * produces a class: it is dropped silently, exactly like an absent attribute.
53
+ * Never throws.
54
+ */
55
+ export function resolveLayoutAttributes(attributes) {
56
+ let rest = attributes;
57
+ const classes = [];
58
+ if (Object.hasOwn(rest, 'width')) {
59
+ const { width, ...remainder } = rest;
60
+ rest = remainder;
61
+ const widthClass = widthClassFor(width);
62
+ if (widthClass)
63
+ classes.push(widthClass);
64
+ }
65
+ if (Object.hasOwn(rest, 'align')) {
66
+ const { align, ...remainder } = rest;
67
+ rest = remainder;
68
+ const alignClass = alignClassFor(align);
69
+ if (alignClass)
70
+ classes.push(alignClass);
71
+ }
72
+ return classes.length > 0
73
+ ? { attributes: rest, className: classes.join(' ') }
74
+ : { attributes: rest };
75
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The registry contract for the HTML engine: the string-emitting twin of
3
+ * `@markii/react`'s `registry.ts`. Everything here is framework-free by
4
+ * construction (no React types), because a component is a plain function
5
+ * from attributes and already-rendered children to an HTML string. The
6
+ * alias, merge, and hostile-configuration rules are deliberately identical to
7
+ * the React renderer's, so a note resolves the same way in both.
8
+ */
9
+ import type { FailureKind, ValueStatus } from '@markii/runtime';
10
+ /**
11
+ * Attributes parsed off a directive, e.g. `{type=warning title="Careful"}`. A
12
+ * bare attribute (present but valueless, e.g. `{collapsed}`) arrives as
13
+ * `null`. A key that was never written is simply absent.
14
+ */
15
+ export type DirectiveAttributes = Record<string, string | null | undefined>;
16
+ /**
17
+ * A `data=`/`:value[...]` name resolved against the render's value store
18
+ * (and, for an `@`-prefixed name, its vault) — the string engine's read-only
19
+ * view of `./resolve.js`'s `StorePathResolution`. Never carries a `Proxy` or
20
+ * any other live handle: `value` is whatever the store returned, but
21
+ * `status`/`error`/`failureKind` are already validated primitives.
22
+ */
23
+ export interface ValueResolution {
24
+ value: unknown;
25
+ status: ValueStatus;
26
+ error?: string;
27
+ failureKind?: FailureKind;
28
+ }
29
+ /**
30
+ * The render context handed to every component. `esc` is the engine's single
31
+ * HTML-escaping primitive (see `./escape`), so a component never hand-rolls
32
+ * escaping.
33
+ *
34
+ * `resolve` looks up a `data=`-style name (dotted paths, `@`-prefixed vault
35
+ * names) against the store/vault the current render was called with; it
36
+ * degrades to `{ value: undefined, status: 'missing' }` when there is no
37
+ * store/vault, or the name doesn't resolve — it never throws. `valueMarker`
38
+ * is the empty/stale-state presentation for a resolved name, matching
39
+ * `@markii/react`'s `ValueDirective` exactly (the missing-value `{name}`
40
+ * span, the stale underline, the failure-kind tooltip); it is what powers
41
+ * the `:value[...]` built-in and is exposed here so a data-bound component
42
+ * can render the identical marker for a name it resolves itself.
43
+ *
44
+ * `data`/`dataStatus`/`dataError`/`dataFailureKind` mirror `@markii/react`'s
45
+ * `MarkComponentProps` fields, just carried on `ctx` instead of a fourth
46
+ * function parameter (the `HtmlComponent` signature is `(attributes,
47
+ * childrenHtml, ctx)`, with no room for a fifth argument). They are present
48
+ * ONLY when the directive actually had a `data=` attribute — `dataStatus`
49
+ * is always one of the four `ValueStatus` values in that case, even when the
50
+ * name didn't resolve (`'missing'`); all four are `undefined` together when
51
+ * there was no `data=` attribute at all, exactly like the `'data' in
52
+ * binding` distinction `@markii/react`'s `renderDirectiveContent` makes.
53
+ */
54
+ export interface HtmlRenderContext {
55
+ /** HTML-escapes a string for safe insertion into text or a quoted attribute value. */
56
+ esc(value: string): string;
57
+ /** Resolves a `data=`/`:value[...]` name against the current render's store/vault. Never throws. */
58
+ resolve(name: string): ValueResolution;
59
+ /** The quiet missing/stale/failure-tinted marker for `name`, matching `@markii/react`'s `ValueDirective` markup exactly. Never throws. */
60
+ valueMarker(name: string): string;
61
+ data?: unknown;
62
+ dataStatus?: ValueStatus;
63
+ dataError?: string;
64
+ dataFailureKind?: FailureKind;
65
+ }
66
+ /**
67
+ * One registry component: receives the directive's raw string attributes
68
+ * (bare attributes as `null`), its inner markdown already rendered to an HTML
69
+ * string, and the render context, and returns the HTML string to emit.
70
+ * Attribute parsing, validation, and defaulting are the component's own job,
71
+ * exactly as in the React contract.
72
+ */
73
+ export type HtmlComponent = (attributes: DirectiveAttributes, childrenHtml: string, ctx: HtmlRenderContext) => string;
74
+ /**
75
+ * One registry entry: the component plus whether it is meant to be used
76
+ * inline (text directive) vs as a block (leaf/container directive). Only an
77
+ * explicit `inline: false` drives the form/kind mismatch rule; `undefined`
78
+ * says nothing about kind.
79
+ */
80
+ export interface HtmlRegistryEntry {
81
+ component: HtmlComponent;
82
+ inline?: boolean;
83
+ }
84
+ /** One alias: a second name for an existing component, optionally carrying preset attributes. */
85
+ export interface RegistryAlias {
86
+ name: string;
87
+ attributes?: DirectiveAttributes;
88
+ }
89
+ /** Alias name -> what it stands for. */
90
+ export type RegistryAliases = Record<string, RegistryAlias>;
91
+ /**
92
+ * The symbol an alias table hangs off a registry under: a symbol rather than
93
+ * a string key so it can never collide with a directive name, never show up
94
+ * in `Object.keys`, and still ride across `Object.assign` (so `mergeHtml`
95
+ * `Registries` carries it). Mirrors `@markii/react`'s `REGISTRY_ALIASES`.
96
+ */
97
+ export declare const REGISTRY_ALIASES: unique symbol;
98
+ /** Directive name -> component registration, plus an optional alias table under `REGISTRY_ALIASES`. */
99
+ export interface HtmlRegistry {
100
+ [name: string]: HtmlRegistryEntry;
101
+ [REGISTRY_ALIASES]?: RegistryAliases;
102
+ }
103
+ /** Reads a registry's alias table, or `undefined` when it has none. Returned as-is; treat as read-only. */
104
+ export declare function registryAliases(registry: HtmlRegistry): RegistryAliases | undefined;
105
+ /**
106
+ * Creates a registry from a plain object of entries plus an optional alias
107
+ * table. The returned map has a `null` prototype so a directive named
108
+ * `constructor`, `toString`, `hasOwnProperty`, etc. cannot resolve to an
109
+ * inherited member; only entries actually registered are ever found.
110
+ */
111
+ export declare function createHtmlRegistry(entries?: HtmlRegistry, aliases?: RegistryAliases): HtmlRegistry;
112
+ /**
113
+ * Merges any number of registries, later ones taking precedence, into a
114
+ * null-prototype map. Alias tables merge per name, not wholesale, so a later
115
+ * registry that defines any alias does not silently drop earlier ones.
116
+ */
117
+ export declare function mergeHtmlRegistries(...registries: HtmlRegistry[]): HtmlRegistry;
118
+ /**
119
+ * Reads `entry.component`, or `undefined` if `entry` is nullish or the read
120
+ * itself throws. A hand-built registry can define `component` as a throwing
121
+ * getter (or a trapping `Proxy`); a throwing read degrades to "no component
122
+ * here", identical to a genuinely absent one, never an exception escaping the
123
+ * renderer (docs/spec.md requirement 4).
124
+ */
125
+ export declare function readRegistryComponent(entry: HtmlRegistryEntry | undefined): HtmlComponent | undefined;
126
+ /** A directive name and attributes after alias resolution. */
127
+ export interface ResolvedDirective {
128
+ name: string;
129
+ attributes: DirectiveAttributes;
130
+ }
131
+ /**
132
+ * Resolves one directive name through the registry's alias table. Four rules,
133
+ * in order: a real component wins over any alias; an unaliased name passes
134
+ * through; an alias is followed exactly one hop (a chain lands on the
135
+ * unknown-directive fallback rather than chaining); author attributes win over
136
+ * the alias's presets. Never throws; a malformed alias degrades to the
137
+ * ordinary unknown-directive path. Identical to `@markii/react`'s rule.
138
+ */
139
+ export declare function resolveDirectiveAlias(registry: HtmlRegistry, name: string, attributes: DirectiveAttributes): ResolvedDirective;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * The registry contract for the HTML engine: the string-emitting twin of
3
+ * `@markii/react`'s `registry.ts`. Everything here is framework-free by
4
+ * construction (no React types), because a component is a plain function
5
+ * from attributes and already-rendered children to an HTML string. The
6
+ * alias, merge, and hostile-configuration rules are deliberately identical to
7
+ * the React renderer's, so a note resolves the same way in both.
8
+ */
9
+ /**
10
+ * The symbol an alias table hangs off a registry under: a symbol rather than
11
+ * a string key so it can never collide with a directive name, never show up
12
+ * in `Object.keys`, and still ride across `Object.assign` (so `mergeHtml`
13
+ * `Registries` carries it). Mirrors `@markii/react`'s `REGISTRY_ALIASES`.
14
+ */
15
+ export const REGISTRY_ALIASES = Symbol('markii.html.registry.aliases');
16
+ /** Reads a registry's alias table, or `undefined` when it has none. Returned as-is; treat as read-only. */
17
+ export function registryAliases(registry) {
18
+ return registry[REGISTRY_ALIASES];
19
+ }
20
+ /**
21
+ * Combines alias tables left-to-right into one null-prototype map, later
22
+ * tables winning per name. Returns `undefined` when no input carried aliases,
23
+ * so an alias-free merge stays alias-free.
24
+ */
25
+ function mergeAliasTables(tables) {
26
+ const present = tables.filter((table) => table !== undefined);
27
+ if (present.length === 0)
28
+ return undefined;
29
+ const merged = Object.create(null);
30
+ for (const table of present) {
31
+ for (const name of Object.keys(table))
32
+ merged[name] = table[name];
33
+ }
34
+ return merged;
35
+ }
36
+ /**
37
+ * Creates a registry from a plain object of entries plus an optional alias
38
+ * table. The returned map has a `null` prototype so a directive named
39
+ * `constructor`, `toString`, `hasOwnProperty`, etc. cannot resolve to an
40
+ * inherited member; only entries actually registered are ever found.
41
+ */
42
+ export function createHtmlRegistry(entries = {}, aliases) {
43
+ const registry = Object.assign(Object.create(null), entries);
44
+ const merged = mergeAliasTables([registryAliases(entries), aliases]);
45
+ if (merged)
46
+ registry[REGISTRY_ALIASES] = merged;
47
+ return registry;
48
+ }
49
+ /**
50
+ * Merges any number of registries, later ones taking precedence, into a
51
+ * null-prototype map. Alias tables merge per name, not wholesale, so a later
52
+ * registry that defines any alias does not silently drop earlier ones.
53
+ */
54
+ export function mergeHtmlRegistries(...registries) {
55
+ const merged = Object.assign(Object.create(null), ...registries);
56
+ const aliases = mergeAliasTables(registries.map(registryAliases));
57
+ if (aliases)
58
+ merged[REGISTRY_ALIASES] = aliases;
59
+ else
60
+ delete merged[REGISTRY_ALIASES];
61
+ return merged;
62
+ }
63
+ /**
64
+ * Reads `entry.component`, or `undefined` if `entry` is nullish or the read
65
+ * itself throws. A hand-built registry can define `component` as a throwing
66
+ * getter (or a trapping `Proxy`); a throwing read degrades to "no component
67
+ * here", identical to a genuinely absent one, never an exception escaping the
68
+ * renderer (docs/spec.md requirement 4).
69
+ */
70
+ export function readRegistryComponent(entry) {
71
+ if (!entry)
72
+ return undefined;
73
+ try {
74
+ return entry.component ?? undefined;
75
+ }
76
+ catch {
77
+ return undefined;
78
+ }
79
+ }
80
+ /** Whether `registry` has a real, usable component under `name` (own property, non-nullish, non-throwing). */
81
+ function hasComponent(registry, name) {
82
+ return (Object.hasOwn(registry, name) &&
83
+ readRegistryComponent(registry[name]) != null);
84
+ }
85
+ /** Merges an alias's preset attributes under the author's own, author winning on collision ("closest to the text wins"). */
86
+ function mergeAliasAttributes(preset, author) {
87
+ if (!preset)
88
+ return author;
89
+ const result = {};
90
+ for (const [key, value] of Object.entries(preset))
91
+ result[key] = value;
92
+ for (const [key, value] of Object.entries(author))
93
+ result[key] = value;
94
+ return result;
95
+ }
96
+ /**
97
+ * Resolves one directive name through the registry's alias table. Four rules,
98
+ * in order: a real component wins over any alias; an unaliased name passes
99
+ * through; an alias is followed exactly one hop (a chain lands on the
100
+ * unknown-directive fallback rather than chaining); author attributes win over
101
+ * the alias's presets. Never throws; a malformed alias degrades to the
102
+ * ordinary unknown-directive path. Identical to `@markii/react`'s rule.
103
+ */
104
+ export function resolveDirectiveAlias(registry, name, attributes) {
105
+ if (hasComponent(registry, name))
106
+ return { name, attributes };
107
+ const aliases = registryAliases(registry);
108
+ if (!aliases || !Object.hasOwn(aliases, name))
109
+ return { name, attributes };
110
+ const alias = aliases[name];
111
+ if (typeof alias?.name !== 'string' || alias.name === '') {
112
+ return { name, attributes };
113
+ }
114
+ return {
115
+ name: alias.name,
116
+ attributes: mergeAliasAttributes(alias.attributes, attributes),
117
+ };
118
+ }
@@ -0,0 +1,34 @@
1
+ import type { MarkNode } from '@markii/core';
2
+ import type { ValueStore, VaultStore } from '@markii/runtime';
3
+ import type { HtmlRegistry } from './registry.js';
4
+ /**
5
+ * Renders Markii text to a static HTML string using `registry` to resolve
6
+ * directive names. Pipeline: `@markii/core`'s `toHast` (parse -> tag directive
7
+ * nodes -> remark-rehype -> sanitize URLs) -> a hast->HTML walk that swaps
8
+ * directive elements for registry components (or the unknown-directive
9
+ * fallback) and folds script fences into markers. Pure and never-throwing:
10
+ * parsing is tolerant, unknown names always render a fallback, and any
11
+ * unexpected internal error degrades to the "failed to render document" box.
12
+ *
13
+ * `store` is the note's value store (`@markii/runtime`, §8's pure read path)
14
+ * — optional, matching how a missing/absent value degrades gracefully: with
15
+ * no store, `:value[name]` renders its missing-value marker and every
16
+ * `data=name` attribute resolves to `dataStatus: 'missing'`, but the
17
+ * document still renders completely.
18
+ *
19
+ * `vault` is the optional app-scoped read seam (`@markii/runtime`'s
20
+ * `VaultStore`) that an `@`-prefixed name (`data=@gh.stars`,
21
+ * `:value[@gh.stars]`) resolves against instead of `store` — "bare name =
22
+ * mine, `@name` = the vault's". With no `vault` supplied, every `@name`
23
+ * degrades to `'missing'` the same way an absent `store` degrades a bare
24
+ * name.
25
+ */
26
+ export declare function renderMarkToHtml(text: string, registry: HtmlRegistry, store?: ValueStore, vault?: VaultStore): string;
27
+ /**
28
+ * The block-level twin of `renderMarkToHtml`: renders one already-parsed mdast
29
+ * node (`@markii/core`'s `MarkNode`) to HTML instead of a whole document's
30
+ * text, via `nodeToHast`. Same registry resolution, same fallbacks, same
31
+ * purity and never-throw guarantees, and the same optional `store`/`vault`
32
+ * value-binding arguments.
33
+ */
34
+ export declare function renderMarkNodeToHtml(node: MarkNode, registry: HtmlRegistry, store?: ValueStore, vault?: VaultStore): string;