@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.
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +336 -0
- package/dist/cli.js.map +1 -0
- package/dist/diagnostics.d.ts +45 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +57 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/diagram/adapter.d.ts +71 -0
- package/dist/diagram/adapter.d.ts.map +1 -0
- package/dist/diagram/adapter.js +87 -0
- package/dist/diagram/adapter.js.map +1 -0
- package/dist/errors.d.ts +37 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +42 -0
- package/dist/errors.js.map +1 -0
- package/dist/hash.d.ts +14 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +27 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/yaml-error.d.ts +14 -0
- package/dist/internal/yaml-error.d.ts.map +1 -0
- package/dist/internal/yaml-error.js +12 -0
- package/dist/internal/yaml-error.js.map +1 -0
- package/dist/ir.d.ts +77 -0
- package/dist/ir.d.ts.map +1 -0
- package/dist/ir.js +20 -0
- package/dist/ir.js.map +1 -0
- package/dist/json.d.ts +23 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +47 -0
- package/dist/json.js.map +1 -0
- package/dist/registry/actions.d.ts +48 -0
- package/dist/registry/actions.d.ts.map +1 -0
- package/dist/registry/actions.js +274 -0
- package/dist/registry/actions.js.map +1 -0
- package/dist/registry/prop-schema.d.ts +108 -0
- package/dist/registry/prop-schema.d.ts.map +1 -0
- package/dist/registry/prop-schema.js +400 -0
- package/dist/registry/prop-schema.js.map +1 -0
- package/dist/registry/registry.d.ts +47 -0
- package/dist/registry/registry.d.ts.map +1 -0
- package/dist/registry/registry.js +201 -0
- package/dist/registry/registry.js.map +1 -0
- package/dist/registry/roster.d.ts +53 -0
- package/dist/registry/roster.d.ts.map +1 -0
- package/dist/registry/roster.js +692 -0
- package/dist/registry/roster.js.map +1 -0
- package/dist/render/blocks.d.ts +41 -0
- package/dist/render/blocks.d.ts.map +1 -0
- package/dist/render/blocks.js +749 -0
- package/dist/render/blocks.js.map +1 -0
- package/dist/render/charts.d.ts +29 -0
- package/dist/render/charts.d.ts.map +1 -0
- package/dist/render/charts.js +235 -0
- package/dist/render/charts.js.map +1 -0
- package/dist/render/document.d.ts +40 -0
- package/dist/render/document.d.ts.map +1 -0
- package/dist/render/document.js +90 -0
- package/dist/render/document.js.map +1 -0
- package/dist/render/escape.d.ts +46 -0
- package/dist/render/escape.d.ts.map +1 -0
- package/dist/render/escape.js +108 -0
- package/dist/render/escape.js.map +1 -0
- package/dist/render/render.d.ts +52 -0
- package/dist/render/render.d.ts.map +1 -0
- package/dist/render/render.js +216 -0
- package/dist/render/render.js.map +1 -0
- package/dist/render/runtime.d.ts +25 -0
- package/dist/render/runtime.d.ts.map +1 -0
- package/dist/render/runtime.js +451 -0
- package/dist/render/runtime.js.map +1 -0
- package/dist/render/styles.d.ts +15 -0
- package/dist/render/styles.d.ts.map +1 -0
- package/dist/render/styles.js +163 -0
- package/dist/render/styles.js.map +1 -0
- package/dist/render/verify.d.ts +25 -0
- package/dist/render/verify.d.ts.map +1 -0
- package/dist/render/verify.js +168 -0
- package/dist/render/verify.js.map +1 -0
- package/dist/spec/bindings.d.ts +19 -0
- package/dist/spec/bindings.d.ts.map +1 -0
- package/dist/spec/bindings.js +51 -0
- package/dist/spec/bindings.js.map +1 -0
- package/dist/spec/bounds.d.ts +43 -0
- package/dist/spec/bounds.d.ts.map +1 -0
- package/dist/spec/bounds.js +144 -0
- package/dist/spec/bounds.js.map +1 -0
- package/dist/spec/forbidden.d.ts +27 -0
- package/dist/spec/forbidden.d.ts.map +1 -0
- package/dist/spec/forbidden.js +91 -0
- package/dist/spec/forbidden.js.map +1 -0
- package/dist/spec/migrate.d.ts +33 -0
- package/dist/spec/migrate.d.ts.map +1 -0
- package/dist/spec/migrate.js +113 -0
- package/dist/spec/migrate.js.map +1 -0
- package/dist/spec/normalize.d.ts +29 -0
- package/dist/spec/normalize.d.ts.map +1 -0
- package/dist/spec/normalize.js +649 -0
- package/dist/spec/normalize.js.map +1 -0
- package/dist/spec/parse.d.ts +22 -0
- package/dist/spec/parse.d.ts.map +1 -0
- package/dist/spec/parse.js +62 -0
- package/dist/spec/parse.js.map +1 -0
- package/dist/spec/providers.d.ts +30 -0
- package/dist/spec/providers.d.ts.map +1 -0
- package/dist/spec/providers.js +64 -0
- package/dist/spec/providers.js.map +1 -0
- package/dist/spec/validate.d.ts +28 -0
- package/dist/spec/validate.d.ts.map +1 -0
- package/dist/spec/validate.js +68 -0
- package/dist/spec/validate.js.map +1 -0
- package/dist/theme/load-theme.d.ts +55 -0
- package/dist/theme/load-theme.d.ts.map +1 -0
- package/dist/theme/load-theme.js +219 -0
- package/dist/theme/load-theme.js.map +1 -0
- package/dist/theme/presets.d.ts +50 -0
- package/dist/theme/presets.d.ts.map +1 -0
- package/dist/theme/presets.js +304 -0
- package/dist/theme/presets.js.map +1 -0
- package/dist/theme/theme-catalog.d.ts +68 -0
- package/dist/theme/theme-catalog.d.ts.map +1 -0
- package/dist/theme/theme-catalog.js +198 -0
- package/dist/theme/theme-catalog.js.map +1 -0
- package/dist/theme/tokens.d.ts +38 -0
- package/dist/theme/tokens.d.ts.map +1 -0
- package/dist/theme/tokens.js +190 -0
- package/dist/theme/tokens.js.map +1 -0
- package/dist/version.d.ts +12 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +12 -0
- package/dist/version.js.map +1 -0
- 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
|