@glw907/cairn-cms 0.62.2 → 0.76.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/CHANGELOG.md +216 -0
- package/dist/ambient.d.ts +2 -0
- package/dist/auth/types.d.ts +7 -0
- package/dist/components/CairnAdmin.svelte.d.ts +2 -7
- package/dist/components/ComponentForm.svelte +44 -27
- package/dist/components/ComponentInsertDialog.svelte +22 -11
- package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
- package/dist/components/ConceptList.svelte +25 -4
- package/dist/components/EditPage.svelte +29 -107
- package/dist/components/EditPage.svelte.d.ts +2 -7
- package/dist/components/EntryPicker.svelte +117 -0
- package/dist/components/EntryPicker.svelte.d.ts +35 -0
- package/dist/components/FieldInput.svelte +218 -0
- package/dist/components/FieldInput.svelte.d.ts +51 -0
- package/dist/components/IconPicker.svelte +2 -2
- package/dist/components/IconPicker.svelte.d.ts +2 -0
- package/dist/components/LinkPicker.svelte +8 -75
- package/dist/components/LinkPicker.svelte.d.ts +4 -5
- package/dist/components/MediaHeroField.svelte +8 -5
- package/dist/components/MediaHeroField.svelte.d.ts +4 -0
- package/dist/components/ObjectGroupField.svelte +54 -0
- package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
- package/dist/components/ReferenceField.svelte +94 -0
- package/dist/components/ReferenceField.svelte.d.ts +27 -0
- package/dist/components/RepeatableField.svelte +221 -0
- package/dist/components/RepeatableField.svelte.d.ts +53 -0
- package/dist/components/cairn-admin.css +179 -2
- package/dist/components/preview-doc.js +5 -1
- package/dist/components/tidy-validate.js +1 -1
- package/dist/content/adapter.js +18 -0
- package/dist/content/advisories.d.ts +2 -2
- package/dist/content/advisories.js +3 -5
- package/dist/content/compose.d.ts +7 -6
- package/dist/content/compose.js +26 -20
- package/dist/content/concepts.d.ts +21 -15
- package/dist/content/concepts.js +55 -32
- package/dist/content/field-rules.d.ts +15 -0
- package/dist/content/field-rules.js +38 -0
- package/dist/content/fields.d.ts +169 -0
- package/dist/content/fields.js +41 -0
- package/dist/content/fieldset.d.ts +107 -0
- package/dist/content/fieldset.js +386 -0
- package/dist/content/frontmatter-region.d.ts +38 -0
- package/dist/content/frontmatter-region.js +75 -0
- package/dist/content/frontmatter.d.ts +35 -2
- package/dist/content/frontmatter.js +232 -11
- package/dist/content/manifest.d.ts +34 -0
- package/dist/content/manifest.js +80 -4
- package/dist/content/media-refs.d.ts +2 -2
- package/dist/content/media-rewrite.js +1 -69
- package/dist/content/reference-index.d.ts +56 -0
- package/dist/content/reference-index.js +95 -0
- package/dist/content/references.d.ts +40 -0
- package/dist/content/references.js +0 -0
- package/dist/content/standard-schema.d.ts +30 -0
- package/dist/content/standard-schema.js +4 -0
- package/dist/content/types.d.ts +127 -178
- package/dist/delivery/data.d.ts +2 -2
- package/dist/delivery/data.js +1 -1
- package/dist/delivery/public-routes.d.ts +10 -5
- package/dist/delivery/public-routes.js +25 -2
- package/dist/delivery/site-descriptors.d.ts +5 -1
- package/dist/delivery/site-descriptors.js +8 -3
- package/dist/delivery/site-indexes.d.ts +2 -2
- package/dist/delivery/site-resolver.d.ts +25 -0
- package/dist/delivery/site-resolver.js +49 -0
- package/dist/doctor/checks-local.js +6 -11
- package/dist/github/backend.d.ts +83 -0
- package/dist/github/backend.js +76 -0
- package/dist/github/credentials.d.ts +11 -5
- package/dist/github/credentials.js +3 -3
- package/dist/github/repo.d.ts +8 -19
- package/dist/github/repo.js +69 -80
- package/dist/github/types.d.ts +1 -1
- package/dist/github/types.js +4 -4
- package/dist/index.d.ts +18 -10
- package/dist/index.js +9 -5
- package/dist/islands/index.d.ts +12 -0
- package/dist/islands/index.js +83 -0
- package/dist/islands/types.d.ts +7 -0
- package/dist/islands/types.js +1 -0
- package/dist/log/events.d.ts +1 -1
- package/dist/media/index.d.ts +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/manifest.d.ts +11 -0
- package/dist/media/manifest.js +13 -0
- package/dist/media/rewrite-plan.d.ts +2 -3
- package/dist/media/rewrite-plan.js +2 -3
- package/dist/media/usage.d.ts +2 -2
- package/dist/media/usage.js +3 -5
- package/dist/nav/site-config.d.ts +0 -6
- package/dist/nav/site-config.js +6 -4
- package/dist/render/component-grammar.js +11 -11
- package/dist/render/component-reference.js +5 -3
- package/dist/render/component-validate.d.ts +4 -1
- package/dist/render/component-validate.js +10 -35
- package/dist/render/highlight.d.ts +9 -0
- package/dist/render/highlight.js +206 -0
- package/dist/render/pipeline.d.ts +0 -6
- package/dist/render/pipeline.js +13 -2
- package/dist/render/registry.d.ts +44 -36
- package/dist/render/registry.js +47 -6
- package/dist/render/rehype-dispatch.d.ts +6 -10
- package/dist/render/rehype-dispatch.js +38 -17
- package/dist/render/remark-directives.js +4 -5
- package/dist/render/sanitize-schema.d.ts +10 -0
- package/dist/render/sanitize-schema.js +30 -1
- package/dist/sveltekit/cairn-admin.d.ts +5 -5
- package/dist/sveltekit/cairn-admin.js +3 -4
- package/dist/sveltekit/content-routes.d.ts +10 -8
- package/dist/sveltekit/content-routes.js +269 -181
- package/dist/sveltekit/guard.js +10 -0
- package/dist/sveltekit/health.d.ts +7 -3
- package/dist/sveltekit/health.js +9 -3
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/nav-routes.d.ts +6 -5
- package/dist/sveltekit/nav-routes.js +22 -20
- package/dist/sveltekit/types.d.ts +2 -0
- package/dist/vite/index.d.ts +3 -3
- package/dist/vite/index.js +17 -8
- package/package.json +17 -2
- package/src/lib/ambient.ts +7 -0
- package/src/lib/auth/types.ts +7 -0
- package/src/lib/components/CairnAdmin.svelte +2 -6
- package/src/lib/components/ComponentForm.svelte +48 -27
- package/src/lib/components/ComponentInsertDialog.svelte +26 -14
- package/src/lib/components/ConceptList.svelte +41 -4
- package/src/lib/components/EditPage.svelte +43 -119
- package/src/lib/components/EntryPicker.svelte +154 -0
- package/src/lib/components/FieldInput.svelte +262 -0
- package/src/lib/components/IconPicker.svelte +4 -2
- package/src/lib/components/LinkPicker.svelte +10 -81
- package/src/lib/components/MediaHeroField.svelte +12 -5
- package/src/lib/components/ObjectGroupField.svelte +97 -0
- package/src/lib/components/ReferenceField.svelte +126 -0
- package/src/lib/components/RepeatableField.svelte +310 -0
- package/src/lib/components/preview-doc.ts +5 -1
- package/src/lib/components/tidy-validate.ts +1 -1
- package/src/lib/content/adapter.ts +21 -0
- package/src/lib/content/advisories.ts +4 -7
- package/src/lib/content/compose.ts +30 -23
- package/src/lib/content/concepts.ts +68 -40
- package/src/lib/content/field-rules.ts +39 -0
- package/src/lib/content/fields.ts +178 -0
- package/src/lib/content/fieldset.ts +470 -0
- package/src/lib/content/frontmatter-region.ts +90 -0
- package/src/lib/content/frontmatter.ts +231 -15
- package/src/lib/content/manifest.ts +101 -4
- package/src/lib/content/media-refs.ts +2 -2
- package/src/lib/content/media-rewrite.ts +7 -80
- package/src/lib/content/reference-index.ts +159 -0
- package/src/lib/content/references.ts +0 -0
- package/src/lib/content/standard-schema.ts +25 -0
- package/src/lib/content/types.ts +128 -195
- package/src/lib/delivery/data.ts +2 -2
- package/src/lib/delivery/public-routes.ts +36 -4
- package/src/lib/delivery/site-descriptors.ts +8 -3
- package/src/lib/delivery/site-indexes.ts +2 -2
- package/src/lib/delivery/site-resolver.ts +64 -0
- package/src/lib/doctor/checks-local.ts +6 -14
- package/src/lib/github/backend.ts +161 -0
- package/src/lib/github/credentials.ts +10 -7
- package/src/lib/github/repo.ts +79 -83
- package/src/lib/github/types.ts +5 -5
- package/src/lib/index.ts +40 -18
- package/src/lib/islands/index.ts +84 -0
- package/src/lib/islands/types.ts +11 -0
- package/src/lib/log/events.ts +1 -0
- package/src/lib/media/index.ts +1 -0
- package/src/lib/media/manifest.ts +14 -0
- package/src/lib/media/rewrite-plan.ts +4 -6
- package/src/lib/media/usage.ts +4 -7
- package/src/lib/nav/site-config.ts +8 -9
- package/src/lib/render/component-grammar.ts +10 -10
- package/src/lib/render/component-reference.ts +4 -3
- package/src/lib/render/component-validate.ts +10 -35
- package/src/lib/render/highlight.ts +259 -0
- package/src/lib/render/pipeline.ts +13 -8
- package/src/lib/render/registry.ts +88 -42
- package/src/lib/render/rehype-dispatch.ts +47 -16
- package/src/lib/render/remark-directives.ts +4 -5
- package/src/lib/render/sanitize-schema.ts +32 -1
- package/src/lib/sveltekit/cairn-admin.ts +8 -9
- package/src/lib/sveltekit/content-routes.ts +330 -221
- package/src/lib/sveltekit/guard.ts +15 -0
- package/src/lib/sveltekit/health.ts +13 -6
- package/src/lib/sveltekit/index.ts +2 -2
- package/src/lib/sveltekit/nav-routes.ts +33 -29
- package/src/lib/sveltekit/types.ts +5 -1
- package/src/lib/vite/index.ts +20 -11
- package/dist/content/schema.d.ts +0 -87
- package/dist/content/schema.js +0 -89
- package/dist/content/validate.d.ts +0 -17
- package/dist/content/validate.js +0 -93
- package/src/lib/content/schema.ts +0 -167
- package/src/lib/content/validate.ts +0 -90
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { toString } from 'hast-util-to-string';
|
|
2
|
+
// The curated language set Shiki preloads. Each id pulls its aliases too (loading `bash` also
|
|
3
|
+
// registers `sh`, `shell`, `zsh`; loading `js` registers `javascript`, `mjs`, `cjs`). A fence whose
|
|
4
|
+
// language is absent or outside this set falls back to plaintext rather than throwing.
|
|
5
|
+
const LANGS = [
|
|
6
|
+
'js',
|
|
7
|
+
'ts',
|
|
8
|
+
'jsx',
|
|
9
|
+
'tsx',
|
|
10
|
+
'svelte',
|
|
11
|
+
'html',
|
|
12
|
+
'css',
|
|
13
|
+
'json',
|
|
14
|
+
'bash',
|
|
15
|
+
'markdown',
|
|
16
|
+
'python',
|
|
17
|
+
'yaml',
|
|
18
|
+
'sql',
|
|
19
|
+
];
|
|
20
|
+
// The plaintext language id. Shiki special-cases it (and `plaintext`/`txt`/`ansi`) as always
|
|
21
|
+
// available, so it is the safe fallback for an unknown or absent fence language: it escapes the
|
|
22
|
+
// code text into the same <pre class="shiki"> wrapper with no token coloring.
|
|
23
|
+
const PLAINTEXT = 'text';
|
|
24
|
+
// Sentinel foreground colors, one per ramp slot. Shiki has no class-emitting mode, so the theme
|
|
25
|
+
// assigns each scope group a unique sentinel hex, then the transformer below maps the resolved
|
|
26
|
+
// sentinel back to its cairn-tok-* class and deletes the style. The sentinels never reach the
|
|
27
|
+
// output. They are arbitrary distinct values; only their uniqueness and exact round-trip matter.
|
|
28
|
+
const SENTINEL_INK = '#000000';
|
|
29
|
+
const SENTINEL_TO_CLASS = {
|
|
30
|
+
'#000010': 'cairn-tok-comment',
|
|
31
|
+
'#000020': 'cairn-tok-keyword',
|
|
32
|
+
'#000030': 'cairn-tok-string',
|
|
33
|
+
'#000040': 'cairn-tok-function',
|
|
34
|
+
'#000050': 'cairn-tok-number',
|
|
35
|
+
'#000060': 'cairn-tok-punct',
|
|
36
|
+
};
|
|
37
|
+
// The Shiki theme that drives the class mapping. Every scope group resolves to its sentinel color;
|
|
38
|
+
// the transformer turns the sentinel into a class. The background and default foreground are
|
|
39
|
+
// sentinels too, stripped from the <pre> by the transformer so the site theme owns the surround.
|
|
40
|
+
const CAIRN_CODE_THEME = {
|
|
41
|
+
name: 'cairn-roles',
|
|
42
|
+
type: 'light',
|
|
43
|
+
fg: SENTINEL_INK,
|
|
44
|
+
bg: '#ffffff',
|
|
45
|
+
settings: [
|
|
46
|
+
{ settings: { foreground: SENTINEL_INK, background: '#ffffff' } },
|
|
47
|
+
{
|
|
48
|
+
scope: ['comment', 'punctuation.definition.comment', 'string.comment'],
|
|
49
|
+
settings: { foreground: '#000010' },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
scope: [
|
|
53
|
+
'keyword',
|
|
54
|
+
'keyword.control',
|
|
55
|
+
'storage',
|
|
56
|
+
'storage.type',
|
|
57
|
+
'storage.modifier',
|
|
58
|
+
'variable.language',
|
|
59
|
+
'keyword.operator.new',
|
|
60
|
+
'keyword.operator.expression',
|
|
61
|
+
],
|
|
62
|
+
settings: { foreground: '#000020' },
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
scope: ['string', 'string.quoted', 'string.template', 'constant.other.symbol'],
|
|
66
|
+
settings: { foreground: '#000030' },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
scope: [
|
|
70
|
+
'entity.name.function',
|
|
71
|
+
'support.function',
|
|
72
|
+
'meta.function-call',
|
|
73
|
+
'entity.name.tag',
|
|
74
|
+
'support.type.property-name',
|
|
75
|
+
],
|
|
76
|
+
settings: { foreground: '#000040' },
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
scope: [
|
|
80
|
+
'constant.numeric',
|
|
81
|
+
'constant.language',
|
|
82
|
+
'constant.character',
|
|
83
|
+
'constant.other',
|
|
84
|
+
'keyword.other.unit',
|
|
85
|
+
],
|
|
86
|
+
settings: { foreground: '#000050' },
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
scope: ['punctuation', 'meta.brace', 'keyword.operator', 'meta.delimiter'],
|
|
90
|
+
settings: { foreground: '#000060' },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
// Read the `color:#rrggbb` hex out of a token span's inline style, lowercased for the sentinel map.
|
|
95
|
+
function styleColor(style) {
|
|
96
|
+
if (typeof style !== 'string')
|
|
97
|
+
return undefined;
|
|
98
|
+
const match = /color:(#[0-9a-fA-F]{6})/.exec(style);
|
|
99
|
+
return match ? match[1].toLowerCase() : undefined;
|
|
100
|
+
}
|
|
101
|
+
// Append a class to a hast element's class list, building the string form Shiki uses.
|
|
102
|
+
function addClass(node, className) {
|
|
103
|
+
const existing = node.properties?.class;
|
|
104
|
+
const prefix = typeof existing === 'string' && existing.length > 0 ? `${existing} ` : '';
|
|
105
|
+
(node.properties ??= {}).class = `${prefix}${className}`;
|
|
106
|
+
}
|
|
107
|
+
// The transformer that converts Shiki's sentinel-colored inline styles into the cairn-tok-* classes
|
|
108
|
+
// and strips every style attribute, so the output is class-only. The <pre> loses its
|
|
109
|
+
// background/foreground style (the site theme owns the surround); each token <span> trades its
|
|
110
|
+
// sentinel color for the matching class, and a default-foreground span is left unclassed.
|
|
111
|
+
const cairnTokenClasses = {
|
|
112
|
+
name: 'cairn-token-classes',
|
|
113
|
+
pre(node) {
|
|
114
|
+
if (node.properties)
|
|
115
|
+
delete node.properties.style;
|
|
116
|
+
},
|
|
117
|
+
code(node) {
|
|
118
|
+
if (node.properties)
|
|
119
|
+
delete node.properties.style;
|
|
120
|
+
},
|
|
121
|
+
span(node) {
|
|
122
|
+
const hex = styleColor(node.properties?.style);
|
|
123
|
+
if (node.properties)
|
|
124
|
+
delete node.properties.style;
|
|
125
|
+
const className = hex ? SENTINEL_TO_CLASS[hex] : undefined;
|
|
126
|
+
if (className)
|
|
127
|
+
addClass(node, className);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
// The cached highlighter. Created once on first use and reused for every render. The dynamic import
|
|
131
|
+
// keeps Shiki off the static client graph; the promise is cached so the WASM grammar load and the
|
|
132
|
+
// theme registration happen at most once per process.
|
|
133
|
+
let highlighterPromise;
|
|
134
|
+
function getHighlighter() {
|
|
135
|
+
if (!highlighterPromise) {
|
|
136
|
+
highlighterPromise = import('shiki').then((shiki) => shiki.createHighlighter({ themes: [CAIRN_CODE_THEME], langs: [...LANGS] }));
|
|
137
|
+
}
|
|
138
|
+
return highlighterPromise;
|
|
139
|
+
}
|
|
140
|
+
// Read the fenced language off a <code> element's class list. Markdown emits the language as a
|
|
141
|
+
// `language-<id>` class on the inner <code>. An empty or missing language returns undefined, which
|
|
142
|
+
// the caller maps to plaintext.
|
|
143
|
+
function codeLanguage(code) {
|
|
144
|
+
const className = code.properties?.className;
|
|
145
|
+
const classes = Array.isArray(className) ? className.map(String) : [];
|
|
146
|
+
const langClass = classes.find((c) => c.startsWith('language-'));
|
|
147
|
+
const lang = langClass?.slice('language-'.length);
|
|
148
|
+
return lang && lang.length > 0 ? lang : undefined;
|
|
149
|
+
}
|
|
150
|
+
// A fenced-code <pre> is a <pre> whose single element child is a <code>. Inline code (a bare <code>
|
|
151
|
+
// with no <pre> parent) and any other <pre> are left untouched.
|
|
152
|
+
function fencedCode(pre) {
|
|
153
|
+
const child = pre.children.find((c) => c.type === 'element');
|
|
154
|
+
return child?.tagName === 'code' ? child : undefined;
|
|
155
|
+
}
|
|
156
|
+
// Descend the tree and collect every fenced-code <pre>. The walk is synchronous and gathers the
|
|
157
|
+
// targets up front, because the highlight itself is async (unist-util-visit cannot await).
|
|
158
|
+
function collectFencedCode(node, jobs) {
|
|
159
|
+
for (const child of node.children) {
|
|
160
|
+
if (child.type !== 'element')
|
|
161
|
+
continue;
|
|
162
|
+
if (child.tagName === 'pre') {
|
|
163
|
+
const code = fencedCode(child);
|
|
164
|
+
if (code) {
|
|
165
|
+
jobs.push({ pre: child, code, lang: codeLanguage(code) ?? PLAINTEXT });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
collectFencedCode(child, jobs);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The Shiki rehype plugin. Highlights every fenced-code block into a `<pre class="shiki">` whose
|
|
174
|
+
* tokens carry the cairn-tok-* classes (no inline style), then leaves the rest of the tree
|
|
175
|
+
* untouched. It runs as an async transformer because Shiki tokenizes asynchronously. Because the
|
|
176
|
+
* output is class-only it needs no special placement; it is safe anywhere after `remarkRehype`.
|
|
177
|
+
* @returns A unified transformer that mutates the hast tree in place.
|
|
178
|
+
*/
|
|
179
|
+
export function rehypeCairnHighlight() {
|
|
180
|
+
return async (tree) => {
|
|
181
|
+
const jobs = [];
|
|
182
|
+
collectFencedCode(tree, jobs);
|
|
183
|
+
if (jobs.length === 0)
|
|
184
|
+
return;
|
|
185
|
+
const highlighter = await getHighlighter();
|
|
186
|
+
const loaded = new Set(highlighter.getLoadedLanguages());
|
|
187
|
+
for (const job of jobs) {
|
|
188
|
+
// Fall back to plaintext for any language outside the preloaded set so Shiki never throws on
|
|
189
|
+
// an unknown fence. Plaintext still produces the <pre class="shiki"> wrapper with escaped text.
|
|
190
|
+
const lang = loaded.has(job.lang) ? job.lang : PLAINTEXT;
|
|
191
|
+
const result = highlighter.codeToHast(toString(job.code), {
|
|
192
|
+
lang,
|
|
193
|
+
theme: 'cairn-roles',
|
|
194
|
+
transformers: [cairnTokenClasses],
|
|
195
|
+
});
|
|
196
|
+
const pre = result.children.find((c) => c.type === 'element' && c.tagName === 'pre');
|
|
197
|
+
if (!pre)
|
|
198
|
+
continue;
|
|
199
|
+
// Rewrite the original <pre> into Shiki's <pre> in place: adopt its tag, properties, and
|
|
200
|
+
// children. Mutating the existing node avoids reindexing the parent's children array.
|
|
201
|
+
job.pre.tagName = pre.tagName;
|
|
202
|
+
job.pre.properties = pre.properties;
|
|
203
|
+
job.pre.children = pre.children;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -4,12 +4,6 @@ import { type MediaResolve } from './resolve-media.js';
|
|
|
4
4
|
import { type ComponentRegistry } from './registry.js';
|
|
5
5
|
import type { LinkResolve } from '../content/links.js';
|
|
6
6
|
export interface RendererOptions {
|
|
7
|
-
/**
|
|
8
|
-
* Stamp a `data-rise` ordinal (0, 1, 2, …) on each top-level component so a site's
|
|
9
|
-
* CSS can drive an entrance-cascade delay off it. Omit for no stagger. The ordinal
|
|
10
|
-
* is inert, so a consumer's sanitize floor can keep `data-rise` and drop `style`.
|
|
11
|
-
*/
|
|
12
|
-
stagger?: boolean;
|
|
13
7
|
/**
|
|
14
8
|
* Extend the sanitize allowlist. Receives cairn's default schema (defaultSchema plus the
|
|
15
9
|
* directive markers and the common benign tags) and returns the schema to use. Add to the
|
package/dist/render/pipeline.js
CHANGED
|
@@ -8,7 +8,8 @@ import rehypeSlug from 'rehype-slug';
|
|
|
8
8
|
import rehypeStringify from 'rehype-stringify';
|
|
9
9
|
import rehypeSanitize from 'rehype-sanitize';
|
|
10
10
|
import { VFile } from 'vfile';
|
|
11
|
-
import { buildSanitizeSchema, rehypeAnchorRel, rehypeSinkGuard } from './sanitize-schema.js';
|
|
11
|
+
import { buildSanitizeSchema, rehypeAnchorRel, rehypeSinkGuard, rehypeTaskListA11y } from './sanitize-schema.js';
|
|
12
|
+
import { rehypeCairnHighlight } from './highlight.js';
|
|
12
13
|
import { remarkDirectiveStamp } from './remark-directives.js';
|
|
13
14
|
import { remarkFigure } from './remark-figure.js';
|
|
14
15
|
import { remarkResolveCairnLinks, CAIRN_RESOLVE } from './resolve-links.js';
|
|
@@ -38,8 +39,18 @@ export function createRenderer(registry = defineRegistry({ components: [] }), op
|
|
|
38
39
|
const rehypePlugins = [
|
|
39
40
|
rehypeRaw,
|
|
40
41
|
...floor,
|
|
41
|
-
[rehypeDispatch, registry
|
|
42
|
+
[rehypeDispatch, registry],
|
|
42
43
|
rehypeSlug,
|
|
44
|
+
// Name each GFM task-list checkbox from its item text. It runs after the sanitize floor (which
|
|
45
|
+
// does not allow aria-label) so the added attribute survives, and is content-not-sink, so it is
|
|
46
|
+
// not gated by unsafeDisableSanitize.
|
|
47
|
+
rehypeTaskListA11y,
|
|
48
|
+
// Build-time syntax highlighting. It emits class-only output (the cairn-tok-* ramp, no inline
|
|
49
|
+
// style), so it is class-driven like the rest of the pipeline and needs no special placement
|
|
50
|
+
// relative to the sanitize floor or the sink guard: the token classes survive the floor because
|
|
51
|
+
// `className` is already allowed on `*`. It runs unconditionally (a code fence is content, not a
|
|
52
|
+
// sink) and ships no client highlighter (Shiki is build-only behind a dynamic import).
|
|
53
|
+
rehypeCairnHighlight,
|
|
43
54
|
];
|
|
44
55
|
if (rel !== false)
|
|
45
56
|
rehypePlugins.push([rehypeAnchorRel, rel]);
|
|
@@ -1,32 +1,6 @@
|
|
|
1
1
|
import type { Element, ElementContent } from 'hast';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
/** One `{key="value"}` attribute on a component directive, or one field of a repeatable item. */
|
|
5
|
-
export interface AttributeField {
|
|
6
|
-
/** The attribute name as it appears in the directive, e.g. `icon`. */
|
|
7
|
-
key: string;
|
|
8
|
-
/** The form label. */
|
|
9
|
-
label: string;
|
|
10
|
-
type: FieldType;
|
|
11
|
-
required?: boolean;
|
|
12
|
-
/** Initial value; a string for text/select/icon, a boolean for boolean. */
|
|
13
|
-
default?: string | boolean;
|
|
14
|
-
/** Allowed values for `type: 'select'`. */
|
|
15
|
-
options?: readonly string[];
|
|
16
|
-
/** Helper text shown under the field. */
|
|
17
|
-
help?: string;
|
|
18
|
-
/** A RegExp `source` to validate the value against, plus the message to show on a mismatch. */
|
|
19
|
-
pattern?: {
|
|
20
|
-
source: string;
|
|
21
|
-
message: string;
|
|
22
|
-
};
|
|
23
|
-
/**
|
|
24
|
-
* A pure, browser-safe cross-field validator. Returns an error string, or null when valid.
|
|
25
|
-
* Receives the field's value and the full {@link ComponentValues} so a rule can read sibling
|
|
26
|
-
* fields. The picker wraps the call in try/catch so an author's throw never crashes the form.
|
|
27
|
-
*/
|
|
28
|
-
validate?: (value: string | boolean, all: ComponentValues) => string | null;
|
|
29
|
-
}
|
|
2
|
+
import type { FieldDescriptor } from '../content/fields.js';
|
|
3
|
+
import type { BehaviorTable, Fieldset } from '../content/fieldset.js';
|
|
30
4
|
export type SlotKind = 'markdown' | 'inline' | 'repeatable';
|
|
31
5
|
/**
|
|
32
6
|
* One named content region of a component. The slots named `title` and `body` are special: `title`
|
|
@@ -39,7 +13,7 @@ export interface SlotDef {
|
|
|
39
13
|
required?: boolean;
|
|
40
14
|
help?: string;
|
|
41
15
|
/** For `kind: 'repeatable'`: the fields composing each list item (v1 uses the first field). */
|
|
42
|
-
itemFields?:
|
|
16
|
+
itemFields?: Record<string, FieldDescriptor>;
|
|
43
17
|
/**
|
|
44
18
|
* For `kind: 'repeatable'`: derives a row's label from its item values and zero-based index.
|
|
45
19
|
* When it returns nothing, the picker falls back to `${label} ${index + 1}`.
|
|
@@ -74,19 +48,45 @@ export interface ComponentDef {
|
|
|
74
48
|
insertTemplate?: string;
|
|
75
49
|
/**
|
|
76
50
|
* Build the final hast element from the component context (attributes plus partitioned
|
|
77
|
-
* slots). The engine stamps the entrance
|
|
51
|
+
* slots). The engine stamps the entrance ordinal (`data-rise`) on the top-level
|
|
78
52
|
* result, so a build fn stays free of any motion concern.
|
|
79
53
|
*/
|
|
80
54
|
build: (ctx: ComponentContext) => Element;
|
|
81
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Opt this directive into client hydration (phase 4b islands). `true` mounts the island eagerly on
|
|
57
|
+
* first load and after client-side navigation; `'visible'` defers the mount to first intersection.
|
|
58
|
+
* The engine wraps {@link ComponentDef.build}'s output in an island boundary, and the site registers
|
|
59
|
+
* the live Svelte component under the same name on `rendering.islands`. Absent leaves the directive a
|
|
60
|
+
* static, server-only component.
|
|
61
|
+
*/
|
|
62
|
+
hydrate?: boolean | 'visible';
|
|
63
|
+
/**
|
|
64
|
+
* Optional role-to-default-icon, e.g. `{ caution: 'warning' }`. Maps a free-string role to a
|
|
65
|
+
* glyph key in the site IconSet; choose a logically representative glyph and prefer glyphs
|
|
66
|
+
* distinct across roles so the picker stays scannable. Overrides the engine
|
|
67
|
+
* {@link DEFAULT_ICON_BY_ROLE} fallback for the roles it names.
|
|
68
|
+
*/
|
|
82
69
|
defaultIconByRole?: Record<string, string>;
|
|
83
70
|
/** One line on when to reach for this component; feeds the picker and the reference file. */
|
|
84
71
|
use?: string;
|
|
85
|
-
/** The `{key="value"}` attributes this component accepts. */
|
|
86
|
-
attributes?:
|
|
72
|
+
/** The `{key="value"}` attributes this component accepts, keyed by attribute name. */
|
|
73
|
+
attributes?: Record<string, FieldDescriptor>;
|
|
74
|
+
/**
|
|
75
|
+
* Per-attribute function-valued behavior (a cross-field `validate`), keyed by attribute name.
|
|
76
|
+
* {@link defineComponent} bundles it into the attribute {@link Fieldset}.
|
|
77
|
+
*/
|
|
78
|
+
behavior?: BehaviorTable;
|
|
79
|
+
/**
|
|
80
|
+
* The attribute validator {@link defineComponent} builds from `attributes` and `behavior`.
|
|
81
|
+
* Engine-internal: the constructor sets it, and {@link validateComponent} runs it.
|
|
82
|
+
*/
|
|
83
|
+
attributeSchema?: Fieldset;
|
|
87
84
|
/** The named content regions this component accepts. */
|
|
88
85
|
slots?: SlotDef[];
|
|
89
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* A glyph key from the site IconSet, shown beside the label in the picker. Choose a logically
|
|
88
|
+
* representative glyph and prefer glyphs distinct across components so the picker stays scannable.
|
|
89
|
+
*/
|
|
90
90
|
icon?: string;
|
|
91
91
|
/** A category heading for the picker. Components order by declaration within a group. */
|
|
92
92
|
group?: string;
|
|
@@ -106,8 +106,8 @@ export interface ComponentRegistry {
|
|
|
106
106
|
names: string[];
|
|
107
107
|
get(name: string): ComponentDef | undefined;
|
|
108
108
|
defaultIcon(name: string, role?: string): string | undefined;
|
|
109
|
-
/** The component's first `type:'icon'` attribute, or undefined when it declares none. */
|
|
110
|
-
iconField(name: string):
|
|
109
|
+
/** The name of the component's first `type:'icon'` attribute, or undefined when it declares none. */
|
|
110
|
+
iconField(name: string): string | undefined;
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
113
|
* The hast property name carrying one declared attribute from stamp to dispatch, e.g. `tone`
|
|
@@ -141,3 +141,11 @@ export declare function emptyValues(def: ComponentDef): ComponentValues;
|
|
|
141
141
|
* the def declares no `preview`, returns exactly the {@link emptyValues} output.
|
|
142
142
|
*/
|
|
143
143
|
export declare function previewValues(def: ComponentDef): ComponentValues;
|
|
144
|
+
/**
|
|
145
|
+
* Declare a site component, building its attribute validator from the `fields.*` descriptors and
|
|
146
|
+
* validating the component at declaration. Mirrors {@link defineConcept}: a malformed attribute type
|
|
147
|
+
* or pattern fails at module load. The built `attributeSchema` is what {@link validateComponent} runs.
|
|
148
|
+
*/
|
|
149
|
+
export declare function defineComponent<const D extends ComponentDef>(def: D): D & {
|
|
150
|
+
attributeSchema: Fieldset;
|
|
151
|
+
};
|
package/dist/render/registry.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fieldset } from '../content/fieldset.js';
|
|
1
2
|
/**
|
|
2
3
|
* The hast property name carrying one declared attribute from stamp to dispatch, e.g. `tone`
|
|
3
4
|
* becomes `dataAttrTone`. The directive stamp writes it and the rehype dispatch reads it, so both
|
|
@@ -7,13 +8,26 @@ export function dataAttrProp(key) {
|
|
|
7
8
|
return `dataAttr${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
8
9
|
}
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
* construction-time guard and the registry's `iconField` derive the icon field from this one
|
|
11
|
+
* The name of a component's first `type:'icon'` attribute, or undefined when it declares none. Both
|
|
12
|
+
* the construction-time guard and the registry's `iconField` derive the icon field from this one
|
|
12
13
|
* predicate rather than spelling the `type === 'icon'` find twice.
|
|
13
14
|
*/
|
|
14
15
|
function findIconField(def) {
|
|
15
|
-
return def.attributes
|
|
16
|
+
return Object.entries(def.attributes ?? {}).find(([, field]) => field.type === 'icon')?.[0];
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* The engine's role-to-glyph-key fallback for the conventional admonition roles, which a site's
|
|
20
|
+
* IconSet may satisfy. A component's own {@link ComponentDef.defaultIconByRole} overrides it.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_ICON_BY_ROLE = {
|
|
23
|
+
note: 'info',
|
|
24
|
+
tip: 'lightbulb',
|
|
25
|
+
important: 'star',
|
|
26
|
+
warning: 'warning',
|
|
27
|
+
caution: 'alert-triangle',
|
|
28
|
+
info: 'info',
|
|
29
|
+
danger: 'flame',
|
|
30
|
+
};
|
|
17
31
|
/**
|
|
18
32
|
* Build a registry from a site's component definitions. The single source the render
|
|
19
33
|
* pipeline (directive stamp plus rehype dispatch) and the editor palette both read.
|
|
@@ -32,7 +46,14 @@ export function defineRegistry({ components }) {
|
|
|
32
46
|
defs: components,
|
|
33
47
|
names: components.map((c) => c.name),
|
|
34
48
|
get: (name) => byName.get(name),
|
|
35
|
-
defaultIcon: (name, role) =>
|
|
49
|
+
defaultIcon: (name, role) => {
|
|
50
|
+
if (!role)
|
|
51
|
+
return undefined;
|
|
52
|
+
const def = byName.get(name);
|
|
53
|
+
if (!def || !findIconField(def))
|
|
54
|
+
return undefined;
|
|
55
|
+
return def.defaultIconByRole?.[role] ?? DEFAULT_ICON_BY_ROLE[role];
|
|
56
|
+
},
|
|
36
57
|
iconField: (name) => {
|
|
37
58
|
const def = byName.get(name);
|
|
38
59
|
return def ? findIconField(def) : undefined;
|
|
@@ -45,8 +66,8 @@ export function defineRegistry({ components }) {
|
|
|
45
66
|
*/
|
|
46
67
|
export function emptyValues(def) {
|
|
47
68
|
const attributes = {};
|
|
48
|
-
for (const field of def.attributes ??
|
|
49
|
-
attributes[
|
|
69
|
+
for (const [name, field] of Object.entries(def.attributes ?? {})) {
|
|
70
|
+
attributes[name] = field.default ?? (field.type === 'boolean' ? false : '');
|
|
50
71
|
}
|
|
51
72
|
const slots = {};
|
|
52
73
|
for (const slot of def.slots ?? []) {
|
|
@@ -68,3 +89,23 @@ export function previewValues(def) {
|
|
|
68
89
|
slots: { ...base.slots, ...def.preview.slots },
|
|
69
90
|
};
|
|
70
91
|
}
|
|
92
|
+
/** The descriptor types that serialize to a single directive-attribute string (decision 2). */
|
|
93
|
+
const ATTRIBUTE_TYPES = new Set(['text', 'textarea', 'number', 'select', 'url', 'email', 'date', 'datetime', 'boolean', 'icon']);
|
|
94
|
+
/** Reject an attribute type that cannot serialize to a single directive-attribute string (decision 2). */
|
|
95
|
+
function checkComponentAttributes(name, attributes) {
|
|
96
|
+
for (const [key, field] of Object.entries(attributes)) {
|
|
97
|
+
if (!ATTRIBUTE_TYPES.has(field.type)) {
|
|
98
|
+
throw new Error(`cairn: component "${name}" attribute "${key}" is type "${field.type}"; a directive attribute must be a single-value scalar (text, textarea, number, select, url, email, date, datetime, boolean, or icon).`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Declare a site component, building its attribute validator from the `fields.*` descriptors and
|
|
104
|
+
* validating the component at declaration. Mirrors {@link defineConcept}: a malformed attribute type
|
|
105
|
+
* or pattern fails at module load. The built `attributeSchema` is what {@link validateComponent} runs.
|
|
106
|
+
*/
|
|
107
|
+
export function defineComponent(def) {
|
|
108
|
+
const attributes = def.attributes ?? {};
|
|
109
|
+
checkComponentAttributes(def.name, attributes);
|
|
110
|
+
return { ...def, attributeSchema: fieldset(attributes, { behavior: def.behavior }) };
|
|
111
|
+
}
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import type { Root, Element, ElementContent } from 'hast';
|
|
2
2
|
import { type ComponentContext, type ComponentRegistry } from './registry.js';
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
5
|
-
*/
|
|
3
|
+
/** Narrow a hast node to an Element, false for a text node, a comment, or undefined. */
|
|
6
4
|
export declare function isElement(node: ElementContent | undefined): node is Element;
|
|
7
5
|
/**
|
|
8
6
|
* Read a declared string attribute off the component context, returning undefined for a boolean or
|
|
9
7
|
* absent value. Replaces the `typeof ctx.attributes[key] === 'string'` narrowing a build repeats.
|
|
10
8
|
*/
|
|
11
9
|
export declare function strAttr(ctx: ComponentContext, key: string): string | undefined;
|
|
12
|
-
/**
|
|
13
|
-
*
|
|
14
|
-
*/
|
|
10
|
+
/** Read a hast element property as a string, or undefined when it is absent or non-string. */
|
|
15
11
|
export declare function strProp(node: Element, name: string): string | undefined;
|
|
16
12
|
/** Wrap a pre-built glyph in an ec-icon span; secondary role adds the modifier. */
|
|
17
13
|
export declare function iconSpan(glyphEl: Element, role?: string): Element;
|
|
@@ -33,10 +29,10 @@ export declare function headRow(title: ElementContent[], icon?: Element, level?:
|
|
|
33
29
|
export declare function markFirstList(children: ElementContent[]): Element | undefined;
|
|
34
30
|
/**
|
|
35
31
|
* Rehype transformer: dispatch each stamped element through its registry `build`
|
|
36
|
-
* fn.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
32
|
+
* fn. Each top-level primitive gets a `data-rise` attribute carrying its
|
|
33
|
+
* document-order index (0, 1, 2, …); the site's CSS maps that ordinal to an
|
|
34
|
+
* entrance delay. The index is inert, so a consumer's sanitize floor can keep
|
|
39
35
|
* `data-rise` while dropping `style`. Nested primitives never get it. Non-primitive
|
|
40
36
|
* content (lede, intro paragraphs, the page-toc nav) passes through untouched.
|
|
41
37
|
*/
|
|
42
|
-
export declare function rehypeDispatch(registry: ComponentRegistry
|
|
38
|
+
export declare function rehypeDispatch(registry: ComponentRegistry): (tree: Root) => void;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { h } from 'hastscript';
|
|
2
2
|
import { dataAttrProp } from './registry.js';
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
5
|
-
*/
|
|
3
|
+
/** Narrow a hast node to an Element, false for a text node, a comment, or undefined. */
|
|
6
4
|
export function isElement(node) {
|
|
7
5
|
return !!node && node.type === 'element';
|
|
8
6
|
}
|
|
@@ -17,9 +15,7 @@ export function strAttr(ctx, key) {
|
|
|
17
15
|
// hast Properties values are PropertyValue (string | number | boolean | array | null).
|
|
18
16
|
// Directive markers (dataPrimitive/dataRole/dataAttr<Key>) are always stamped as strings;
|
|
19
17
|
// this reads them back with that guarantee instead of casting at each call site.
|
|
20
|
-
/**
|
|
21
|
-
*
|
|
22
|
-
*/
|
|
18
|
+
/** Read a hast element property as a string, or undefined when it is absent or non-string. */
|
|
23
19
|
export function strProp(node, name) {
|
|
24
20
|
const value = node.properties?.[name];
|
|
25
21
|
return typeof value === 'string' ? value : undefined;
|
|
@@ -60,7 +56,7 @@ export function markFirstList(children) {
|
|
|
60
56
|
}
|
|
61
57
|
// Recurse into a node's children, transforming any nested primitive sections
|
|
62
58
|
// (a grid inside a card, panels inside a split). Nested primitives never carry the
|
|
63
|
-
// entrance
|
|
59
|
+
// entrance ordinal; only top-level ones do (stamped in the transformer below).
|
|
64
60
|
function transformChildren(children, registry) {
|
|
65
61
|
return children.map((c) => {
|
|
66
62
|
if (isElement(c) && c.properties?.dataPrimitive)
|
|
@@ -74,11 +70,11 @@ function transformChildren(children, registry) {
|
|
|
74
70
|
// 'true'/'false'; everything else is the literal string the author wrote.
|
|
75
71
|
function readAttributes(node, def) {
|
|
76
72
|
const out = {};
|
|
77
|
-
for (const field of def.attributes ??
|
|
78
|
-
const value = strProp(node, dataAttrProp(
|
|
73
|
+
for (const [name, field] of Object.entries(def.attributes ?? {})) {
|
|
74
|
+
const value = strProp(node, dataAttrProp(name));
|
|
79
75
|
if (value == null)
|
|
80
76
|
continue;
|
|
81
|
-
out[
|
|
77
|
+
out[name] = field.type === 'boolean' ? value === 'true' : value;
|
|
82
78
|
}
|
|
83
79
|
return out;
|
|
84
80
|
}
|
|
@@ -130,6 +126,31 @@ function partitionSlots(node) {
|
|
|
130
126
|
},
|
|
131
127
|
};
|
|
132
128
|
}
|
|
129
|
+
// Serialize a hydrate component's declared attributes into the island prop payload. A `number` field is
|
|
130
|
+
// coerced from its stamped string to a JSON number here; a `boolean` already arrived as a real boolean
|
|
131
|
+
// from readAttributes (which coerces 'true'/'false' upstream), and every other field stays the literal
|
|
132
|
+
// string the author wrote. The result is JSON.stringify-ed into data-cairn-props and parsed on the client.
|
|
133
|
+
function serializeIslandProps(def, attributes) {
|
|
134
|
+
const out = {};
|
|
135
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
136
|
+
const type = def.attributes?.[key]?.type;
|
|
137
|
+
out[key] = type === 'number' && typeof value === 'string' ? Number(value) : value;
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
// Wrap a hydrate component's static fallback in its island boundary. The boundary carries the directive
|
|
142
|
+
// name and the JSON prop payload; a 'visible' island also carries data-cairn-hydrate="visible". The
|
|
143
|
+
// boundary attributes are inert data-* and survive both the sanitize floor (this runs after it) and the
|
|
144
|
+
// sink guard (which strips only style/on*). The fallback is build()'s no-JS, first-paint representation.
|
|
145
|
+
function islandBoundary(name, def, attributes, fallback) {
|
|
146
|
+
const properties = {
|
|
147
|
+
dataCairnIsland: name,
|
|
148
|
+
dataCairnProps: JSON.stringify(serializeIslandProps(def, attributes)),
|
|
149
|
+
};
|
|
150
|
+
if (def.hydrate === 'visible')
|
|
151
|
+
properties.dataCairnHydrate = 'visible';
|
|
152
|
+
return { type: 'element', tagName: 'div', properties, children: [fallback] };
|
|
153
|
+
}
|
|
133
154
|
function transformNode(node, registry) {
|
|
134
155
|
node.children = transformChildren(node.children, registry);
|
|
135
156
|
const name = strProp(node, 'dataPrimitive');
|
|
@@ -143,24 +164,24 @@ function transformNode(node, registry) {
|
|
|
143
164
|
items: parts.items,
|
|
144
165
|
node,
|
|
145
166
|
};
|
|
146
|
-
|
|
167
|
+
const built = def.build(ctx);
|
|
168
|
+
return def.hydrate ? islandBoundary(name, def, ctx.attributes, built) : built;
|
|
147
169
|
}
|
|
148
170
|
/**
|
|
149
171
|
* Rehype transformer: dispatch each stamped element through its registry `build`
|
|
150
|
-
* fn.
|
|
151
|
-
*
|
|
152
|
-
*
|
|
172
|
+
* fn. Each top-level primitive gets a `data-rise` attribute carrying its
|
|
173
|
+
* document-order index (0, 1, 2, …); the site's CSS maps that ordinal to an
|
|
174
|
+
* entrance delay. The index is inert, so a consumer's sanitize floor can keep
|
|
153
175
|
* `data-rise` while dropping `style`. Nested primitives never get it. Non-primitive
|
|
154
176
|
* content (lede, intro paragraphs, the page-toc nav) passes through untouched.
|
|
155
177
|
*/
|
|
156
|
-
export function rehypeDispatch(registry
|
|
178
|
+
export function rehypeDispatch(registry) {
|
|
157
179
|
return (tree) => {
|
|
158
180
|
let idx = 0;
|
|
159
181
|
tree.children = tree.children.map((child) => {
|
|
160
182
|
if (isElement(child) && child.properties?.dataPrimitive) {
|
|
161
183
|
const el = transformNode(child, registry);
|
|
162
|
-
|
|
163
|
-
el.properties = { ...el.properties, dataRise: String(idx++) };
|
|
184
|
+
el.properties = { ...el.properties, dataRise: String(idx++) };
|
|
164
185
|
return el;
|
|
165
186
|
}
|
|
166
187
|
if (isElement(child))
|
|
@@ -63,8 +63,7 @@ export function remarkDirectiveStamp(registry) {
|
|
|
63
63
|
const def = registry.get(node.name);
|
|
64
64
|
const attrs = node.attributes ?? {};
|
|
65
65
|
const role = attrs.role || undefined;
|
|
66
|
-
const
|
|
67
|
-
const iconKey = iconField?.key ?? 'icon';
|
|
66
|
+
const iconKey = registry.iconField(node.name) ?? 'icon';
|
|
68
67
|
let icon = attrs[iconKey] || undefined;
|
|
69
68
|
if (!icon && role)
|
|
70
69
|
icon = registry.defaultIcon(node.name, role);
|
|
@@ -78,10 +77,10 @@ export function remarkDirectiveStamp(registry) {
|
|
|
78
77
|
// back to that default the same way a missing one does. data-attr-<key> survives to the
|
|
79
78
|
// element; build() consumes it and returns a fresh element, so the marker never reaches the
|
|
80
79
|
// published DOM.
|
|
81
|
-
for (const
|
|
82
|
-
const raw =
|
|
80
|
+
for (const name of Object.keys(def?.attributes ?? {})) {
|
|
81
|
+
const raw = name === iconKey ? icon : attrs[name];
|
|
83
82
|
if (raw != null)
|
|
84
|
-
properties[dataAttrProp(
|
|
83
|
+
properties[dataAttrProp(name)] = raw;
|
|
85
84
|
}
|
|
86
85
|
const data = node.data ?? (node.data = {});
|
|
87
86
|
data.hName = 'div';
|
|
@@ -20,6 +20,16 @@ export declare function buildSanitizeSchema(registry: ComponentRegistry, extend?
|
|
|
20
20
|
* `anchorRel` option (default `noopener noreferrer`); a site can override it or disable it entirely.
|
|
21
21
|
*/
|
|
22
22
|
export declare function rehypeAnchorRel(rel: string): (tree: Root) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Give every GFM task-list checkbox an accessible name from its item text. remark-gfm emits a real
|
|
25
|
+
* `<input type="checkbox" disabled>` with no label, which axe's `label` rule flags as a critical
|
|
26
|
+
* violation even though the control is read-only; the visible label is the surrounding `<li>` text,
|
|
27
|
+
* not associated programmatically. This sets `aria-label` on each task-list checkbox to its item's
|
|
28
|
+
* text so the name travels with the control, keeping the engine's real disabled input (the bar's
|
|
29
|
+
* non-color cue) while clearing the violation on every site. It must run after the sanitize floor,
|
|
30
|
+
* which does not allow `aria-label`, so the attribute is added once the floor has run.
|
|
31
|
+
*/
|
|
32
|
+
export declare function rehypeTaskListA11y(): (tree: Root) => void;
|
|
23
33
|
/**
|
|
24
34
|
* Post-dispatch safety floor over the fully-built tree. The pre-dispatch rehype-sanitize floor
|
|
25
35
|
* cleans author content, but a component build() runs after it and can route a raw author
|