@caelo-cms/shared 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/ai-tools.d.ts +571 -0
  2. package/dist/ai-tools.d.ts.map +1 -0
  3. package/dist/ai-tools.js +696 -0
  4. package/dist/ai-tools.js.map +1 -0
  5. package/dist/auth-forms.d.ts +24 -0
  6. package/dist/auth-forms.d.ts.map +1 -0
  7. package/dist/auth-forms.js +27 -0
  8. package/dist/auth-forms.js.map +1 -0
  9. package/dist/cap-failures.d.ts +17 -0
  10. package/dist/cap-failures.d.ts.map +1 -0
  11. package/dist/cap-failures.js +58 -0
  12. package/dist/cap-failures.js.map +1 -0
  13. package/dist/content.d.ts +111 -0
  14. package/dist/content.d.ts.map +1 -0
  15. package/dist/content.js +137 -0
  16. package/dist/content.js.map +1 -0
  17. package/dist/context.d.ts +40 -0
  18. package/dist/context.d.ts.map +1 -0
  19. package/dist/context.js +3 -0
  20. package/dist/context.js.map +1 -0
  21. package/dist/i18n.d.ts +49 -0
  22. package/dist/i18n.d.ts.map +1 -0
  23. package/dist/i18n.js +154 -0
  24. package/dist/i18n.js.map +1 -0
  25. package/dist/index.d.ts +20 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +21 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/logger.d.ts +56 -0
  30. package/dist/logger.d.ts.map +1 -0
  31. package/dist/logger.js +84 -0
  32. package/dist/logger.js.map +1 -0
  33. package/dist/media.d.ts +143 -0
  34. package/dist/media.d.ts.map +1 -0
  35. package/dist/media.js +168 -0
  36. package/dist/media.js.map +1 -0
  37. package/dist/preview-compose.d.ts +84 -0
  38. package/dist/preview-compose.d.ts.map +1 -0
  39. package/dist/preview-compose.js +385 -0
  40. package/dist/preview-compose.js.map +1 -0
  41. package/dist/preview-scanner.d.ts +44 -0
  42. package/dist/preview-scanner.d.ts.map +1 -0
  43. package/dist/preview-scanner.js +177 -0
  44. package/dist/preview-scanner.js.map +1 -0
  45. package/dist/result.d.ts +21 -0
  46. package/dist/result.d.ts.map +1 -0
  47. package/dist/result.js +14 -0
  48. package/dist/result.js.map +1 -0
  49. package/dist/seo.d.ts +128 -0
  50. package/dist/seo.d.ts.map +1 -0
  51. package/dist/seo.js +176 -0
  52. package/dist/seo.js.map +1 -0
  53. package/dist/skills.d.ts +88 -0
  54. package/dist/skills.d.ts.map +1 -0
  55. package/dist/skills.js +127 -0
  56. package/dist/skills.js.map +1 -0
  57. package/dist/snapshots.d.ts +54 -0
  58. package/dist/snapshots.d.ts.map +1 -0
  59. package/dist/snapshots.js +59 -0
  60. package/dist/snapshots.js.map +1 -0
  61. package/dist/structured-sets.d.ts +116 -0
  62. package/dist/structured-sets.d.ts.map +1 -0
  63. package/dist/structured-sets.js +154 -0
  64. package/dist/structured-sets.js.map +1 -0
  65. package/dist/subagents.d.ts +123 -0
  66. package/dist/subagents.d.ts.map +1 -0
  67. package/dist/subagents.js +202 -0
  68. package/dist/subagents.js.map +1 -0
  69. package/dist/translation.d.ts +127 -0
  70. package/dist/translation.d.ts.map +1 -0
  71. package/dist/translation.js +208 -0
  72. package/dist/translation.js.map +1 -0
  73. package/dist/version.d.ts +46 -0
  74. package/dist/version.d.ts.map +1 -0
  75. package/dist/version.js +46 -0
  76. package/dist/version.js.map +1 -0
  77. package/package.json +38 -0
package/dist/media.js ADDED
@@ -0,0 +1,168 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+ /**
3
+ * Media library — shared primitives.
4
+ *
5
+ * Browser-safe: Zod schemas, MIME allowlist, size caps, the variant
6
+ * convention. Sharp + filesystem adapters live in `@caelo-cms/admin-core`
7
+ * (server-only). The storage-key shape is stable here so the static
8
+ * generator's URL rewriter and the admin's iframe resolver agree on
9
+ * the canonical form `<sha>/<variant>.<ext>`.
10
+ */
11
+ import { z } from "zod";
12
+ /**
13
+ * Allowlisted MIME types. Anything outside this set is rejected at the
14
+ * upload endpoint with `415 Unsupported Media Type`. SVG is allowed
15
+ * but capped tight to discourage XSS via embedded scripts; the upload
16
+ * pipeline strips `<script>` and event-handler attributes before
17
+ * persisting (see {@link sanitizeSvg} in admin-core).
18
+ */
19
+ export const MEDIA_ALLOWED_MIMES = [
20
+ "image/jpeg",
21
+ "image/png",
22
+ "image/webp",
23
+ "image/avif",
24
+ "image/gif",
25
+ "image/svg+xml",
26
+ "application/pdf",
27
+ "video/mp4",
28
+ ];
29
+ /** Per-MIME size caps (bytes). Server enforces; client display only. */
30
+ export const MEDIA_SIZE_CAPS = {
31
+ "image/jpeg": 10 * 1024 * 1024,
32
+ "image/png": 10 * 1024 * 1024,
33
+ "image/webp": 10 * 1024 * 1024,
34
+ "image/avif": 10 * 1024 * 1024,
35
+ "image/gif": 8 * 1024 * 1024,
36
+ "image/svg+xml": 1 * 1024 * 1024,
37
+ "application/pdf": 20 * 1024 * 1024,
38
+ "video/mp4": 50 * 1024 * 1024,
39
+ };
40
+ /** Hard ceiling on the multipart body. Per-MIME caps narrow further. */
41
+ export const MEDIA_HARD_LIMIT_BYTES = 50 * 1024 * 1024;
42
+ /**
43
+ * Variant tags. `orig` is always present (re-encoded only for SVG
44
+ * sanitisation). Image-only WebP variants are emitted at breakpoints
45
+ * the source can satisfy — a 600px-wide source skips webp-1200 +
46
+ * webp-1600 entirely.
47
+ */
48
+ export const MEDIA_VARIANT_TAGS = [
49
+ "orig",
50
+ "webp-1600",
51
+ "webp-1200",
52
+ "webp-800",
53
+ "webp-400",
54
+ ];
55
+ /** Width-in-pixels target for each WebP variant. */
56
+ export const MEDIA_VARIANT_WIDTHS = {
57
+ "webp-1600": 1600,
58
+ "webp-1200": 1200,
59
+ "webp-800": 800,
60
+ "webp-400": 400,
61
+ };
62
+ /**
63
+ * Renderer-agnostic asset URL used in module HTML. Both the SvelteKit
64
+ * admin endpoint and the static generator's media-pass parse this
65
+ * shape; the static generator rewrites to `/_assets/...` (or a CDN
66
+ * URL) at deploy time.
67
+ *
68
+ * Format: `/_caelo/media/<asset-id>/<variant>`. The asset id, not the
69
+ * sha, so URLs survive a re-upload of the same content under a new id.
70
+ */
71
+ export const MEDIA_URL_PREFIX = "/_caelo/media";
72
+ export function buildMediaUrl(assetId, variant) {
73
+ return `${MEDIA_URL_PREFIX}/${assetId}/${variant}`;
74
+ }
75
+ // Variant token: `orig`, `webp-<width>`, or `<crop-name>-<width>`. We
76
+ // accept any kebab-case slug so focal-point crop fan-outs like
77
+ // `square-800` and `wide-1200` (added by P7 optimization #2) round-trip
78
+ // without a regex update per crop name.
79
+ const mediaUrlPattern = new RegExp(`${MEDIA_URL_PREFIX}/([0-9a-f-]{36})/([a-z][a-z0-9-]{0,63})`, "g");
80
+ /**
81
+ * Extract every (assetId, variant) pair referenced in an HTML string.
82
+ * Used by the post-write usage-tracker and by the static-generator
83
+ * media-pass. Returns a deduped list to keep callers' work proportional
84
+ * to unique assets, not raw match count.
85
+ */
86
+ export function extractMediaRefs(html) {
87
+ const seen = new Set();
88
+ const out = [];
89
+ for (const m of html.matchAll(mediaUrlPattern)) {
90
+ const key = `${m[1]}/${m[2]}`;
91
+ if (seen.has(key))
92
+ continue;
93
+ seen.add(key);
94
+ out.push({ assetId: m[1], variant: m[2] });
95
+ }
96
+ return out;
97
+ }
98
+ // ---------------------------------------------------------------------
99
+ // Zod schemas — exposed at the Query-API boundary.
100
+ // ---------------------------------------------------------------------
101
+ const sha256Schema = z.string().regex(/^[0-9a-f]{64}$/, "must be hex sha256");
102
+ export const mediaUploadInputSchema = z
103
+ .object({
104
+ sha256: sha256Schema,
105
+ originalName: z.string().min(1).max(512),
106
+ mime: z.enum(MEDIA_ALLOWED_MIMES),
107
+ sizeBytes: z.number().int().positive(),
108
+ width: z.number().int().positive().nullable(),
109
+ height: z.number().int().positive().nullable(),
110
+ alt: z.string().max(2048).default(""),
111
+ storageKey: z.string().min(1),
112
+ /** P7 optimization #3 — stamped by the upload endpoint via getMediaStorageProvider(). */
113
+ storageProvider: z.string().min(1).max(64).default("local"),
114
+ variants: z
115
+ .array(z.object({
116
+ variant: z.string().min(1).max(64),
117
+ format: z.string().min(1).max(32),
118
+ width: z.number().int().positive().nullable(),
119
+ height: z.number().int().positive().nullable(),
120
+ sizeBytes: z.number().int().positive(),
121
+ storageKey: z.string().min(1),
122
+ }))
123
+ .min(1),
124
+ })
125
+ .strict();
126
+ export const mediaListInputSchema = z
127
+ .object({
128
+ query: z.string().max(256).optional(),
129
+ mime: z.enum(MEDIA_ALLOWED_MIMES).optional(),
130
+ sort: z.enum(["recent", "most_used"]).default("recent"),
131
+ limit: z.number().int().positive().max(200).default(60),
132
+ offset: z.number().int().nonnegative().default(0),
133
+ })
134
+ .strict();
135
+ export const mediaUpdateAltInputSchema = z
136
+ .object({
137
+ assetId: z.string().uuid(),
138
+ alt: z.string().max(2048),
139
+ })
140
+ .strict();
141
+ export const mediaDeleteInputSchema = z
142
+ .object({
143
+ assetId: z.string().uuid(),
144
+ force: z.boolean().default(false),
145
+ })
146
+ .strict();
147
+ export const mediaRecordUsageInputSchema = z
148
+ .object({
149
+ /** Map of assetId → net delta (positive when added, negative when removed). */
150
+ deltas: z.record(z.string().uuid(), z.number().int()),
151
+ })
152
+ .strict();
153
+ export const mediaRecentForAiInputSchema = z
154
+ .object({
155
+ limit: z.number().int().positive().max(60).default(30),
156
+ })
157
+ .strict();
158
+ export const mediaSetCdnInputSchema = z
159
+ .object({
160
+ enabled: z.boolean(),
161
+ threshold: z.number().int().min(1).max(10000),
162
+ })
163
+ .strict();
164
+ /** Build the canonical storage key for a given asset variant. */
165
+ export function buildStorageKey(sha256, variant, ext) {
166
+ return `${sha256}/${variant}.${ext}`;
167
+ }
168
+ //# sourceMappingURL=media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.js","sourceRoot":"","sources":["../src/media.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ export interface ComposeModule {
2
+ readonly moduleId: string;
3
+ readonly slug: string;
4
+ readonly displayName: string;
5
+ readonly html: string;
6
+ readonly css: string;
7
+ readonly js: string;
8
+ }
9
+ export interface ComposeBlock {
10
+ readonly blockName: string;
11
+ readonly modules: readonly ComposeModule[];
12
+ }
13
+ /** P6.7.5 — structured sets carried into the composer so nav-menu
14
+ * modules render from typed items and theme tokens flow into <head>. */
15
+ export interface ComposeStructuredSets {
16
+ /** Map keyed by `<kind>/<slug>` (e.g. `nav-menu/header-main`). */
17
+ readonly byKindSlug: Readonly<Record<string, readonly unknown[]>>;
18
+ }
19
+ /**
20
+ * P9 — language-selector context. The caller (preview op + static
21
+ * generator) resolves the current page's per-locale URLs once and
22
+ * threads them in. The composer renders the selector when a module
23
+ * slug starts with `language-selector-`.
24
+ */
25
+ export interface ComposeLanguageSelector {
26
+ readonly availableLocales: ReadonlyArray<{
27
+ code: string;
28
+ displayName: string;
29
+ href: string;
30
+ isCurrent: boolean;
31
+ }>;
32
+ }
33
+ export interface ComposeInput {
34
+ readonly templateHtml: string;
35
+ readonly templateCss: string;
36
+ readonly blocks: readonly ComposeBlock[];
37
+ readonly structuredSets?: ComposeStructuredSets;
38
+ readonly languageSelector?: ComposeLanguageSelector;
39
+ }
40
+ export interface ComposeOutput {
41
+ readonly html: string;
42
+ readonly replacedSlots: readonly string[];
43
+ readonly missingSlots: readonly string[];
44
+ }
45
+ export declare function composePagePreview(input: ComposeInput): ComposeOutput;
46
+ export declare function tagModuleId(html: string, moduleId: string): string;
47
+ /**
48
+ * P6.7.6 — layout-aware composer. Runs the template composer first,
49
+ * extracts the resulting body content, then renders the layout HTML
50
+ * substituting:
51
+ * - `<caelo-slot name="content">` → the body of the rendered template
52
+ * - other layout blocks (header / footer / etc.) → concatenated HTML
53
+ * from `layoutBlocks` (per-block module attachments)
54
+ *
55
+ * Per CLAUDE.md §2 no-fallbacks: validates the layout has the required
56
+ * `<caelo-slot name="content">` slot before rendering. Throws
57
+ * `ComposeError` if the layout is malformed so callers (preview op +
58
+ * static generator) surface it as a structured failure rather than
59
+ * silently emitting broken HTML.
60
+ */
61
+ export interface ComposeLayoutBlock {
62
+ readonly blockName: string;
63
+ readonly modules: readonly ComposeModule[];
64
+ }
65
+ export interface ComposeWithLayoutInput extends ComposeInput {
66
+ readonly layoutHtml: string;
67
+ readonly layoutCss: string;
68
+ readonly layoutBlocks: readonly ComposeLayoutBlock[];
69
+ /** Optional layout slug carried into ComposeError messages. */
70
+ readonly layoutSlug?: string;
71
+ }
72
+ /**
73
+ * Typed failure for the layout-aware composer. Use `kind` to dispatch:
74
+ * - `layout-missing-content`: the layout HTML lacks
75
+ * `<caelo-slot name="content">…</caelo-slot>` so the page body has
76
+ * nowhere to land.
77
+ */
78
+ export declare class ComposeError extends Error {
79
+ readonly kind: "layout-missing-content";
80
+ readonly layoutSlug: string | undefined;
81
+ constructor(kind: "layout-missing-content", message: string, layoutSlug?: string);
82
+ }
83
+ export declare function composePageWithLayout(input: ComposeWithLayoutInput): ComposeOutput;
84
+ //# sourceMappingURL=preview-compose.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview-compose.d.ts","sourceRoot":"","sources":["../src/preview-compose.ts"],"names":[],"mappings":"AAkCA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;CAC5C;AAED;yEACyE;AACzE,MAAM,WAAW,qBAAqB;IACpC,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC,CAAC,CAAC;CACnE;AAED;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,gBAAgB,EAAE,aAAa,CAAC;QACvC,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,OAAO,CAAC;KACpB,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;IACzC,QAAQ,CAAC,cAAc,CAAC,EAAE,qBAAqB,CAAC;IAChD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;CACrD;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAYD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,GAAG,aAAa,CA8DrE;AA2HD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUlE;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,sBAAuB,SAAQ,YAAY;IAC1D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrD,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;gBAC5B,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAMjF;AAkCD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,sBAAsB,GAAG,aAAa,CAwHlF"}
@@ -0,0 +1,385 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+ /**
3
+ * Compose a page's HTML from its template + module references.
4
+ *
5
+ * The composed output is what the admin preview iframe renders. Production
6
+ * static-gen (P6) will reuse this same composer once Astro is wired up, so the
7
+ * function is pure and dependency-free — no DB calls, no IO. The Query API op
8
+ * does the loads and hands the data here.
9
+ *
10
+ * Output shape:
11
+ * 1. `<caelo-slot name="X">` blocks have their inner HTML replaced by the
12
+ * concatenated module HTML for block X (in `position` order).
13
+ * 2. All module CSS is concatenated into a single
14
+ * `<style data-source="modules">` injected before `</head>` — template
15
+ * stays the source of truth for `<head>`; we just append.
16
+ * 3. All module JS is concatenated into a single
17
+ * `<script defer data-source="modules">` injected before `</body>`.
18
+ * 4. Template CSS is injected ahead of module CSS so module rules can
19
+ * override template defaults via specificity.
20
+ *
21
+ * The composer never escapes module HTML — modules ARE the place where raw
22
+ * HTML lives (CMS_REQUIREMENTS §3.1). Templates ARE the place where the
23
+ * `<head>` skeleton lives. Sandboxing happens one layer up (preview iframe in
24
+ * the admin; in P11, plugin Web Components inside Shadow DOM).
25
+ */
26
+ import { applySlotReplacements, extractInnerOfTopLevelContentSlot, listSlotNames, } from "./preview-scanner.js";
27
+ import { renderLanguageSelector } from "./structured-sets.js";
28
+ const HEAD_CLOSE_RE = /<\/head\s*>/i;
29
+ const BODY_CLOSE_RE = /<\/body\s*>/i;
30
+ function injectBefore(source, marker, fragment) {
31
+ const m = marker.exec(source);
32
+ if (!m)
33
+ return source + fragment; // template lacks the tag — append as fallback
34
+ const idx = m.index;
35
+ return source.slice(0, idx) + fragment + source.slice(idx);
36
+ }
37
+ export function composePagePreview(input) {
38
+ const contentByName = new Map();
39
+ const allCss = [];
40
+ const allJs = [];
41
+ // Template CSS first so module CSS can override it via source-order specificity.
42
+ if (input.templateCss.trim().length > 0)
43
+ allCss.push(input.templateCss);
44
+ for (const block of input.blocks) {
45
+ // P6.7 — tag every module's outermost element with
46
+ // `data-caelo-module-id="<uuid>"` so the live-edit overlay's iframe
47
+ // hover affordances can identify the clicked module.
48
+ //
49
+ // P6.7.5 — modules whose slug matches a `nav-menu/<slug>` set get
50
+ // their HTML replaced by a fresh render of the menu items. That's
51
+ // what makes a slug change update every menu without touching
52
+ // module HTML.
53
+ const renderedModuleHtml = block.modules.map((m) => {
54
+ const navMenuItems = lookupNavMenuItems(m.slug, input.structuredSets);
55
+ const langSelector = lookupLanguageSelector(m.slug, input);
56
+ let baseHtml;
57
+ if (navMenuItems !== null) {
58
+ baseHtml = renderNavMenuHtml(navMenuItems);
59
+ }
60
+ else if (langSelector !== null) {
61
+ baseHtml = langSelector;
62
+ }
63
+ else {
64
+ baseHtml = m.html;
65
+ }
66
+ return tagModuleId(baseHtml, m.moduleId);
67
+ });
68
+ const html = renderedModuleHtml.join("\n");
69
+ contentByName.set(block.blockName, html);
70
+ for (const m of block.modules) {
71
+ if (m.css.trim().length > 0)
72
+ allCss.push(m.css);
73
+ if (m.js.trim().length > 0)
74
+ allJs.push(m.js);
75
+ }
76
+ }
77
+ const replaced = applySlotReplacements(input.templateHtml, { contentByName });
78
+ let html = replaced.html;
79
+ // P6.7.5 — theme tokens become CSS custom properties on :root. Goes
80
+ // first so module CSS can `var(--color-primary)` and override.
81
+ const themeCss = renderThemeCss(input.structuredSets);
82
+ if (themeCss !== null) {
83
+ const styleTag = `<style data-source="theme">${themeCss}</style>`;
84
+ html = injectBefore(html, HEAD_CLOSE_RE, styleTag);
85
+ }
86
+ if (allCss.length > 0) {
87
+ const styleTag = `<style data-source="modules">\n${allCss.join("\n")}\n</style>`;
88
+ html = injectBefore(html, HEAD_CLOSE_RE, styleTag);
89
+ }
90
+ if (allJs.length > 0) {
91
+ const scriptTag = `<script defer data-source="modules">\n${allJs.join("\n")}\n</script>`;
92
+ html = injectBefore(html, BODY_CLOSE_RE, scriptTag);
93
+ }
94
+ return {
95
+ html,
96
+ replacedSlots: replaced.replacedSlots,
97
+ missingSlots: replaced.missingSlots,
98
+ };
99
+ }
100
+ /**
101
+ * Insert `data-caelo-module-id="<id>"` into the first opening tag of
102
+ * the module's HTML. Idempotent — re-tagging an already-tagged module
103
+ * is a no-op. Comments / DOCTYPE / leading whitespace before the first
104
+ * tag are tolerated. Modules that have no opening tag (pure text)
105
+ * return unchanged because there's nothing to attach to.
106
+ *
107
+ * Exported so callers (admin preview endpoint, static generator,
108
+ * tests) can reuse the same logic.
109
+ */
110
+ /**
111
+ * P6.7.5 — return the items for a `nav-menu/<slug>` set when a module's
112
+ * slug starts with `nav-menu-`. Returns null when the module is not a
113
+ * nav menu (so the composer falls back to its stored HTML).
114
+ *
115
+ * Convention: a module slug `nav-menu-header-main` resolves to
116
+ * structuredSets[`nav-menu/header-main`].
117
+ */
118
+ function lookupNavMenuItems(moduleSlug, sets) {
119
+ if (!sets)
120
+ return null;
121
+ const prefix = "nav-menu-";
122
+ if (!moduleSlug.startsWith(prefix))
123
+ return null;
124
+ const setSlug = moduleSlug.slice(prefix.length);
125
+ const items = sets.byKindSlug[`nav-menu/${setSlug}`];
126
+ return items ?? null;
127
+ }
128
+ /**
129
+ * P9 — return rendered language-selector HTML when a module's slug
130
+ * starts with `language-selector-`. The set's items act as overrides
131
+ * (relabel a locale, hide one); the available-locale list comes from
132
+ * the caller via `input.languageSelector`. Returns null when the
133
+ * module is not a language selector.
134
+ *
135
+ * Convention: a module slug `language-selector-header` resolves to
136
+ * structuredSets[`language-selector/header`] for overrides, and the
137
+ * rendered HTML lists every locale that has a published variant of
138
+ * the current page.
139
+ */
140
+ function lookupLanguageSelector(moduleSlug, input) {
141
+ const prefix = "language-selector-";
142
+ if (!moduleSlug.startsWith(prefix))
143
+ return null;
144
+ if (!input.languageSelector || input.languageSelector.availableLocales.length === 0) {
145
+ return null;
146
+ }
147
+ const setSlug = moduleSlug.slice(prefix.length);
148
+ const overridesUnknown = input.structuredSets?.byKindSlug[`language-selector/${setSlug}`];
149
+ const overrides = Array.isArray(overridesUnknown)
150
+ ? overridesUnknown
151
+ : undefined;
152
+ return renderLanguageSelector({
153
+ availableLocales: input.languageSelector.availableLocales,
154
+ overrides,
155
+ });
156
+ }
157
+ function escapeAttr(s) {
158
+ return s
159
+ .replace(/&/g, "&amp;")
160
+ .replace(/"/g, "&quot;")
161
+ .replace(/</g, "&lt;")
162
+ .replace(/>/g, "&gt;");
163
+ }
164
+ function escapeText(s) {
165
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
166
+ }
167
+ /**
168
+ * Render a nav-menu's typed items into HTML. Recursively handles
169
+ * children for submenus. Plain `<nav><ul><li>` so site CSS can theme
170
+ * it via the `caelo-nav-menu` class.
171
+ */
172
+ function renderNavMenuHtml(items) {
173
+ const safeItems = items.filter((it) => {
174
+ if (!it || typeof it !== "object")
175
+ return false;
176
+ const o = it;
177
+ return typeof o.label === "string" && typeof o.href === "string";
178
+ });
179
+ return `<nav class="caelo-nav-menu"><ul>${safeItems.map(renderNavItem).join("")}</ul></nav>`;
180
+ }
181
+ function renderNavItem(item) {
182
+ const target = item.target === "_blank" ? ' target="_blank" rel="noopener"' : "";
183
+ const inner = item.children && item.children.length > 0
184
+ ? `<ul>${item.children.map(renderNavItem).join("")}</ul>`
185
+ : "";
186
+ return `<li><a href="${escapeAttr(item.href)}"${target}>${escapeText(item.label)}</a>${inner}</li>`;
187
+ }
188
+ /**
189
+ * Render the theme/site set's tokens as `:root { --token: value; }` CSS.
190
+ * Returns null when no theme is configured so the composer skips
191
+ * injecting an empty <style> tag.
192
+ */
193
+ function renderThemeCss(sets) {
194
+ if (!sets)
195
+ return null;
196
+ const items = sets.byKindSlug["theme/site"];
197
+ if (!items || items.length === 0)
198
+ return null;
199
+ const tokens = items.filter((it) => {
200
+ if (!it || typeof it !== "object")
201
+ return false;
202
+ const o = it;
203
+ return typeof o.token === "string" && typeof o.value === "string";
204
+ });
205
+ if (tokens.length === 0)
206
+ return null;
207
+ return `:root{${tokens.map((t) => `--${t.token}: ${t.value};`).join("")}}`;
208
+ }
209
+ export function tagModuleId(html, moduleId) {
210
+ if (!html)
211
+ return html;
212
+ const firstOpen = /<([a-zA-Z][a-zA-Z0-9-]*)\b([^>]*)>/;
213
+ const m = firstOpen.exec(html);
214
+ if (!m)
215
+ return html;
216
+ // Already tagged?
217
+ const tagAttrs = m[2] ?? "";
218
+ if (/\sdata-caelo-module-id\s*=/.test(tagAttrs))
219
+ return html;
220
+ const replaced = `<${m[1]}${tagAttrs} data-caelo-module-id="${moduleId}">`;
221
+ return html.slice(0, m.index) + replaced + html.slice(m.index + m[0].length);
222
+ }
223
+ /**
224
+ * Typed failure for the layout-aware composer. Use `kind` to dispatch:
225
+ * - `layout-missing-content`: the layout HTML lacks
226
+ * `<caelo-slot name="content">…</caelo-slot>` so the page body has
227
+ * nowhere to land.
228
+ */
229
+ export class ComposeError extends Error {
230
+ kind;
231
+ layoutSlug;
232
+ constructor(kind, message, layoutSlug) {
233
+ super(message);
234
+ this.name = "ComposeError";
235
+ this.kind = kind;
236
+ this.layoutSlug = layoutSlug;
237
+ }
238
+ }
239
+ const BODY_OPEN_RE = /<body\b[^>]*>/i;
240
+ /**
241
+ * Extract the inner body HTML from a fully rendered template document.
242
+ * If the template HTML has no <body> (legacy fragment templates), the
243
+ * whole composed string is returned as-is — the layout's
244
+ * `<caelo-slot name="content">` becomes a generic mount point and the
245
+ * layout owns <html><head><body>.
246
+ *
247
+ * Legacy templates often wrap their slot in `<body><caelo-slot
248
+ * name="content">…</caelo-slot></body>` — peel off the redundant
249
+ * `<caelo-slot>` so we don't end up with the layout's own slot
250
+ * containing yet another `<caelo-slot>`. The peel uses the same
251
+ * htmlparser2 Parser as `applySlotReplacements` so quoting / attribute
252
+ * ordering / whitespace variations are handled uniformly (the previous
253
+ * regex silently fell through on `name='content'`, attr reordering,
254
+ * etc., producing nested-slot output).
255
+ */
256
+ function extractBodyInner(composedHtml) {
257
+ const open = BODY_OPEN_RE.exec(composedHtml);
258
+ const close = BODY_CLOSE_RE.exec(composedHtml);
259
+ let inner;
260
+ if (!open || !close || close.index < open.index) {
261
+ inner = composedHtml;
262
+ }
263
+ else {
264
+ const start = open.index + open[0].length;
265
+ inner = composedHtml.slice(start, close.index);
266
+ }
267
+ const peeled = extractInnerOfTopLevelContentSlot(inner);
268
+ return peeled ?? inner;
269
+ }
270
+ export function composePageWithLayout(input) {
271
+ // No-fallbacks (CLAUDE.md §2): validate the layout declares a
272
+ // `content` slot up-front, before rendering. The htmlparser2-based
273
+ // walk handles attribute quoting / ordering uniformly; a layout
274
+ // without the slot is a misconfiguration that must surface to the
275
+ // caller, not silently emit a body-less page.
276
+ if (!listSlotNames(input.layoutHtml).includes("content")) {
277
+ const slug = input.layoutSlug ?? "(unknown)";
278
+ throw new ComposeError("layout-missing-content", `layout "${slug}" is missing the required \`<caelo-slot name="content">\` slot — fix via /security/layouts`, input.layoutSlug);
279
+ }
280
+ // CSS / JS aggregation order: layout (ground) → template (overrides
281
+ // layout) → modules (highest specificity). The array's source order
282
+ // drives cascade order in the emitted <style> tag, so we push in
283
+ // priority sequence rather than mixing push + unshift (which is
284
+ // brittle and reads as a bug).
285
+ const cssParts = [];
286
+ const jsParts = [];
287
+ if (input.layoutCss.trim().length > 0)
288
+ cssParts.push(input.layoutCss);
289
+ if (input.templateCss.trim().length > 0)
290
+ cssParts.push(input.templateCss);
291
+ // 1. Render the page modules into the template (slot replacement only;
292
+ // no head/body manipulation here — that belongs to the layout).
293
+ const templateContentByName = new Map();
294
+ for (const block of input.blocks) {
295
+ const renderedModuleHtml = block.modules.map((m) => {
296
+ const navMenuItems = lookupNavMenuItems(m.slug, input.structuredSets);
297
+ const langSelector = lookupLanguageSelector(m.slug, input);
298
+ let baseHtml;
299
+ if (navMenuItems !== null) {
300
+ baseHtml = renderNavMenuHtml(navMenuItems);
301
+ }
302
+ else if (langSelector !== null) {
303
+ baseHtml = langSelector;
304
+ }
305
+ else {
306
+ baseHtml = m.html;
307
+ }
308
+ return tagModuleId(baseHtml, m.moduleId);
309
+ });
310
+ templateContentByName.set(block.blockName, renderedModuleHtml.join("\n"));
311
+ for (const m of block.modules) {
312
+ if (m.css.trim().length > 0)
313
+ cssParts.push(m.css);
314
+ if (m.js.trim().length > 0)
315
+ jsParts.push(m.js);
316
+ }
317
+ }
318
+ const renderedTemplate = applySlotReplacements(input.templateHtml, {
319
+ contentByName: templateContentByName,
320
+ });
321
+ const innerBody = extractBodyInner(renderedTemplate.html);
322
+ // 2. Build per-layout-block contents (header / footer / etc.) +
323
+ // aggregate their CSS/JS at module specificity (already higher
324
+ // than layout/template because the layout/template parts went
325
+ // in first above).
326
+ const layoutContentByName = new Map();
327
+ layoutContentByName.set("content", innerBody);
328
+ for (const block of input.layoutBlocks) {
329
+ if (block.blockName === "content")
330
+ continue; // reserved for the page body
331
+ const renderedModuleHtml = block.modules.map((m) => {
332
+ const navMenuItems = lookupNavMenuItems(m.slug, input.structuredSets);
333
+ const langSelector = lookupLanguageSelector(m.slug, input);
334
+ let baseHtml;
335
+ if (navMenuItems !== null) {
336
+ baseHtml = renderNavMenuHtml(navMenuItems);
337
+ }
338
+ else if (langSelector !== null) {
339
+ baseHtml = langSelector;
340
+ }
341
+ else {
342
+ baseHtml = m.html;
343
+ }
344
+ return tagModuleId(baseHtml, m.moduleId);
345
+ });
346
+ layoutContentByName.set(block.blockName, renderedModuleHtml.join("\n"));
347
+ for (const m of block.modules) {
348
+ if (m.css.trim().length > 0)
349
+ cssParts.push(m.css);
350
+ if (m.js.trim().length > 0)
351
+ jsParts.push(m.js);
352
+ }
353
+ }
354
+ // 3. Render the layout HTML, substituting all named slots.
355
+ const replaced = applySlotReplacements(input.layoutHtml, {
356
+ contentByName: layoutContentByName,
357
+ });
358
+ let html = replaced.html;
359
+ const themeCss = renderThemeCss(input.structuredSets);
360
+ if (themeCss !== null) {
361
+ html = injectBefore(html, HEAD_CLOSE_RE, `<style data-source="theme">${themeCss}</style>`);
362
+ }
363
+ if (cssParts.length > 0) {
364
+ html = injectBefore(html, HEAD_CLOSE_RE, `<style data-source="modules">\n${cssParts.join("\n")}\n</style>`);
365
+ }
366
+ if (jsParts.length > 0) {
367
+ html = injectBefore(html, BODY_CLOSE_RE, `<script defer data-source="modules">\n${jsParts.join("\n")}\n</script>`);
368
+ }
369
+ // De-duplicate slot accounting across both passes — the template's
370
+ // `content` slot and the layout's `content` slot are conceptually the
371
+ // same surface to a caller asking "did content get filled?".
372
+ const replacedSet = new Set([
373
+ ...renderedTemplate.replacedSlots,
374
+ ...replaced.replacedSlots,
375
+ ]);
376
+ const missingSet = new Set([...renderedTemplate.missingSlots, ...replaced.missingSlots]);
377
+ for (const name of replacedSet)
378
+ missingSet.delete(name);
379
+ return {
380
+ html,
381
+ replacedSlots: [...replacedSet],
382
+ missingSlots: [...missingSet],
383
+ };
384
+ }
385
+ //# sourceMappingURL=preview-compose.js.map