@ampless/runtime 1.0.0-alpha.49 → 1.0.0-alpha.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +169 -50
- package/dist/index.js +1350 -1005
- package/dist/routes/index.js +1 -1
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -239,8 +239,8 @@ function createSeo(cmsConfig, settingsApi) {
|
|
|
239
239
|
|
|
240
240
|
// src/plugin-head.ts
|
|
241
241
|
import {
|
|
242
|
-
Fragment,
|
|
243
|
-
createElement
|
|
242
|
+
Fragment as Fragment2,
|
|
243
|
+
createElement as createElement2
|
|
244
244
|
} from "react";
|
|
245
245
|
import sanitizeHtml from "sanitize-html";
|
|
246
246
|
import {
|
|
@@ -248,1100 +248,1435 @@ import {
|
|
|
248
248
|
resolvePluginSettings
|
|
249
249
|
} from "ampless";
|
|
250
250
|
|
|
251
|
-
// src/
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const fs = proc.getBuiltinModule("node:fs");
|
|
260
|
-
const url = proc.getBuiltinModule("node:url");
|
|
261
|
-
if (!fs || !url) return null;
|
|
262
|
-
const resolve = import.meta.resolve.bind(import.meta);
|
|
263
|
-
return {
|
|
264
|
-
readFile: (u) => fs.readFileSync(url.fileURLToPath(u), "utf8"),
|
|
265
|
-
resolve
|
|
266
|
-
};
|
|
267
|
-
} catch {
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
251
|
+
// src/rendering.ts
|
|
252
|
+
import {
|
|
253
|
+
Fragment,
|
|
254
|
+
createElement
|
|
255
|
+
} from "react";
|
|
256
|
+
import { marked } from "marked";
|
|
257
|
+
function escape(s) {
|
|
258
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
270
259
|
}
|
|
271
|
-
function
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (typeof v.name !== "string") return false;
|
|
276
|
-
if (typeof v.trustLevel !== "string") return false;
|
|
277
|
-
if (!TRUST_LEVELS.includes(v.trustLevel)) return false;
|
|
278
|
-
if (!Array.isArray(v.capabilities)) return false;
|
|
279
|
-
for (const c of v.capabilities) {
|
|
280
|
-
if (typeof c !== "string") return false;
|
|
260
|
+
function textAlignStyle(attrs) {
|
|
261
|
+
const v = attrs?.textAlign;
|
|
262
|
+
if (v === "left" || v === "center" || v === "right" || v === "justify") {
|
|
263
|
+
return ` style="text-align: ${v}"`;
|
|
281
264
|
}
|
|
282
|
-
return
|
|
265
|
+
return "";
|
|
283
266
|
}
|
|
284
|
-
function
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
try {
|
|
301
|
-
pkg = JSON.parse(raw);
|
|
302
|
-
} catch {
|
|
303
|
-
return null;
|
|
267
|
+
function renderTiptapString(node) {
|
|
268
|
+
if (node.type === "text") {
|
|
269
|
+
let html = escape(node.text ?? "");
|
|
270
|
+
for (const mark of node.marks ?? []) {
|
|
271
|
+
if (mark.type === "bold") html = `<strong>${html}</strong>`;
|
|
272
|
+
else if (mark.type === "italic") html = `<em>${html}</em>`;
|
|
273
|
+
else if (mark.type === "code") html = `<code>${html}</code>`;
|
|
274
|
+
else if (mark.type === "strike") html = `<s>${html}</s>`;
|
|
275
|
+
else if (mark.type === "underline") html = `<u>${html}</u>`;
|
|
276
|
+
else if (mark.type === "highlight") html = `<mark>${html}</mark>`;
|
|
277
|
+
else if (mark.type === "link") {
|
|
278
|
+
const href = escape(String(mark.attrs?.href ?? "#"));
|
|
279
|
+
html = `<a href="${href}" target="_blank" rel="noopener">${html}</a>`;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return html;
|
|
304
283
|
}
|
|
305
|
-
|
|
306
|
-
|
|
284
|
+
const children = (node.content ?? []).map(renderTiptapString).join("");
|
|
285
|
+
switch (node.type) {
|
|
286
|
+
case "doc":
|
|
287
|
+
return children;
|
|
288
|
+
case "paragraph":
|
|
289
|
+
return `<p${textAlignStyle(node.attrs)}>${children}</p>`;
|
|
290
|
+
case "heading": {
|
|
291
|
+
const level = Number(node.attrs?.level ?? 1);
|
|
292
|
+
return `<h${level}${textAlignStyle(node.attrs)}>${children}</h${level}>`;
|
|
293
|
+
}
|
|
294
|
+
case "bulletList":
|
|
295
|
+
return `<ul>${children}</ul>`;
|
|
296
|
+
case "orderedList":
|
|
297
|
+
return `<ol>${children}</ol>`;
|
|
298
|
+
case "listItem":
|
|
299
|
+
return `<li>${children}</li>`;
|
|
300
|
+
case "codeBlock": {
|
|
301
|
+
const lang = node.attrs?.language ? ` class="language-${escape(String(node.attrs.language))}"` : "";
|
|
302
|
+
return `<pre><code${lang}>${children}</code></pre>`;
|
|
303
|
+
}
|
|
304
|
+
case "blockquote":
|
|
305
|
+
return `<blockquote>${children}</blockquote>`;
|
|
306
|
+
case "hardBreak":
|
|
307
|
+
return "<br />";
|
|
308
|
+
case "horizontalRule":
|
|
309
|
+
return "<hr />";
|
|
310
|
+
case "image": {
|
|
311
|
+
const src = escape(String(node.attrs?.src ?? ""));
|
|
312
|
+
const alt = escape(String(node.attrs?.alt ?? ""));
|
|
313
|
+
const title = node.attrs?.title ? ` title="${escape(String(node.attrs.title))}"` : "";
|
|
314
|
+
const display = node.attrs?.display ? ` data-display="${escape(String(node.attrs.display))}"` : "";
|
|
315
|
+
return `<img src="${src}" alt="${alt}"${title}${display} loading="lazy" />`;
|
|
316
|
+
}
|
|
317
|
+
case "table":
|
|
318
|
+
return `<table class="tiptap-table"><tbody>${children}</tbody></table>`;
|
|
319
|
+
case "tableRow":
|
|
320
|
+
return `<tr>${children}</tr>`;
|
|
321
|
+
case "tableHeader":
|
|
322
|
+
return `<th${tableCellAttrs(node.attrs)}>${children}</th>`;
|
|
323
|
+
case "tableCell":
|
|
324
|
+
return `<td${tableCellAttrs(node.attrs)}>${children}</td>`;
|
|
325
|
+
case "taskList":
|
|
326
|
+
return `<ul data-type="taskList">${children}</ul>`;
|
|
327
|
+
case "taskItem": {
|
|
328
|
+
const checked = node.attrs?.checked === true ? "true" : "false";
|
|
329
|
+
return `<li data-type="taskItem" data-checked="${checked}">${children}</li>`;
|
|
330
|
+
}
|
|
331
|
+
default:
|
|
332
|
+
return children;
|
|
307
333
|
}
|
|
308
|
-
const manifest = pkg.amplessPlugin;
|
|
309
|
-
if (!isValidManifest(manifest)) return null;
|
|
310
|
-
return manifest;
|
|
311
|
-
}
|
|
312
|
-
var SUPPORTED_API_VERSION = 1;
|
|
313
|
-
|
|
314
|
-
// src/plugin-head.ts
|
|
315
|
-
function isPlugin2(p) {
|
|
316
|
-
return typeof p === "object" && p !== null && "apiVersion" in p;
|
|
317
|
-
}
|
|
318
|
-
var ALLOWED_ATTRS = /* @__PURE__ */ new Set([
|
|
319
|
-
"crossorigin",
|
|
320
|
-
"referrerpolicy",
|
|
321
|
-
"integrity",
|
|
322
|
-
"fetchpriority",
|
|
323
|
-
// `nonce` is intentionally NOT in the allowlist for Phase 1. CSP
|
|
324
|
-
// nonce propagation is scoped out of Phase 1 (see spec §7); attrs
|
|
325
|
-
// shouldn't let plugins smuggle nonces past the design decision.
|
|
326
|
-
// The CSP nonce RFP will reintroduce it through `cspNonce` on
|
|
327
|
-
// PluginPublicRenderContext + `inlineScript.nonce: 'auto'`, not via
|
|
328
|
-
// the `attrs` bag.
|
|
329
|
-
"loading",
|
|
330
|
-
// iframe lazy-loading
|
|
331
|
-
"sandbox",
|
|
332
|
-
// iframe sandbox attribute
|
|
333
|
-
"allow",
|
|
334
|
-
// iframe permissions policy
|
|
335
|
-
"allowfullscreen"
|
|
336
|
-
// iframe fullscreen
|
|
337
|
-
]);
|
|
338
|
-
function isAllowedAttr(name) {
|
|
339
|
-
if (name.startsWith("data-")) return true;
|
|
340
|
-
return ALLOWED_ATTRS.has(name.toLowerCase());
|
|
341
|
-
}
|
|
342
|
-
function isSafeUrl(value) {
|
|
343
|
-
const trimmed = value.trim();
|
|
344
|
-
if (trimmed.length === 0) return false;
|
|
345
|
-
const schemeMatch = trimmed.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
|
|
346
|
-
if (!schemeMatch) return true;
|
|
347
|
-
const scheme = schemeMatch[1].toLowerCase();
|
|
348
|
-
return scheme === "http" || scheme === "https";
|
|
349
|
-
}
|
|
350
|
-
function isDev() {
|
|
351
|
-
const env = typeof process !== "undefined" && process.env ? process.env.NODE_ENV : void 0;
|
|
352
|
-
return env !== "production";
|
|
353
|
-
}
|
|
354
|
-
function warn(message) {
|
|
355
|
-
if (!isDev()) return;
|
|
356
|
-
console.warn(`[ampless plugin-head] ${message}`);
|
|
357
334
|
}
|
|
358
|
-
function
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
if (
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
throw new Error(
|
|
369
|
-
`${label}: package.json#amplessPlugin.apiVersion ${manifest.apiVersion} is newer than this runtime supports (max ${SUPPORTED_API_VERSION}). Upgrade @ampless/runtime, or pin an older version of the plugin.`
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
if (manifest.apiVersion !== plugin.apiVersion) {
|
|
373
|
-
throw new Error(
|
|
374
|
-
`${label}: apiVersion mismatch \u2014 package.json declares ${manifest.apiVersion}, factory declares ${plugin.apiVersion}. The two must agree.`
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
if (manifest.name !== plugin.name) {
|
|
378
|
-
warn(
|
|
379
|
-
`${label}: name mismatch \u2014 package.json#amplessPlugin.name is "${manifest.name}", factory returns name="${plugin.name}". Align them so admin UI / capability gates can identify the plugin consistently.`
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
if (manifest.trustLevel !== plugin.trust_level) {
|
|
383
|
-
warn(
|
|
384
|
-
`${label}: trustLevel mismatch \u2014 package.json declares "${manifest.trustLevel}", factory declares trust_level="${plugin.trust_level}". The processor IAM policies are wired off trust_level; drift here usually means the deployment lambda runs in the wrong context.`
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
const factoryCaps = Array.isArray(plugin.capabilities) ? plugin.capabilities : [];
|
|
388
|
-
const manifestCaps = manifest.capabilities;
|
|
389
|
-
if (!setsEqual(factoryCaps, manifestCaps)) {
|
|
390
|
-
warn(
|
|
391
|
-
`${label}: capabilities mismatch \u2014 package.json declares [${manifestCaps.join(", ")}], factory declares [${factoryCaps.join(", ")}]. The static manifest is what npm registry / admin UI surfaces show before the plugin loads, so it should match what the factory actually returns.`
|
|
392
|
-
);
|
|
335
|
+
function tableCellAttrs(attrs) {
|
|
336
|
+
let out = "";
|
|
337
|
+
const colspan = Number(attrs?.colspan ?? 1);
|
|
338
|
+
if (colspan > 1) out += ` colspan="${colspan}"`;
|
|
339
|
+
const rowspan = Number(attrs?.rowspan ?? 1);
|
|
340
|
+
if (rowspan > 1) out += ` rowspan="${rowspan}"`;
|
|
341
|
+
const colwidth = attrs?.colwidth;
|
|
342
|
+
if (Array.isArray(colwidth) && colwidth.length > 0) {
|
|
343
|
+
const w = Number(colwidth[0]);
|
|
344
|
+
if (Number.isFinite(w) && w > 0) out += ` style="width: ${w}px"`;
|
|
393
345
|
}
|
|
346
|
+
return out;
|
|
394
347
|
}
|
|
395
|
-
function
|
|
396
|
-
|
|
397
|
-
const sb = new Set(b);
|
|
398
|
-
if (sa.size !== sb.size) return false;
|
|
399
|
-
for (const c of sb) if (!sa.has(c)) return false;
|
|
400
|
-
return true;
|
|
348
|
+
function renderMarkdownString(md) {
|
|
349
|
+
return marked.parse(md, { gfm: true, breaks: false, async: false });
|
|
401
350
|
}
|
|
402
|
-
function
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
351
|
+
function buildContentFieldRegistry(plugins) {
|
|
352
|
+
const tiptap = /* @__PURE__ */ new Map();
|
|
353
|
+
const seenMarkdownPatterns = /* @__PURE__ */ new Set();
|
|
354
|
+
const markdownUrl = [];
|
|
355
|
+
for (const plugin of plugins) {
|
|
356
|
+
const fields = plugin.contentFields;
|
|
357
|
+
if (!fields) continue;
|
|
358
|
+
for (const field of fields) {
|
|
359
|
+
if (field.kind === "tiptap") {
|
|
360
|
+
if (tiptap.has(field.nodeType)) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`[ampless contentFields] duplicate tiptap nodeType "${field.nodeType}" \u2014 already registered by another plugin. Each nodeType may be claimed by at most one plugin.`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
tiptap.set(field.nodeType, { plugin, renderer: field });
|
|
366
|
+
} else if (field.kind === "markdown-url") {
|
|
367
|
+
const key = field.pattern.source;
|
|
368
|
+
if (seenMarkdownPatterns.has(key)) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
`[ampless contentFields] duplicate markdown-url pattern "${key}" \u2014 already registered by another plugin. Each pattern may be claimed by at most one plugin.`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
seenMarkdownPatterns.add(key);
|
|
374
|
+
markdownUrl.push({ plugin, renderer: field });
|
|
375
|
+
}
|
|
410
376
|
}
|
|
411
|
-
target[k] = v;
|
|
412
377
|
}
|
|
378
|
+
return { tiptap, markdownUrl };
|
|
413
379
|
}
|
|
414
|
-
function
|
|
415
|
-
return
|
|
380
|
+
function htmlPassthrough(html) {
|
|
381
|
+
return createElement("span", { dangerouslySetInnerHTML: { __html: html } });
|
|
416
382
|
}
|
|
417
|
-
function
|
|
418
|
-
|
|
419
|
-
if (
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
return false;
|
|
383
|
+
function htmlPassthroughBlock(html, key) {
|
|
384
|
+
const props = { dangerouslySetInnerHTML: { __html: html } };
|
|
385
|
+
if (key !== void 0) props.key = key;
|
|
386
|
+
return createElement("div", props);
|
|
423
387
|
}
|
|
424
|
-
function
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
)
|
|
429
|
-
|
|
388
|
+
function renderTiptapNode(node, opts, key) {
|
|
389
|
+
const reg = opts.contentFields?.tiptap.get(node.type);
|
|
390
|
+
if (reg) {
|
|
391
|
+
const ctx = opts.ctxForPlugin?.(reg.plugin);
|
|
392
|
+
if (!ctx) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const out = reg.renderer.render(node, ctx);
|
|
397
|
+
return createElement(Fragment, { key }, out);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
console.warn(
|
|
400
|
+
`[ampless renderBody] plugin "${reg.plugin.instanceId ?? reg.plugin.name}" threw inside contentFields tiptap renderer for nodeType "${node.type}": ${err instanceof Error ? err.message : String(err)}`
|
|
401
|
+
);
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
430
404
|
}
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
const allowed = surface === "body-for-post" ? `"application/ld+json" (required on publicBodyForPost \u2014 the per-post surface is scoped to JSON-LD)` : `undefined or "application/ld+json"`;
|
|
434
|
-
warn(
|
|
435
|
-
`${pluginLabel}: inlineScript "${descriptor.id}" dropped \u2014 scriptType ${got} not allowed on ${surface}. Allowed: ${allowed}.`
|
|
436
|
-
);
|
|
437
|
-
return null;
|
|
405
|
+
if (node.type === "text") {
|
|
406
|
+
return htmlPassthrough(renderTiptapString(node));
|
|
438
407
|
}
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
function renderHeadDescriptor(descriptor, pluginLabel, index) {
|
|
451
|
-
switch (descriptor.type) {
|
|
452
|
-
case "script": {
|
|
453
|
-
if (!isSafeUrl(descriptor.src)) {
|
|
454
|
-
warn(
|
|
455
|
-
`${pluginLabel}: script descriptor #${index} dropped \u2014 unsafe src "${descriptor.src}".`
|
|
456
|
-
);
|
|
457
|
-
return null;
|
|
458
|
-
}
|
|
459
|
-
const props = {
|
|
460
|
-
src: descriptor.src
|
|
461
|
-
};
|
|
462
|
-
if (descriptor.id) props.id = descriptor.id;
|
|
463
|
-
const hasAsync = typeof descriptor.async === "boolean";
|
|
464
|
-
const hasDefer = typeof descriptor.defer === "boolean";
|
|
465
|
-
if (hasAsync) props.async = descriptor.async;
|
|
466
|
-
if (hasDefer) props.defer = descriptor.defer;
|
|
467
|
-
if (!hasAsync && !hasDefer) {
|
|
468
|
-
if (descriptor.strategy === "lazyOnload") {
|
|
469
|
-
props.defer = true;
|
|
470
|
-
} else {
|
|
471
|
-
props.async = true;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
applyAttrs(props, descriptor.attrs, `${pluginLabel} script#${descriptor.id ?? index}`);
|
|
475
|
-
return {
|
|
476
|
-
id: descriptor.id ?? null,
|
|
477
|
-
element: createElement("script", props)
|
|
478
|
-
};
|
|
408
|
+
const childNodes = node.content ?? [];
|
|
409
|
+
const children = childNodes.map(
|
|
410
|
+
(c, i) => renderTiptapNode(c, opts, `${key}.${i}`)
|
|
411
|
+
);
|
|
412
|
+
switch (node.type) {
|
|
413
|
+
case "doc":
|
|
414
|
+
return createElement(Fragment, { key }, ...children);
|
|
415
|
+
case "paragraph": {
|
|
416
|
+
const align = textAlignStyleProp(node.attrs);
|
|
417
|
+
return createElement("p", { key, ...align }, ...children);
|
|
479
418
|
}
|
|
480
|
-
case "
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
if (descriptor.property) props.property = descriptor.property;
|
|
486
|
-
return {
|
|
487
|
-
// meta has no id channel in the descriptor. Don't derive a
|
|
488
|
-
// dedup id from name/property — multiple `<meta name=...>`
|
|
489
|
-
// entries with the same name are legitimate (e.g. theme-color
|
|
490
|
-
// media variants, and two plugins emitting overlapping names
|
|
491
|
-
// is a real case the runtime shouldn't silently collapse).
|
|
492
|
-
// Position-based React keys handle the stable-key requirement.
|
|
493
|
-
id: null,
|
|
494
|
-
element: createElement("meta", props)
|
|
495
|
-
};
|
|
419
|
+
case "heading": {
|
|
420
|
+
const level = Number(node.attrs?.level ?? 1);
|
|
421
|
+
const tag = `h${level}`;
|
|
422
|
+
const align = textAlignStyleProp(node.attrs);
|
|
423
|
+
return createElement(tag, { key, ...align }, ...children);
|
|
496
424
|
}
|
|
497
|
-
case "
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
425
|
+
case "bulletList":
|
|
426
|
+
return createElement("ul", { key }, ...children);
|
|
427
|
+
case "orderedList":
|
|
428
|
+
return createElement("ol", { key }, ...children);
|
|
429
|
+
case "listItem":
|
|
430
|
+
return createElement("li", { key }, ...children);
|
|
431
|
+
case "codeBlock": {
|
|
432
|
+
const codeProps = {};
|
|
433
|
+
if (node.attrs?.language) {
|
|
434
|
+
codeProps.className = `language-${String(node.attrs.language)}`;
|
|
503
435
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (descriptor.typeAttr) props.type = descriptor.typeAttr;
|
|
510
|
-
return {
|
|
511
|
-
id: null,
|
|
512
|
-
element: createElement("link", props)
|
|
513
|
-
};
|
|
436
|
+
return createElement(
|
|
437
|
+
"pre",
|
|
438
|
+
{ key },
|
|
439
|
+
createElement("code", codeProps, ...children)
|
|
440
|
+
);
|
|
514
441
|
}
|
|
515
|
-
case "
|
|
442
|
+
case "blockquote":
|
|
443
|
+
return createElement("blockquote", { key }, ...children);
|
|
444
|
+
case "hardBreak":
|
|
445
|
+
return createElement("br", { key });
|
|
446
|
+
case "horizontalRule":
|
|
447
|
+
return createElement("hr", { key });
|
|
448
|
+
case "image": {
|
|
449
|
+
const src = String(node.attrs?.src ?? "");
|
|
450
|
+
const alt = String(node.attrs?.alt ?? "");
|
|
451
|
+
const title = node.attrs?.title ? String(node.attrs.title) : void 0;
|
|
452
|
+
const display = node.attrs?.display ? String(node.attrs.display) : void 0;
|
|
516
453
|
const props = {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
id: descriptor.id ?? null,
|
|
522
|
-
element: createElement("noscript", props)
|
|
454
|
+
key,
|
|
455
|
+
src,
|
|
456
|
+
alt,
|
|
457
|
+
loading: "lazy"
|
|
523
458
|
};
|
|
459
|
+
if (title) props.title = title;
|
|
460
|
+
if (display) props["data-display"] = display;
|
|
461
|
+
return createElement("img", props);
|
|
524
462
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
463
|
+
case "table":
|
|
464
|
+
return createElement(
|
|
465
|
+
"table",
|
|
466
|
+
{ key, className: "tiptap-table" },
|
|
467
|
+
createElement("tbody", null, ...children)
|
|
468
|
+
);
|
|
469
|
+
case "tableRow":
|
|
470
|
+
return createElement("tr", { key }, ...children);
|
|
471
|
+
case "tableHeader":
|
|
472
|
+
return createElement("th", { key, ...tableCellProps(node.attrs) }, ...children);
|
|
473
|
+
case "tableCell":
|
|
474
|
+
return createElement("td", { key, ...tableCellProps(node.attrs) }, ...children);
|
|
475
|
+
case "taskList":
|
|
476
|
+
return createElement("ul", { key, "data-type": "taskList" }, ...children);
|
|
477
|
+
case "taskItem": {
|
|
478
|
+
const checked = node.attrs?.checked === true ? "true" : "false";
|
|
479
|
+
return createElement(
|
|
480
|
+
"li",
|
|
481
|
+
{ key, "data-type": "taskItem", "data-checked": checked },
|
|
482
|
+
...children
|
|
532
483
|
);
|
|
533
|
-
return null;
|
|
534
484
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
};
|
|
538
|
-
if (descriptor.id) props.id = descriptor.id;
|
|
539
|
-
if (descriptor.title) props.title = descriptor.title;
|
|
540
|
-
if (typeof descriptor.width === "number") props.width = descriptor.width;
|
|
541
|
-
if (typeof descriptor.height === "number") props.height = descriptor.height;
|
|
542
|
-
applyAttrs(props, descriptor.attrs, `${pluginLabel} iframe#${descriptor.id ?? index}`);
|
|
543
|
-
return {
|
|
544
|
-
id: descriptor.id ?? null,
|
|
545
|
-
element: createElement("iframe", props)
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
if (descriptor.type === "inlineScript") {
|
|
549
|
-
return renderInlineScript(descriptor, pluginLabel, index, "body-end");
|
|
485
|
+
default:
|
|
486
|
+
return createElement(Fragment, { key }, ...children);
|
|
550
487
|
}
|
|
551
|
-
return renderHeadDescriptor(descriptor, pluginLabel, index);
|
|
552
488
|
}
|
|
553
|
-
function
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
);
|
|
558
|
-
return null;
|
|
489
|
+
function textAlignStyleProp(attrs) {
|
|
490
|
+
const v = attrs?.textAlign;
|
|
491
|
+
if (v === "left" || v === "center" || v === "right" || v === "justify") {
|
|
492
|
+
return { style: { textAlign: v } };
|
|
559
493
|
}
|
|
560
|
-
return
|
|
494
|
+
return {};
|
|
561
495
|
}
|
|
562
|
-
function
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const kept = [];
|
|
573
|
-
for (let i = 0; i < entries.length; i++) {
|
|
574
|
-
const { id, element } = entries[i];
|
|
575
|
-
if (id !== null && lastIndexById.get(id) !== i) continue;
|
|
576
|
-
const key = id ?? `__pos-${i}`;
|
|
577
|
-
const existingProps = element.props;
|
|
578
|
-
kept.push(createElement(element.type, { ...existingProps, key }));
|
|
496
|
+
function tableCellProps(attrs) {
|
|
497
|
+
const out = {};
|
|
498
|
+
const colspan = Number(attrs?.colspan ?? 1);
|
|
499
|
+
if (colspan > 1) out.colSpan = colspan;
|
|
500
|
+
const rowspan = Number(attrs?.rowspan ?? 1);
|
|
501
|
+
if (rowspan > 1) out.rowSpan = rowspan;
|
|
502
|
+
const colwidth = attrs?.colwidth;
|
|
503
|
+
if (Array.isArray(colwidth) && colwidth.length > 0) {
|
|
504
|
+
const w = Number(colwidth[0]);
|
|
505
|
+
if (Number.isFinite(w) && w > 0) out.style = { width: `${w}px` };
|
|
579
506
|
}
|
|
580
|
-
return
|
|
507
|
+
return out;
|
|
581
508
|
}
|
|
582
|
-
function
|
|
583
|
-
const
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
509
|
+
function renderMarkdownNode(md, opts) {
|
|
510
|
+
const tokens = marked.lexer(md, { gfm: true, breaks: false });
|
|
511
|
+
const children = [];
|
|
512
|
+
let pending = [];
|
|
513
|
+
let chunk = 0;
|
|
514
|
+
const flush = () => {
|
|
515
|
+
if (pending.length === 0) return;
|
|
516
|
+
const html = marked.parser(pending, { gfm: true, breaks: false });
|
|
517
|
+
children.push(htmlPassthroughBlock(html, `md-html-${chunk++}`));
|
|
518
|
+
pending = [];
|
|
593
519
|
};
|
|
520
|
+
tokens.forEach((token, i) => {
|
|
521
|
+
const match = matchMarkdownUrlEmbed(token, opts);
|
|
522
|
+
if (match) {
|
|
523
|
+
flush();
|
|
524
|
+
children.push(createElement(Fragment, { key: `md-embed-${i}` }, match));
|
|
525
|
+
} else {
|
|
526
|
+
pending.push(token);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
flush();
|
|
530
|
+
return createElement(Fragment, null, ...children);
|
|
594
531
|
}
|
|
595
|
-
function
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
532
|
+
function matchMarkdownUrlEmbed(token, opts) {
|
|
533
|
+
if (!opts.contentFields || opts.contentFields.markdownUrl.length === 0) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
if (token.type !== "paragraph") return null;
|
|
537
|
+
const para = token;
|
|
538
|
+
const url = extractSingleUrl(para);
|
|
539
|
+
if (!url) return null;
|
|
540
|
+
for (const entry of opts.contentFields.markdownUrl) {
|
|
541
|
+
const m = url.match(entry.renderer.pattern);
|
|
542
|
+
if (!m) continue;
|
|
543
|
+
const ctx = opts.ctxForPlugin?.(entry.plugin);
|
|
544
|
+
if (!ctx) return null;
|
|
602
545
|
try {
|
|
603
|
-
|
|
546
|
+
return entry.renderer.render({ match: m, raw: url }, ctx);
|
|
604
547
|
} catch (err) {
|
|
605
|
-
warn(
|
|
606
|
-
`plugin "${plugin.instanceId ?? plugin.name}" threw inside
|
|
548
|
+
console.warn(
|
|
549
|
+
`[ampless renderBody] plugin "${entry.plugin.instanceId ?? entry.plugin.name}" threw inside contentFields markdown-url renderer for pattern "${entry.renderer.pattern.source}": ${err instanceof Error ? err.message : String(err)}`
|
|
607
550
|
);
|
|
608
|
-
|
|
609
|
-
}
|
|
610
|
-
const label = `plugin "${plugin.instanceId ?? plugin.name}"`;
|
|
611
|
-
for (let i = 0; i < descriptors.length; i++) {
|
|
612
|
-
const entry = renderOne(descriptors[i], label, i);
|
|
613
|
-
if (entry) entries.push(entry);
|
|
551
|
+
return null;
|
|
614
552
|
}
|
|
615
553
|
}
|
|
616
|
-
|
|
617
|
-
const keyed = dedupeAndKey(entries);
|
|
618
|
-
return createElement(Fragment, null, ...keyed);
|
|
554
|
+
return null;
|
|
619
555
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
if (!parts.includes("noreferrer")) parts.push("noreferrer");
|
|
636
|
-
out["rel"] = parts.join(" ");
|
|
637
|
-
}
|
|
638
|
-
return { tagName, attribs: out };
|
|
639
|
-
}
|
|
556
|
+
function extractSingleUrl(para) {
|
|
557
|
+
const tokens = para.tokens ?? [];
|
|
558
|
+
if (tokens.length === 0) return null;
|
|
559
|
+
const trimmed = tokens.filter((t2) => {
|
|
560
|
+
if (t2.type === "text") return (t2.raw ?? "").trim().length > 0;
|
|
561
|
+
return true;
|
|
562
|
+
});
|
|
563
|
+
if (trimmed.length !== 1) return null;
|
|
564
|
+
const t = trimmed[0];
|
|
565
|
+
if (t.type === "link") {
|
|
566
|
+
const link = t;
|
|
567
|
+
const href = (link.href ?? "").trim();
|
|
568
|
+
const raw = (link.raw ?? "").trim();
|
|
569
|
+
if (!href || raw !== href) return null;
|
|
570
|
+
return href;
|
|
640
571
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
warn(
|
|
645
|
-
`${pluginLabel}: publicHtmlForPost descriptor #${index} dropped \u2014 must be an object.`
|
|
646
|
-
);
|
|
647
|
-
return false;
|
|
572
|
+
if (t.type === "text") {
|
|
573
|
+
const text = (t.text ?? "").trim();
|
|
574
|
+
return text || null;
|
|
648
575
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
);
|
|
654
|
-
return false;
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
function renderBody(post, opts = {}) {
|
|
579
|
+
if (post.format === "html") {
|
|
580
|
+
return htmlPassthroughBlock(String(post.body));
|
|
655
581
|
}
|
|
656
|
-
if (
|
|
657
|
-
|
|
658
|
-
`${pluginLabel}: publicHtmlForPost descriptor #${index} dropped \u2014 "id" must be a string.`
|
|
659
|
-
);
|
|
660
|
-
return false;
|
|
582
|
+
if (post.format === "markdown") {
|
|
583
|
+
return renderMarkdownNode(String(post.body), opts);
|
|
661
584
|
}
|
|
662
|
-
if (
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
return
|
|
667
|
-
}
|
|
668
|
-
if (d.position !== "beforeContent" && d.position !== "afterContent") {
|
|
669
|
-
warn(
|
|
670
|
-
`${pluginLabel}: publicHtmlForPost descriptor "${d.id}" dropped \u2014 "position" must be "beforeContent" or "afterContent" (got ${typeof d.position === "string" ? `"${d.position}"` : typeof d.position}).`
|
|
671
|
-
);
|
|
672
|
-
return false;
|
|
585
|
+
if (post.format === "tiptap") {
|
|
586
|
+
if (typeof post.body === "string") {
|
|
587
|
+
return htmlPassthroughBlock(post.body);
|
|
588
|
+
}
|
|
589
|
+
return renderTiptapNode(post.body, opts, "root");
|
|
673
590
|
}
|
|
674
|
-
return
|
|
591
|
+
return null;
|
|
675
592
|
}
|
|
676
|
-
function
|
|
677
|
-
if (
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
warn(
|
|
683
|
-
`${pluginLabel}: publicHtmlForPost descriptor "${id}" dropped \u2014 "id" contains control characters.`
|
|
684
|
-
);
|
|
685
|
-
return false;
|
|
686
|
-
}
|
|
687
|
-
if (id.length > 64) {
|
|
688
|
-
warn(
|
|
689
|
-
`${pluginLabel}: publicHtmlForPost descriptor "${id.slice(0, 32)}\u2026" dropped \u2014 "id" exceeds 64 characters.`
|
|
690
|
-
);
|
|
691
|
-
return false;
|
|
593
|
+
function renderBodyHtmlString(post) {
|
|
594
|
+
if (post.format === "html") return String(post.body);
|
|
595
|
+
if (post.format === "markdown") return renderMarkdownString(String(post.body));
|
|
596
|
+
if (post.format === "tiptap") {
|
|
597
|
+
if (typeof post.body === "string") return post.body;
|
|
598
|
+
return renderTiptapString(post.body);
|
|
692
599
|
}
|
|
693
|
-
return
|
|
600
|
+
return "";
|
|
694
601
|
}
|
|
695
|
-
function
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
const factory = plugin.publicBodyForPost;
|
|
699
|
-
if (!factory) continue;
|
|
700
|
-
const ctx = makeCtx(plugin, site, snapshot);
|
|
701
|
-
let descriptors;
|
|
702
|
-
try {
|
|
703
|
-
descriptors = factory.call(plugin, post, ctx) ?? [];
|
|
704
|
-
} catch (err) {
|
|
705
|
-
warn(
|
|
706
|
-
`plugin "${plugin.instanceId ?? plugin.name}" threw inside publicBodyForPost callback: ${err instanceof Error ? err.message : String(err)}`
|
|
707
|
-
);
|
|
708
|
-
continue;
|
|
709
|
-
}
|
|
710
|
-
const label = `plugin "${plugin.instanceId ?? plugin.name}"`;
|
|
711
|
-
for (let i = 0; i < descriptors.length; i++) {
|
|
712
|
-
const entry = renderPostBodyDescriptor(descriptors[i], label, i);
|
|
713
|
-
if (entry) entries.push(entry);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
if (entries.length === 0) return null;
|
|
717
|
-
const keyed = dedupeAndKey(entries);
|
|
718
|
-
return createElement(Fragment, null, ...keyed);
|
|
602
|
+
function tiptapToHtml(doc) {
|
|
603
|
+
if (typeof doc === "string") return doc;
|
|
604
|
+
return renderTiptapString(doc);
|
|
719
605
|
}
|
|
720
|
-
function
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
)
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
for (let i = 0; i < descriptors.length; i++) {
|
|
741
|
-
const raw = descriptors[i];
|
|
742
|
-
if (!validateHtmlDescriptor(raw, label, i)) continue;
|
|
743
|
-
const descriptor = raw;
|
|
744
|
-
if (!validateHtmlId(descriptor.id, label)) continue;
|
|
745
|
-
const dedupeKey = `${namespace}:${descriptor.id}`;
|
|
746
|
-
const position = descriptor.position;
|
|
747
|
-
const seenSet = position === "beforeContent" ? seenBefore : seenAfter;
|
|
748
|
-
const bucket = position === "beforeContent" ? before : after;
|
|
749
|
-
if (seenSet.has(dedupeKey)) {
|
|
750
|
-
warn(
|
|
751
|
-
`${label}: publicHtmlForPost descriptor "${descriptor.id}" at position "${position}" is a duplicate \u2014 keeping the first occurrence.`
|
|
752
|
-
);
|
|
753
|
-
continue;
|
|
754
|
-
}
|
|
755
|
-
seenSet.add(dedupeKey);
|
|
756
|
-
const cleanHtml = sanitizeHtml(descriptor.body, SANITIZE_OPTIONS);
|
|
757
|
-
bucket.push({ key: dedupeKey, cleanHtml, namespace });
|
|
606
|
+
function markdownToHtml(md) {
|
|
607
|
+
return renderMarkdownString(md);
|
|
608
|
+
}
|
|
609
|
+
function tiptapToMarkdown(doc) {
|
|
610
|
+
if (typeof doc === "string") return htmlToMarkdown(doc);
|
|
611
|
+
const node = doc;
|
|
612
|
+
return tiptapNodeToMarkdown(node).trim() + "\n";
|
|
613
|
+
}
|
|
614
|
+
function tiptapNodeToMarkdown(node) {
|
|
615
|
+
if (node.type === "text") {
|
|
616
|
+
let txt = node.text ?? "";
|
|
617
|
+
for (const mark of node.marks ?? []) {
|
|
618
|
+
if (mark.type === "bold") txt = `**${txt}**`;
|
|
619
|
+
else if (mark.type === "italic") txt = `*${txt}*`;
|
|
620
|
+
else if (mark.type === "code") txt = `\`${txt}\``;
|
|
621
|
+
else if (mark.type === "strike") txt = `~~${txt}~~`;
|
|
622
|
+
else if (mark.type === "underline") txt = `<u>${txt}</u>`;
|
|
623
|
+
else if (mark.type === "highlight") txt = `<mark>${txt}</mark>`;
|
|
624
|
+
else if (mark.type === "link") txt = `[${txt}](${String(mark.attrs?.href ?? "#")})`;
|
|
758
625
|
}
|
|
626
|
+
return txt;
|
|
759
627
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
);
|
|
770
|
-
return createElement(Fragment, null, ...elements);
|
|
771
|
-
}
|
|
772
|
-
return {
|
|
773
|
-
beforeContent: toReactNode(before, "beforeContent"),
|
|
774
|
-
afterContent: toReactNode(after, "afterContent")
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
|
-
function createPluginHead(cmsConfig, pluginSettings) {
|
|
778
|
-
const plugins = (cmsConfig.plugins ?? []).filter(isPlugin2);
|
|
779
|
-
const validPlugins = [];
|
|
780
|
-
const seenNamespaces = /* @__PURE__ */ new Set();
|
|
781
|
-
for (const plugin of plugins) {
|
|
782
|
-
const ns = plugin.instanceId ?? plugin.name;
|
|
783
|
-
const label = plugin.instanceId ? `${plugin.name}#${plugin.instanceId}` : plugin.name;
|
|
784
|
-
if (!isValidPluginKey(ns)) {
|
|
785
|
-
warn(
|
|
786
|
-
`${label}: plugin namespace "${ns}" violates ${`/^[a-zA-Z0-9_-]+$/`}. Use a simple identifier (letters / digits / "-" / "_"). Skipping plugin.`
|
|
787
|
-
);
|
|
788
|
-
continue;
|
|
628
|
+
const children = (node.content ?? []).map(tiptapNodeToMarkdown).join("");
|
|
629
|
+
switch (node.type) {
|
|
630
|
+
case "doc":
|
|
631
|
+
return children;
|
|
632
|
+
case "paragraph":
|
|
633
|
+
return children + "\n\n";
|
|
634
|
+
case "heading": {
|
|
635
|
+
const level = Math.max(1, Math.min(6, Number(node.attrs?.level ?? 1)));
|
|
636
|
+
return "#".repeat(level) + " " + children + "\n\n";
|
|
789
637
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
638
|
+
case "bulletList":
|
|
639
|
+
return children + "\n";
|
|
640
|
+
case "orderedList":
|
|
641
|
+
return children + "\n";
|
|
642
|
+
case "listItem": {
|
|
643
|
+
const trimmed = children.replace(/\n+$/, "");
|
|
644
|
+
return "- " + trimmed + "\n";
|
|
794
645
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (!isValidPluginKey(field.key)) {
|
|
799
|
-
warn(
|
|
800
|
-
`${label}: settings.public field key "${field.key}" violates ${`/^[a-zA-Z0-9_-]+$/`}. The field will not be readable through ctx.setting(). Rename the field key.`
|
|
801
|
-
);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
646
|
+
case "codeBlock": {
|
|
647
|
+
const lang = node.attrs?.language ? String(node.attrs.language) : "";
|
|
648
|
+
return "```" + lang + "\n" + children + "\n```\n\n";
|
|
804
649
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
650
|
+
case "blockquote":
|
|
651
|
+
return children.replace(/\n+$/, "").split("\n").map((l) => "> " + l).join("\n") + "\n\n";
|
|
652
|
+
case "hardBreak":
|
|
653
|
+
return " \n";
|
|
654
|
+
case "horizontalRule":
|
|
655
|
+
return "\n---\n\n";
|
|
656
|
+
case "image": {
|
|
657
|
+
const src = String(node.attrs?.src ?? "");
|
|
658
|
+
const alt = String(node.attrs?.alt ?? "");
|
|
659
|
+
return ``;
|
|
808
660
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
}
|
|
821
|
-
if (plugin.publicHead && !caps.includes("publicHead")) {
|
|
822
|
-
warn(
|
|
823
|
-
`${label}: implements \`publicHead\` but "publicHead" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
824
|
-
);
|
|
825
|
-
}
|
|
826
|
-
if (plugin.publicBodyEnd && !caps.includes("publicBody")) {
|
|
827
|
-
warn(
|
|
828
|
-
`${label}: implements \`publicBodyEnd\` but "publicBody" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
829
|
-
);
|
|
830
|
-
}
|
|
831
|
-
if (caps.includes("schema") && !plugin.publicBodyForPost) {
|
|
832
|
-
warn(
|
|
833
|
-
`${label}: declares capability "schema" but no \`publicBodyForPost\` implementation. Drop the capability or add the function.`
|
|
834
|
-
);
|
|
835
|
-
}
|
|
836
|
-
if (plugin.publicBodyForPost && !caps.includes("schema")) {
|
|
837
|
-
warn(
|
|
838
|
-
`${label}: implements \`publicBodyForPost\` but "schema" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
if (caps.includes("publicHtmlForPost") && !plugin.publicHtmlForPost) {
|
|
842
|
-
warn(
|
|
843
|
-
`${label}: declares capability "publicHtmlForPost" but no \`publicHtmlForPost\` implementation. Drop the capability or add the function.`
|
|
844
|
-
);
|
|
845
|
-
}
|
|
846
|
-
if (plugin.publicHtmlForPost && !caps.includes("publicHtmlForPost")) {
|
|
847
|
-
warn(
|
|
848
|
-
`${label}: implements \`publicHtmlForPost\` but "publicHtmlForPost" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
849
|
-
);
|
|
850
|
-
}
|
|
661
|
+
case "table":
|
|
662
|
+
return tiptapTableToMarkdown(node);
|
|
663
|
+
case "taskList":
|
|
664
|
+
return children + "\n";
|
|
665
|
+
case "taskItem": {
|
|
666
|
+
const checked = node.attrs?.checked === true ? "x" : " ";
|
|
667
|
+
const inner = children.replace(/\n+$/, "");
|
|
668
|
+
const [first, ...rest] = inner.split("\n");
|
|
669
|
+
const cont = rest.map((l) => l ? " " + l : l).join("\n");
|
|
670
|
+
return `- [${checked}] ${first ?? ""}${cont ? "\n" + cont : ""}
|
|
671
|
+
`;
|
|
851
672
|
}
|
|
673
|
+
default:
|
|
674
|
+
return children;
|
|
852
675
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
676
|
+
}
|
|
677
|
+
function tiptapTableToMarkdown(node) {
|
|
678
|
+
const rows = node.content ?? [];
|
|
679
|
+
if (rows.length === 0) return "";
|
|
680
|
+
const renderedRows = [];
|
|
681
|
+
let headerIdx = -1;
|
|
682
|
+
for (let i = 0; i < rows.length; i++) {
|
|
683
|
+
const row = rows[i];
|
|
684
|
+
const cells = row.content ?? [];
|
|
685
|
+
const cellTexts = cells.map((c) => {
|
|
686
|
+
const inner = (c.content ?? []).map(tiptapNodeToMarkdown).join("");
|
|
687
|
+
return inner.replace(/\n+$/, "").replace(/\n/g, "<br>").replace(/\|/g, "\\|");
|
|
688
|
+
});
|
|
689
|
+
renderedRows.push(cellTexts);
|
|
690
|
+
if (headerIdx === -1 && cells.some((c) => c.type === "tableHeader")) headerIdx = i;
|
|
691
|
+
}
|
|
692
|
+
if (headerIdx === -1) headerIdx = 0;
|
|
693
|
+
const header = renderedRows[headerIdx] ?? [];
|
|
694
|
+
const body = renderedRows.filter((_, i) => i !== headerIdx);
|
|
695
|
+
const cols = header.length;
|
|
696
|
+
const headerLine = "| " + header.join(" | ") + " |";
|
|
697
|
+
const sepLine = "| " + Array.from({ length: cols }, () => "---").join(" | ") + " |";
|
|
698
|
+
const bodyLines = body.map((r) => {
|
|
699
|
+
const cells = Array.from({ length: cols }, (_, i) => r[i] ?? "");
|
|
700
|
+
return "| " + cells.join(" | ") + " |";
|
|
701
|
+
});
|
|
702
|
+
return "\n" + [headerLine, sepLine, ...bodyLines].join("\n") + "\n\n";
|
|
703
|
+
}
|
|
704
|
+
function htmlToMarkdown(html) {
|
|
705
|
+
let md = html;
|
|
706
|
+
md = md.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (_, inner) => {
|
|
707
|
+
return "\n" + convertHtmlTable(String(inner)) + "\n";
|
|
708
|
+
});
|
|
709
|
+
md = md.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, level, text) => {
|
|
710
|
+
return "\n" + "#".repeat(Number(level)) + " " + String(text).trim() + "\n\n";
|
|
711
|
+
});
|
|
712
|
+
md = md.replace(
|
|
713
|
+
/<pre[^>]*><code[^>]*(?:\sclass="language-([^"]+)")?[^>]*>([\s\S]*?)<\/code><\/pre>/gi,
|
|
714
|
+
(_, lang, code) => {
|
|
715
|
+
return "\n```" + (lang ?? "") + "\n" + String(code) + "\n```\n\n";
|
|
716
|
+
}
|
|
717
|
+
);
|
|
718
|
+
md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
|
|
719
|
+
return "\n" + String(content).trim().split("\n").map((l) => "> " + l).join("\n") + "\n\n";
|
|
720
|
+
});
|
|
721
|
+
md = md.replace(/<ul[^>]*data-type="taskList"[^>]*>([\s\S]*?)<\/ul>/gi, (_, items) => {
|
|
722
|
+
return "\n" + convertHtmlTaskList(String(items)) + "\n";
|
|
723
|
+
});
|
|
724
|
+
md = md.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, (_, items) => {
|
|
725
|
+
return "\n" + String(items).replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "- $1\n") + "\n";
|
|
726
|
+
});
|
|
727
|
+
md = md.replace(/<ol[^>]*>([\s\S]*?)<\/ol>/gi, (_, items) => {
|
|
728
|
+
let i = 1;
|
|
729
|
+
return "\n" + String(items).replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, () => `${i++}. $1
|
|
730
|
+
`) + "\n";
|
|
731
|
+
});
|
|
732
|
+
md = md.replace(/<hr\s*\/?>/gi, "\n---\n\n");
|
|
733
|
+
md = md.replace(/<br\s*\/?>/gi, " \n");
|
|
734
|
+
md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, "$1\n\n");
|
|
735
|
+
md = md.replace(/<img[^>]*?src="([^"]*)"[^>]*?alt="([^"]*)"[^>]*?\/?>/gi, "");
|
|
736
|
+
md = md.replace(/<img[^>]*?alt="([^"]*)"[^>]*?src="([^"]*)"[^>]*?\/?>/gi, "");
|
|
737
|
+
md = md.replace(/<img[^>]*?src="([^"]*)"[^>]*?\/?>/gi, "");
|
|
738
|
+
md = md.replace(/<a[^>]*?href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)");
|
|
739
|
+
md = md.replace(/<(strong|b)>([\s\S]*?)<\/\1>/gi, "**$2**");
|
|
740
|
+
md = md.replace(/<(em|i)>([\s\S]*?)<\/\1>/gi, "*$2*");
|
|
741
|
+
md = md.replace(/<s>([\s\S]*?)<\/s>/gi, "~~$1~~");
|
|
742
|
+
md = md.replace(/<code>([\s\S]*?)<\/code>/gi, "`$1`");
|
|
743
|
+
const PH_U_OPEN = "AMP_U_OPEN";
|
|
744
|
+
const PH_U_CLOSE = "AMP_U_CLOSE";
|
|
745
|
+
const PH_MARK_OPEN = "AMP_MARK_OPEN";
|
|
746
|
+
const PH_MARK_CLOSE = "AMP_MARK_CLOSE";
|
|
747
|
+
md = md.replace(/<u>([\s\S]*?)<\/u>/gi, `${PH_U_OPEN}$1${PH_U_CLOSE}`);
|
|
748
|
+
md = md.replace(/<mark>([\s\S]*?)<\/mark>/gi, `${PH_MARK_OPEN}$1${PH_MARK_CLOSE}`);
|
|
749
|
+
md = md.replace(/<\/?[^>]+>/g, "");
|
|
750
|
+
md = md.split(PH_U_OPEN).join("<u>").split(PH_U_CLOSE).join("</u>").split(PH_MARK_OPEN).join("<mark>").split(PH_MARK_CLOSE).join("</mark>");
|
|
751
|
+
md = md.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ");
|
|
752
|
+
md = md.replace(/\n{3,}/g, "\n\n");
|
|
753
|
+
return md.trim() + "\n";
|
|
754
|
+
}
|
|
755
|
+
function convertHtmlTable(inner) {
|
|
756
|
+
const stripped = inner.replace(/<\/?(thead|tbody)[^>]*>/gi, "");
|
|
757
|
+
const rowRe = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
|
758
|
+
const rows = [];
|
|
759
|
+
let m;
|
|
760
|
+
while ((m = rowRe.exec(stripped)) !== null) {
|
|
761
|
+
const rowHtml = m[1] ?? "";
|
|
762
|
+
const cellRe = /<(th|td)[^>]*>([\s\S]*?)<\/\1>/gi;
|
|
763
|
+
const cells = [];
|
|
764
|
+
let isHeader = false;
|
|
765
|
+
let cm;
|
|
766
|
+
while ((cm = cellRe.exec(rowHtml)) !== null) {
|
|
767
|
+
if ((cm[1] ?? "").toLowerCase() === "th") isHeader = true;
|
|
768
|
+
cells.push(normalizeTableCell(cm[2] ?? ""));
|
|
769
|
+
}
|
|
770
|
+
if (cells.length > 0) rows.push({ isHeader, cells });
|
|
771
|
+
}
|
|
772
|
+
if (rows.length === 0) return "";
|
|
773
|
+
let headerIdx = rows.findIndex((r) => r.isHeader);
|
|
774
|
+
if (headerIdx === -1) headerIdx = 0;
|
|
775
|
+
const header = rows[headerIdx].cells;
|
|
776
|
+
const body = rows.filter((_, i) => i !== headerIdx).map((r) => r.cells);
|
|
777
|
+
const cols = header.length;
|
|
778
|
+
const headerLine = "| " + header.join(" | ") + " |";
|
|
779
|
+
const sepLine = "| " + Array.from({ length: cols }, () => "---").join(" | ") + " |";
|
|
780
|
+
const bodyLines = body.map((cells) => {
|
|
781
|
+
const padded = Array.from({ length: cols }, (_, i) => cells[i] ?? "");
|
|
782
|
+
return "| " + padded.join(" | ") + " |";
|
|
783
|
+
});
|
|
784
|
+
return [headerLine, sepLine, ...bodyLines].join("\n") + "\n";
|
|
785
|
+
}
|
|
786
|
+
function normalizeTableCell(html) {
|
|
787
|
+
return html.replace(/<br\s*\/?>/gi, " ").replace(/<\/?[^>]+>/g, "").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
788
|
+
}
|
|
789
|
+
function convertHtmlTaskList(items) {
|
|
790
|
+
const liRe = /<li[^>]*data-checked="(true|false)"[^>]*>([\s\S]*?)<\/li>/gi;
|
|
791
|
+
const out = [];
|
|
792
|
+
let m;
|
|
793
|
+
while ((m = liRe.exec(items)) !== null) {
|
|
794
|
+
const checked = m[1] === "true" ? "x" : " ";
|
|
795
|
+
const inner = String(m[2] ?? "").replace(/<\/?p[^>]*>/gi, "").trim();
|
|
796
|
+
out.push(`- [${checked}] ${inner}`);
|
|
797
|
+
}
|
|
798
|
+
return out.join("\n");
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/plugin-package-manifest.ts
|
|
802
|
+
var TRUST_LEVELS = ["untrusted", "trusted", "privileged"];
|
|
803
|
+
function getNodeApi() {
|
|
804
|
+
if (typeof window !== "undefined") return null;
|
|
805
|
+
const proc = typeof process !== "undefined" ? process : void 0;
|
|
806
|
+
if (typeof proc?.getBuiltinModule !== "function") return null;
|
|
807
|
+
if (typeof import.meta.resolve !== "function") return null;
|
|
808
|
+
try {
|
|
809
|
+
const fs = proc.getBuiltinModule("node:fs");
|
|
810
|
+
const url = proc.getBuiltinModule("node:url");
|
|
811
|
+
if (!fs || !url) return null;
|
|
812
|
+
const resolve = import.meta.resolve.bind(import.meta);
|
|
813
|
+
return {
|
|
814
|
+
readFile: (u) => fs.readFileSync(url.fileURLToPath(u), "utf8"),
|
|
815
|
+
resolve
|
|
816
|
+
};
|
|
817
|
+
} catch {
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function isValidManifest(value) {
|
|
822
|
+
if (typeof value !== "object" || value === null) return false;
|
|
823
|
+
const v = value;
|
|
824
|
+
if (typeof v.apiVersion !== "number") return false;
|
|
825
|
+
if (typeof v.name !== "string") return false;
|
|
826
|
+
if (typeof v.trustLevel !== "string") return false;
|
|
827
|
+
if (!TRUST_LEVELS.includes(v.trustLevel)) return false;
|
|
828
|
+
if (!Array.isArray(v.capabilities)) return false;
|
|
829
|
+
for (const c of v.capabilities) {
|
|
830
|
+
if (typeof c !== "string") return false;
|
|
831
|
+
}
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
function loadPackageManifest(packageName) {
|
|
835
|
+
const node = getNodeApi();
|
|
836
|
+
if (!node) return null;
|
|
837
|
+
let resolvedUrl;
|
|
838
|
+
try {
|
|
839
|
+
resolvedUrl = node.resolve(`${packageName}/package.json`);
|
|
840
|
+
} catch {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
let raw;
|
|
844
|
+
try {
|
|
845
|
+
raw = node.readFile(resolvedUrl);
|
|
846
|
+
} catch {
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
let pkg;
|
|
850
|
+
try {
|
|
851
|
+
pkg = JSON.parse(raw);
|
|
852
|
+
} catch {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
if (typeof pkg !== "object" || pkg === null || !("amplessPlugin" in pkg)) {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
const manifest = pkg.amplessPlugin;
|
|
859
|
+
if (!isValidManifest(manifest)) return null;
|
|
860
|
+
return manifest;
|
|
861
|
+
}
|
|
862
|
+
var SUPPORTED_API_VERSION = 1;
|
|
863
|
+
|
|
864
|
+
// src/plugin-head.ts
|
|
865
|
+
function isPlugin2(p) {
|
|
866
|
+
return typeof p === "object" && p !== null && "apiVersion" in p;
|
|
867
|
+
}
|
|
868
|
+
var ALLOWED_ATTRS = /* @__PURE__ */ new Set([
|
|
869
|
+
"crossorigin",
|
|
870
|
+
"referrerpolicy",
|
|
871
|
+
"integrity",
|
|
872
|
+
"fetchpriority",
|
|
873
|
+
// `nonce` is intentionally NOT in the allowlist for Phase 1. CSP
|
|
874
|
+
// nonce propagation is scoped out of Phase 1 (see spec §7); attrs
|
|
875
|
+
// shouldn't let plugins smuggle nonces past the design decision.
|
|
876
|
+
// The CSP nonce RFP will reintroduce it through `cspNonce` on
|
|
877
|
+
// PluginPublicRenderContext + `inlineScript.nonce: 'auto'`, not via
|
|
878
|
+
// the `attrs` bag.
|
|
879
|
+
"loading",
|
|
880
|
+
// iframe lazy-loading
|
|
881
|
+
"sandbox",
|
|
882
|
+
// iframe sandbox attribute
|
|
883
|
+
"allow",
|
|
884
|
+
// iframe permissions policy
|
|
885
|
+
"allowfullscreen"
|
|
886
|
+
// iframe fullscreen
|
|
887
|
+
]);
|
|
888
|
+
function isAllowedAttr(name) {
|
|
889
|
+
if (name.startsWith("data-")) return true;
|
|
890
|
+
return ALLOWED_ATTRS.has(name.toLowerCase());
|
|
891
|
+
}
|
|
892
|
+
function isSafeUrl(value) {
|
|
893
|
+
const trimmed = value.trim();
|
|
894
|
+
if (trimmed.length === 0) return false;
|
|
895
|
+
const schemeMatch = trimmed.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
|
|
896
|
+
if (!schemeMatch) return true;
|
|
897
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
898
|
+
return scheme === "http" || scheme === "https";
|
|
899
|
+
}
|
|
900
|
+
function isDev() {
|
|
901
|
+
const env = typeof process !== "undefined" && process.env ? process.env.NODE_ENV : void 0;
|
|
902
|
+
return env !== "production";
|
|
903
|
+
}
|
|
904
|
+
function warn(message) {
|
|
905
|
+
if (!isDev()) return;
|
|
906
|
+
console.warn(`[ampless plugin-head] ${message}`);
|
|
907
|
+
}
|
|
908
|
+
function crossCheckStaticManifest(plugin, label) {
|
|
909
|
+
const packageName = plugin.packageName;
|
|
910
|
+
const manifest = loadPackageManifest(packageName);
|
|
911
|
+
if (!manifest) return;
|
|
912
|
+
if (typeof manifest.apiVersion !== "number") {
|
|
913
|
+
throw new Error(
|
|
914
|
+
`${label}: package.json#amplessPlugin.apiVersion is not a number (got ${JSON.stringify(manifest.apiVersion)}). Update the plugin's package.json or remove the amplessPlugin field.`
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
if (manifest.apiVersion > SUPPORTED_API_VERSION) {
|
|
918
|
+
throw new Error(
|
|
919
|
+
`${label}: package.json#amplessPlugin.apiVersion ${manifest.apiVersion} is newer than this runtime supports (max ${SUPPORTED_API_VERSION}). Upgrade @ampless/runtime, or pin an older version of the plugin.`
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
if (manifest.apiVersion !== plugin.apiVersion) {
|
|
923
|
+
throw new Error(
|
|
924
|
+
`${label}: apiVersion mismatch \u2014 package.json declares ${manifest.apiVersion}, factory declares ${plugin.apiVersion}. The two must agree.`
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
if (manifest.name !== plugin.name) {
|
|
928
|
+
warn(
|
|
929
|
+
`${label}: name mismatch \u2014 package.json#amplessPlugin.name is "${manifest.name}", factory returns name="${plugin.name}". Align them so admin UI / capability gates can identify the plugin consistently.`
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
if (manifest.trustLevel !== plugin.trust_level) {
|
|
933
|
+
warn(
|
|
934
|
+
`${label}: trustLevel mismatch \u2014 package.json declares "${manifest.trustLevel}", factory declares trust_level="${plugin.trust_level}". The processor IAM policies are wired off trust_level; drift here usually means the deployment lambda runs in the wrong context.`
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
const factoryCaps = Array.isArray(plugin.capabilities) ? plugin.capabilities : [];
|
|
938
|
+
const manifestCaps = manifest.capabilities;
|
|
939
|
+
if (!setsEqual(factoryCaps, manifestCaps)) {
|
|
940
|
+
warn(
|
|
941
|
+
`${label}: capabilities mismatch \u2014 package.json declares [${manifestCaps.join(", ")}], factory declares [${factoryCaps.join(", ")}]. The static manifest is what npm registry / admin UI surfaces show before the plugin loads, so it should match what the factory actually returns.`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
function setsEqual(a, b) {
|
|
946
|
+
const sa = new Set(a);
|
|
947
|
+
const sb = new Set(b);
|
|
948
|
+
if (sa.size !== sb.size) return false;
|
|
949
|
+
for (const c of sb) if (!sa.has(c)) return false;
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
952
|
+
function applyAttrs(target, attrs, ownerLabel) {
|
|
953
|
+
if (!attrs) return;
|
|
954
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
955
|
+
if (!isAllowedAttr(k)) {
|
|
956
|
+
warn(
|
|
957
|
+
`${ownerLabel}: attr "${k}" not in allowlist (data-* / crossorigin / referrerpolicy / integrity / fetchpriority / loading / sandbox / allow / allowfullscreen). skipping.`
|
|
958
|
+
);
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
target[k] = v;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function escapeJsonLdInlineBody(value) {
|
|
965
|
+
return value.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
966
|
+
}
|
|
967
|
+
function inlineScriptTypeAllowed(surface, scriptType) {
|
|
968
|
+
if (scriptType === "application/ld+json") return true;
|
|
969
|
+
if (scriptType === void 0) {
|
|
970
|
+
return surface === "head" || surface === "body-end";
|
|
971
|
+
}
|
|
972
|
+
return false;
|
|
973
|
+
}
|
|
974
|
+
function renderInlineScript(descriptor, pluginLabel, index, surface) {
|
|
975
|
+
if (!descriptor.id) {
|
|
976
|
+
warn(
|
|
977
|
+
`${pluginLabel}: inlineScript descriptor #${index} dropped \u2014 missing required "id".`
|
|
978
|
+
);
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
if (!inlineScriptTypeAllowed(surface, descriptor.scriptType)) {
|
|
982
|
+
const got = descriptor.scriptType === void 0 ? "undefined" : `"${descriptor.scriptType}"`;
|
|
983
|
+
const allowed = surface === "body-for-post" ? `"application/ld+json" (required on publicBodyForPost \u2014 the per-post surface is scoped to JSON-LD)` : `undefined or "application/ld+json"`;
|
|
984
|
+
warn(
|
|
985
|
+
`${pluginLabel}: inlineScript "${descriptor.id}" dropped \u2014 scriptType ${got} not allowed on ${surface}. Allowed: ${allowed}.`
|
|
986
|
+
);
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
const body = descriptor.scriptType === "application/ld+json" ? escapeJsonLdInlineBody(descriptor.body) : descriptor.body;
|
|
990
|
+
const props = {
|
|
991
|
+
id: descriptor.id,
|
|
992
|
+
dangerouslySetInnerHTML: { __html: body }
|
|
993
|
+
};
|
|
994
|
+
if (descriptor.scriptType) props.type = descriptor.scriptType;
|
|
995
|
+
return {
|
|
996
|
+
id: descriptor.id,
|
|
997
|
+
element: createElement2("script", props)
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
function renderHeadDescriptor(descriptor, pluginLabel, index) {
|
|
1001
|
+
switch (descriptor.type) {
|
|
1002
|
+
case "script": {
|
|
1003
|
+
if (!isSafeUrl(descriptor.src)) {
|
|
1004
|
+
warn(
|
|
1005
|
+
`${pluginLabel}: script descriptor #${index} dropped \u2014 unsafe src "${descriptor.src}".`
|
|
1006
|
+
);
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
const props = {
|
|
1010
|
+
src: descriptor.src
|
|
1011
|
+
};
|
|
1012
|
+
if (descriptor.id) props.id = descriptor.id;
|
|
1013
|
+
const hasAsync = typeof descriptor.async === "boolean";
|
|
1014
|
+
const hasDefer = typeof descriptor.defer === "boolean";
|
|
1015
|
+
if (hasAsync) props.async = descriptor.async;
|
|
1016
|
+
if (hasDefer) props.defer = descriptor.defer;
|
|
1017
|
+
if (!hasAsync && !hasDefer) {
|
|
1018
|
+
if (descriptor.strategy === "lazyOnload") {
|
|
1019
|
+
props.defer = true;
|
|
1020
|
+
} else {
|
|
1021
|
+
props.async = true;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
applyAttrs(props, descriptor.attrs, `${pluginLabel} script#${descriptor.id ?? index}`);
|
|
1025
|
+
return {
|
|
1026
|
+
id: descriptor.id ?? null,
|
|
1027
|
+
element: createElement2("script", props)
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
case "inlineScript":
|
|
1031
|
+
return renderInlineScript(descriptor, pluginLabel, index, "head");
|
|
1032
|
+
case "meta": {
|
|
1033
|
+
const props = { content: descriptor.content };
|
|
1034
|
+
if (descriptor.name) props.name = descriptor.name;
|
|
1035
|
+
if (descriptor.property) props.property = descriptor.property;
|
|
1036
|
+
return {
|
|
1037
|
+
// meta has no id channel in the descriptor. Don't derive a
|
|
1038
|
+
// dedup id from name/property — multiple `<meta name=...>`
|
|
1039
|
+
// entries with the same name are legitimate (e.g. theme-color
|
|
1040
|
+
// media variants, and two plugins emitting overlapping names
|
|
1041
|
+
// is a real case the runtime shouldn't silently collapse).
|
|
1042
|
+
// Position-based React keys handle the stable-key requirement.
|
|
1043
|
+
id: null,
|
|
1044
|
+
element: createElement2("meta", props)
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
case "link": {
|
|
1048
|
+
if (!isSafeUrl(descriptor.href)) {
|
|
1049
|
+
warn(
|
|
1050
|
+
`${pluginLabel}: link descriptor #${index} dropped \u2014 unsafe href "${descriptor.href}".`
|
|
1051
|
+
);
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
const props = {
|
|
1055
|
+
rel: descriptor.rel,
|
|
1056
|
+
href: descriptor.href
|
|
1057
|
+
};
|
|
1058
|
+
if (descriptor.as) props.as = descriptor.as;
|
|
1059
|
+
if (descriptor.typeAttr) props.type = descriptor.typeAttr;
|
|
1060
|
+
return {
|
|
1061
|
+
id: null,
|
|
1062
|
+
element: createElement2("link", props)
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
case "noscript": {
|
|
1066
|
+
const props = {
|
|
1067
|
+
dangerouslySetInnerHTML: { __html: descriptor.html }
|
|
1068
|
+
};
|
|
1069
|
+
if (descriptor.id) props.id = descriptor.id;
|
|
1070
|
+
return {
|
|
1071
|
+
id: descriptor.id ?? null,
|
|
1072
|
+
element: createElement2("noscript", props)
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function renderBodyDescriptor(descriptor, pluginLabel, index) {
|
|
1078
|
+
if (descriptor.type === "iframe") {
|
|
1079
|
+
if (!isSafeUrl(descriptor.src)) {
|
|
1080
|
+
warn(
|
|
1081
|
+
`${pluginLabel}: iframe descriptor #${index} dropped \u2014 unsafe src "${descriptor.src}".`
|
|
1082
|
+
);
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
const props = {
|
|
1086
|
+
src: descriptor.src
|
|
1087
|
+
};
|
|
1088
|
+
if (descriptor.id) props.id = descriptor.id;
|
|
1089
|
+
if (descriptor.title) props.title = descriptor.title;
|
|
1090
|
+
if (typeof descriptor.width === "number") props.width = descriptor.width;
|
|
1091
|
+
if (typeof descriptor.height === "number") props.height = descriptor.height;
|
|
1092
|
+
applyAttrs(props, descriptor.attrs, `${pluginLabel} iframe#${descriptor.id ?? index}`);
|
|
1093
|
+
return {
|
|
1094
|
+
id: descriptor.id ?? null,
|
|
1095
|
+
element: createElement2("iframe", props)
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
if (descriptor.type === "inlineScript") {
|
|
1099
|
+
return renderInlineScript(descriptor, pluginLabel, index, "body-end");
|
|
1100
|
+
}
|
|
1101
|
+
return renderHeadDescriptor(descriptor, pluginLabel, index);
|
|
1102
|
+
}
|
|
1103
|
+
function renderPostBodyDescriptor(descriptor, pluginLabel, index) {
|
|
1104
|
+
if (descriptor.type !== "inlineScript") {
|
|
1105
|
+
warn(
|
|
1106
|
+
`${pluginLabel}: publicBodyForPost descriptor #${index} dropped \u2014 only inlineScript with scriptType "application/ld+json" is allowed on this surface.`
|
|
1107
|
+
);
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
return renderInlineScript(descriptor, pluginLabel, index, "body-for-post");
|
|
1111
|
+
}
|
|
1112
|
+
function dedupeAndKey(entries) {
|
|
1113
|
+
const lastIndexById = /* @__PURE__ */ new Map();
|
|
1114
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1115
|
+
const id = entries[i].id;
|
|
1116
|
+
if (id === null) continue;
|
|
1117
|
+
if (lastIndexById.has(id)) {
|
|
1118
|
+
warn(`duplicate descriptor id "${id}" \u2014 keeping the last occurrence.`);
|
|
1119
|
+
}
|
|
1120
|
+
lastIndexById.set(id, i);
|
|
1121
|
+
}
|
|
1122
|
+
const kept = [];
|
|
1123
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1124
|
+
const { id, element } = entries[i];
|
|
1125
|
+
if (id !== null && lastIndexById.get(id) !== i) continue;
|
|
1126
|
+
const key = id ?? `__pos-${i}`;
|
|
1127
|
+
const existingProps = element.props;
|
|
1128
|
+
kept.push(createElement2(element.type, { ...existingProps, key }));
|
|
1129
|
+
}
|
|
1130
|
+
return kept;
|
|
1131
|
+
}
|
|
1132
|
+
function makeCtx(plugin, site, snapshot) {
|
|
1133
|
+
const instanceId = plugin.instanceId ?? plugin.name;
|
|
1134
|
+
const stored = snapshot.get(instanceId) ?? {};
|
|
1135
|
+
const resolved = resolvePluginSettings(plugin.settings, stored);
|
|
1136
|
+
return {
|
|
1137
|
+
site,
|
|
1138
|
+
setting(key) {
|
|
1139
|
+
if (!isValidPluginKey(key)) return void 0;
|
|
1140
|
+
const v = resolved[key];
|
|
1141
|
+
return v === void 0 ? void 0 : v;
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
function collectFor(plugins, site, snapshot, surface, renderOne) {
|
|
1146
|
+
const entries = [];
|
|
1147
|
+
for (const plugin of plugins) {
|
|
1148
|
+
const factory = surface(plugin);
|
|
1149
|
+
if (!factory) continue;
|
|
1150
|
+
const ctx = makeCtx(plugin, site, snapshot);
|
|
1151
|
+
let descriptors;
|
|
1152
|
+
try {
|
|
1153
|
+
descriptors = factory.call(plugin, ctx) ?? [];
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
warn(
|
|
1156
|
+
`plugin "${plugin.instanceId ?? plugin.name}" threw inside descriptor callback: ${err instanceof Error ? err.message : String(err)}`
|
|
1157
|
+
);
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
const label = `plugin "${plugin.instanceId ?? plugin.name}"`;
|
|
1161
|
+
for (let i = 0; i < descriptors.length; i++) {
|
|
1162
|
+
const entry = renderOne(descriptors[i], label, i);
|
|
1163
|
+
if (entry) entries.push(entry);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (entries.length === 0) return null;
|
|
1167
|
+
const keyed = dedupeAndKey(entries);
|
|
1168
|
+
return createElement2(Fragment2, null, ...keyed);
|
|
1169
|
+
}
|
|
1170
|
+
var SANITIZE_OPTIONS = {
|
|
1171
|
+
allowedTags: ["p", "span", "strong", "em", "a", "code", "br", "ul", "ol", "li"],
|
|
1172
|
+
allowedAttributes: {
|
|
1173
|
+
"*": ["class", "data-words", "data-minutes", "data-ampless-*"],
|
|
1174
|
+
a: ["href", "rel", "target"]
|
|
1175
|
+
},
|
|
1176
|
+
allowedSchemes: ["http", "https"],
|
|
1177
|
+
allowedSchemesAppliedToAttributes: ["href"],
|
|
1178
|
+
allowProtocolRelative: false,
|
|
1179
|
+
transformTags: {
|
|
1180
|
+
a: (tagName, attribs) => {
|
|
1181
|
+
const out = { ...attribs };
|
|
1182
|
+
if (out["target"] === "_blank") {
|
|
1183
|
+
const parts = (out["rel"] ?? "").split(/\s+/).filter(Boolean);
|
|
1184
|
+
if (!parts.includes("noopener")) parts.push("noopener");
|
|
1185
|
+
if (!parts.includes("noreferrer")) parts.push("noreferrer");
|
|
1186
|
+
out["rel"] = parts.join(" ");
|
|
1187
|
+
}
|
|
1188
|
+
return { tagName, attribs: out };
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
function validateHtmlDescriptor(descriptor, pluginLabel, index) {
|
|
1193
|
+
if (descriptor === null || typeof descriptor !== "object") {
|
|
1194
|
+
warn(
|
|
1195
|
+
`${pluginLabel}: publicHtmlForPost descriptor #${index} dropped \u2014 must be an object.`
|
|
1196
|
+
);
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
const d = descriptor;
|
|
1200
|
+
if (d.type !== "html") {
|
|
1201
|
+
warn(
|
|
1202
|
+
`${pluginLabel}: publicHtmlForPost descriptor #${index} dropped \u2014 "type" must be "html" (got ${typeof d.type === "string" ? `"${d.type}"` : typeof d.type}).`
|
|
1203
|
+
);
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
if (typeof d.id !== "string") {
|
|
1207
|
+
warn(
|
|
1208
|
+
`${pluginLabel}: publicHtmlForPost descriptor #${index} dropped \u2014 "id" must be a string.`
|
|
1209
|
+
);
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
if (typeof d.body !== "string") {
|
|
1213
|
+
warn(
|
|
1214
|
+
`${pluginLabel}: publicHtmlForPost descriptor "${d.id}" dropped \u2014 "body" must be a string.`
|
|
1215
|
+
);
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
if (d.position !== "beforeContent" && d.position !== "afterContent") {
|
|
1219
|
+
warn(
|
|
1220
|
+
`${pluginLabel}: publicHtmlForPost descriptor "${d.id}" dropped \u2014 "position" must be "beforeContent" or "afterContent" (got ${typeof d.position === "string" ? `"${d.position}"` : typeof d.position}).`
|
|
1221
|
+
);
|
|
1222
|
+
return false;
|
|
1223
|
+
}
|
|
1224
|
+
return true;
|
|
1225
|
+
}
|
|
1226
|
+
function validateHtmlId(id, pluginLabel) {
|
|
1227
|
+
if (id.length === 0) {
|
|
1228
|
+
warn(`${pluginLabel}: publicHtmlForPost descriptor dropped \u2014 "id" must not be empty.`);
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
if (/[\x00-\x1f]/.test(id)) {
|
|
1232
|
+
warn(
|
|
1233
|
+
`${pluginLabel}: publicHtmlForPost descriptor "${id}" dropped \u2014 "id" contains control characters.`
|
|
1234
|
+
);
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
if (id.length > 64) {
|
|
1238
|
+
warn(
|
|
1239
|
+
`${pluginLabel}: publicHtmlForPost descriptor "${id.slice(0, 32)}\u2026" dropped \u2014 "id" exceeds 64 characters.`
|
|
1240
|
+
);
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
return true;
|
|
1244
|
+
}
|
|
1245
|
+
function collectForPost(plugins, site, snapshot, post) {
|
|
1246
|
+
const entries = [];
|
|
1247
|
+
for (const plugin of plugins) {
|
|
1248
|
+
const factory = plugin.publicBodyForPost;
|
|
1249
|
+
if (!factory) continue;
|
|
1250
|
+
const ctx = makeCtx(plugin, site, snapshot);
|
|
1251
|
+
let descriptors;
|
|
1252
|
+
try {
|
|
1253
|
+
descriptors = factory.call(plugin, post, ctx) ?? [];
|
|
1254
|
+
} catch (err) {
|
|
1255
|
+
warn(
|
|
1256
|
+
`plugin "${plugin.instanceId ?? plugin.name}" threw inside publicBodyForPost callback: ${err instanceof Error ? err.message : String(err)}`
|
|
872
1257
|
);
|
|
873
|
-
|
|
874
|
-
async renderBodyForPost(post) {
|
|
875
|
-
const snapshot = await pluginSettings.loadAll();
|
|
876
|
-
return collectForPost(validPlugins, cmsConfig.site, snapshot, post);
|
|
877
|
-
},
|
|
878
|
-
async renderHtmlForPost(post) {
|
|
879
|
-
const snapshot = await pluginSettings.loadAll();
|
|
880
|
-
return collectHtmlForPost(validPlugins, cmsConfig.site, snapshot, post);
|
|
1258
|
+
continue;
|
|
881
1259
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
import { unflattenSettings as unflattenSettings2 } from "ampless";
|
|
887
|
-
function createPluginSettings(storage) {
|
|
888
|
-
return {
|
|
889
|
-
async loadAll() {
|
|
890
|
-
const out = /* @__PURE__ */ new Map();
|
|
891
|
-
if (!storage.isStorageConfigured()) return out;
|
|
892
|
-
let url;
|
|
893
|
-
try {
|
|
894
|
-
url = storage.publicAssetUrl("public/site-settings.json");
|
|
895
|
-
} catch {
|
|
896
|
-
return out;
|
|
897
|
-
}
|
|
898
|
-
let flat;
|
|
899
|
-
try {
|
|
900
|
-
const res = await fetch(url, {
|
|
901
|
-
// Same revalidate + tag as site-settings.ts so a single
|
|
902
|
-
// request to the public route hits the JSON once and both
|
|
903
|
-
// helpers share the cached body.
|
|
904
|
-
next: { revalidate: 60, tags: ["site-settings"] }
|
|
905
|
-
});
|
|
906
|
-
if (!res.ok) return out;
|
|
907
|
-
flat = await res.json();
|
|
908
|
-
} catch {
|
|
909
|
-
return out;
|
|
910
|
-
}
|
|
911
|
-
const nested = unflattenSettings2(flat);
|
|
912
|
-
const pluginsBlock = nested.plugins;
|
|
913
|
-
if (!pluginsBlock || typeof pluginsBlock !== "object") return out;
|
|
914
|
-
for (const [instanceId, block] of Object.entries(pluginsBlock)) {
|
|
915
|
-
if (!block || typeof block !== "object" || Array.isArray(block)) continue;
|
|
916
|
-
out.set(instanceId, { ...block });
|
|
917
|
-
}
|
|
918
|
-
return out;
|
|
1260
|
+
const label = `plugin "${plugin.instanceId ?? plugin.name}"`;
|
|
1261
|
+
for (let i = 0; i < descriptors.length; i++) {
|
|
1262
|
+
const entry = renderPostBodyDescriptor(descriptors[i], label, i);
|
|
1263
|
+
if (entry) entries.push(entry);
|
|
919
1264
|
}
|
|
920
|
-
}
|
|
1265
|
+
}
|
|
1266
|
+
if (entries.length === 0) return null;
|
|
1267
|
+
const keyed = dedupeAndKey(entries);
|
|
1268
|
+
return createElement2(Fragment2, null, ...keyed);
|
|
921
1269
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1270
|
+
function collectHtmlForPost(plugins, site, snapshot, post) {
|
|
1271
|
+
const before = [];
|
|
1272
|
+
const after = [];
|
|
1273
|
+
const seenBefore = /* @__PURE__ */ new Set();
|
|
1274
|
+
const seenAfter = /* @__PURE__ */ new Set();
|
|
1275
|
+
for (const plugin of plugins) {
|
|
1276
|
+
const factory = plugin.publicHtmlForPost;
|
|
1277
|
+
if (!factory) continue;
|
|
1278
|
+
const namespace = plugin.instanceId ?? plugin.name;
|
|
1279
|
+
const label = `plugin "${plugin.instanceId ?? plugin.name}"`;
|
|
1280
|
+
const ctx = makeCtx(plugin, site, snapshot);
|
|
1281
|
+
let descriptors;
|
|
928
1282
|
try {
|
|
929
|
-
|
|
930
|
-
} catch {
|
|
931
|
-
|
|
1283
|
+
descriptors = factory.call(plugin, post, ctx) ?? [];
|
|
1284
|
+
} catch (err) {
|
|
1285
|
+
warn(
|
|
1286
|
+
`${label}: threw inside publicHtmlForPost callback: ${err instanceof Error ? err.message : String(err)}`
|
|
1287
|
+
);
|
|
1288
|
+
continue;
|
|
932
1289
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
async function fetchActiveFresh() {
|
|
946
|
-
const url = settingsUrl();
|
|
947
|
-
if (!url) return null;
|
|
948
|
-
const res = await fetch(url, { cache: "no-store" });
|
|
949
|
-
if (!res.ok) return null;
|
|
950
|
-
const flat = await res.json();
|
|
951
|
-
const v = flat["theme.active"];
|
|
952
|
-
return typeof v === "string" ? v : null;
|
|
953
|
-
}
|
|
954
|
-
return {
|
|
955
|
-
readStoredActiveThemeFresh: () => fetchActiveFresh(),
|
|
956
|
-
async resolveActiveTheme() {
|
|
957
|
-
let previewOverride = null;
|
|
958
|
-
try {
|
|
959
|
-
const h = await headers();
|
|
960
|
-
previewOverride = h.get("x-preview-theme");
|
|
961
|
-
} catch {
|
|
962
|
-
}
|
|
963
|
-
if (previewOverride && previewOverride in registry.themes) {
|
|
964
|
-
const mod2 = registry.themes[previewOverride];
|
|
965
|
-
if (mod2) return { name: previewOverride, module: mod2 };
|
|
966
|
-
}
|
|
967
|
-
const stored = await fetchActiveFromCache().catch(() => null);
|
|
968
|
-
const name = stored && stored in registry.themes ? stored : registry.defaultTheme;
|
|
969
|
-
const mod = registry.themes[name] ?? registry.themes[registry.defaultTheme];
|
|
970
|
-
if (!mod) {
|
|
971
|
-
throw new Error(
|
|
972
|
-
`themes registry is empty \u2014 at least one theme must be registered before resolveActiveTheme is called`
|
|
1290
|
+
for (let i = 0; i < descriptors.length; i++) {
|
|
1291
|
+
const raw = descriptors[i];
|
|
1292
|
+
if (!validateHtmlDescriptor(raw, label, i)) continue;
|
|
1293
|
+
const descriptor = raw;
|
|
1294
|
+
if (!validateHtmlId(descriptor.id, label)) continue;
|
|
1295
|
+
const dedupeKey = `${namespace}:${descriptor.id}`;
|
|
1296
|
+
const position = descriptor.position;
|
|
1297
|
+
const seenSet = position === "beforeContent" ? seenBefore : seenAfter;
|
|
1298
|
+
const bucket = position === "beforeContent" ? before : after;
|
|
1299
|
+
if (seenSet.has(dedupeKey)) {
|
|
1300
|
+
warn(
|
|
1301
|
+
`${label}: publicHtmlForPost descriptor "${descriptor.id}" at position "${position}" is a duplicate \u2014 keeping the first occurrence.`
|
|
973
1302
|
);
|
|
1303
|
+
continue;
|
|
974
1304
|
}
|
|
975
|
-
|
|
1305
|
+
seenSet.add(dedupeKey);
|
|
1306
|
+
const cleanHtml = sanitizeHtml(descriptor.body, SANITIZE_OPTIONS);
|
|
1307
|
+
bucket.push({ key: dedupeKey, cleanHtml, namespace });
|
|
976
1308
|
}
|
|
1309
|
+
}
|
|
1310
|
+
function toReactNode(entries, position) {
|
|
1311
|
+
if (entries.length === 0) return null;
|
|
1312
|
+
const elements = entries.map(
|
|
1313
|
+
({ key, cleanHtml, namespace }) => createElement2("div", {
|
|
1314
|
+
key,
|
|
1315
|
+
"data-ampless-plugin": namespace,
|
|
1316
|
+
"data-ampless-position": position,
|
|
1317
|
+
dangerouslySetInnerHTML: { __html: cleanHtml }
|
|
1318
|
+
})
|
|
1319
|
+
);
|
|
1320
|
+
return createElement2(Fragment2, null, ...elements);
|
|
1321
|
+
}
|
|
1322
|
+
return {
|
|
1323
|
+
beforeContent: toReactNode(before, "beforeContent"),
|
|
1324
|
+
afterContent: toReactNode(after, "afterContent")
|
|
977
1325
|
};
|
|
978
1326
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
function createThemeConfig(themeActive, storage) {
|
|
992
|
-
async function fetchRemote() {
|
|
993
|
-
if (!storage.isStorageConfigured()) return null;
|
|
994
|
-
let url;
|
|
995
|
-
try {
|
|
996
|
-
url = storage.publicAssetUrl("public/site-settings.json");
|
|
997
|
-
} catch {
|
|
998
|
-
return null;
|
|
1327
|
+
function createPluginHead(cmsConfig, pluginSettings) {
|
|
1328
|
+
const plugins = (cmsConfig.plugins ?? []).filter(isPlugin2);
|
|
1329
|
+
const validPlugins = [];
|
|
1330
|
+
const seenNamespaces = /* @__PURE__ */ new Set();
|
|
1331
|
+
for (const plugin of plugins) {
|
|
1332
|
+
const ns = plugin.instanceId ?? plugin.name;
|
|
1333
|
+
const label = plugin.instanceId ? `${plugin.name}#${plugin.instanceId}` : plugin.name;
|
|
1334
|
+
if (!isValidPluginKey(ns)) {
|
|
1335
|
+
warn(
|
|
1336
|
+
`${label}: plugin namespace "${ns}" violates ${`/^[a-zA-Z0-9_-]+$/`}. Use a simple identifier (letters / digits / "-" / "_"). Skipping plugin.`
|
|
1337
|
+
);
|
|
1338
|
+
continue;
|
|
999
1339
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
if (flat) {
|
|
1013
|
-
for (const field of manifest.fields) {
|
|
1014
|
-
const k = themeSettingKey(field.key);
|
|
1015
|
-
if (k in flat) stored[k] = flat[k];
|
|
1340
|
+
if (seenNamespaces.has(ns)) {
|
|
1341
|
+
warn(
|
|
1342
|
+
`duplicate plugin namespace "${ns}" detected in cms.config.plugins. Set distinct \`instanceId\` on each instance to disambiguate.`
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
seenNamespaces.add(ns);
|
|
1346
|
+
if (plugin.settings?.public) {
|
|
1347
|
+
for (const field of plugin.settings.public) {
|
|
1348
|
+
if (!isValidPluginKey(field.key)) {
|
|
1349
|
+
warn(
|
|
1350
|
+
`${label}: settings.public field key "${field.key}" violates ${`/^[a-zA-Z0-9_-]+$/`}. The field will not be readable through ctx.setting(). Rename the field key.`
|
|
1351
|
+
);
|
|
1016
1352
|
}
|
|
1017
1353
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
else if (mark.type === "highlight") html = `<mark>${html}</mark>`;
|
|
1065
|
-
else if (mark.type === "link") {
|
|
1066
|
-
const href = escape(String(mark.attrs?.href ?? "#"));
|
|
1067
|
-
html = `<a href="${href}" target="_blank" rel="noopener">${html}</a>`;
|
|
1354
|
+
}
|
|
1355
|
+
validPlugins.push(plugin);
|
|
1356
|
+
if (plugin.packageName) {
|
|
1357
|
+
crossCheckStaticManifest(plugin, label);
|
|
1358
|
+
}
|
|
1359
|
+
const caps = plugin.capabilities;
|
|
1360
|
+
if (caps) {
|
|
1361
|
+
if (caps.includes("publicHead") && !plugin.publicHead) {
|
|
1362
|
+
warn(
|
|
1363
|
+
`${label}: declares capability "publicHead" but no \`publicHead\` implementation. Drop the capability or add the function.`
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
if (caps.includes("publicBody") && !plugin.publicBodyEnd) {
|
|
1367
|
+
warn(
|
|
1368
|
+
`${label}: declares capability "publicBody" but no \`publicBodyEnd\` implementation. Drop the capability or add the function.`
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
if (plugin.publicHead && !caps.includes("publicHead")) {
|
|
1372
|
+
warn(
|
|
1373
|
+
`${label}: implements \`publicHead\` but "publicHead" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
if (plugin.publicBodyEnd && !caps.includes("publicBody")) {
|
|
1377
|
+
warn(
|
|
1378
|
+
`${label}: implements \`publicBodyEnd\` but "publicBody" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
if (caps.includes("schema") && !plugin.publicBodyForPost) {
|
|
1382
|
+
warn(
|
|
1383
|
+
`${label}: declares capability "schema" but no \`publicBodyForPost\` implementation. Drop the capability or add the function.`
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
if (plugin.publicBodyForPost && !caps.includes("schema")) {
|
|
1387
|
+
warn(
|
|
1388
|
+
`${label}: implements \`publicBodyForPost\` but "schema" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
if (caps.includes("publicHtmlForPost") && !plugin.publicHtmlForPost) {
|
|
1392
|
+
warn(
|
|
1393
|
+
`${label}: declares capability "publicHtmlForPost" but no \`publicHtmlForPost\` implementation. Drop the capability or add the function.`
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
if (plugin.publicHtmlForPost && !caps.includes("publicHtmlForPost")) {
|
|
1397
|
+
warn(
|
|
1398
|
+
`${label}: implements \`publicHtmlForPost\` but "publicHtmlForPost" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
1399
|
+
);
|
|
1068
1400
|
}
|
|
1069
1401
|
}
|
|
1070
|
-
return html;
|
|
1071
1402
|
}
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1403
|
+
const contentFieldsRegistry = buildContentFieldRegistry(validPlugins);
|
|
1404
|
+
for (const plugin of validPlugins) {
|
|
1405
|
+
const caps = plugin.capabilities;
|
|
1406
|
+
if (!caps) continue;
|
|
1407
|
+
const label = plugin.instanceId ? `${plugin.name}#${plugin.instanceId}` : plugin.name;
|
|
1408
|
+
if (caps.includes("contentFields") && !plugin.contentFields) {
|
|
1409
|
+
warn(
|
|
1410
|
+
`${label}: declares capability "contentFields" but no \`contentFields\` array. Drop the capability or add the renderers.`
|
|
1411
|
+
);
|
|
1081
1412
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
case "listItem":
|
|
1087
|
-
return `<li>${children}</li>`;
|
|
1088
|
-
case "codeBlock": {
|
|
1089
|
-
const lang = node.attrs?.language ? ` class="language-${escape(String(node.attrs.language))}"` : "";
|
|
1090
|
-
return `<pre><code${lang}>${children}</code></pre>`;
|
|
1413
|
+
if (plugin.contentFields && !caps.includes("contentFields")) {
|
|
1414
|
+
warn(
|
|
1415
|
+
`${label}: implements \`contentFields\` but "contentFields" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
1416
|
+
);
|
|
1091
1417
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
case "horizontalRule":
|
|
1097
|
-
return "<hr />";
|
|
1098
|
-
case "image": {
|
|
1099
|
-
const src = escape(String(node.attrs?.src ?? ""));
|
|
1100
|
-
const alt = escape(String(node.attrs?.alt ?? ""));
|
|
1101
|
-
const title = node.attrs?.title ? ` title="${escape(String(node.attrs.title))}"` : "";
|
|
1102
|
-
const display = node.attrs?.display ? ` data-display="${escape(String(node.attrs.display))}"` : "";
|
|
1103
|
-
return `<img src="${src}" alt="${alt}"${title}${display} loading="lazy" />`;
|
|
1418
|
+
if (caps.includes("publicPostScript") && !plugin.publicPostScript) {
|
|
1419
|
+
warn(
|
|
1420
|
+
`${label}: declares capability "publicPostScript" but no \`publicPostScript\` implementation. Drop the capability or add the function.`
|
|
1421
|
+
);
|
|
1104
1422
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
case "tableHeader":
|
|
1110
|
-
return `<th${tableCellAttrs(node.attrs)}>${children}</th>`;
|
|
1111
|
-
case "tableCell":
|
|
1112
|
-
return `<td${tableCellAttrs(node.attrs)}>${children}</td>`;
|
|
1113
|
-
case "taskList":
|
|
1114
|
-
return `<ul data-type="taskList">${children}</ul>`;
|
|
1115
|
-
case "taskItem": {
|
|
1116
|
-
const checked = node.attrs?.checked === true ? "true" : "false";
|
|
1117
|
-
return `<li data-type="taskItem" data-checked="${checked}">${children}</li>`;
|
|
1423
|
+
if (plugin.publicPostScript && !caps.includes("publicPostScript")) {
|
|
1424
|
+
warn(
|
|
1425
|
+
`${label}: implements \`publicPostScript\` but "publicPostScript" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
1426
|
+
);
|
|
1118
1427
|
}
|
|
1119
|
-
default:
|
|
1120
|
-
return children;
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
function tableCellAttrs(attrs) {
|
|
1124
|
-
let out = "";
|
|
1125
|
-
const colspan = Number(attrs?.colspan ?? 1);
|
|
1126
|
-
if (colspan > 1) out += ` colspan="${colspan}"`;
|
|
1127
|
-
const rowspan = Number(attrs?.rowspan ?? 1);
|
|
1128
|
-
if (rowspan > 1) out += ` rowspan="${rowspan}"`;
|
|
1129
|
-
const colwidth = attrs?.colwidth;
|
|
1130
|
-
if (Array.isArray(colwidth) && colwidth.length > 0) {
|
|
1131
|
-
const w = Number(colwidth[0]);
|
|
1132
|
-
if (Number.isFinite(w) && w > 0) out += ` style="width: ${w}px"`;
|
|
1133
|
-
}
|
|
1134
|
-
return out;
|
|
1135
|
-
}
|
|
1136
|
-
function renderMarkdown(md) {
|
|
1137
|
-
return marked.parse(md, { gfm: true, breaks: false, async: false });
|
|
1138
|
-
}
|
|
1139
|
-
function renderBody(post) {
|
|
1140
|
-
if (post.format === "html") return String(post.body);
|
|
1141
|
-
if (post.format === "markdown") return renderMarkdown(String(post.body));
|
|
1142
|
-
if (post.format === "tiptap") {
|
|
1143
|
-
if (typeof post.body === "string") return post.body;
|
|
1144
|
-
return renderTiptap(post.body);
|
|
1145
1428
|
}
|
|
1146
|
-
return
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1429
|
+
return {
|
|
1430
|
+
async renderHead() {
|
|
1431
|
+
const snapshot = await pluginSettings.loadAll();
|
|
1432
|
+
return collectFor(
|
|
1433
|
+
validPlugins,
|
|
1434
|
+
cmsConfig.site,
|
|
1435
|
+
snapshot,
|
|
1436
|
+
(p) => p.publicHead,
|
|
1437
|
+
renderHeadDescriptor
|
|
1438
|
+
);
|
|
1439
|
+
},
|
|
1440
|
+
async renderBodyEnd() {
|
|
1441
|
+
const snapshot = await pluginSettings.loadAll();
|
|
1442
|
+
return collectFor(
|
|
1443
|
+
validPlugins,
|
|
1444
|
+
cmsConfig.site,
|
|
1445
|
+
snapshot,
|
|
1446
|
+
(p) => p.publicBodyEnd,
|
|
1447
|
+
renderBodyDescriptor
|
|
1448
|
+
);
|
|
1449
|
+
},
|
|
1450
|
+
async renderBodyForPost(post) {
|
|
1451
|
+
const snapshot = await pluginSettings.loadAll();
|
|
1452
|
+
return collectForPost(validPlugins, cmsConfig.site, snapshot, post);
|
|
1453
|
+
},
|
|
1454
|
+
async renderHtmlForPost(post) {
|
|
1455
|
+
const snapshot = await pluginSettings.loadAll();
|
|
1456
|
+
return collectHtmlForPost(validPlugins, cmsConfig.site, snapshot, post);
|
|
1457
|
+
},
|
|
1458
|
+
async renderPostScriptsForPage(posts) {
|
|
1459
|
+
const snapshot = await pluginSettings.loadAll();
|
|
1460
|
+
return collectPostScriptsForPage(validPlugins, cmsConfig.site, snapshot, posts);
|
|
1461
|
+
},
|
|
1462
|
+
contentFieldsRegistry,
|
|
1463
|
+
async contextForPlugins() {
|
|
1464
|
+
const snapshot = await pluginSettings.loadAll();
|
|
1465
|
+
return (plugin) => makeCtx(plugin, cmsConfig.site, snapshot);
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1159
1468
|
}
|
|
1160
|
-
function
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1469
|
+
function collectPostScriptsForPage(plugins, site, snapshot, posts) {
|
|
1470
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1471
|
+
const elements = [];
|
|
1472
|
+
for (const post of posts) {
|
|
1473
|
+
for (const plugin of plugins) {
|
|
1474
|
+
const factory = plugin.publicPostScript;
|
|
1475
|
+
if (!factory) continue;
|
|
1476
|
+
const ctx = makeCtx(plugin, site, snapshot);
|
|
1477
|
+
const label = `plugin "${plugin.instanceId ?? plugin.name}"`;
|
|
1478
|
+
let descriptors;
|
|
1479
|
+
try {
|
|
1480
|
+
descriptors = factory.call(plugin, post, ctx) ?? [];
|
|
1481
|
+
} catch (err) {
|
|
1482
|
+
warn(
|
|
1483
|
+
`${label}: threw inside publicPostScript callback: ${err instanceof Error ? err.message : String(err)}`
|
|
1484
|
+
);
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
for (let i = 0; i < descriptors.length; i++) {
|
|
1488
|
+
const d = descriptors[i];
|
|
1489
|
+
if (!d || typeof d !== "object") {
|
|
1490
|
+
warn(`${label}: publicPostScript descriptor #${i} dropped \u2014 not an object.`);
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
if (typeof d.id !== "string" || d.id.length === 0) {
|
|
1494
|
+
warn(
|
|
1495
|
+
`${label}: publicPostScript descriptor #${i} dropped \u2014 "id" must be a non-empty string.`
|
|
1496
|
+
);
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
if (typeof d.src !== "string" || !isSafeUrl(d.src)) {
|
|
1500
|
+
warn(
|
|
1501
|
+
`${label}: publicPostScript descriptor "${d.id}" dropped \u2014 unsafe / missing src.`
|
|
1502
|
+
);
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
if (seen.has(d.id)) continue;
|
|
1506
|
+
seen.add(d.id);
|
|
1507
|
+
const props = { key: d.id, src: d.src };
|
|
1508
|
+
const hasAsync = typeof d.async === "boolean";
|
|
1509
|
+
const hasDefer = typeof d.defer === "boolean";
|
|
1510
|
+
if (hasAsync) props.async = d.async;
|
|
1511
|
+
if (hasDefer) props.defer = d.defer;
|
|
1512
|
+
if (!hasAsync && !hasDefer) {
|
|
1513
|
+
props.async = true;
|
|
1514
|
+
props.defer = true;
|
|
1515
|
+
}
|
|
1516
|
+
elements.push(createElement2("script", props));
|
|
1517
|
+
}
|
|
1171
1518
|
}
|
|
1172
|
-
return txt;
|
|
1173
1519
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1520
|
+
if (elements.length === 0) return null;
|
|
1521
|
+
return createElement2(Fragment2, null, ...elements);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// src/plugin-settings.ts
|
|
1525
|
+
import { unflattenSettings as unflattenSettings2 } from "ampless";
|
|
1526
|
+
function createPluginSettings(storage) {
|
|
1527
|
+
return {
|
|
1528
|
+
async loadAll() {
|
|
1529
|
+
const out = /* @__PURE__ */ new Map();
|
|
1530
|
+
if (!storage.isStorageConfigured()) return out;
|
|
1531
|
+
let url;
|
|
1532
|
+
try {
|
|
1533
|
+
url = storage.publicAssetUrl("public/site-settings.json");
|
|
1534
|
+
} catch {
|
|
1535
|
+
return out;
|
|
1536
|
+
}
|
|
1537
|
+
let flat;
|
|
1538
|
+
try {
|
|
1539
|
+
const res = await fetch(url, {
|
|
1540
|
+
// Same revalidate + tag as site-settings.ts so a single
|
|
1541
|
+
// request to the public route hits the JSON once and both
|
|
1542
|
+
// helpers share the cached body.
|
|
1543
|
+
next: { revalidate: 60, tags: ["site-settings"] }
|
|
1544
|
+
});
|
|
1545
|
+
if (!res.ok) return out;
|
|
1546
|
+
flat = await res.json();
|
|
1547
|
+
} catch {
|
|
1548
|
+
return out;
|
|
1549
|
+
}
|
|
1550
|
+
const nested = unflattenSettings2(flat);
|
|
1551
|
+
const pluginsBlock = nested.plugins;
|
|
1552
|
+
if (!pluginsBlock || typeof pluginsBlock !== "object") return out;
|
|
1553
|
+
for (const [instanceId, block] of Object.entries(pluginsBlock)) {
|
|
1554
|
+
if (!block || typeof block !== "object" || Array.isArray(block)) continue;
|
|
1555
|
+
out.set(instanceId, { ...block });
|
|
1556
|
+
}
|
|
1557
|
+
return out;
|
|
1206
1558
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
return
|
|
1217
|
-
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// src/theme-active.ts
|
|
1563
|
+
import { headers } from "next/headers";
|
|
1564
|
+
function createThemeActive(registry, storage) {
|
|
1565
|
+
function settingsUrl() {
|
|
1566
|
+
if (!storage.isStorageConfigured()) return null;
|
|
1567
|
+
try {
|
|
1568
|
+
return storage.publicAssetUrl("public/site-settings.json");
|
|
1569
|
+
} catch {
|
|
1570
|
+
return null;
|
|
1218
1571
|
}
|
|
1219
|
-
default:
|
|
1220
|
-
return children;
|
|
1221
1572
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
let headerIdx = -1;
|
|
1228
|
-
for (let i = 0; i < rows.length; i++) {
|
|
1229
|
-
const row = rows[i];
|
|
1230
|
-
const cells = row.content ?? [];
|
|
1231
|
-
const cellTexts = cells.map((c) => {
|
|
1232
|
-
const inner = (c.content ?? []).map(tiptapNodeToMarkdown).join("");
|
|
1233
|
-
return inner.replace(/\n+$/, "").replace(/\n/g, "<br>").replace(/\|/g, "\\|");
|
|
1573
|
+
async function fetchActiveFromCache() {
|
|
1574
|
+
const url = settingsUrl();
|
|
1575
|
+
if (!url) return null;
|
|
1576
|
+
const res = await fetch(url, {
|
|
1577
|
+
next: { revalidate: 60, tags: ["site-settings"] }
|
|
1234
1578
|
});
|
|
1235
|
-
|
|
1236
|
-
|
|
1579
|
+
if (!res.ok) return null;
|
|
1580
|
+
const flat = await res.json();
|
|
1581
|
+
const v = flat["theme.active"];
|
|
1582
|
+
return typeof v === "string" ? v : null;
|
|
1237
1583
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1584
|
+
async function fetchActiveFresh() {
|
|
1585
|
+
const url = settingsUrl();
|
|
1586
|
+
if (!url) return null;
|
|
1587
|
+
const res = await fetch(url, { cache: "no-store" });
|
|
1588
|
+
if (!res.ok) return null;
|
|
1589
|
+
const flat = await res.json();
|
|
1590
|
+
const v = flat["theme.active"];
|
|
1591
|
+
return typeof v === "string" ? v : null;
|
|
1592
|
+
}
|
|
1593
|
+
return {
|
|
1594
|
+
readStoredActiveThemeFresh: () => fetchActiveFresh(),
|
|
1595
|
+
async resolveActiveTheme() {
|
|
1596
|
+
let previewOverride = null;
|
|
1597
|
+
try {
|
|
1598
|
+
const h = await headers();
|
|
1599
|
+
previewOverride = h.get("x-preview-theme");
|
|
1600
|
+
} catch {
|
|
1601
|
+
}
|
|
1602
|
+
if (previewOverride && previewOverride in registry.themes) {
|
|
1603
|
+
const mod2 = registry.themes[previewOverride];
|
|
1604
|
+
if (mod2) return { name: previewOverride, module: mod2 };
|
|
1605
|
+
}
|
|
1606
|
+
const stored = await fetchActiveFromCache().catch(() => null);
|
|
1607
|
+
const name = stored && stored in registry.themes ? stored : registry.defaultTheme;
|
|
1608
|
+
const mod = registry.themes[name] ?? registry.themes[registry.defaultTheme];
|
|
1609
|
+
if (!mod) {
|
|
1610
|
+
throw new Error(
|
|
1611
|
+
`themes registry is empty \u2014 at least one theme must be registered before resolveActiveTheme is called`
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
return { name, module: mod };
|
|
1262
1615
|
}
|
|
1263
|
-
|
|
1264
|
-
md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
|
|
1265
|
-
return "\n" + String(content).trim().split("\n").map((l) => "> " + l).join("\n") + "\n\n";
|
|
1266
|
-
});
|
|
1267
|
-
md = md.replace(/<ul[^>]*data-type="taskList"[^>]*>([\s\S]*?)<\/ul>/gi, (_, items) => {
|
|
1268
|
-
return "\n" + convertHtmlTaskList(String(items)) + "\n";
|
|
1269
|
-
});
|
|
1270
|
-
md = md.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, (_, items) => {
|
|
1271
|
-
return "\n" + String(items).replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "- $1\n") + "\n";
|
|
1272
|
-
});
|
|
1273
|
-
md = md.replace(/<ol[^>]*>([\s\S]*?)<\/ol>/gi, (_, items) => {
|
|
1274
|
-
let i = 1;
|
|
1275
|
-
return "\n" + String(items).replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, () => `${i++}. $1
|
|
1276
|
-
`) + "\n";
|
|
1277
|
-
});
|
|
1278
|
-
md = md.replace(/<hr\s*\/?>/gi, "\n---\n\n");
|
|
1279
|
-
md = md.replace(/<br\s*\/?>/gi, " \n");
|
|
1280
|
-
md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, "$1\n\n");
|
|
1281
|
-
md = md.replace(/<img[^>]*?src="([^"]*)"[^>]*?alt="([^"]*)"[^>]*?\/?>/gi, "");
|
|
1282
|
-
md = md.replace(/<img[^>]*?alt="([^"]*)"[^>]*?src="([^"]*)"[^>]*?\/?>/gi, "");
|
|
1283
|
-
md = md.replace(/<img[^>]*?src="([^"]*)"[^>]*?\/?>/gi, "");
|
|
1284
|
-
md = md.replace(/<a[^>]*?href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)");
|
|
1285
|
-
md = md.replace(/<(strong|b)>([\s\S]*?)<\/\1>/gi, "**$2**");
|
|
1286
|
-
md = md.replace(/<(em|i)>([\s\S]*?)<\/\1>/gi, "*$2*");
|
|
1287
|
-
md = md.replace(/<s>([\s\S]*?)<\/s>/gi, "~~$1~~");
|
|
1288
|
-
md = md.replace(/<code>([\s\S]*?)<\/code>/gi, "`$1`");
|
|
1289
|
-
const PH_U_OPEN = "AMP_U_OPEN";
|
|
1290
|
-
const PH_U_CLOSE = "AMP_U_CLOSE";
|
|
1291
|
-
const PH_MARK_OPEN = "AMP_MARK_OPEN";
|
|
1292
|
-
const PH_MARK_CLOSE = "AMP_MARK_CLOSE";
|
|
1293
|
-
md = md.replace(/<u>([\s\S]*?)<\/u>/gi, `${PH_U_OPEN}$1${PH_U_CLOSE}`);
|
|
1294
|
-
md = md.replace(/<mark>([\s\S]*?)<\/mark>/gi, `${PH_MARK_OPEN}$1${PH_MARK_CLOSE}`);
|
|
1295
|
-
md = md.replace(/<\/?[^>]+>/g, "");
|
|
1296
|
-
md = md.split(PH_U_OPEN).join("<u>").split(PH_U_CLOSE).join("</u>").split(PH_MARK_OPEN).join("<mark>").split(PH_MARK_CLOSE).join("</mark>");
|
|
1297
|
-
md = md.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ");
|
|
1298
|
-
md = md.replace(/\n{3,}/g, "\n\n");
|
|
1299
|
-
return md.trim() + "\n";
|
|
1616
|
+
};
|
|
1300
1617
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1618
|
+
|
|
1619
|
+
// src/theme-config.ts
|
|
1620
|
+
import {
|
|
1621
|
+
resolveThemeValues,
|
|
1622
|
+
themeSettingKey
|
|
1623
|
+
} from "ampless";
|
|
1624
|
+
var DEFAULT_COLOR_SCHEME = "auto";
|
|
1625
|
+
var COLOR_SCHEME_SETTING_KEY = "theme.colorScheme";
|
|
1626
|
+
function validateColorScheme(raw) {
|
|
1627
|
+
if (raw === "light" || raw === "dark" || raw === "auto") return raw;
|
|
1628
|
+
return DEFAULT_COLOR_SCHEME;
|
|
1629
|
+
}
|
|
1630
|
+
function createThemeConfig(themeActive, storage) {
|
|
1631
|
+
async function fetchRemote() {
|
|
1632
|
+
if (!storage.isStorageConfigured()) return null;
|
|
1633
|
+
let url;
|
|
1634
|
+
try {
|
|
1635
|
+
url = storage.publicAssetUrl("public/site-settings.json");
|
|
1636
|
+
} catch {
|
|
1637
|
+
return null;
|
|
1315
1638
|
}
|
|
1316
|
-
|
|
1639
|
+
const res = await fetch(url, { next: { revalidate: 60, tags: ["site-settings"] } });
|
|
1640
|
+
if (!res.ok) return null;
|
|
1641
|
+
return await res.json();
|
|
1317
1642
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1643
|
+
return {
|
|
1644
|
+
async loadThemeConfig() {
|
|
1645
|
+
const [active, flat] = await Promise.all([
|
|
1646
|
+
themeActive.resolveActiveTheme(),
|
|
1647
|
+
fetchRemote().catch(() => null)
|
|
1648
|
+
]);
|
|
1649
|
+
const manifest = active.module.manifest;
|
|
1650
|
+
const stored = {};
|
|
1651
|
+
if (flat) {
|
|
1652
|
+
for (const field of manifest.fields) {
|
|
1653
|
+
const k = themeSettingKey(field.key);
|
|
1654
|
+
if (k in flat) stored[k] = flat[k];
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
const values = resolveThemeValues(manifest, stored);
|
|
1658
|
+
const cssVars = collectCssVars(manifest.fields, values);
|
|
1659
|
+
const colorScheme = validateColorScheme(flat?.[COLOR_SCHEME_SETTING_KEY]);
|
|
1660
|
+
return { activeTheme: active.name, manifest, values, cssVars, colorScheme };
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1334
1663
|
}
|
|
1335
|
-
function
|
|
1336
|
-
const
|
|
1337
|
-
const
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
out.push(`- [${checked}] ${inner}`);
|
|
1664
|
+
function collectCssVars(fields, values) {
|
|
1665
|
+
const out = {};
|
|
1666
|
+
for (const field of fields) {
|
|
1667
|
+
if (field.type === "linkList") continue;
|
|
1668
|
+
if (!field.cssVar) continue;
|
|
1669
|
+
const v = values[field.key];
|
|
1670
|
+
if (v) out[field.cssVar] = v;
|
|
1343
1671
|
}
|
|
1344
|
-
return out
|
|
1672
|
+
return out;
|
|
1673
|
+
}
|
|
1674
|
+
function renderThemeCss(cssVars) {
|
|
1675
|
+
const lines = Object.entries(cssVars).map(([name, value]) => ` ${name}: ${value};`);
|
|
1676
|
+
if (lines.length === 0) return "";
|
|
1677
|
+
return `:root {
|
|
1678
|
+
${lines.join("\n")}
|
|
1679
|
+
}`;
|
|
1345
1680
|
}
|
|
1346
1681
|
|
|
1347
1682
|
// src/index.ts
|
|
@@ -1371,7 +1706,15 @@ function createAmpless(opts) {
|
|
|
1371
1706
|
publicBodyEnd: () => pluginHead.renderBodyEnd(),
|
|
1372
1707
|
publicBodyForPost: (post) => pluginHead.renderBodyForPost(post),
|
|
1373
1708
|
publicHtmlForPost: (post) => pluginHead.renderHtmlForPost(post),
|
|
1374
|
-
|
|
1709
|
+
publicPostScriptsForPage: (posts2) => pluginHead.renderPostScriptsForPage(posts2),
|
|
1710
|
+
renderBody: async (post) => {
|
|
1711
|
+
const ctxForPlugin = await pluginHead.contextForPlugins();
|
|
1712
|
+
return renderBody(post, {
|
|
1713
|
+
contentFields: pluginHead.contentFieldsRegistry,
|
|
1714
|
+
ctxForPlugin
|
|
1715
|
+
});
|
|
1716
|
+
},
|
|
1717
|
+
renderBodyHtmlString: (post) => renderBodyHtmlString(post),
|
|
1375
1718
|
renderThemeCss: (cssVars) => renderThemeCss(cssVars),
|
|
1376
1719
|
publicAssetUrl: (key) => storage.publicAssetUrl(key),
|
|
1377
1720
|
isStorageConfigured: () => storage.isStorageConfigured(),
|
|
@@ -1394,6 +1737,7 @@ export {
|
|
|
1394
1737
|
DEFAULT_COLOR_SCHEME,
|
|
1395
1738
|
SUPPORTED_API_VERSION,
|
|
1396
1739
|
_resetStreamS3Cache,
|
|
1740
|
+
buildContentFieldRegistry,
|
|
1397
1741
|
createAmpless,
|
|
1398
1742
|
createPluginHead,
|
|
1399
1743
|
createPluginSettings,
|
|
@@ -1402,6 +1746,7 @@ export {
|
|
|
1402
1746
|
loadPackageManifest,
|
|
1403
1747
|
markdownToHtml,
|
|
1404
1748
|
renderBody,
|
|
1749
|
+
renderBodyHtmlString,
|
|
1405
1750
|
renderThemeCss,
|
|
1406
1751
|
streamS3Object,
|
|
1407
1752
|
streamS3ObjectWithRunner,
|