@open-agent-toolkit/cli 0.2.72 → 0.2.74
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/assets/agents/oat-reviewer.md +4 -3
- package/assets/agents/recon-worker.md +42 -8
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/configuration.md +3 -56
- package/assets/docs/cli-utilities/tool-packs.md +4 -4
- package/assets/docs/contributing/code.md +2 -2
- package/assets/docs/contributing/index.md +0 -1
- package/assets/docs/reference/cli-reference.md +1 -1
- package/assets/docs/reference/troubleshooting.md +17 -40
- package/assets/docs/workflows/projects/artifacts.md +16 -34
- package/assets/docs/workflows/projects/lifecycle.md +20 -19
- package/assets/docs/workflows/skills/explainer-kit.md +180 -440
- package/assets/docs/workflows/skills/index.md +1 -2
- package/assets/docs/workflows/skills/recon.md +114 -23
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +96 -171
- package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
- package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
- package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
- package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
- package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
- package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
- package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
- package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
- package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
- package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
- package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
- package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
- package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
- package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
- package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
- package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
- package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
- package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
- package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
- package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
- package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
- package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
- package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
- package/assets/skills/oat-project-complete/SKILL.md +168 -349
- package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
- package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
- package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
- package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
- package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
- package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
- package/assets/skills/oat-project-document/SKILL.md +1 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-lite/SKILL.md +1 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-plan/SKILL.md +5 -6
- package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-review-receive/SKILL.md +14 -1
- package/assets/skills/oat-project-summary/SKILL.md +35 -25
- package/assets/skills/oat-wave-execute/SKILL.md +11 -36
- package/assets/skills/oat-wave-program/SKILL.md +13 -37
- package/assets/skills/recon/SKILL.md +103 -40
- package/assets/skills/recon/references/packet-contract.md +150 -67
- package/assets/skills/recon/references/profiles.md +38 -16
- package/assets/skills/recon/references/worker-contract.md +39 -8
- package/assets/skills/recon/scripts/lib/contracts.mjs +703 -55
- package/assets/skills/recon/scripts/lib/routing.mjs +588 -0
- package/assets/skills/recon/scripts/lib/validated-run.mjs +2 -0
- package/assets/skills/recon/scripts/prepare-routing.mjs +88 -0
- package/assets/skills/recon/scripts/render-packet.mjs +69 -1
- package/assets/skills/recon/scripts/validate-packet.mjs +473 -56
- package/assets/skills/subagent-orchestration/SKILL.md +18 -9
- package/assets/skills/subagent-orchestration/references/model-selection-principles.md +17 -8
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +9 -113
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +32 -184
- package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
- package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
- package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
- package/dist/config/oat-config.d.ts +0 -11
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +0 -34
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +0 -8
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +2 -10
- package/dist/validation/project-state.d.ts.map +1 -1
- package/dist/validation/project-state.js +42 -3
- package/package.json +2 -2
- package/assets/docs/contributing/explainer-kit-verification.md +0 -133
- package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
- package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
- package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
- package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
- package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
- package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
- package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
- package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
- package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
- package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
- package/assets/skills/explainer-kit/references/contracts.md +0 -324
- package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
- package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
- package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
- package/assets/skills/explainer-kit/references/visual-review.md +0 -71
- package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
- package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
- package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
- package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
- package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
- package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
- package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
- package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
- package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
- package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
- package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
- package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
- package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
- package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
- package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
- package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
- package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
- package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
- package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
- package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
- package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
- package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
- package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
- package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
- package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
- package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
- package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
- package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
- package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
- package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
- package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
- package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
- package/dist/commands/project/archive/explainer-terminal-evidence.js +0 -37
|
@@ -1,703 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { posix } from 'node:path';
|
|
3
|
-
|
|
4
|
-
import { canonicalHash, validateContract } from './contracts.mjs';
|
|
5
|
-
import { renderDiagram as renderDiagramBlock } from './diagram.mjs';
|
|
6
|
-
import { MarkdownSafetyError, parseMarkdownDocument } from './markdown.mjs';
|
|
7
|
-
|
|
8
|
-
const RENDER_STRATEGIES = new Set(['default-only', 'user-switchable']);
|
|
9
|
-
const TEMPLATE_BY_TYPE = new Map([
|
|
10
|
-
['hub', 'house-style'],
|
|
11
|
-
['diagram', 'diagram-shell'],
|
|
12
|
-
['explainer', 'engineer-tour'],
|
|
13
|
-
['deck', 'deck-shell'],
|
|
14
|
-
]);
|
|
15
|
-
const TYPE_DIRECTORIES = new Map([
|
|
16
|
-
['hub', 'initiatives'],
|
|
17
|
-
['diagram', 'diagrams'],
|
|
18
|
-
['explainer', 'explainers'],
|
|
19
|
-
['deck', 'decks'],
|
|
20
|
-
]);
|
|
21
|
-
const CONTENT_KEYS = new Set([
|
|
22
|
-
'artifactId',
|
|
23
|
-
'artifactLinks',
|
|
24
|
-
'description',
|
|
25
|
-
'eyebrow',
|
|
26
|
-
'footer',
|
|
27
|
-
'sections',
|
|
28
|
-
'slug',
|
|
29
|
-
'title',
|
|
30
|
-
]);
|
|
31
|
-
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
32
|
-
const TOKEN_PATTERN = /{{([A-Z][A-Z_]*)}}/g;
|
|
33
|
-
|
|
34
|
-
export async function renderArtifact({
|
|
35
|
-
recipeArtifact,
|
|
36
|
-
content,
|
|
37
|
-
factBase,
|
|
38
|
-
theme,
|
|
39
|
-
renderStrategy,
|
|
40
|
-
publicBaseUrl,
|
|
41
|
-
}) {
|
|
42
|
-
assertRecipeArtifact(recipeArtifact);
|
|
43
|
-
assertContent(content, recipeArtifact);
|
|
44
|
-
assertFactBase(factBase);
|
|
45
|
-
assertTheme(theme);
|
|
46
|
-
assertRenderStrategy(renderStrategy);
|
|
47
|
-
|
|
48
|
-
const baseUrl = normalizePublicBaseUrl(publicBaseUrl);
|
|
49
|
-
const renderedPath = artifactPath(recipeArtifact, content.slug);
|
|
50
|
-
const template = await readFile(
|
|
51
|
-
new URL(`../../templates/${recipeArtifact.template}.html`, import.meta.url),
|
|
52
|
-
'utf8',
|
|
53
|
-
);
|
|
54
|
-
const sections = content.sections.map((section) => ({
|
|
55
|
-
...section,
|
|
56
|
-
anchor: section.id,
|
|
57
|
-
backlinks: sourceBacklinksForSection(factBase, section.id),
|
|
58
|
-
...prepareSectionMarkdown(section.content, theme),
|
|
59
|
-
}));
|
|
60
|
-
const links = content.artifactLinks ?? [];
|
|
61
|
-
const values = templateValues({
|
|
62
|
-
recipeArtifact,
|
|
63
|
-
content,
|
|
64
|
-
sections,
|
|
65
|
-
links,
|
|
66
|
-
theme,
|
|
67
|
-
renderedPath,
|
|
68
|
-
baseUrl,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
let html = substituteTemplate(template, values);
|
|
72
|
-
html = html.replace(
|
|
73
|
-
'<html lang="en">',
|
|
74
|
-
`<html lang="en" data-render-strategy="${renderStrategy}" data-theme-mode="${theme.defaultMode}">`,
|
|
75
|
-
);
|
|
76
|
-
if (renderStrategy === 'user-switchable') {
|
|
77
|
-
html = html
|
|
78
|
-
.replace('</head>', `${switchableThemeStyle(theme)}\n </head>`)
|
|
79
|
-
.replace('</body>', `${switchableThemeControl(theme)}\n </body>`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
artifactId: recipeArtifact.id,
|
|
84
|
-
type: recipeArtifact.type,
|
|
85
|
-
renderedPath,
|
|
86
|
-
publicUrl: baseUrl
|
|
87
|
-
? `${baseUrl}/${renderedPath.slice('site/'.length)}`
|
|
88
|
-
: undefined,
|
|
89
|
-
mediaType: 'text/html',
|
|
90
|
-
html,
|
|
91
|
-
warnings: sections.flatMap((section) => section.warnings),
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function substituteTemplate(template, values) {
|
|
96
|
-
if (typeof template !== 'string' || !isObject(values)) {
|
|
97
|
-
throw new TypeError('Template substitution requires a string and values.');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const expected = new Set(
|
|
101
|
-
[...template.matchAll(TOKEN_PATTERN)].map(([, key]) => key),
|
|
102
|
-
);
|
|
103
|
-
const unknown = Object.keys(values).filter((key) => !expected.has(key));
|
|
104
|
-
if (unknown.length > 0) {
|
|
105
|
-
throw new Error(`Unknown template token: ${unknown[0]}.`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const missing = [...expected].filter(
|
|
109
|
-
(key) => !Object.hasOwn(values, key) || typeof values[key] !== 'string',
|
|
110
|
-
);
|
|
111
|
-
if (missing.length > 0) {
|
|
112
|
-
throw new Error(`Unresolved template token: ${missing[0]}.`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const normalizedTemplate = normalizeEmptySubstitutionWhitespace(
|
|
116
|
-
template,
|
|
117
|
-
values,
|
|
118
|
-
);
|
|
119
|
-
const rendered = normalizedTemplate.replace(
|
|
120
|
-
TOKEN_PATTERN,
|
|
121
|
-
(_, key) => values[key],
|
|
122
|
-
);
|
|
123
|
-
const unresolved = rendered.match(TOKEN_PATTERN)?.[1];
|
|
124
|
-
if (unresolved) {
|
|
125
|
-
throw new Error(`Unresolved template token: ${unresolved}.`);
|
|
126
|
-
}
|
|
127
|
-
return rendered;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function normalizeEmptySubstitutionWhitespace(template, values) {
|
|
131
|
-
let normalized = template;
|
|
132
|
-
let previous;
|
|
133
|
-
do {
|
|
134
|
-
previous = normalized;
|
|
135
|
-
normalized = normalized.replace(
|
|
136
|
-
/[ \t]*{{([A-Z][A-Z_]*)}}[ \t]*(?=\r?$)/gm,
|
|
137
|
-
(match, key) => (values[key] === '' ? '' : match),
|
|
138
|
-
);
|
|
139
|
-
} while (normalized !== previous);
|
|
140
|
-
return normalized;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function templateValues({
|
|
144
|
-
recipeArtifact,
|
|
145
|
-
content,
|
|
146
|
-
sections,
|
|
147
|
-
links,
|
|
148
|
-
theme,
|
|
149
|
-
renderedPath,
|
|
150
|
-
baseUrl,
|
|
151
|
-
}) {
|
|
152
|
-
const title = escapeHtml(content.title ?? humanize(recipeArtifact.id));
|
|
153
|
-
const description = escapeHtml(content.description ?? '');
|
|
154
|
-
const eyebrow = escapeHtml(content.eyebrow ?? recipeArtifact.type);
|
|
155
|
-
const footer = [
|
|
156
|
-
escapeHtml(content.footer ?? ''),
|
|
157
|
-
renderRelatedLinks(links, content.slug, renderedPath, baseUrl),
|
|
158
|
-
]
|
|
159
|
-
.filter(Boolean)
|
|
160
|
-
.join(' ');
|
|
161
|
-
const common = {
|
|
162
|
-
THEME_CSS: themeDeclarations(theme.modes[theme.defaultMode], theme),
|
|
163
|
-
TITLE: title,
|
|
164
|
-
DESCRIPTION: description,
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
switch (recipeArtifact.template) {
|
|
168
|
-
case 'house-style':
|
|
169
|
-
return {
|
|
170
|
-
...common,
|
|
171
|
-
EYEBROW: eyebrow,
|
|
172
|
-
NAVIGATION: renderNavigation(
|
|
173
|
-
sections,
|
|
174
|
-
links,
|
|
175
|
-
content.slug,
|
|
176
|
-
renderedPath,
|
|
177
|
-
baseUrl,
|
|
178
|
-
),
|
|
179
|
-
CONTENT: renderSections(sections),
|
|
180
|
-
FOOTER: footer,
|
|
181
|
-
};
|
|
182
|
-
case 'engineer-tour':
|
|
183
|
-
return {
|
|
184
|
-
...common,
|
|
185
|
-
EYEBROW: eyebrow,
|
|
186
|
-
NAVIGATION: renderNavigation(
|
|
187
|
-
sections,
|
|
188
|
-
links,
|
|
189
|
-
content.slug,
|
|
190
|
-
renderedPath,
|
|
191
|
-
baseUrl,
|
|
192
|
-
),
|
|
193
|
-
CONTENT: renderSections(sections, { tour: true }),
|
|
194
|
-
DIAGRAM: renderLegacyDiagram(sections),
|
|
195
|
-
FOOTER: footer,
|
|
196
|
-
};
|
|
197
|
-
case 'deck-shell':
|
|
198
|
-
return {
|
|
199
|
-
...common,
|
|
200
|
-
SLIDES: renderSlides(
|
|
201
|
-
sections,
|
|
202
|
-
links,
|
|
203
|
-
content.slug,
|
|
204
|
-
renderedPath,
|
|
205
|
-
baseUrl,
|
|
206
|
-
),
|
|
207
|
-
};
|
|
208
|
-
case 'diagram-shell':
|
|
209
|
-
return {
|
|
210
|
-
...common,
|
|
211
|
-
DIAGRAM: renderLegacyDiagram(sections),
|
|
212
|
-
LEGEND: [
|
|
213
|
-
sections
|
|
214
|
-
.map(
|
|
215
|
-
(section) =>
|
|
216
|
-
`<span>${escapeHtml(section.title ?? humanize(section.id))}</span>`,
|
|
217
|
-
)
|
|
218
|
-
.join(''),
|
|
219
|
-
renderSourceBacklinks(sections.flatMap(({ backlinks }) => backlinks)),
|
|
220
|
-
renderRelatedLinks(links, content.slug, renderedPath, baseUrl),
|
|
221
|
-
].join(''),
|
|
222
|
-
};
|
|
223
|
-
default:
|
|
224
|
-
throw new Error(`Unknown bundled template: ${recipeArtifact.template}.`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function renderNavigation(sections, links, slug, renderedPath, baseUrl) {
|
|
229
|
-
const sectionLinks = sections
|
|
230
|
-
.map(
|
|
231
|
-
(section) =>
|
|
232
|
-
`<a href="#${escapeAttribute(section.anchor)}" data-target="${escapeAttribute(section.anchor)}">${escapeHtml(section.title ?? humanize(section.id))}</a>`,
|
|
233
|
-
)
|
|
234
|
-
.join('');
|
|
235
|
-
return `${sectionLinks}${renderRelatedLinks(links, slug, renderedPath, baseUrl)}`;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function renderSections(sections, { tour = false } = {}) {
|
|
239
|
-
return sections
|
|
240
|
-
.map((section, index) => {
|
|
241
|
-
const tourAttributes = tour
|
|
242
|
-
? ` data-active-nodes="node-${index + 1}" data-active-edges=""`
|
|
243
|
-
: '';
|
|
244
|
-
return `<section id="${escapeAttribute(section.anchor)}"${tourAttributes}><h2>${escapeHtml(section.title ?? humanize(section.id))}</h2>${renderMarkdownNodes(section.ast.children)}${renderSourceBacklinks(section.backlinks)}</section>`;
|
|
245
|
-
})
|
|
246
|
-
.join('');
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function prepareSectionMarkdown(content, theme) {
|
|
250
|
-
try {
|
|
251
|
-
const parsed = parseMarkdownDocument(content);
|
|
252
|
-
const diagramWarnings = [];
|
|
253
|
-
visitMarkdownNodes(parsed.ast.children, (node) => {
|
|
254
|
-
if (node.type !== 'diagram') return;
|
|
255
|
-
const rendered = renderDiagramBlock(node.source, { theme });
|
|
256
|
-
node.renderedHtml = rendered.html;
|
|
257
|
-
diagramWarnings.push(...rendered.warnings);
|
|
258
|
-
});
|
|
259
|
-
return {
|
|
260
|
-
...parsed,
|
|
261
|
-
warnings: [...parsed.warnings, ...diagramWarnings],
|
|
262
|
-
};
|
|
263
|
-
} catch (error) {
|
|
264
|
-
if (
|
|
265
|
-
!(error instanceof MarkdownSafetyError) ||
|
|
266
|
-
!/raw html|node type: html/i.test(error.message)
|
|
267
|
-
) {
|
|
268
|
-
throw error;
|
|
269
|
-
}
|
|
270
|
-
return {
|
|
271
|
-
ast: {
|
|
272
|
-
type: 'document',
|
|
273
|
-
children: [
|
|
274
|
-
{
|
|
275
|
-
type: 'paragraph',
|
|
276
|
-
children: [{ type: 'text', value: content }],
|
|
277
|
-
},
|
|
278
|
-
],
|
|
279
|
-
},
|
|
280
|
-
warnings: [
|
|
281
|
-
{
|
|
282
|
-
code: 'legacy-raw-html-escaped',
|
|
283
|
-
message: 'Legacy section markup was rendered as escaped text.',
|
|
284
|
-
},
|
|
285
|
-
],
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function visitMarkdownNodes(nodes, callback) {
|
|
291
|
-
for (const node of nodes) {
|
|
292
|
-
callback(node);
|
|
293
|
-
if (Array.isArray(node.children)) {
|
|
294
|
-
visitMarkdownNodes(node.children, callback);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function renderMarkdownNodes(nodes) {
|
|
300
|
-
return nodes.map(renderMarkdownNode).join('');
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function renderMarkdownNode(node) {
|
|
304
|
-
switch (node.type) {
|
|
305
|
-
case 'text':
|
|
306
|
-
return escapeHtml(node.value);
|
|
307
|
-
case 'paragraph':
|
|
308
|
-
if (node.children.length === 1 && node.children[0].type === 'figure') {
|
|
309
|
-
return renderMarkdownNode(node.children[0]);
|
|
310
|
-
}
|
|
311
|
-
return `<p>${renderMarkdownNodes(node.children)}</p>`;
|
|
312
|
-
case 'heading':
|
|
313
|
-
return `<h${node.depth}>${renderMarkdownNodes(node.children)}</h${node.depth}>`;
|
|
314
|
-
case 'strong':
|
|
315
|
-
return `<strong>${renderMarkdownNodes(node.children)}</strong>`;
|
|
316
|
-
case 'emphasis':
|
|
317
|
-
return `<em>${renderMarkdownNodes(node.children)}</em>`;
|
|
318
|
-
case 'delete':
|
|
319
|
-
return `<del>${renderMarkdownNodes(node.children)}</del>`;
|
|
320
|
-
case 'inlineCode':
|
|
321
|
-
return `<code>${escapeHtml(node.value)}</code>`;
|
|
322
|
-
case 'link':
|
|
323
|
-
return `<a href="${escapeAttribute(node.url)}"${node.title ? ` title="${escapeAttribute(node.title)}"` : ''}>${renderMarkdownNodes(node.children)}</a>`;
|
|
324
|
-
case 'figure':
|
|
325
|
-
return `<figure><a href="${escapeAttribute(node.url)}">${escapeHtml(node.alt || 'View figure')}</a>${node.title || node.alt ? `<figcaption>${escapeHtml(node.title ?? node.alt)}</figcaption>` : ''}</figure>`;
|
|
326
|
-
case 'blockquote':
|
|
327
|
-
return `<blockquote>${renderMarkdownNodes(node.children)}</blockquote>`;
|
|
328
|
-
case 'callout':
|
|
329
|
-
return `<aside class="callout callout--${escapeAttribute(node.kind)}" data-callout="${escapeAttribute(node.kind)}"><div class="callout__label">${escapeHtml(humanize(node.kind))}</div>${renderMarkdownNodes(node.children)}</aside>`;
|
|
330
|
-
case 'timeline':
|
|
331
|
-
return `<ol class="timeline">${node.entries
|
|
332
|
-
.map(
|
|
333
|
-
(entry) =>
|
|
334
|
-
`<li><time>${escapeHtml(entry.date)}</time><span>${escapeHtml(entry.label)}</span></li>`,
|
|
335
|
-
)
|
|
336
|
-
.join('')}</ol>`;
|
|
337
|
-
case 'code':
|
|
338
|
-
return `<pre><code${node.language ? ` class="language-${escapeAttribute(node.language)}"` : ''}>${escapeHtml(node.value)}</code></pre>`;
|
|
339
|
-
case 'diagram':
|
|
340
|
-
return `<div class="diagram-scroll">${node.renderedHtml}</div>`;
|
|
341
|
-
case 'list':
|
|
342
|
-
return renderMarkdownList(node);
|
|
343
|
-
case 'listItem':
|
|
344
|
-
return `<li>${node.checked === null ? '' : `<input type="checkbox" disabled${node.checked ? ' checked' : ''} aria-label="${node.checked ? 'Completed' : 'Incomplete'} task" />`}${renderMarkdownNodes(node.children)}</li>`;
|
|
345
|
-
case 'table':
|
|
346
|
-
return `<div class="table-scroll"><table><thead><tr>${node.header
|
|
347
|
-
.map((cell) => `<th>${renderMarkdownNodes(cell)}</th>`)
|
|
348
|
-
.join('')}</tr></thead><tbody>${node.rows
|
|
349
|
-
.map(
|
|
350
|
-
(row) =>
|
|
351
|
-
`<tr>${row
|
|
352
|
-
.map((cell) => `<td>${renderMarkdownNodes(cell)}</td>`)
|
|
353
|
-
.join('')}</tr>`,
|
|
354
|
-
)
|
|
355
|
-
.join('')}</tbody></table></div>`;
|
|
356
|
-
default:
|
|
357
|
-
throw new MarkdownSafetyError(
|
|
358
|
-
`Unsupported markdown render node: ${String(node.type)}.`,
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function renderMarkdownList(node) {
|
|
364
|
-
const tag = node.ordered ? 'ol' : 'ul';
|
|
365
|
-
const attributes = [
|
|
366
|
-
node.task ? ' class="task-list"' : '',
|
|
367
|
-
node.ordered && node.start !== 1 ? ` start="${node.start}"` : '',
|
|
368
|
-
].join('');
|
|
369
|
-
return `<${tag}${attributes}>${renderMarkdownNodes(node.children)}</${tag}>`;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function renderSlides(sections, links, slug, renderedPath, baseUrl) {
|
|
373
|
-
const slides = sections
|
|
374
|
-
.map(
|
|
375
|
-
(section) =>
|
|
376
|
-
`<section class="slide" id="${escapeAttribute(section.anchor)}"><div class="slide__content"><h2>${escapeHtml(section.title ?? humanize(section.id))}</h2><p>${escapeHtml(section.content)}</p>${renderSourceBacklinks(section.backlinks)}</div></section>`,
|
|
377
|
-
)
|
|
378
|
-
.join('');
|
|
379
|
-
const related = renderRelatedLinks(links, slug, renderedPath, baseUrl);
|
|
380
|
-
return related
|
|
381
|
-
? `${slides}<section class="slide"><div class="slide__content"><h2>Related artifacts</h2>${related}</div></section>`
|
|
382
|
-
: slides;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Nodes must fit the shell's 360x540 viewBox, so the step shrinks as sections
|
|
386
|
-
// are added rather than overflowing the canvas.
|
|
387
|
-
function renderLegacyDiagram(sections) {
|
|
388
|
-
const margin = 16;
|
|
389
|
-
const span = 540 - margin * 2;
|
|
390
|
-
const step = Math.min(96, span / Math.max(sections.length, 1));
|
|
391
|
-
const height = Math.max(24, step - 16);
|
|
392
|
-
return sections
|
|
393
|
-
.map((section, index) => {
|
|
394
|
-
const y = margin + index * step;
|
|
395
|
-
const label = section.title ?? humanize(section.id);
|
|
396
|
-
return `<g data-node="node-${index + 1}" class="node"><rect x="${margin}" y="${y}" width="${360 - margin * 2}" height="${height}" rx="8"></rect><text x="${margin + 14}" y="${y + height / 2 + 5}">${escapeHtml(label)}</text></g>`;
|
|
397
|
-
})
|
|
398
|
-
.join('');
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function renderRelatedLinks(links, slug, renderedPath, baseUrl) {
|
|
402
|
-
if (links.length === 0) return '';
|
|
403
|
-
return links
|
|
404
|
-
.map((link) => {
|
|
405
|
-
const targetPath = artifactPath(link, slug);
|
|
406
|
-
const href = baseUrl
|
|
407
|
-
? `${baseUrl}/${targetPath.slice('site/'.length)}`
|
|
408
|
-
: posix.relative(posix.dirname(renderedPath), targetPath);
|
|
409
|
-
return `<a href="${escapeAttribute(href)}">${escapeHtml(link.label)}</a>`;
|
|
410
|
-
})
|
|
411
|
-
.join(' ');
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function sourceBacklinksForSection(factBase, sectionId) {
|
|
415
|
-
if (factBase === undefined) return [];
|
|
416
|
-
return [...factBase.claims, ...factBase.unresolvedClaims]
|
|
417
|
-
.filter(
|
|
418
|
-
(claim) =>
|
|
419
|
-
claim.sections === undefined || claim.sections.includes(sectionId),
|
|
420
|
-
)
|
|
421
|
-
.flatMap((claim) =>
|
|
422
|
-
claim.citations
|
|
423
|
-
.filter(({ url }) => typeof url === 'string')
|
|
424
|
-
.map((citation) => ({
|
|
425
|
-
claimId: claim.id,
|
|
426
|
-
claimText: claim.text,
|
|
427
|
-
repository: citation.repository,
|
|
428
|
-
path: citation.path,
|
|
429
|
-
lineRange: citation.lineRange,
|
|
430
|
-
url: citation.url,
|
|
431
|
-
})),
|
|
432
|
-
);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function renderSourceBacklinks(backlinks) {
|
|
436
|
-
if (backlinks.length === 0) return '';
|
|
437
|
-
const items = backlinks
|
|
438
|
-
.map(
|
|
439
|
-
(backlink) =>
|
|
440
|
-
`<li><span>${escapeHtml(backlink.claimText)}</span> <a href="${escapeAttribute(backlink.url)}">${escapeHtml(`${backlink.repository}/${backlink.path}:L${backlink.lineRange.start}${backlink.lineRange.end === backlink.lineRange.start ? '' : `-L${backlink.lineRange.end}`}`)}</a></li>`,
|
|
441
|
-
)
|
|
442
|
-
.join('');
|
|
443
|
-
return `<aside class="source-backlinks" aria-label="Sources"><h3>Sources</h3><ul>${items}</ul></aside>`;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
export function artifactPath(artifact, slug) {
|
|
447
|
-
const directory = TYPE_DIRECTORIES.get(artifact.type);
|
|
448
|
-
if (!directory)
|
|
449
|
-
throw new Error(`Unsupported artifact type: ${artifact.type}.`);
|
|
450
|
-
const parts = ['site', directory, slug];
|
|
451
|
-
const origin = artifact.origin ?? 'floor';
|
|
452
|
-
if (
|
|
453
|
-
origin === 'expansion' ||
|
|
454
|
-
(origin === 'floor' && ['diagram', 'deck'].includes(artifact.type))
|
|
455
|
-
) {
|
|
456
|
-
parts.push(artifact.id);
|
|
457
|
-
}
|
|
458
|
-
parts.push('index.html');
|
|
459
|
-
return parts.join('/');
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function themeDeclarations(mode, theme) {
|
|
463
|
-
return [
|
|
464
|
-
['canvas', mode.surface.canvas],
|
|
465
|
-
['panel', mode.surface.panel],
|
|
466
|
-
['panel-muted', mode.surface.elevated],
|
|
467
|
-
['border', mode.surface.elevated],
|
|
468
|
-
['ink', mode.ink.primary],
|
|
469
|
-
['muted', mode.ink.muted],
|
|
470
|
-
['accent', mode.accent.primary],
|
|
471
|
-
['success', mode.status.success],
|
|
472
|
-
['warning', mode.status.warning],
|
|
473
|
-
['danger', mode.status.danger],
|
|
474
|
-
['sans', theme.typography.sans.join(', ')],
|
|
475
|
-
['serif', theme.typography.serif.join(', ')],
|
|
476
|
-
['mono', theme.typography.mono.join(', ')],
|
|
477
|
-
['radius', `${theme.geometry.radius.md}px`],
|
|
478
|
-
]
|
|
479
|
-
.map(([name, value]) => `--${name}: ${value};`)
|
|
480
|
-
.join('\n ');
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function switchableThemeStyle(theme) {
|
|
484
|
-
return `<style data-theme-mode="${oppositeMode(theme.defaultMode)}">
|
|
485
|
-
:root[data-theme-mode="${oppositeMode(theme.defaultMode)}"] {
|
|
486
|
-
${themeDeclarations(theme.modes[oppositeMode(theme.defaultMode)], theme)}
|
|
487
|
-
}
|
|
488
|
-
[data-theme-toggle] {
|
|
489
|
-
position: fixed;
|
|
490
|
-
z-index: 10;
|
|
491
|
-
inset: 1rem 1rem auto auto;
|
|
492
|
-
border: 1px solid var(--border);
|
|
493
|
-
border-radius: var(--radius);
|
|
494
|
-
background: var(--panel);
|
|
495
|
-
color: var(--ink);
|
|
496
|
-
padding: 0.55rem 0.75rem;
|
|
497
|
-
font: 0.875rem var(--sans);
|
|
498
|
-
cursor: pointer;
|
|
499
|
-
}
|
|
500
|
-
[data-theme-toggle]:focus-visible {
|
|
501
|
-
outline: 2px solid var(--accent);
|
|
502
|
-
outline-offset: 2px;
|
|
503
|
-
}
|
|
504
|
-
</style>`;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function switchableThemeControl(theme) {
|
|
508
|
-
const defaultMode = theme.defaultMode;
|
|
509
|
-
const alternateMode = oppositeMode(defaultMode);
|
|
510
|
-
return `<button type="button" data-theme-toggle aria-pressed="${defaultMode === 'dark'}" aria-label="Switch color theme">Theme: ${humanize(defaultMode)}</button>
|
|
511
|
-
<script>
|
|
512
|
-
(() => {
|
|
513
|
-
const root = document.documentElement;
|
|
514
|
-
const toggle = document.querySelector('[data-theme-toggle]');
|
|
515
|
-
const storageKey = 'explainer-theme-mode:' + location.pathname;
|
|
516
|
-
const supported = new Set(['${defaultMode}', '${alternateMode}']);
|
|
517
|
-
const apply = (mode, persist = true) => {
|
|
518
|
-
root.dataset.themeMode = mode;
|
|
519
|
-
toggle.setAttribute('aria-pressed', String(mode === 'dark'));
|
|
520
|
-
toggle.textContent = 'Theme: ' + mode[0].toUpperCase() + mode.slice(1);
|
|
521
|
-
if (persist) {
|
|
522
|
-
try { localStorage.setItem(storageKey, mode); } catch {}
|
|
523
|
-
}
|
|
524
|
-
};
|
|
525
|
-
let saved;
|
|
526
|
-
try { saved = localStorage.getItem(storageKey); } catch {}
|
|
527
|
-
if (supported.has(saved)) apply(saved, false);
|
|
528
|
-
toggle.addEventListener('click', () =>
|
|
529
|
-
apply(root.dataset.themeMode === '${defaultMode}' ? '${alternateMode}' : '${defaultMode}')
|
|
530
|
-
);
|
|
531
|
-
})();
|
|
532
|
-
</script>`;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function assertRecipeArtifact(artifact) {
|
|
536
|
-
const hasLegacyKeys =
|
|
537
|
-
isObject(artifact) &&
|
|
538
|
-
hasExactKeys(artifact, ['id', 'type', 'template', 'required']);
|
|
539
|
-
const hasOriginKeys =
|
|
540
|
-
isObject(artifact) &&
|
|
541
|
-
hasExactKeys(artifact, ['id', 'type', 'template', 'required', 'origin']);
|
|
542
|
-
if (
|
|
543
|
-
!isObject(artifact) ||
|
|
544
|
-
(!hasLegacyKeys && !hasOriginKeys) ||
|
|
545
|
-
!SLUG_PATTERN.test(artifact.id ?? '') ||
|
|
546
|
-
typeof artifact.required !== 'boolean' ||
|
|
547
|
-
(artifact.origin !== undefined &&
|
|
548
|
-
!['floor', 'expansion'].includes(artifact.origin))
|
|
549
|
-
) {
|
|
550
|
-
throw new TypeError(
|
|
551
|
-
'Recipe artifact is not a validated artifact descriptor.',
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
const expectedTemplate = TEMPLATE_BY_TYPE.get(artifact.type);
|
|
555
|
-
if (!expectedTemplate || artifact.template !== expectedTemplate) {
|
|
556
|
-
throw new Error(
|
|
557
|
-
`Unknown or incompatible bundled template: ${String(artifact.template)}.`,
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function assertContent(content, recipeArtifact) {
|
|
563
|
-
if (!isObject(content))
|
|
564
|
-
throw new TypeError('Render content must be an object.');
|
|
565
|
-
const unknown = Object.keys(content).filter((key) => !CONTENT_KEYS.has(key));
|
|
566
|
-
if (unknown.length > 0) {
|
|
567
|
-
throw new TypeError(`Unknown render content field: ${unknown[0]}.`);
|
|
568
|
-
}
|
|
569
|
-
if (
|
|
570
|
-
content.artifactId !== recipeArtifact.id ||
|
|
571
|
-
!SLUG_PATTERN.test(content.slug ?? '') ||
|
|
572
|
-
!Array.isArray(content.sections) ||
|
|
573
|
-
content.sections.length === 0
|
|
574
|
-
) {
|
|
575
|
-
throw new TypeError('Render content does not match the recipe artifact.');
|
|
576
|
-
}
|
|
577
|
-
for (const field of ['title', 'description', 'eyebrow', 'footer']) {
|
|
578
|
-
if (content[field] !== undefined && typeof content[field] !== 'string') {
|
|
579
|
-
throw new TypeError(`Render content ${field} must be a string.`);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
for (const section of content.sections) {
|
|
583
|
-
if (
|
|
584
|
-
!isObject(section) ||
|
|
585
|
-
!hasOnlyKeys(section, ['id', 'title', 'content']) ||
|
|
586
|
-
!SLUG_PATTERN.test(section.id ?? '') ||
|
|
587
|
-
typeof section.content !== 'string' ||
|
|
588
|
-
(section.title !== undefined && typeof section.title !== 'string')
|
|
589
|
-
) {
|
|
590
|
-
throw new TypeError('Render sections require safe ids and text content.');
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
const links = content.artifactLinks ?? [];
|
|
594
|
-
if (!Array.isArray(links)) {
|
|
595
|
-
throw new TypeError('Artifact links must be an array.');
|
|
596
|
-
}
|
|
597
|
-
for (const link of links) {
|
|
598
|
-
const hasLegacyKeys =
|
|
599
|
-
isObject(link) && hasExactKeys(link, ['id', 'type', 'label']);
|
|
600
|
-
const hasOriginKeys =
|
|
601
|
-
isObject(link) && hasExactKeys(link, ['id', 'type', 'label', 'origin']);
|
|
602
|
-
if (
|
|
603
|
-
!isObject(link) ||
|
|
604
|
-
(!hasLegacyKeys && !hasOriginKeys) ||
|
|
605
|
-
!SLUG_PATTERN.test(link.id ?? '') ||
|
|
606
|
-
!TYPE_DIRECTORIES.has(link.type) ||
|
|
607
|
-
typeof link.label !== 'string' ||
|
|
608
|
-
(link.origin !== undefined &&
|
|
609
|
-
!['floor', 'expansion'].includes(link.origin))
|
|
610
|
-
) {
|
|
611
|
-
throw new TypeError(
|
|
612
|
-
'Artifact cross-links must use typed artifact targets.',
|
|
613
|
-
);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function assertFactBase(factBase) {
|
|
619
|
-
if (factBase === undefined) return;
|
|
620
|
-
const validation = validateContract('fact-base', factBase);
|
|
621
|
-
if (!validation.valid) {
|
|
622
|
-
throw new TypeError('Renderer requires a validated fact base.');
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function assertTheme(theme) {
|
|
627
|
-
const validation = validateContract('theme', theme);
|
|
628
|
-
const identity = isObject(theme) ? structuredClone(theme) : {};
|
|
629
|
-
delete identity.bundleHash;
|
|
630
|
-
if (!validation.valid || theme.bundleHash !== canonicalHash(identity)) {
|
|
631
|
-
throw new Error('Renderer requires a validated resolved theme.');
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function assertRenderStrategy(renderStrategy) {
|
|
636
|
-
if (!RENDER_STRATEGIES.has(renderStrategy)) {
|
|
637
|
-
throw new Error(`Unsupported render strategy: ${String(renderStrategy)}.`);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function normalizePublicBaseUrl(value) {
|
|
642
|
-
if (value === undefined) return undefined;
|
|
643
|
-
if (typeof value !== 'string') {
|
|
644
|
-
throw new TypeError('Public base URL must be an HTTPS URL.');
|
|
645
|
-
}
|
|
646
|
-
let url;
|
|
647
|
-
try {
|
|
648
|
-
url = new URL(value);
|
|
649
|
-
} catch {
|
|
650
|
-
throw new TypeError('Public base URL must be an HTTPS URL.');
|
|
651
|
-
}
|
|
652
|
-
if (
|
|
653
|
-
url.protocol !== 'https:' ||
|
|
654
|
-
url.username ||
|
|
655
|
-
url.password ||
|
|
656
|
-
url.search ||
|
|
657
|
-
url.hash
|
|
658
|
-
) {
|
|
659
|
-
throw new TypeError(
|
|
660
|
-
'Public base URL must be an HTTPS URL without credentials.',
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
return url.href.replace(/\/+$/, '');
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function escapeHtml(value) {
|
|
667
|
-
return String(value)
|
|
668
|
-
.replaceAll('&', '&')
|
|
669
|
-
.replaceAll('<', '<')
|
|
670
|
-
.replaceAll('>', '>')
|
|
671
|
-
.replaceAll('"', '"')
|
|
672
|
-
.replaceAll("'", ''');
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function escapeAttribute(value) {
|
|
676
|
-
return escapeHtml(value);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function humanize(value) {
|
|
680
|
-
return value
|
|
681
|
-
.split('-')
|
|
682
|
-
.map((part) => `${part[0].toUpperCase()}${part.slice(1)}`)
|
|
683
|
-
.join(' ');
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
function oppositeMode(mode) {
|
|
687
|
-
return mode === 'light' ? 'dark' : 'light';
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function hasExactKeys(value, keys) {
|
|
691
|
-
return (
|
|
692
|
-
Object.keys(value).length === keys.length &&
|
|
693
|
-
keys.every((key) => Object.hasOwn(value, key))
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function hasOnlyKeys(value, keys) {
|
|
698
|
-
return Object.keys(value).every((key) => keys.includes(key));
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function isObject(value) {
|
|
702
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
703
|
-
}
|