@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.
- package/dist/components/badge.d.ts +9 -0
- package/dist/components/badge.js +24 -0
- package/dist/components/callout.d.ts +10 -0
- package/dist/components/callout.js +29 -0
- package/dist/components/card.d.ts +9 -0
- package/dist/components/card.js +15 -0
- package/dist/components/cell.d.ts +9 -0
- package/dist/components/cell.js +10 -0
- package/dist/components/chart.d.ts +15 -0
- package/dist/components/chart.js +158 -0
- package/dist/components/details.d.ts +10 -0
- package/dist/components/details.js +16 -0
- package/dist/components/figure.d.ts +18 -0
- package/dist/components/figure.js +28 -0
- package/dist/components/index.d.ts +27 -0
- package/dist/components/index.js +91 -0
- package/dist/components/kbd.d.ts +7 -0
- package/dist/components/kbd.js +8 -0
- package/dist/components/layout-wrapper.d.ts +23 -0
- package/dist/components/layout-wrapper.js +45 -0
- package/dist/components/progress.d.ts +13 -0
- package/dist/components/progress.js +79 -0
- package/dist/components/rating.d.ts +9 -0
- package/dist/components/rating.js +33 -0
- package/dist/components/row.d.ts +9 -0
- package/dist/components/row.js +19 -0
- package/dist/components/stat.d.ts +18 -0
- package/dist/components/stat.js +81 -0
- package/dist/components/tab.d.ts +18 -0
- package/dist/components/tab.js +19 -0
- package/dist/components/tabs.d.ts +30 -0
- package/dist/components/tabs.js +33 -0
- package/dist/doc-css.generated.d.ts +2 -0
- package/dist/doc-css.generated.js +5 -0
- package/dist/document.d.ts +42 -0
- package/dist/document.js +44 -0
- package/dist/escape.d.ts +7 -0
- package/dist/escape.js +23 -0
- package/dist/failure-presentation.d.ts +14 -0
- package/dist/failure-presentation.js +56 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +13 -0
- package/dist/layout.d.ts +31 -0
- package/dist/layout.js +75 -0
- package/dist/registry.d.ts +139 -0
- package/dist/registry.js +118 -0
- package/dist/render.d.ts +34 -0
- package/dist/render.js +417 -0
- package/dist/resolve.d.ts +59 -0
- package/dist/resolve.js +154 -0
- package/dist/test/html-context.d.ts +9 -0
- package/dist/test/html-context.js +21 -0
- package/dist/value-format.d.ts +12 -0
- package/dist/value-format.js +41 -0
- package/package.json +64 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HtmlComponent } from '../registry.js';
|
|
2
|
+
export type BadgeVariant = 'neutral' | 'info' | 'success' | 'warning' | 'danger';
|
|
3
|
+
/**
|
|
4
|
+
* `:badge[New]{variant=success}` — a status pill for an inline text
|
|
5
|
+
* directive. Unknown/missing `variant` falls back to `neutral` rather than
|
|
6
|
+
* throwing. Matches `@markii/react`'s `Badge` markup byte-for-byte so one
|
|
7
|
+
* stylesheet covers both renderers.
|
|
8
|
+
*/
|
|
9
|
+
export declare const Badge: HtmlComponent;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const BADGE_VARIANTS = [
|
|
2
|
+
'neutral',
|
|
3
|
+
'info',
|
|
4
|
+
'success',
|
|
5
|
+
'warning',
|
|
6
|
+
'danger',
|
|
7
|
+
];
|
|
8
|
+
const DEFAULT_VARIANT = 'neutral';
|
|
9
|
+
function isBadgeVariant(value) {
|
|
10
|
+
return BADGE_VARIANTS.includes(value);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `:badge[New]{variant=success}` — a status pill for an inline text
|
|
14
|
+
* directive. Unknown/missing `variant` falls back to `neutral` rather than
|
|
15
|
+
* throwing. Matches `@markii/react`'s `Badge` markup byte-for-byte so one
|
|
16
|
+
* stylesheet covers both renderers.
|
|
17
|
+
*/
|
|
18
|
+
export const Badge = (attributes, childrenHtml) => {
|
|
19
|
+
const rawVariant = attributes.variant ?? DEFAULT_VARIANT;
|
|
20
|
+
const variant = isBadgeVariant(rawVariant)
|
|
21
|
+
? rawVariant
|
|
22
|
+
: DEFAULT_VARIANT;
|
|
23
|
+
return `<span class="mk-badge mk-badge--${variant}">${childrenHtml}</span>`;
|
|
24
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HtmlComponent } from '../registry.js';
|
|
2
|
+
export type CalloutType = 'info' | 'warning' | 'danger';
|
|
3
|
+
/**
|
|
4
|
+
* `:::callout{type=info|warning|danger title="..."}` — a colored box for an
|
|
5
|
+
* aside, warning, or danger note. Unknown/missing `type` falls back to
|
|
6
|
+
* `info` rather than throwing. Matches `@markii/react`'s `Callout` markup
|
|
7
|
+
* byte-for-byte so one stylesheet covers both renderers. No outer margin:
|
|
8
|
+
* the document stylesheet owns spacing between this and its siblings.
|
|
9
|
+
*/
|
|
10
|
+
export declare const Callout: HtmlComponent;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const CALLOUT_TYPES = ['info', 'warning', 'danger'];
|
|
2
|
+
const CALLOUT_ICONS = {
|
|
3
|
+
info: 'ℹ',
|
|
4
|
+
warning: '▲',
|
|
5
|
+
danger: '✕',
|
|
6
|
+
};
|
|
7
|
+
function isCalloutType(value) {
|
|
8
|
+
return CALLOUT_TYPES.includes(value);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* `:::callout{type=info|warning|danger title="..."}` — a colored box for an
|
|
12
|
+
* aside, warning, or danger note. Unknown/missing `type` falls back to
|
|
13
|
+
* `info` rather than throwing. Matches `@markii/react`'s `Callout` markup
|
|
14
|
+
* byte-for-byte so one stylesheet covers both renderers. No outer margin:
|
|
15
|
+
* the document stylesheet owns spacing between this and its siblings.
|
|
16
|
+
*/
|
|
17
|
+
export const Callout = (attributes, childrenHtml, ctx) => {
|
|
18
|
+
const rawType = attributes.type ?? 'info';
|
|
19
|
+
const type = isCalloutType(rawType) ? rawType : 'info';
|
|
20
|
+
const title = attributes.title ?? null;
|
|
21
|
+
const titleHtml = title
|
|
22
|
+
? `<span class="mk-callout__title">${ctx.esc(title)}</span>`
|
|
23
|
+
: '';
|
|
24
|
+
return (`<div class="mk-callout mk-callout--${type}" role="note">` +
|
|
25
|
+
`<div class="mk-callout__header">` +
|
|
26
|
+
`<span class="mk-callout__icon" aria-hidden="true">${CALLOUT_ICONS[type]}</span>` +
|
|
27
|
+
`${titleHtml}</div>` +
|
|
28
|
+
`<div class="mk-callout__body">${childrenHtml}</div></div>`);
|
|
29
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HtmlComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `:::card{title="..."} ... :::` — a titled panel. `title` is optional; the
|
|
4
|
+
* title row is omitted entirely (not rendered empty) when absent. Matches
|
|
5
|
+
* `@markii/react`'s `Card` markup byte-for-byte so one stylesheet covers
|
|
6
|
+
* both renderers. No outer margin: the document stylesheet owns spacing
|
|
7
|
+
* between this and its siblings.
|
|
8
|
+
*/
|
|
9
|
+
export declare const Card: HtmlComponent;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `:::card{title="..."} ... :::` — a titled panel. `title` is optional; the
|
|
3
|
+
* title row is omitted entirely (not rendered empty) when absent. Matches
|
|
4
|
+
* `@markii/react`'s `Card` markup byte-for-byte so one stylesheet covers
|
|
5
|
+
* both renderers. No outer margin: the document stylesheet owns spacing
|
|
6
|
+
* between this and its siblings.
|
|
7
|
+
*/
|
|
8
|
+
export const Card = (attributes, childrenHtml, ctx) => {
|
|
9
|
+
const title = attributes.title ?? null;
|
|
10
|
+
const titleHtml = title
|
|
11
|
+
? `<div class="mk-card__title">${ctx.esc(title)}</div>`
|
|
12
|
+
: '';
|
|
13
|
+
return (`<div class="mk-card">${titleHtml}` +
|
|
14
|
+
`<div class="mk-card__body">${childrenHtml}</div></div>`);
|
|
15
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HtmlComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `:::cell ... :::` — a transparent grouping container whose ONLY job is
|
|
4
|
+
* letting several blocks count as ONE cell of `:::row`. Matches
|
|
5
|
+
* `@markii/react`'s `Cell` markup byte-for-byte: a plain `<div class="mk-cell">`
|
|
6
|
+
* with no look of its own (no border, background, padding, or outer margin).
|
|
7
|
+
* Deliberately never reads `attributes`, matching the React component.
|
|
8
|
+
*/
|
|
9
|
+
export declare const Cell: HtmlComponent;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `:::cell ... :::` — a transparent grouping container whose ONLY job is
|
|
3
|
+
* letting several blocks count as ONE cell of `:::row`. Matches
|
|
4
|
+
* `@markii/react`'s `Cell` markup byte-for-byte: a plain `<div class="mk-cell">`
|
|
5
|
+
* with no look of its own (no border, background, padding, or outer margin).
|
|
6
|
+
* Deliberately never reads `attributes`, matching the React component.
|
|
7
|
+
*/
|
|
8
|
+
export const Cell = (_attributes, childrenHtml) => {
|
|
9
|
+
return `<div class="mk-cell">${childrenHtml}</div>`;
|
|
10
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HtmlComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `::chart{kind=line values="1,3,2,5"}` — a minimal hand-rolled inline SVG
|
|
4
|
+
* chart (no chart library; the SVG is a plain string built by this module).
|
|
5
|
+
* Data binding (§8): a bound `data` array of numbers (or `{value}` objects)
|
|
6
|
+
* takes priority over the static `values=` attribute; a numeric-only,
|
|
7
|
+
* defensively-filtered, capped series is what ever reaches the geometry
|
|
8
|
+
* math. All plotted values are numbers formatted as plain digits — no
|
|
9
|
+
* user-controlled text is ever interpolated into the SVG markup.
|
|
10
|
+
* Missing/error binding with no static fallback renders a small neutral
|
|
11
|
+
* empty state rather than a broken or empty `<svg>`. Never throws.
|
|
12
|
+
*
|
|
13
|
+
* Markup and class names match `@markii/react`'s `Chart` byte-for-byte.
|
|
14
|
+
*/
|
|
15
|
+
export declare const Chart: HtmlComponent;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { safeRead } from '../resolve.js';
|
|
2
|
+
import { dataStateClassName, failureTitle } from '../failure-presentation.js';
|
|
3
|
+
const CHART_KINDS = ['line', 'bar'];
|
|
4
|
+
function isChartKind(value) {
|
|
5
|
+
return CHART_KINDS.includes(value);
|
|
6
|
+
}
|
|
7
|
+
const DEFAULT_KIND = 'line';
|
|
8
|
+
const DEFAULT_WIDTH = 240;
|
|
9
|
+
const DEFAULT_HEIGHT = 60;
|
|
10
|
+
const PADDING = 4;
|
|
11
|
+
/**
|
|
12
|
+
* Hard cap on rendered points, independent of source. A hostile or merely
|
|
13
|
+
* huge `data` array must not translate into thousands of SVG nodes.
|
|
14
|
+
*/
|
|
15
|
+
const MAX_POINTS = 200;
|
|
16
|
+
/** Clamps `value` into `[0, 1]`; a non-finite input maps to the neutral mid-point `0.5`. */
|
|
17
|
+
function clamp01(value) {
|
|
18
|
+
if (!Number.isFinite(value))
|
|
19
|
+
return 0.5;
|
|
20
|
+
if (value < 0)
|
|
21
|
+
return 0;
|
|
22
|
+
if (value > 1)
|
|
23
|
+
return 1;
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
/** Returns `value` if finite, otherwise `fallback`. Last line of defense so no `NaN`/`Infinity` ever reaches emitted SVG markup. */
|
|
27
|
+
function safeCoord(value, fallback) {
|
|
28
|
+
return Number.isFinite(value) ? value : fallback;
|
|
29
|
+
}
|
|
30
|
+
/** Parses a numeric string strictly: empty/whitespace-only text is treated as non-numeric. */
|
|
31
|
+
function parseNumericString(raw) {
|
|
32
|
+
const trimmed = raw.trim();
|
|
33
|
+
if (trimmed === '')
|
|
34
|
+
return undefined;
|
|
35
|
+
const parsed = Number(trimmed);
|
|
36
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
37
|
+
}
|
|
38
|
+
/** Extracts one numeric point from an unknown array entry: a finite number as-is, or a plain object's finite `.value` field. */
|
|
39
|
+
function coercePoint(entry) {
|
|
40
|
+
if (typeof entry === 'number') {
|
|
41
|
+
return Number.isFinite(entry) ? entry : undefined;
|
|
42
|
+
}
|
|
43
|
+
if (typeof entry === 'string') {
|
|
44
|
+
return parseNumericString(entry);
|
|
45
|
+
}
|
|
46
|
+
if (entry !== null && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
47
|
+
const value = entry.value;
|
|
48
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
49
|
+
return value;
|
|
50
|
+
if (typeof value === 'string')
|
|
51
|
+
return parseNumericString(value);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Builds the numeric point series to plot, a bound `data` array taking
|
|
57
|
+
* priority over a static `values=` attribute. MAY THROW for a hostile bound
|
|
58
|
+
* value — guarded at the call site via `safeRead`.
|
|
59
|
+
*/
|
|
60
|
+
function resolvePoints(data, dataStatus, rawValues) {
|
|
61
|
+
if (dataStatus !== 'missing' &&
|
|
62
|
+
dataStatus !== 'error' &&
|
|
63
|
+
Array.isArray(data)) {
|
|
64
|
+
const points = [];
|
|
65
|
+
for (const entry of data) {
|
|
66
|
+
const point = coercePoint(entry);
|
|
67
|
+
if (point !== undefined)
|
|
68
|
+
points.push(point);
|
|
69
|
+
if (points.length >= MAX_POINTS)
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
return points;
|
|
73
|
+
}
|
|
74
|
+
if (rawValues) {
|
|
75
|
+
const points = [];
|
|
76
|
+
for (const token of rawValues.split(',')) {
|
|
77
|
+
const point = coercePoint(token.trim());
|
|
78
|
+
if (point !== undefined)
|
|
79
|
+
points.push(point);
|
|
80
|
+
if (points.length >= MAX_POINTS)
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
return points;
|
|
84
|
+
}
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Scales `points` into `[PADDING, width - PADDING] x [PADDING, height - PADDING]`
|
|
89
|
+
* SVG coordinates, flat series mapping to the vertical mid-line.
|
|
90
|
+
*/
|
|
91
|
+
function scalePoints(points, width, height) {
|
|
92
|
+
const min = Math.min(...points);
|
|
93
|
+
const max = Math.max(...points);
|
|
94
|
+
const range = max - min;
|
|
95
|
+
const innerWidth = Math.max(width - PADDING * 2, 0);
|
|
96
|
+
const innerHeight = Math.max(height - PADDING * 2, 0);
|
|
97
|
+
const step = points.length > 1 ? innerWidth / (points.length - 1) : 0;
|
|
98
|
+
return points.map((value, index) => {
|
|
99
|
+
const x = points.length > 1 ? PADDING + step * index : width / 2;
|
|
100
|
+
const normalized = Number.isFinite(range) && range > 0
|
|
101
|
+
? clamp01((value - min) / range)
|
|
102
|
+
: 0.5;
|
|
103
|
+
const y = PADDING + innerHeight * (1 - normalized);
|
|
104
|
+
return { x: safeCoord(x, width / 2), y: safeCoord(y, height / 2) };
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* `::chart{kind=line values="1,3,2,5"}` — a minimal hand-rolled inline SVG
|
|
109
|
+
* chart (no chart library; the SVG is a plain string built by this module).
|
|
110
|
+
* Data binding (§8): a bound `data` array of numbers (or `{value}` objects)
|
|
111
|
+
* takes priority over the static `values=` attribute; a numeric-only,
|
|
112
|
+
* defensively-filtered, capped series is what ever reaches the geometry
|
|
113
|
+
* math. All plotted values are numbers formatted as plain digits — no
|
|
114
|
+
* user-controlled text is ever interpolated into the SVG markup.
|
|
115
|
+
* Missing/error binding with no static fallback renders a small neutral
|
|
116
|
+
* empty state rather than a broken or empty `<svg>`. Never throws.
|
|
117
|
+
*
|
|
118
|
+
* Markup and class names match `@markii/react`'s `Chart` byte-for-byte.
|
|
119
|
+
*/
|
|
120
|
+
export const Chart = (attributes, _childrenHtml, ctx) => {
|
|
121
|
+
const { data, dataStatus, dataError, dataFailureKind } = ctx;
|
|
122
|
+
const rawKind = attributes.kind ?? DEFAULT_KIND;
|
|
123
|
+
const kind = isChartKind(rawKind) ? rawKind : DEFAULT_KIND;
|
|
124
|
+
const width = DEFAULT_WIDTH;
|
|
125
|
+
const height = DEFAULT_HEIGHT;
|
|
126
|
+
const bound = safeRead(() => resolvePoints(data, dataStatus, attributes.values), () => resolvePoints(undefined, 'missing', attributes.values));
|
|
127
|
+
const points = bound.fields;
|
|
128
|
+
const title = failureTitle(dataError ?? bound.fault, dataFailureKind);
|
|
129
|
+
const titleAttr = title ? ` title="${ctx.esc(title)}"` : '';
|
|
130
|
+
if (points.length === 0) {
|
|
131
|
+
const className = dataStateClassName('mk-chart', dataStatus, dataFailureKind, ['mk-chart--empty']);
|
|
132
|
+
return (`<div class="${className}"${titleAttr} role="img" aria-label="chart: no data" ` +
|
|
133
|
+
`style="width: ${String(width)}px; height: ${String(height)}px">no data</div>`);
|
|
134
|
+
}
|
|
135
|
+
const scaled = scalePoints(points, width, height);
|
|
136
|
+
const label = `${kind} chart, ${String(points.length)} point${points.length === 1 ? '' : 's'}`;
|
|
137
|
+
const className = dataStateClassName('mk-chart', dataStatus, dataFailureKind);
|
|
138
|
+
const titleEl = title ? `<title>${ctx.esc(title)}</title>` : '';
|
|
139
|
+
const body = kind === 'line'
|
|
140
|
+
? `<polyline class="mk-chart__line" points="${scaled
|
|
141
|
+
.map((p) => `${String(safeCoord(p.x, 0))},${String(safeCoord(p.y, 0))}`)
|
|
142
|
+
.join(' ')}" />`
|
|
143
|
+
: scaled
|
|
144
|
+
.map((p) => {
|
|
145
|
+
const barWidth = safeCoord(scaled.length > 1
|
|
146
|
+
? Math.max((width - PADDING * 2) / scaled.length - 2, 1)
|
|
147
|
+
: Math.max(width - PADDING * 2, 1), 1);
|
|
148
|
+
const x = safeCoord(p.x - barWidth / 2, 0);
|
|
149
|
+
const y = safeCoord(p.y, 0);
|
|
150
|
+
const barHeight = safeCoord(Math.max(height - PADDING - y, 0), 0);
|
|
151
|
+
return (`<rect class="mk-chart__bar" x="${String(x)}" y="${String(y)}" ` +
|
|
152
|
+
`width="${String(barWidth)}" height="${String(barHeight)}" />`);
|
|
153
|
+
})
|
|
154
|
+
.join('');
|
|
155
|
+
return (`<svg class="${className}" viewBox="0 0 ${String(width)} ${String(height)}" ` +
|
|
156
|
+
`width="${String(width)}" height="${String(height)}" role="img" aria-label="${ctx.esc(label)}">` +
|
|
157
|
+
`${titleEl}${body}</svg>`);
|
|
158
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HtmlComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `:::details{title="..." open} ... :::` — a collapsible disclosure, folded
|
|
4
|
+
* by default. `open` is a bare attribute (present -> starts expanded); any
|
|
5
|
+
* value it carries is irrelevant. Matches `@markii/react`'s `Details`
|
|
6
|
+
* markup byte-for-byte so one stylesheet covers both renderers. No outer
|
|
7
|
+
* margin: the document stylesheet owns spacing between this and its
|
|
8
|
+
* siblings.
|
|
9
|
+
*/
|
|
10
|
+
export declare const Details: HtmlComponent;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const DEFAULT_TITLE = 'Details';
|
|
2
|
+
/**
|
|
3
|
+
* `:::details{title="..." open} ... :::` — a collapsible disclosure, folded
|
|
4
|
+
* by default. `open` is a bare attribute (present -> starts expanded); any
|
|
5
|
+
* value it carries is irrelevant. Matches `@markii/react`'s `Details`
|
|
6
|
+
* markup byte-for-byte so one stylesheet covers both renderers. No outer
|
|
7
|
+
* margin: the document stylesheet owns spacing between this and its
|
|
8
|
+
* siblings.
|
|
9
|
+
*/
|
|
10
|
+
export const Details = (attributes, childrenHtml, ctx) => {
|
|
11
|
+
const title = attributes.title ?? DEFAULT_TITLE;
|
|
12
|
+
const open = Object.hasOwn(attributes, 'open');
|
|
13
|
+
return (`<details class="mk-details"${open ? ' open' : ''}>` +
|
|
14
|
+
`<summary class="mk-details__summary">${ctx.esc(title)}</summary>` +
|
|
15
|
+
`<div class="mk-details__body">${childrenHtml}</div></details>`);
|
|
16
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HtmlComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `:::figure{src="..." alt="..."} caption markdown :::` — an image with a
|
|
4
|
+
* rich (markdown) caption. `src` is required; a missing `src` renders no
|
|
5
|
+
* image at all (the caption still renders, matching the graceful-
|
|
6
|
+
* degradation spirit of the unknown-directive fallback rather than
|
|
7
|
+
* throwing).
|
|
8
|
+
*
|
|
9
|
+
* Security: `src` is a directive *attribute*, handed to this component as a
|
|
10
|
+
* raw string and put straight into `<img src>` — that assignment BYPASSES
|
|
11
|
+
* `@markii/core`'s `to-hast.ts` URL sanitizer, which only walks the hast
|
|
12
|
+
* tree produced from ordinary markdown links/images. This component closes
|
|
13
|
+
* that gap by running `src` through `@markii/core`'s own `isSafeUrl` (the
|
|
14
|
+
* exact same allowlist check the sanitizer uses) and dropping the image
|
|
15
|
+
* entirely when it fails, rather than re-implementing URL-scheme parsing
|
|
16
|
+
* here. Matches `@markii/react`'s `Figure` markup byte-for-byte.
|
|
17
|
+
*/
|
|
18
|
+
export declare const Figure: HtmlComponent;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { isSafeUrl } from '@markii/core';
|
|
2
|
+
const DEFAULT_ALT = '';
|
|
3
|
+
/**
|
|
4
|
+
* `:::figure{src="..." alt="..."} caption markdown :::` — an image with a
|
|
5
|
+
* rich (markdown) caption. `src` is required; a missing `src` renders no
|
|
6
|
+
* image at all (the caption still renders, matching the graceful-
|
|
7
|
+
* degradation spirit of the unknown-directive fallback rather than
|
|
8
|
+
* throwing).
|
|
9
|
+
*
|
|
10
|
+
* Security: `src` is a directive *attribute*, handed to this component as a
|
|
11
|
+
* raw string and put straight into `<img src>` — that assignment BYPASSES
|
|
12
|
+
* `@markii/core`'s `to-hast.ts` URL sanitizer, which only walks the hast
|
|
13
|
+
* tree produced from ordinary markdown links/images. This component closes
|
|
14
|
+
* that gap by running `src` through `@markii/core`'s own `isSafeUrl` (the
|
|
15
|
+
* exact same allowlist check the sanitizer uses) and dropping the image
|
|
16
|
+
* entirely when it fails, rather than re-implementing URL-scheme parsing
|
|
17
|
+
* here. Matches `@markii/react`'s `Figure` markup byte-for-byte.
|
|
18
|
+
*/
|
|
19
|
+
export const Figure = (attributes, childrenHtml, ctx) => {
|
|
20
|
+
const rawSrc = attributes.src ?? null;
|
|
21
|
+
const alt = attributes.alt ?? DEFAULT_ALT;
|
|
22
|
+
const src = rawSrc && isSafeUrl(rawSrc) ? rawSrc : null;
|
|
23
|
+
const imgHtml = src
|
|
24
|
+
? `<img class="mk-figure__img" src="${ctx.esc(src)}" alt="${ctx.esc(alt)}">`
|
|
25
|
+
: '';
|
|
26
|
+
return (`<figure class="mk-figure">${imgHtml}` +
|
|
27
|
+
`<figcaption class="mk-figure__caption">${childrenHtml}</figcaption></figure>`);
|
|
28
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { HtmlRegistry } from '../registry.js';
|
|
2
|
+
export { Badge } from './badge.js';
|
|
3
|
+
export type { BadgeVariant } from './badge.js';
|
|
4
|
+
export { Callout } from './callout.js';
|
|
5
|
+
export type { CalloutType } from './callout.js';
|
|
6
|
+
export { Card } from './card.js';
|
|
7
|
+
export { Cell } from './cell.js';
|
|
8
|
+
export { Chart } from './chart.js';
|
|
9
|
+
export { Details } from './details.js';
|
|
10
|
+
export { Figure } from './figure.js';
|
|
11
|
+
export { Kbd } from './kbd.js';
|
|
12
|
+
export { createLayoutWrapper, LAYOUT_WRAPPER_PRESETS, } from './layout-wrapper.js';
|
|
13
|
+
export type { LayoutWrapperPreset } from './layout-wrapper.js';
|
|
14
|
+
export { Progress } from './progress.js';
|
|
15
|
+
export { Rating } from './rating.js';
|
|
16
|
+
export { Row } from './row.js';
|
|
17
|
+
export { Stat } from './stat.js';
|
|
18
|
+
export { Tab, DEFAULT_TAB_LABEL, tabPanel } from './tab.js';
|
|
19
|
+
export { Tabs } from './tabs.js';
|
|
20
|
+
/**
|
|
21
|
+
* The built-in standard components, pre-registered under their names —
|
|
22
|
+
* matching `@markii/react`'s `defaultRegistry` in full, including the
|
|
23
|
+
* data-bound `stat`/`progress`/`chart` trio now that this engine's
|
|
24
|
+
* `HtmlRenderContext` carries a value-resolution seam (`./stat.js`,
|
|
25
|
+
* `./progress.js`, `./chart.js`; see `render.ts`'s `resolveDataAttribute`).
|
|
26
|
+
*/
|
|
27
|
+
export declare const defaultHtmlRegistry: HtmlRegistry;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { getContract } from '@markii/stdlib';
|
|
2
|
+
import { createHtmlRegistry } from '../registry.js';
|
|
3
|
+
import { Badge } from './badge.js';
|
|
4
|
+
import { Callout } from './callout.js';
|
|
5
|
+
import { Card } from './card.js';
|
|
6
|
+
import { Cell } from './cell.js';
|
|
7
|
+
import { Chart } from './chart.js';
|
|
8
|
+
import { Details } from './details.js';
|
|
9
|
+
import { Figure } from './figure.js';
|
|
10
|
+
import { Kbd } from './kbd.js';
|
|
11
|
+
import { createLayoutWrapper } from './layout-wrapper.js';
|
|
12
|
+
import { Progress } from './progress.js';
|
|
13
|
+
import { Rating } from './rating.js';
|
|
14
|
+
import { Row } from './row.js';
|
|
15
|
+
import { Stat } from './stat.js';
|
|
16
|
+
import { Tab } from './tab.js';
|
|
17
|
+
import { Tabs } from './tabs.js';
|
|
18
|
+
export { Badge } from './badge.js';
|
|
19
|
+
export { Callout } from './callout.js';
|
|
20
|
+
export { Card } from './card.js';
|
|
21
|
+
export { Cell } from './cell.js';
|
|
22
|
+
export { Chart } from './chart.js';
|
|
23
|
+
export { Details } from './details.js';
|
|
24
|
+
export { Figure } from './figure.js';
|
|
25
|
+
export { Kbd } from './kbd.js';
|
|
26
|
+
export { createLayoutWrapper, LAYOUT_WRAPPER_PRESETS, } from './layout-wrapper.js';
|
|
27
|
+
export { Progress } from './progress.js';
|
|
28
|
+
export { Rating } from './rating.js';
|
|
29
|
+
export { Row } from './row.js';
|
|
30
|
+
export { Stat } from './stat.js';
|
|
31
|
+
export { Tab, DEFAULT_TAB_LABEL, tabPanel } from './tab.js';
|
|
32
|
+
export { Tabs } from './tabs.js';
|
|
33
|
+
/**
|
|
34
|
+
* Derives a registry entry's `inline` flag from `@markii/stdlib`'s standard
|
|
35
|
+
* component contract for `name`, matching `@markii/react`'s
|
|
36
|
+
* `inlineFromContract`: `@markii/stdlib`'s `ComponentKind` is the source of
|
|
37
|
+
* truth for whether a standard component is written as a text directive
|
|
38
|
+
* (`kind: 'inline'` -> `inline: true`) or a leaf/container directive
|
|
39
|
+
* (`kind: 'leaf' | 'container'` -> `inline: false`). Falls back to `false`
|
|
40
|
+
* if `name` has no standard contract.
|
|
41
|
+
*/
|
|
42
|
+
function inlineFromContract(name) {
|
|
43
|
+
return getContract(name)?.kind === 'inline';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The built-in standard components, pre-registered under their names —
|
|
47
|
+
* matching `@markii/react`'s `defaultRegistry` in full, including the
|
|
48
|
+
* data-bound `stat`/`progress`/`chart` trio now that this engine's
|
|
49
|
+
* `HtmlRenderContext` carries a value-resolution seam (`./stat.js`,
|
|
50
|
+
* `./progress.js`, `./chart.js`; see `render.ts`'s `resolveDataAttribute`).
|
|
51
|
+
*/
|
|
52
|
+
export const defaultHtmlRegistry = createHtmlRegistry({
|
|
53
|
+
callout: { component: Callout, inline: inlineFromContract('callout') },
|
|
54
|
+
kbd: { component: Kbd, inline: inlineFromContract('kbd') },
|
|
55
|
+
rating: { component: Rating, inline: inlineFromContract('rating') },
|
|
56
|
+
details: { component: Details, inline: inlineFromContract('details') },
|
|
57
|
+
card: { component: Card, inline: inlineFromContract('card') },
|
|
58
|
+
badge: { component: Badge, inline: inlineFromContract('badge') },
|
|
59
|
+
figure: { component: Figure, inline: inlineFromContract('figure') },
|
|
60
|
+
tabs: { component: Tabs, inline: inlineFromContract('tabs') },
|
|
61
|
+
tab: { component: Tab, inline: inlineFromContract('tab') },
|
|
62
|
+
row: { component: Row, inline: inlineFromContract('row') },
|
|
63
|
+
cell: { component: Cell, inline: inlineFromContract('cell') },
|
|
64
|
+
stat: { component: Stat, inline: inlineFromContract('stat') },
|
|
65
|
+
progress: { component: Progress, inline: inlineFromContract('progress') },
|
|
66
|
+
chart: { component: Chart, inline: inlineFromContract('chart') },
|
|
67
|
+
center: {
|
|
68
|
+
component: createLayoutWrapper('center'),
|
|
69
|
+
inline: inlineFromContract('center'),
|
|
70
|
+
},
|
|
71
|
+
left: {
|
|
72
|
+
component: createLayoutWrapper('left'),
|
|
73
|
+
inline: inlineFromContract('left'),
|
|
74
|
+
},
|
|
75
|
+
right: {
|
|
76
|
+
component: createLayoutWrapper('right'),
|
|
77
|
+
inline: inlineFromContract('right'),
|
|
78
|
+
},
|
|
79
|
+
wide: {
|
|
80
|
+
component: createLayoutWrapper('wide'),
|
|
81
|
+
inline: inlineFromContract('wide'),
|
|
82
|
+
},
|
|
83
|
+
narrow: {
|
|
84
|
+
component: createLayoutWrapper('narrow'),
|
|
85
|
+
inline: inlineFromContract('narrow'),
|
|
86
|
+
},
|
|
87
|
+
full: {
|
|
88
|
+
component: createLayoutWrapper('full'),
|
|
89
|
+
inline: inlineFromContract('full'),
|
|
90
|
+
},
|
|
91
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HtmlComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `:kbd[Ctrl+S]` — a styled keycap for an inline text directive. Matches
|
|
4
|
+
* `@markii/react`'s `Kbd` markup byte-for-byte so one stylesheet covers
|
|
5
|
+
* both renderers.
|
|
6
|
+
*/
|
|
7
|
+
export declare const Kbd: HtmlComponent;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `:kbd[Ctrl+S]` — a styled keycap for an inline text directive. Matches
|
|
3
|
+
* `@markii/react`'s `Kbd` markup byte-for-byte so one stylesheet covers
|
|
4
|
+
* both renderers.
|
|
5
|
+
*/
|
|
6
|
+
export const Kbd = (_attributes, childrenHtml) => {
|
|
7
|
+
return `<kbd class="mk-kbd">${childrenHtml}</kbd>`;
|
|
8
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { HtmlComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* The closed set of layout-wrapper container names (docs/format.md): six
|
|
4
|
+
* aliases of the one shared implementation below (`createLayoutWrapper`),
|
|
5
|
+
* matching `@markii/react`'s `layout-wrapper.tsx`. There is deliberately no
|
|
6
|
+
* `normal` alias (the default needs no wrapper at all) and no
|
|
7
|
+
* attribute-bearing form.
|
|
8
|
+
*/
|
|
9
|
+
export declare const LAYOUT_WRAPPER_PRESETS: readonly ["center", "left", "right", "wide", "narrow", "full"];
|
|
10
|
+
/** One of the six closed layout-wrapper preset names. */
|
|
11
|
+
export type LayoutWrapperPreset = (typeof LAYOUT_WRAPPER_PRESETS)[number];
|
|
12
|
+
/**
|
|
13
|
+
* Creates the registry component for one of docs/format.md's six layout-
|
|
14
|
+
* wrapper container names — `:::center`, `:::left`, `:::right`, `:::wide`,
|
|
15
|
+
* `:::narrow`, `:::full`. One shared implementation, bound to `preset` at
|
|
16
|
+
* registration time, matching `@markii/react`'s `createLayoutWrapper`.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately never reads `attributes`: docs/format.md gives these
|
|
19
|
+
* wrappers no attribute-bearing form. `width`/`align` written on a wrapper
|
|
20
|
+
* are intercepted earlier by the engine's reserved-attribute handling
|
|
21
|
+
* (`layout.ts`), before this component ever runs.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createLayoutWrapper(preset: LayoutWrapperPreset): HtmlComponent;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The closed set of layout-wrapper container names (docs/format.md): six
|
|
3
|
+
* aliases of the one shared implementation below (`createLayoutWrapper`),
|
|
4
|
+
* matching `@markii/react`'s `layout-wrapper.tsx`. There is deliberately no
|
|
5
|
+
* `normal` alias (the default needs no wrapper at all) and no
|
|
6
|
+
* attribute-bearing form.
|
|
7
|
+
*/
|
|
8
|
+
export const LAYOUT_WRAPPER_PRESETS = [
|
|
9
|
+
'center',
|
|
10
|
+
'left',
|
|
11
|
+
'right',
|
|
12
|
+
'wide',
|
|
13
|
+
'narrow',
|
|
14
|
+
'full',
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* Preset -> class string. Null-prototype, mirroring `layout.ts`'s
|
|
18
|
+
* `WIDTH_CLASSES`/`ALIGN_CLASSES`. `center`/`left`/`right` reuse the
|
|
19
|
+
* existing `mk-align-*` classes; `wide`/`narrow`/`full` reuse the existing
|
|
20
|
+
* `mk-width-*` classes. `mk-layout` is the one class every preset adds on
|
|
21
|
+
* top. Matches `@markii/react`'s `WRAPPER_CLASSES` byte-for-byte.
|
|
22
|
+
*/
|
|
23
|
+
const WRAPPER_CLASSES = Object.assign(Object.create(null), {
|
|
24
|
+
center: 'mk-layout mk-align-center',
|
|
25
|
+
left: 'mk-layout mk-align-left',
|
|
26
|
+
right: 'mk-layout mk-align-right',
|
|
27
|
+
wide: 'mk-layout mk-width-wide',
|
|
28
|
+
narrow: 'mk-layout mk-width-narrow',
|
|
29
|
+
full: 'mk-layout mk-width-full',
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* Creates the registry component for one of docs/format.md's six layout-
|
|
33
|
+
* wrapper container names — `:::center`, `:::left`, `:::right`, `:::wide`,
|
|
34
|
+
* `:::narrow`, `:::full`. One shared implementation, bound to `preset` at
|
|
35
|
+
* registration time, matching `@markii/react`'s `createLayoutWrapper`.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately never reads `attributes`: docs/format.md gives these
|
|
38
|
+
* wrappers no attribute-bearing form. `width`/`align` written on a wrapper
|
|
39
|
+
* are intercepted earlier by the engine's reserved-attribute handling
|
|
40
|
+
* (`layout.ts`), before this component ever runs.
|
|
41
|
+
*/
|
|
42
|
+
export function createLayoutWrapper(preset) {
|
|
43
|
+
const className = WRAPPER_CLASSES[preset] ?? 'mk-layout';
|
|
44
|
+
return (_attributes, childrenHtml) => `<div class="${className}">${childrenHtml}</div>`;
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HtmlComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `::progress{value=3 max=5 label="tasks"}` — a meter bar. Data binding
|
|
4
|
+
* (§8): a bound numeric `data` supplies `value`; a bound object may supply
|
|
5
|
+
* `value`/`max` — explicit directive attributes always win. Parses
|
|
6
|
+
* defensively: non-numeric/`NaN`/`Infinity` input (from either source) falls
|
|
7
|
+
* back to `0` (value) or the default `max` of `1`; `value` is then clamped
|
|
8
|
+
* to `[0, max]` and `max` is guarded to be positive. Missing/error binding
|
|
9
|
+
* renders a `0%` bar rather than crashing.
|
|
10
|
+
*
|
|
11
|
+
* Markup and class names match `@markii/react`'s `Progress` byte-for-byte.
|
|
12
|
+
*/
|
|
13
|
+
export declare const Progress: HtmlComponent;
|