@bestagentkits/render 0.1.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 (140) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/dist/cli.d.ts +18 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +336 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/diagnostics.d.ts +45 -0
  9. package/dist/diagnostics.d.ts.map +1 -0
  10. package/dist/diagnostics.js +57 -0
  11. package/dist/diagnostics.js.map +1 -0
  12. package/dist/diagram/adapter.d.ts +71 -0
  13. package/dist/diagram/adapter.d.ts.map +1 -0
  14. package/dist/diagram/adapter.js +87 -0
  15. package/dist/diagram/adapter.js.map +1 -0
  16. package/dist/errors.d.ts +37 -0
  17. package/dist/errors.d.ts.map +1 -0
  18. package/dist/errors.js +42 -0
  19. package/dist/errors.js.map +1 -0
  20. package/dist/hash.d.ts +14 -0
  21. package/dist/hash.d.ts.map +1 -0
  22. package/dist/hash.js +27 -0
  23. package/dist/hash.js.map +1 -0
  24. package/dist/index.d.ts +43 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +25 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/internal/yaml-error.d.ts +14 -0
  29. package/dist/internal/yaml-error.d.ts.map +1 -0
  30. package/dist/internal/yaml-error.js +12 -0
  31. package/dist/internal/yaml-error.js.map +1 -0
  32. package/dist/ir.d.ts +77 -0
  33. package/dist/ir.d.ts.map +1 -0
  34. package/dist/ir.js +20 -0
  35. package/dist/ir.js.map +1 -0
  36. package/dist/json.d.ts +23 -0
  37. package/dist/json.d.ts.map +1 -0
  38. package/dist/json.js +47 -0
  39. package/dist/json.js.map +1 -0
  40. package/dist/registry/actions.d.ts +48 -0
  41. package/dist/registry/actions.d.ts.map +1 -0
  42. package/dist/registry/actions.js +274 -0
  43. package/dist/registry/actions.js.map +1 -0
  44. package/dist/registry/prop-schema.d.ts +108 -0
  45. package/dist/registry/prop-schema.d.ts.map +1 -0
  46. package/dist/registry/prop-schema.js +400 -0
  47. package/dist/registry/prop-schema.js.map +1 -0
  48. package/dist/registry/registry.d.ts +47 -0
  49. package/dist/registry/registry.d.ts.map +1 -0
  50. package/dist/registry/registry.js +201 -0
  51. package/dist/registry/registry.js.map +1 -0
  52. package/dist/registry/roster.d.ts +53 -0
  53. package/dist/registry/roster.d.ts.map +1 -0
  54. package/dist/registry/roster.js +692 -0
  55. package/dist/registry/roster.js.map +1 -0
  56. package/dist/render/blocks.d.ts +41 -0
  57. package/dist/render/blocks.d.ts.map +1 -0
  58. package/dist/render/blocks.js +749 -0
  59. package/dist/render/blocks.js.map +1 -0
  60. package/dist/render/charts.d.ts +29 -0
  61. package/dist/render/charts.d.ts.map +1 -0
  62. package/dist/render/charts.js +235 -0
  63. package/dist/render/charts.js.map +1 -0
  64. package/dist/render/document.d.ts +40 -0
  65. package/dist/render/document.d.ts.map +1 -0
  66. package/dist/render/document.js +90 -0
  67. package/dist/render/document.js.map +1 -0
  68. package/dist/render/escape.d.ts +46 -0
  69. package/dist/render/escape.d.ts.map +1 -0
  70. package/dist/render/escape.js +108 -0
  71. package/dist/render/escape.js.map +1 -0
  72. package/dist/render/render.d.ts +52 -0
  73. package/dist/render/render.d.ts.map +1 -0
  74. package/dist/render/render.js +216 -0
  75. package/dist/render/render.js.map +1 -0
  76. package/dist/render/runtime.d.ts +25 -0
  77. package/dist/render/runtime.d.ts.map +1 -0
  78. package/dist/render/runtime.js +451 -0
  79. package/dist/render/runtime.js.map +1 -0
  80. package/dist/render/styles.d.ts +15 -0
  81. package/dist/render/styles.d.ts.map +1 -0
  82. package/dist/render/styles.js +163 -0
  83. package/dist/render/styles.js.map +1 -0
  84. package/dist/render/verify.d.ts +25 -0
  85. package/dist/render/verify.d.ts.map +1 -0
  86. package/dist/render/verify.js +168 -0
  87. package/dist/render/verify.js.map +1 -0
  88. package/dist/spec/bindings.d.ts +19 -0
  89. package/dist/spec/bindings.d.ts.map +1 -0
  90. package/dist/spec/bindings.js +51 -0
  91. package/dist/spec/bindings.js.map +1 -0
  92. package/dist/spec/bounds.d.ts +43 -0
  93. package/dist/spec/bounds.d.ts.map +1 -0
  94. package/dist/spec/bounds.js +144 -0
  95. package/dist/spec/bounds.js.map +1 -0
  96. package/dist/spec/forbidden.d.ts +27 -0
  97. package/dist/spec/forbidden.d.ts.map +1 -0
  98. package/dist/spec/forbidden.js +91 -0
  99. package/dist/spec/forbidden.js.map +1 -0
  100. package/dist/spec/migrate.d.ts +33 -0
  101. package/dist/spec/migrate.d.ts.map +1 -0
  102. package/dist/spec/migrate.js +113 -0
  103. package/dist/spec/migrate.js.map +1 -0
  104. package/dist/spec/normalize.d.ts +29 -0
  105. package/dist/spec/normalize.d.ts.map +1 -0
  106. package/dist/spec/normalize.js +649 -0
  107. package/dist/spec/normalize.js.map +1 -0
  108. package/dist/spec/parse.d.ts +22 -0
  109. package/dist/spec/parse.d.ts.map +1 -0
  110. package/dist/spec/parse.js +62 -0
  111. package/dist/spec/parse.js.map +1 -0
  112. package/dist/spec/providers.d.ts +30 -0
  113. package/dist/spec/providers.d.ts.map +1 -0
  114. package/dist/spec/providers.js +64 -0
  115. package/dist/spec/providers.js.map +1 -0
  116. package/dist/spec/validate.d.ts +28 -0
  117. package/dist/spec/validate.d.ts.map +1 -0
  118. package/dist/spec/validate.js +68 -0
  119. package/dist/spec/validate.js.map +1 -0
  120. package/dist/theme/load-theme.d.ts +55 -0
  121. package/dist/theme/load-theme.d.ts.map +1 -0
  122. package/dist/theme/load-theme.js +219 -0
  123. package/dist/theme/load-theme.js.map +1 -0
  124. package/dist/theme/presets.d.ts +50 -0
  125. package/dist/theme/presets.d.ts.map +1 -0
  126. package/dist/theme/presets.js +304 -0
  127. package/dist/theme/presets.js.map +1 -0
  128. package/dist/theme/theme-catalog.d.ts +68 -0
  129. package/dist/theme/theme-catalog.d.ts.map +1 -0
  130. package/dist/theme/theme-catalog.js +198 -0
  131. package/dist/theme/theme-catalog.js.map +1 -0
  132. package/dist/theme/tokens.d.ts +38 -0
  133. package/dist/theme/tokens.d.ts.map +1 -0
  134. package/dist/theme/tokens.js +190 -0
  135. package/dist/theme/tokens.js.map +1 -0
  136. package/dist/version.d.ts +12 -0
  137. package/dist/version.d.ts.map +1 -0
  138. package/dist/version.js +12 -0
  139. package/dist/version.js.map +1 -0
  140. package/package.json +80 -0
@@ -0,0 +1,749 @@
1
+ /**
2
+ * Block renderers.
3
+ *
4
+ * One renderer per registry type. Renderers are pure functions of a node, the
5
+ * IR, and the resolved theme: no clock, no randomness, no module-level mutable
6
+ * state, because the emitted bytes must be reproducible.
7
+ *
8
+ * Two conventions apply to every renderer:
9
+ * 1. Exactly one element in the emitted markup carries `data-ak-id`, so a
10
+ * `copy`/`download`/`toggle` target resolves to the content a reader means.
11
+ * 2. Action bindings become `data-ak-on-*` attributes; the runtime owns what
12
+ * they do. No renderer emits a handler, a style attribute, or a script.
13
+ */
14
+ import { runDiagramAdapter } from '../diagram/adapter.js';
15
+ import { isPlainObject } from '../json.js';
16
+ import { EMBED_PROVIDERS, hostMatchesAnyProvider, hostMatchesProvider } from '../spec/providers.js';
17
+ import { renderChart } from './charts.js';
18
+ import { escapeAttribute, escapeText, escapeUrl, renderAttributes, } from './escape.js';
19
+ const NO_OP_RENDERER = () => '';
20
+ function stringProp(node, key, fallback = '') {
21
+ const value = node.props[key];
22
+ return typeof value === 'string' ? value : fallback;
23
+ }
24
+ function numberProp(node, key, fallback = 0) {
25
+ const value = node.props[key];
26
+ return typeof value === 'number' ? value : fallback;
27
+ }
28
+ function listProp(node, key) {
29
+ const value = node.props[key];
30
+ return Array.isArray(value) ? value : [];
31
+ }
32
+ function objectListProp(node, key) {
33
+ return listProp(node, key).filter(isPlainObject);
34
+ }
35
+ function str(value, fallback = '') {
36
+ return typeof value === 'string' ? value : fallback;
37
+ }
38
+ function numProp(value, fallback = 0) {
39
+ return typeof value === 'number' ? value : fallback;
40
+ }
41
+ /** `data-ak-id` plus every non-click binding on the node. */
42
+ function nodeAttributes(node, extra = {}) {
43
+ const attributes = { 'data-ak-id': node.id, ...extra };
44
+ for (const [event, actions] of Object.entries(node.bindings)) {
45
+ if (event === 'click')
46
+ continue;
47
+ attributes[`data-ak-on-${event}`] = JSON.stringify(actions);
48
+ }
49
+ return attributes;
50
+ }
51
+ /** `data-ak-on-click` for a control, when the node binds a click action. */
52
+ function clickAttribute(bindings) {
53
+ const actions = bindings.click;
54
+ if (actions === undefined)
55
+ return {};
56
+ return { 'data-ak-on-click': JSON.stringify(actions) };
57
+ }
58
+ function element(tag, attributes, inner) {
59
+ const rendered = renderAttributes(attributes);
60
+ return inner === '' ? `<${tag}${rendered}></${tag}>` : `<${tag}${rendered}>${inner}</${tag}>`;
61
+ }
62
+ function heading(level, text, className = 'ak-section-head') {
63
+ return `<header class="${className}"><h${level}>${escapeText(text)}</h${level}></header>`;
64
+ }
65
+ /** A section heading for blocks whose `title` is optional. */
66
+ function titleHeader(node, level = 2) {
67
+ const title = stringProp(node, 'title');
68
+ return title === '' ? '' : heading(level, title);
69
+ }
70
+ function figureCaption(value) {
71
+ return value === '' ? '' : `<figcaption>${escapeText(value)}</figcaption>`;
72
+ }
73
+ function toneAttribute(node, fallback = 'info') {
74
+ return { 'data-tone': stringProp(node, 'tone', fallback) };
75
+ }
76
+ function isRemote(reference) {
77
+ return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(reference);
78
+ }
79
+ function capabilityAllowed(policy, capability) {
80
+ if (policy === 'deny')
81
+ return false;
82
+ return policy.allow.includes(capability);
83
+ }
84
+ /**
85
+ * Decide whether a remote reference may be loaded.
86
+ *
87
+ * Two gates apply. The capability must be opted into, and when the page declares
88
+ * a provider allowlist the reference's host must belong to one of those
89
+ * providers. A block that names a provider itself requires that provider to be
90
+ * allowlisted, so a spec cannot promote an arbitrary host to trusted by
91
+ * labelling it "youtube".
92
+ */
93
+ function networkAllows(policy, capability, reference, provider) {
94
+ if (!capabilityAllowed(policy, capability))
95
+ return false;
96
+ if (policy === 'deny')
97
+ return false;
98
+ const declared = policy.providers ?? [];
99
+ if (provider !== '') {
100
+ return declared.includes(provider) && hostMatchesProvider(provider, reference);
101
+ }
102
+ if (declared.length === 0)
103
+ return true;
104
+ return hostMatchesAnyProvider(declared, reference);
105
+ }
106
+ /**
107
+ * Resolve a media reference against the network policy.
108
+ *
109
+ * A relative path is local and always allowed; a remote reference needs the
110
+ * matching capability and, when a provider allowlist exists, an allowlisted
111
+ * host. Nothing here can produce an embed the spec did not ask for in a way the
112
+ * policy did not allow.
113
+ */
114
+ function resolveMedia(reference, policy, capability, provider = '') {
115
+ if (!isRemote(reference))
116
+ return { allowed: true, src: escapeUrl(reference) };
117
+ return {
118
+ allowed: networkAllows(policy, capability, reference, provider),
119
+ src: escapeUrl(reference),
120
+ };
121
+ }
122
+ function mediaFallbackBody(title, description, url, poster, linkText, note = 'Remote media is not embedded because the page denies network access.') {
123
+ const posterMarkup = poster === '' ? '' : `<img src="${escapeAttribute(escapeUrl(poster))}" alt="" />`;
124
+ const link = url === ''
125
+ ? ''
126
+ : `<a href="${escapeAttribute(escapeUrl(url))}" rel="noreferrer noopener">${escapeText(linkText === '' ? url : linkText)}</a>`;
127
+ return [
128
+ posterMarkup,
129
+ title === '' ? '' : `<p><strong>${escapeText(title)}</strong></p>`,
130
+ `<p class="ak-muted">${escapeText(description)}</p>`,
131
+ `<p class="ak-caption">${escapeText(note)}</p>`,
132
+ link,
133
+ ]
134
+ .filter((part) => part !== '')
135
+ .join('');
136
+ }
137
+ const RENDERERS = {
138
+ page: (node, context) => {
139
+ // Every page needs exactly one h1. A hero (or an explicit level-1 heading)
140
+ // provides it; otherwise the document title does, so a page without a hero
141
+ // is still correctly structured.
142
+ const hasH1 = context.ir.nodes.some((candidate) => candidate.type === 'hero' ||
143
+ (candidate.type === 'heading' && numberProp(candidate, 'level', 2) === 1));
144
+ const title = hasH1
145
+ ? ''
146
+ : `<h1 class="ak-page-title">${escapeText(context.ir.meta.title)}</h1>`;
147
+ return element('main', { class: 'ak-main', id: 'ak-main-content', 'aria-label': context.ir.meta.title }, `${title}${context.renderChildren(node)}`);
148
+ },
149
+ section: (node, context) => element('section', nodeAttributes(node, { class: 'ak-block ak-section' }), `${titleHeader(node)}${context.renderChildren(node)}`),
150
+ stack: (node, context) => element('div', nodeAttributes(node, {
151
+ class: 'ak-block ak-stack',
152
+ 'data-gap': stringProp(node, 'gap', 'normal'),
153
+ }), context.renderChildren(node)),
154
+ grid: (node, context) => element('div', nodeAttributes(node, {
155
+ class: 'ak-block ak-grid',
156
+ 'data-ak-columns': String(Math.min(Math.max(numberProp(node, 'columns', 3), 1), 6)),
157
+ }), context.renderChildren(node)),
158
+ split: (node, context) => element('div', nodeAttributes(node, {
159
+ class: 'ak-block ak-split',
160
+ 'data-ratio': stringProp(node, 'ratio', 'even'),
161
+ }), context.renderChildren(node)),
162
+ spacer: (node) => element('div', {
163
+ class: 'ak-spacer',
164
+ 'data-size': stringProp(node, 'size', 'medium'),
165
+ 'aria-hidden': 'true',
166
+ }, ''),
167
+ divider: (node) => {
168
+ const label = stringProp(node, 'label');
169
+ return element('div', nodeAttributes(node, { class: 'ak-block' }), `<hr class="ak-divider" />${label === '' ? '' : `<p class="ak-label">${escapeText(label)}</p>`}`);
170
+ },
171
+ heading: (node) => {
172
+ const level = Math.min(Math.max(numberProp(node, 'level', 2), 1), 6);
173
+ return `<h${level}${renderAttributes(nodeAttributes(node, { class: 'ak-block' }))}>${escapeText(stringProp(node, 'text'))}</h${level}>`;
174
+ },
175
+ text: (node) => {
176
+ const variant = stringProp(node, 'variant', 'body');
177
+ const className = variant === 'lead'
178
+ ? 'ak-block ak-lead'
179
+ : variant === 'caption'
180
+ ? 'ak-block ak-caption'
181
+ : 'ak-block';
182
+ return `<p${renderAttributes(nodeAttributes(node, { class: className }))}>${escapeText(stringProp(node, 'text'))}</p>`;
183
+ },
184
+ 'rich-text': (node) => {
185
+ const paragraphs = stringProp(node, 'text')
186
+ .split(/\n{2,}/u)
187
+ .map((part) => part.trim())
188
+ .filter((part) => part !== '');
189
+ return element('div', nodeAttributes(node, { class: 'ak-block ak-prose' }), paragraphs.map((part) => `<p>${escapeText(part.replace(/\n/gu, ' '))}</p>`).join(''));
190
+ },
191
+ quote: (node) => {
192
+ const cite = stringProp(node, 'cite');
193
+ return element('blockquote', nodeAttributes(node, { class: 'ak-block ak-quote' }), `<p>${escapeText(stringProp(node, 'text'))}</p>${cite === '' ? '' : `<cite>${escapeText(cite)}</cite>`}`);
194
+ },
195
+ code: (node) => {
196
+ const title = stringProp(node, 'title');
197
+ const language = stringProp(node, 'language', 'text');
198
+ return [
199
+ `<div class="ak-block ak-code">`,
200
+ title === '' && language === ''
201
+ ? ''
202
+ : `<div class="ak-code-head"><span class="ak-label">${escapeText(title === '' ? language : title)}</span></div>`,
203
+ `<pre${renderAttributes({ 'data-ak-id': node.id })}><code${renderAttributes({
204
+ class: `language-${language}`,
205
+ })}>${escapeText(stringProp(node, 'text'))}</code></pre>`,
206
+ '</div>',
207
+ ]
208
+ .filter((part) => part !== '')
209
+ .join('');
210
+ },
211
+ badge: (node) => `<span${renderAttributes(nodeAttributes(node, { class: 'ak-badge', ...toneAttribute(node, 'neutral') }))}>${escapeText(stringProp(node, 'text'))}</span>`,
212
+ kbd: (node) => element('span', nodeAttributes(node, { class: 'ak-block ak-kbd' }), listProp(node, 'keys')
213
+ .map((key) => `<kbd>${escapeText(str(key))}</kbd>`)
214
+ .join('')),
215
+ 'key-value': (node) => element('div', nodeAttributes(node, { class: 'ak-block' }), `${titleHeader(node)}<dl class="ak-kv">${objectListProp(node, 'items')
216
+ .map((item) => `<dt>${escapeText(str(item.key))}</dt><dd>${escapeText(str(item.value))}</dd>`)
217
+ .join('')}</dl>`),
218
+ card: (node, context) => {
219
+ const text = stringProp(node, 'text');
220
+ return element('div', nodeAttributes(node, { class: 'ak-block ak-card ak-card--elevated' }), [
221
+ stringProp(node, 'title') === ''
222
+ ? ''
223
+ : heading(3, stringProp(node, 'title'), 'ak-card-title'),
224
+ text === '' ? '' : `<p>${escapeText(text)}</p>`,
225
+ context.renderChildren(node),
226
+ ].join(''));
227
+ },
228
+ alert: (node) => {
229
+ const tone = stringProp(node, 'tone', 'info');
230
+ const role = tone === 'danger' || tone === 'warning' ? ' role="alert"' : '';
231
+ return [
232
+ `<div${renderAttributes(nodeAttributes(node, { class: 'ak-block ak-alert', ...toneAttribute(node) }))}${role}>`,
233
+ `<p><strong>${escapeText(stringProp(node, 'title'))}</strong></p>`,
234
+ `<p>${escapeText(stringProp(node, 'text'))}</p>`,
235
+ '</div>',
236
+ ].join('');
237
+ },
238
+ callout: (node) => {
239
+ const tone = stringProp(node, 'tone', 'info');
240
+ const icon = tone === 'danger'
241
+ ? 'Danger'
242
+ : tone === 'warning'
243
+ ? 'Warning'
244
+ : tone === 'success'
245
+ ? 'Done'
246
+ : 'Note';
247
+ return [
248
+ `<aside${renderAttributes(nodeAttributes(node, { class: 'ak-block ak-callout', ...toneAttribute(node) }))}>`,
249
+ `<p class="ak-label">${escapeText(icon)}</p>`,
250
+ `<p><strong>${escapeText(stringProp(node, 'title'))}</strong></p>`,
251
+ `<p>${escapeText(stringProp(node, 'text'))}</p>`,
252
+ '</aside>',
253
+ ].join('');
254
+ },
255
+ hero: (node) => {
256
+ const eyebrow = stringProp(node, 'eyebrow');
257
+ const description = stringProp(node, 'description');
258
+ return [
259
+ `<div${renderAttributes(nodeAttributes(node, { class: 'ak-block ak-hero' }))}>`,
260
+ eyebrow === '' ? '' : `<p class="ak-eyebrow">${escapeText(eyebrow)}</p>`,
261
+ `<h1>${escapeText(stringProp(node, 'title'))}</h1>`,
262
+ description === '' ? '' : `<p>${escapeText(description)}</p>`,
263
+ '</div>',
264
+ ].join('');
265
+ },
266
+ stats: (node) => {
267
+ const items = objectListProp(node, 'items');
268
+ return element('section', nodeAttributes(node, { class: 'ak-block' }), [
269
+ titleHeader(node),
270
+ items.length === 0
271
+ ? ''
272
+ : `<dl class="ak-stats">${items
273
+ .map((item) => `<div class="ak-stat"><dt>${escapeText(str(item.label))}</dt><dd>${escapeText(str(item.value))}</dd></div>`)
274
+ .join('')}</dl>`,
275
+ ].join(''));
276
+ },
277
+ progress: (node) => {
278
+ const value = numberProp(node, 'value');
279
+ const max = Math.max(numberProp(node, 'max', 100), 1);
280
+ const bounded = Math.min(Math.max(value, 0), max);
281
+ const label = stringProp(node, 'label');
282
+ return element('div', nodeAttributes(node, { class: 'ak-block ak-progress' }), [
283
+ `<p class="ak-label">${escapeText(label)}</p>`,
284
+ `<progress value="${bounded}" max="${max}">${bounded} / ${max}</progress>`,
285
+ `<p class="ak-caption">${bounded} of ${max}</p>`,
286
+ ].join(''));
287
+ },
288
+ steps: (node) => {
289
+ const items = objectListProp(node, 'items');
290
+ return element('section', nodeAttributes(node, { class: 'ak-block' }), [
291
+ titleHeader(node),
292
+ `<ol class="ak-steps">${items
293
+ .map((item) => {
294
+ const text = str(item.text);
295
+ return `<li><p><strong>${escapeText(str(item.title))}</strong></p>${text === '' ? '' : `<p>${escapeText(text)}</p>`}</li>`;
296
+ })
297
+ .join('')}</ol>`,
298
+ ].join(''));
299
+ },
300
+ timeline: (node) => {
301
+ const items = objectListProp(node, 'items');
302
+ return element('section', nodeAttributes(node, { class: 'ak-block' }), [
303
+ titleHeader(node),
304
+ `<ol class="ak-timeline">${items
305
+ .map((item) => {
306
+ const text = str(item.text);
307
+ return `<li><time>${escapeText(str(item.when))}</time><p><strong>${escapeText(str(item.title))}</strong></p>${text === '' ? '' : `<p>${escapeText(text)}</p>`}</li>`;
308
+ })
309
+ .join('')}</ol>`,
310
+ ].join(''));
311
+ },
312
+ list: (node) => {
313
+ const items = objectListProp(node, 'items');
314
+ return element('section', nodeAttributes(node, { class: 'ak-block' }), [
315
+ titleHeader(node),
316
+ `<ul class="ak-list">${items
317
+ .map((item) => {
318
+ const badge = str(item.badge);
319
+ return `<li data-ak-filter-item data-ak-label="${escapeAttribute(str(item.text))}"><span>${escapeText(str(item.text))}</span>${badge === '' ? '' : `<span class="ak-badge">${escapeText(badge)}</span>`}</li>`;
320
+ })
321
+ .join('')}</ul>`,
322
+ ].join(''));
323
+ },
324
+ 'card-grid': (node) => {
325
+ const items = objectListProp(node, 'items');
326
+ return element('section', nodeAttributes(node, { class: 'ak-block' }), [
327
+ titleHeader(node),
328
+ `<ul class="ak-grid" data-ak-columns="3">${items
329
+ .map((item) => {
330
+ const text = str(item.text);
331
+ return `<li class="ak-card ak-card--elevated"><p><strong>${escapeText(str(item.title))}</strong></p>${text === '' ? '' : `<p>${escapeText(text)}</p>`}</li>`;
332
+ })
333
+ .join('')}</ul>`,
334
+ ].join(''));
335
+ },
336
+ table: (node) => {
337
+ const columns = listProp(node, 'columns');
338
+ const rows = listProp(node, 'rows');
339
+ const title = stringProp(node, 'title');
340
+ const head = columns
341
+ .map((column) => `<th scope="col">${escapeText(str(column))}</th>`)
342
+ .join('');
343
+ const body = rows
344
+ .map((row) => {
345
+ const cells = (Array.isArray(row) ? row : [])
346
+ .map((cell) => `<td>${escapeText(str(cell))}</td>`)
347
+ .join('');
348
+ return `<tr>${cells}</tr>`;
349
+ })
350
+ .join('');
351
+ return element('section', nodeAttributes(node, { class: 'ak-block' }), [
352
+ title === '' ? '' : heading(2, title),
353
+ `<div class="ak-table-wrap"><table>${title === '' ? '' : `<caption>${escapeText(title)}</caption>`}<thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`,
354
+ ].join(''));
355
+ },
356
+ comparison: (node) => {
357
+ const left = isPlainObject(node.props.left) ? node.props.left : {};
358
+ const right = isPlainObject(node.props.right) ? node.props.right : {};
359
+ const side = (sideValue) => `<div class="ak-surface"><h3>${escapeText(str(sideValue.label))}</h3><ul>${(Array.isArray(sideValue.items)
360
+ ? sideValue.items
361
+ : [])
362
+ .map((item) => `<li>${escapeText(str(item))}</li>`)
363
+ .join('')}</ul></div>`;
364
+ return element('section', nodeAttributes(node, { class: 'ak-block' }), [titleHeader(node), `<div class="ak-compare">${side(left)}${side(right)}</div>`].join(''));
365
+ },
366
+ 'risk-matrix': (node) => {
367
+ const items = objectListProp(node, 'items');
368
+ return element('section', nodeAttributes(node, { class: 'ak-block' }), [
369
+ titleHeader(node),
370
+ `<div class="ak-table-wrap"><table><caption>Risk by impact and likelihood</caption><thead><tr><th scope="col">Area</th><th scope="col">Impact</th><th scope="col">Likelihood</th><th scope="col">Note</th></tr></thead><tbody>${items
371
+ .map((item) => `<tr><th scope="row">${escapeText(str(item.area))}</th><td>${escapeText(str(item.impact))}</td><td>${escapeText(str(item.likelihood))}</td><td>${escapeText(str(item.note))}</td></tr>`)
372
+ .join('')}</tbody></table></div>`,
373
+ ].join(''));
374
+ },
375
+ 'diff-summary': (node) => {
376
+ const items = objectListProp(node, 'items');
377
+ return element('section', nodeAttributes(node, { class: 'ak-block' }), [
378
+ titleHeader(node),
379
+ `<div class="ak-table-wrap"><table><caption>Changed files</caption><thead><tr><th scope="col">Path</th><th scope="col">Status</th><th scope="col">Additions</th><th scope="col">Deletions</th></tr></thead><tbody>${items
380
+ .map((item) => `<tr><th scope="row"><code>${escapeText(str(item.path))}</code></th><td>${escapeText(str(item.status))}</td><td>${escapeText(String(numProp(item.additions, 0)))}</td><td>${escapeText(String(numProp(item.deletions, 0)))}</td></tr>`)
381
+ .join('')}</tbody></table></div>`,
382
+ ].join(''));
383
+ },
384
+ 'code-review': (node) => {
385
+ const items = objectListProp(node, 'items');
386
+ return element('section', nodeAttributes(node, { class: 'ak-block' }), [
387
+ titleHeader(node),
388
+ `<ul class="ak-grid" data-ak-columns="2">${items
389
+ .map((item) => `<li class="ak-card ak-surface"><p class="ak-label">${escapeText(str(item.kind))}</p><p><strong>${escapeText(str(item.title))}</strong></p><p>${escapeText(str(item.text))}</p></li>`)
390
+ .join('')}</ul>`,
391
+ ].join(''));
392
+ },
393
+ tabs: (node) => {
394
+ const items = objectListProp(node, 'items');
395
+ const base = node.id;
396
+ const tabs = items
397
+ .map((item, index) => {
398
+ const tabId = str(item.id, `${base}-tab-${index}`);
399
+ return `<button type="button" role="tab" id="${escapeAttribute(tabId)}" data-ak-tab-id="${escapeAttribute(tabId)}" aria-controls="${escapeAttribute(`${base}-panel-${index}`)}" aria-selected="${index === 0 ? 'true' : 'false'}" tabindex="${index === 0 ? '0' : '-1'}">${escapeText(str(item.title))}</button>`;
400
+ })
401
+ .join('');
402
+ const panels = items
403
+ .map((item, index) => `<div role="tabpanel" id="${escapeAttribute(`${base}-panel-${index}`)}" aria-labelledby="${escapeAttribute(str(item.id, `${base}-tab-${index}`))}" tabindex="${index === 0 ? '0' : '-1'}"${index === 0 ? '' : ' hidden'}>${escapeText(str(item.text))}</div>`)
404
+ .join('');
405
+ return element('section', nodeAttributes(node, { class: 'ak-block ak-tabs', 'data-ak-tabs': 'true' }), [
406
+ titleHeader(node),
407
+ `<div role="tablist" aria-label="${escapeAttribute(stringProp(node, 'title', 'Tabs'))}">${tabs}</div>${panels}`,
408
+ ].join(''));
409
+ },
410
+ accordion: (node) => {
411
+ const items = objectListProp(node, 'items');
412
+ return element('section', nodeAttributes(node, { class: 'ak-block ak-accordion' }), [
413
+ titleHeader(node),
414
+ items
415
+ .map((item, index) =>
416
+ // Each disclosure carries its own id so a declarative expand or
417
+ // collapse action can address one section. Native <details> remains
418
+ // the behavior; the action only drives it.
419
+ `<details${index === 0 ? ' open' : ''}${renderAttributes({
420
+ 'data-ak-id': `${node.id}-item-${index}`,
421
+ })}><summary>${escapeText(str(item.title))}</summary><p>${escapeText(str(item.text))}</p></details>`)
422
+ .join(''),
423
+ ].join(''));
424
+ },
425
+ carousel: (node) => {
426
+ const items = objectListProp(node, 'items');
427
+ const label = stringProp(node, 'ariaLabel', 'Carousel');
428
+ const slides = items
429
+ .map((item, index) => `<div class="ak-carousel-slide" data-ak-slide tabindex="-1" aria-hidden="${index === 0 ? 'false' : 'true'}"${index === 0 ? '' : ' hidden'}><h3>${escapeText(str(item.title))}</h3><p>${escapeText(str(item.text))}</p></div>`)
430
+ .join('');
431
+ return element('section', nodeAttributes(node, {
432
+ class: 'ak-block ak-carousel',
433
+ 'data-ak-carousel-root': 'true',
434
+ role: 'group',
435
+ 'aria-roledescription': 'carousel',
436
+ 'aria-label': label,
437
+ }), [
438
+ `<div class="ak-carousel-slides">${slides}</div>`,
439
+ `<div class="ak-carousel-controls">
440
+ <button type="button" class="ak-btn" data-ak-carousel="prev" aria-label="Previous slide">Previous</button>
441
+ <button type="button" class="ak-btn" data-ak-carousel="next" aria-label="Next slide">Next</button>
442
+ <span class="ak-carousel-status" data-ak-carousel-status aria-live="polite">1 / ${Math.max(items.length, 1)}</span>
443
+ </div>`,
444
+ ].join(''));
445
+ },
446
+ toolbar: (node) => {
447
+ const items = objectListProp(node, 'items');
448
+ return element('div', nodeAttributes(node, { class: 'ak-block ak-toolbar', role: 'toolbar' }), [
449
+ titleHeader(node),
450
+ ...items.map((item) => {
451
+ const kind = str(item.type);
452
+ if (kind === 'badge') {
453
+ return `<span class="ak-badge" data-tone="${escapeAttribute(str(item.tone, 'neutral'))}">${escapeText(str(item.text))}</span>`;
454
+ }
455
+ if (kind === 'link') {
456
+ const href = str(item.href);
457
+ return `<a${renderAttributes({
458
+ href: escapeUrl(href),
459
+ rel: 'noreferrer noopener',
460
+ ...clickAttribute(isPlainObject(item.on) ? item.on : {}),
461
+ })}>${escapeText(str(item.label))}</a>`;
462
+ }
463
+ if (kind === 'button') {
464
+ const on = isPlainObject(item.on) ? item.on : {};
465
+ return `<button type="button" class="ak-btn"${renderAttributes({
466
+ 'data-variant': str(item.variant, 'secondary'),
467
+ 'data-ak-id': str(item.id) === '' ? undefined : str(item.id),
468
+ ...clickAttribute(on),
469
+ })}>${escapeText(str(item.label))}</button>`;
470
+ }
471
+ return '';
472
+ }),
473
+ ].join(''));
474
+ },
475
+ chart: (node) => {
476
+ const series = objectListProp(node, 'series').map((entry) => ({
477
+ label: str(entry.label),
478
+ values: (Array.isArray(entry.values) ? entry.values : []).map((value) => numProp(value, 0)),
479
+ }));
480
+ const description = stringProp(node, 'description');
481
+ const title = stringProp(node, 'title');
482
+ return `<div${renderAttributes(nodeAttributes(node, { class: 'ak-block' }))}>${renderChart({
483
+ kind: stringProp(node, 'kind', 'bar'),
484
+ id: node.id,
485
+ labels: listProp(node, 'labels').map((label) => str(label)),
486
+ series,
487
+ ...(title === '' ? {} : { title }),
488
+ ...(description === '' ? {} : { description }),
489
+ })}</div>`;
490
+ },
491
+ 'diagram-panel': (node, context) => {
492
+ const spec = isPlainObject(node.props.spec) ? node.props.spec : {};
493
+ const asObjects = (value) => (Array.isArray(value) ? value.filter(isPlainObject) : []);
494
+ const described = [...asObjects(spec.components), ...asObjects(spec.nodes)];
495
+ const edges = asObjects(spec.connections);
496
+ const meta = isPlainObject(spec.meta) ? spec.meta : {};
497
+ const diagramTitle = str(meta.title, stringProp(node, 'title', 'Diagram'));
498
+ const caption = stringProp(node, 'caption');
499
+ // The semantic fallback is always available: it is the only rendering when
500
+ // no adapter is installed, and it stays in the document as the accessible
501
+ // description when one is.
502
+ const fallbackBody = [
503
+ described.length === 0
504
+ ? ''
505
+ : `<ul class="ak-list">${described
506
+ .map((component) => `<li><code>${escapeText(str(component.id))}</code><span>${escapeText(str(component.label))}</span></li>`)
507
+ .join('')}</ul>`,
508
+ edges.length === 0
509
+ ? ''
510
+ : `<ul class="ak-list">${edges
511
+ .map((edge) => `<li><code>${escapeText(str(edge.from))} → ${escapeText(str(edge.to))}</code><span>${escapeText(str(edge.label))}</span></li>`)
512
+ .join('')}</ul>`,
513
+ ]
514
+ .filter((part) => part !== '')
515
+ .join('');
516
+ const adapterResult = runDiagramAdapter(context.diagramAdapter, {
517
+ spec: (node.props.spec ?? null),
518
+ title: diagramTitle,
519
+ nodeId: node.id,
520
+ });
521
+ if (adapterResult?.rejected !== undefined) {
522
+ context.warnings?.push({
523
+ code: 'POLICY_VIOLATION',
524
+ severity: 'warning',
525
+ path: `$.blocks.${node.id}`,
526
+ message: `diagram adapter "${adapterResult.adapter}" output was rejected (${adapterResult.rejected}); the structured fallback is used`,
527
+ });
528
+ }
529
+ const accepted = adapterResult !== undefined &&
530
+ adapterResult.rejected === undefined &&
531
+ adapterResult.markup !== '';
532
+ return [
533
+ `<figure${renderAttributes(nodeAttributes(node, { class: 'ak-block ak-diagram' }))}>`,
534
+ titleHeader(node, 2),
535
+ accepted
536
+ ? `<div class="ak-diagram-rendered" data-ak-diagram-adapter="${escapeAttribute(adapterResult.adapter)}">${adapterResult.markup}</div>`
537
+ : '',
538
+ `<div class="ak-diagram-fallback" data-ak-diagram-fallback>`,
539
+ `<p><strong>${escapeText(diagramTitle)}</strong></p>`,
540
+ fallbackBody,
541
+ accepted
542
+ ? '<p class="ak-caption">Rendered by a diagram adapter. The structured description below is the accessible equivalent.</p>'
543
+ : '<p class="ak-caption">Rendered as a structured fallback because no diagram adapter is configured.</p>',
544
+ '</div>',
545
+ figureCaption(caption),
546
+ '</figure>',
547
+ ]
548
+ .filter((part) => part !== '')
549
+ .join('');
550
+ },
551
+ image: (node, context) => {
552
+ const resolved = resolveMedia(stringProp(node, 'src'), context.ir.policy.network, 'images');
553
+ const alt = stringProp(node, 'alt');
554
+ const captionText = stringProp(node, 'caption');
555
+ if (!resolved.allowed) {
556
+ return [
557
+ `<figure${renderAttributes(nodeAttributes(node, { class: 'ak-block ak-media' }))}>`,
558
+ `<div class="ak-media-fallback"><p><strong>${escapeText(alt)}</strong></p><p class="ak-caption">Remote image not loaded: the page denies network access.</p><a href="${escapeAttribute(resolved.src)}" rel="noreferrer noopener">Open image</a></div>`,
559
+ figureCaption(captionText),
560
+ '</figure>',
561
+ ].join('');
562
+ }
563
+ return [
564
+ `<figure${renderAttributes(nodeAttributes(node, { class: 'ak-block ak-media' }))}>`,
565
+ `<img src="${escapeAttribute(resolved.src)}" alt="${escapeAttribute(alt)}" loading="lazy" decoding="async" />`,
566
+ figureCaption(captionText),
567
+ '</figure>',
568
+ ].join('');
569
+ },
570
+ gallery: (node, context) => {
571
+ const columns = Math.min(Math.max(numberProp(node, 'columns', 3), 1), 6);
572
+ const items = objectListProp(node, 'items');
573
+ return element('section', nodeAttributes(node, { class: 'ak-block' }), [
574
+ titleHeader(node),
575
+ `<ul class="ak-gallery" data-ak-columns="${columns}">${items
576
+ .map((item) => {
577
+ const resolved = resolveMedia(str(item.src), context.ir.policy.network, 'images');
578
+ const captionText = str(item.caption);
579
+ const body = resolved.allowed
580
+ ? `<img src="${escapeAttribute(resolved.src)}" alt="${escapeAttribute(str(item.alt))}" loading="lazy" decoding="async" />`
581
+ : `<a href="${escapeAttribute(resolved.src)}" rel="noreferrer noopener">${escapeText(str(item.alt))}</a>`;
582
+ return `<li><figure>${body}${captionText === '' ? '' : `<figcaption>${escapeText(captionText)}</figcaption>`}</figure></li>`;
583
+ })
584
+ .join('')}</ul>`,
585
+ ].join(''));
586
+ },
587
+ video: (node, context) => {
588
+ const source = stringProp(node, 'src');
589
+ const provider = stringProp(node, 'provider');
590
+ const resolved = resolveMedia(source, context.ir.policy.network, 'media', provider);
591
+ const fallback = isPlainObject(node.props.fallback) ? node.props.fallback : {};
592
+ const captionText = stringProp(node, 'caption');
593
+ // A provider reference never becomes a frame, whether or not it is allowed:
594
+ // the page links to the provider and keeps the poster, so the artifact stays
595
+ // frame-free. Only the explanation differs between allowed and denied.
596
+ if (provider !== '') {
597
+ const label = EMBED_PROVIDERS[provider]?.label ?? provider;
598
+ return [
599
+ `<figure${renderAttributes(nodeAttributes(node, { class: 'ak-block ak-media' }))}>`,
600
+ `<div class="ak-media-fallback">${mediaFallbackBody(stringProp(node, 'title'), str(fallback.description, `${label} content`), source, stringProp(node, 'poster'), str(fallback.linkText, `Open on ${label}`), resolved.allowed
601
+ ? `Embedded players are not emitted; this page links to ${label} instead.`
602
+ : `Embedded players are not emitted and the page denies network access; this page links to ${label} instead.`)}</div>`,
603
+ figureCaption(captionText),
604
+ '</figure>',
605
+ ].join('');
606
+ }
607
+ if (!resolved.allowed) {
608
+ return [
609
+ `<figure${renderAttributes(nodeAttributes(node, { class: 'ak-block ak-media' }))}>`,
610
+ `<div class="ak-media-fallback">${mediaFallbackBody(stringProp(node, 'title'), str(fallback.description, 'Remote video is unavailable without network access.'), str(fallback.url, source), stringProp(node, 'poster'), str(fallback.linkText, 'Open video'))}</div>`,
611
+ figureCaption(captionText),
612
+ '</figure>',
613
+ ].join('');
614
+ }
615
+ const poster = stringProp(node, 'poster');
616
+ return [
617
+ `<figure${renderAttributes(nodeAttributes(node, { class: 'ak-block ak-media' }))}>`,
618
+ `<video controls preload="metadata"${poster === '' ? '' : ` poster="${escapeAttribute(poster)}"`}><source src="${escapeAttribute(resolved.src)}" />${escapeText(str(fallback.description, 'Video content'))}</video>`,
619
+ figureCaption(captionText),
620
+ '</figure>',
621
+ ].join('');
622
+ },
623
+ audio: (node, context) => {
624
+ const source = stringProp(node, 'src');
625
+ const provider = stringProp(node, 'provider');
626
+ const resolved = resolveMedia(source, context.ir.policy.network, 'media', provider);
627
+ const fallback = isPlainObject(node.props.fallback) ? node.props.fallback : {};
628
+ const captionText = stringProp(node, 'caption');
629
+ // A provider reference never becomes a frame, whether or not it is allowed:
630
+ // the page links to the provider. Only the explanation differs.
631
+ if (provider !== '') {
632
+ const label = EMBED_PROVIDERS[provider]?.label ?? provider;
633
+ return [
634
+ `<figure${renderAttributes(nodeAttributes(node, { class: 'ak-block ak-media' }))}>`,
635
+ `<div class="ak-media-fallback">${mediaFallbackBody(stringProp(node, 'title'), str(fallback.description, `${label} audio`), source, '', str(fallback.linkText, `Open on ${label}`), resolved.allowed
636
+ ? `Embedded players are not emitted; this page links to ${label} instead.`
637
+ : `Embedded players are not emitted and the page denies network access; this page links to ${label} instead.`)}</div>`,
638
+ figureCaption(captionText),
639
+ '</figure>',
640
+ ].join('');
641
+ }
642
+ if (!resolved.allowed) {
643
+ return [
644
+ `<figure${renderAttributes(nodeAttributes(node, { class: 'ak-block ak-media' }))}>`,
645
+ `<div class="ak-media-fallback">${mediaFallbackBody(stringProp(node, 'title'), str(fallback.description, 'Remote audio is unavailable without network access.'), str(fallback.url, source), '', str(fallback.linkText, 'Open audio'))}</div>`,
646
+ figureCaption(captionText),
647
+ '</figure>',
648
+ ].join('');
649
+ }
650
+ return [
651
+ `<figure${renderAttributes(nodeAttributes(node, { class: 'ak-block ak-media' }))}>`,
652
+ `<audio controls preload="metadata" src="${escapeAttribute(resolved.src)}">${escapeText(str(fallback.description, 'Audio content'))}</audio>`,
653
+ figureCaption(captionText),
654
+ '</figure>',
655
+ ].join('');
656
+ },
657
+ button: (node) => {
658
+ const variant = stringProp(node, 'variant', 'secondary');
659
+ return `<button type="button"${renderAttributes({
660
+ class: 'ak-btn',
661
+ 'data-variant': variant,
662
+ ...nodeAttributes(node, {}),
663
+ ...clickAttribute(node.bindings),
664
+ })}>${escapeText(stringProp(node, 'label'))}</button>`;
665
+ },
666
+ link: (node) => {
667
+ const href = stringProp(node, 'href');
668
+ return `<a${renderAttributes({
669
+ href: escapeUrl(href),
670
+ rel: 'noreferrer noopener',
671
+ ...nodeAttributes(node, {}),
672
+ })}>${escapeText(stringProp(node, 'label'))}</a>`;
673
+ },
674
+ slider: (node) => {
675
+ const min = numberProp(node, 'min', 0);
676
+ const max = numberProp(node, 'max', 100);
677
+ const step = numberProp(node, 'step', 1);
678
+ const value = Math.min(Math.max(numberProp(node, 'value', min), min), max);
679
+ const inputId = `${node.id}-input`;
680
+ const outputId = `${node.id}-output`;
681
+ return [
682
+ `<div${renderAttributes(nodeAttributes(node, { class: 'ak-block ak-slider' }))}>`,
683
+ `<label for="${escapeAttribute(inputId)}">${escapeText(stringProp(node, 'label'))}</label>`,
684
+ `<input type="range" id="${escapeAttribute(inputId)}" data-ak-slider min="${min}" max="${max}" step="${step}" value="${value}" aria-describedby="${escapeAttribute(outputId)}" />`,
685
+ `<output id="${escapeAttribute(outputId)}" for="${escapeAttribute(inputId)}" data-ak-slider-output>${value}</output>`,
686
+ '</div>',
687
+ ].join('');
688
+ },
689
+ search: (node) => {
690
+ const inputId = `${node.id}-input`;
691
+ return [
692
+ `<div${renderAttributes(nodeAttributes(node, { class: 'ak-block ak-search' }))}>`,
693
+ `<label for="${escapeAttribute(inputId)}">${escapeText(stringProp(node, 'label'))}</label>`,
694
+ `<input type="search" id="${escapeAttribute(inputId)}" data-ak-search="${escapeAttribute(searchTarget(node))}" data-ak-match="${escapeAttribute(searchMatch(node))}" placeholder="${escapeAttribute(stringProp(node, 'placeholder'))}" />`,
695
+ '</div>',
696
+ ].join('');
697
+ },
698
+ dialog: (node) => {
699
+ const actions = objectListProp(node, 'actions');
700
+ const text = stringProp(node, 'text');
701
+ return [
702
+ `<dialog${renderAttributes(nodeAttributes(node, {
703
+ class: 'ak-dialog',
704
+ 'aria-label': stringProp(node, 'ariaLabel', stringProp(node, 'title')),
705
+ }))}>`,
706
+ heading(2, stringProp(node, 'title'), 'ak-dialog-title'),
707
+ text === '' ? '' : `<p>${escapeText(text)}</p>`,
708
+ `<div class="ak-dialog-actions">${actions
709
+ .map((action) => {
710
+ const on = isPlainObject(action.on) ? action.on : {};
711
+ return `<button type="button" class="ak-btn"${renderAttributes(clickAttribute(on))}>${escapeText(str(action.label))}</button>`;
712
+ })
713
+ .join('')}<button type="button" class="ak-btn" data-ak-dialog-close>Close</button></div>`,
714
+ '</dialog>',
715
+ ].join('');
716
+ },
717
+ };
718
+ /** The collection a `search` block filters, taken from its filter binding. */
719
+ function searchTarget(node) {
720
+ for (const actions of Object.values(node.bindings)) {
721
+ for (const action of actions) {
722
+ const target = action.target;
723
+ if (typeof target === 'string')
724
+ return target;
725
+ }
726
+ }
727
+ return '';
728
+ }
729
+ function searchMatch(node) {
730
+ for (const actions of Object.values(node.bindings)) {
731
+ for (const action of actions) {
732
+ const match = action.match;
733
+ if (typeof match === 'string')
734
+ return match;
735
+ }
736
+ }
737
+ return 'text';
738
+ }
739
+ /** Render one node, falling back to an empty string for an unregistered type. */
740
+ export function renderNode(node, context) {
741
+ const renderer = RENDERERS[node.type] ?? NO_OP_RENDERER;
742
+ return renderer(node, context);
743
+ }
744
+ /** True when a renderer exists for the type. */
745
+ export function hasRenderer(type) {
746
+ return RENDERERS[type] !== undefined;
747
+ }
748
+ export { element, nodeAttributes };
749
+ //# sourceMappingURL=blocks.js.map