@reddoorla/maintenance 0.69.0 → 0.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +15 -14
  2. package/dist/airtable-U2GTDTK5.js +9 -0
  3. package/dist/{announce-72R3LRNT.js → announce-HJCGEVJA.js} +6 -6
  4. package/dist/blux-FKZYMKLF.js +2205 -0
  5. package/dist/blux-FKZYMKLF.js.map +1 -0
  6. package/dist/{bump-deps-QNM3H4GI.js → bump-deps-GZEJAUKI.js} +3 -2
  7. package/dist/bump-deps-GZEJAUKI.js.map +1 -0
  8. package/dist/chunk-7MJBTV2R.js +145 -0
  9. package/dist/chunk-7MJBTV2R.js.map +1 -0
  10. package/dist/{chunk-2DCA7FHZ.js → chunk-7P7YETHG.js} +34 -12
  11. package/dist/chunk-7P7YETHG.js.map +1 -0
  12. package/dist/{chunk-42KKVLYN.js → chunk-APTAITXZ.js} +10 -2
  13. package/dist/chunk-APTAITXZ.js.map +1 -0
  14. package/dist/{chunk-6G7BC5SN.js → chunk-BHE5CUWI.js} +392 -7
  15. package/dist/chunk-BHE5CUWI.js.map +1 -0
  16. package/dist/{chunk-XWPZ46IN.js → chunk-BJCA7O4A.js} +2 -2
  17. package/dist/{chunk-C3PDS623.js → chunk-BWKK2ZXS.js} +67 -8
  18. package/dist/chunk-BWKK2ZXS.js.map +1 -0
  19. package/dist/{chunk-45AUGDFR.js → chunk-D4FVAIEJ.js} +6 -20
  20. package/dist/chunk-D4FVAIEJ.js.map +1 -0
  21. package/dist/{chunk-YQMYQAJX.js → chunk-GI4M2AMO.js} +2 -2
  22. package/dist/{chunk-OTBA7DON.js → chunk-GYNRZCM6.js} +7 -2
  23. package/dist/chunk-GYNRZCM6.js.map +1 -0
  24. package/dist/{chunk-6MT5W6UF.js → chunk-HR5SONJC.js} +6 -3
  25. package/dist/chunk-HR5SONJC.js.map +1 -0
  26. package/dist/{chunk-6JFOAPW4.js → chunk-JCITWZQR.js} +51 -19
  27. package/dist/chunk-JCITWZQR.js.map +1 -0
  28. package/dist/{chunk-GKSXZIMX.js → chunk-JGLAWXUC.js} +77 -3
  29. package/dist/chunk-JGLAWXUC.js.map +1 -0
  30. package/dist/{chunk-E2GKKUEX.js → chunk-KLB2OLPW.js} +26 -6
  31. package/dist/chunk-KLB2OLPW.js.map +1 -0
  32. package/dist/{chunk-RINXPU3H.js → chunk-KLVMJYZZ.js} +2 -2
  33. package/dist/chunk-KRDLHAKK.js +17 -0
  34. package/dist/chunk-KRDLHAKK.js.map +1 -0
  35. package/dist/{chunk-AESJBEM7.js → chunk-KRHPS3R4.js} +3 -3
  36. package/dist/{chunk-LYCKN7ZR.js → chunk-LJPCMXR7.js} +8 -5
  37. package/dist/chunk-LJPCMXR7.js.map +1 -0
  38. package/dist/{chunk-O6G4HHMF.js → chunk-NRISG4XD.js} +308 -104
  39. package/dist/chunk-NRISG4XD.js.map +1 -0
  40. package/dist/chunk-PKVZMFEB.js +315 -0
  41. package/dist/chunk-PKVZMFEB.js.map +1 -0
  42. package/dist/chunk-SL24DCGC.js +41 -0
  43. package/dist/chunk-SL24DCGC.js.map +1 -0
  44. package/dist/{chunk-A5KYRGQA.js → chunk-SQJ53Z7R.js} +4 -4
  45. package/dist/cli/bin.js +63 -19
  46. package/dist/cli/bin.js.map +1 -1
  47. package/dist/cli/commands/audit.d.ts +1 -1
  48. package/dist/cli/commands/audit.js +11 -9
  49. package/dist/cli/commands/audit.js.map +1 -1
  50. package/dist/configs/playwright-a11y.d.ts +1 -1
  51. package/dist/configs/playwright-a11y.js +1 -3
  52. package/dist/{convert-to-pnpm-XH7NYRVK.js → convert-to-pnpm-SVFDLXZS.js} +3 -2
  53. package/dist/convert-to-pnpm-SVFDLXZS.js.map +1 -0
  54. package/dist/{digest-Q2TAQHXF.js → digest-VX7Q4MNF.js} +8 -8
  55. package/dist/{ensure-site-2PF522VL.js → ensure-site-O7G5ZJS7.js} +2 -2
  56. package/dist/forms/index.d.ts +5 -2
  57. package/dist/forms/index.js +22 -9
  58. package/dist/forms/index.js.map +1 -1
  59. package/dist/{github-signals-CLQC2IPY.js → github-signals-QVW2LB7N.js} +5 -5
  60. package/dist/health-endpoint-IYR5VBUT.js +54 -0
  61. package/dist/health-endpoint-IYR5VBUT.js.map +1 -0
  62. package/dist/index.d.ts +136 -20
  63. package/dist/index.js +207 -64
  64. package/dist/index.js.map +1 -1
  65. package/dist/{init-UA6D7GKW.js → init-CU6PVZRG.js} +9 -5
  66. package/dist/init-CU6PVZRG.js.map +1 -0
  67. package/dist/{launch-AENAS2O2.js → launch-JWXCTJRG.js} +9 -9
  68. package/dist/{lighthouse-airtable-UBSXKHAQ.js → lighthouse-airtable-J5TLASJC.js} +3 -3
  69. package/dist/{onboard-X6KJ5Z2V.js → onboard-6UQE5QIO.js} +2 -2
  70. package/dist/{orchestrate-GADNFGDH.js → orchestrate-ZDH24Z6Y.js} +5 -5
  71. package/dist/{preflight-D5RQGX4V.js → preflight-BNTMWTPG.js} +7 -7
  72. package/dist/probe-DJAMLVTT.js +46 -0
  73. package/dist/probe-DJAMLVTT.js.map +1 -0
  74. package/dist/recipes/sync-configs.d.ts +1 -1
  75. package/dist/{renovate-dispatch-LG4RJGDY.js → renovate-dispatch-C3ALLACX.js} +2 -2
  76. package/dist/{report-3FJ224H5.js → report-YLFSGNWR.js} +12 -12
  77. package/dist/run-migration-LJNNZWIV.js +205 -0
  78. package/dist/run-migration-LJNNZWIV.js.map +1 -0
  79. package/dist/{self-updating-JTTFOJ7Q.js → self-updating-64FNRQB5.js} +2 -2
  80. package/dist/{selftest-55OJIWEB.js → selftest-7DPNGL6R.js} +12 -9
  81. package/dist/selftest-7DPNGL6R.js.map +1 -0
  82. package/dist/smoke-suite-TWILWDSF.js +54 -0
  83. package/dist/smoke-suite-TWILWDSF.js.map +1 -0
  84. package/dist/{svelte-codemods-KJLNH6FQ.js → svelte-codemods-RVDTTXFD.js} +3 -2
  85. package/dist/svelte-codemods-RVDTTXFD.js.map +1 -0
  86. package/dist/{sync-configs-TIBO6DUJ.js → sync-configs-BUUD6L5Q.js} +3 -2
  87. package/dist/sync-configs-BUUD6L5Q.js.map +1 -0
  88. package/dist/{types-Dg4mHvyf.d.ts → types-0jreMCF5.d.ts} +2 -2
  89. package/dist/{upgrade-TPTE26FG.js → upgrade-HHKWYJRN.js} +3 -2
  90. package/dist/upgrade-HHKWYJRN.js.map +1 -0
  91. package/dist/{websites-6YTT5ZTO.js → websites-EH2ULELZ.js} +8 -2
  92. package/dist/{write-audits-to-airtable-IWOO4FFH.js → write-audits-to-airtable-LZDYTNX5.js} +4 -4
  93. package/package.json +4 -1
  94. package/dist/airtable-WGQQ3KAD.js +0 -9
  95. package/dist/bump-deps-QNM3H4GI.js.map +0 -1
  96. package/dist/chunk-2DCA7FHZ.js.map +0 -1
  97. package/dist/chunk-42KKVLYN.js.map +0 -1
  98. package/dist/chunk-45AUGDFR.js.map +0 -1
  99. package/dist/chunk-6G7BC5SN.js.map +0 -1
  100. package/dist/chunk-6JFOAPW4.js.map +0 -1
  101. package/dist/chunk-6MT5W6UF.js.map +0 -1
  102. package/dist/chunk-C3PDS623.js.map +0 -1
  103. package/dist/chunk-E2GKKUEX.js.map +0 -1
  104. package/dist/chunk-GKSXZIMX.js.map +0 -1
  105. package/dist/chunk-LYCKN7ZR.js.map +0 -1
  106. package/dist/chunk-O6G4HHMF.js.map +0 -1
  107. package/dist/chunk-OTBA7DON.js.map +0 -1
  108. package/dist/convert-to-pnpm-XH7NYRVK.js.map +0 -1
  109. package/dist/init-UA6D7GKW.js.map +0 -1
  110. package/dist/selftest-55OJIWEB.js.map +0 -1
  111. package/dist/svelte-codemods-KJLNH6FQ.js.map +0 -1
  112. package/dist/sync-configs-TIBO6DUJ.js.map +0 -1
  113. package/dist/upgrade-TPTE26FG.js.map +0 -1
  114. /package/dist/{airtable-WGQQ3KAD.js.map → airtable-U2GTDTK5.js.map} +0 -0
  115. /package/dist/{announce-72R3LRNT.js.map → announce-HJCGEVJA.js.map} +0 -0
  116. /package/dist/{chunk-XWPZ46IN.js.map → chunk-BJCA7O4A.js.map} +0 -0
  117. /package/dist/{chunk-YQMYQAJX.js.map → chunk-GI4M2AMO.js.map} +0 -0
  118. /package/dist/{chunk-RINXPU3H.js.map → chunk-KLVMJYZZ.js.map} +0 -0
  119. /package/dist/{chunk-AESJBEM7.js.map → chunk-KRHPS3R4.js.map} +0 -0
  120. /package/dist/{chunk-A5KYRGQA.js.map → chunk-SQJ53Z7R.js.map} +0 -0
  121. /package/dist/{digest-Q2TAQHXF.js.map → digest-VX7Q4MNF.js.map} +0 -0
  122. /package/dist/{ensure-site-2PF522VL.js.map → ensure-site-O7G5ZJS7.js.map} +0 -0
  123. /package/dist/{github-signals-CLQC2IPY.js.map → github-signals-QVW2LB7N.js.map} +0 -0
  124. /package/dist/{launch-AENAS2O2.js.map → launch-JWXCTJRG.js.map} +0 -0
  125. /package/dist/{lighthouse-airtable-UBSXKHAQ.js.map → lighthouse-airtable-J5TLASJC.js.map} +0 -0
  126. /package/dist/{onboard-X6KJ5Z2V.js.map → onboard-6UQE5QIO.js.map} +0 -0
  127. /package/dist/{orchestrate-GADNFGDH.js.map → orchestrate-ZDH24Z6Y.js.map} +0 -0
  128. /package/dist/{preflight-D5RQGX4V.js.map → preflight-BNTMWTPG.js.map} +0 -0
  129. /package/dist/{renovate-dispatch-LG4RJGDY.js.map → renovate-dispatch-C3ALLACX.js.map} +0 -0
  130. /package/dist/{report-3FJ224H5.js.map → report-YLFSGNWR.js.map} +0 -0
  131. /package/dist/{self-updating-JTTFOJ7Q.js.map → self-updating-64FNRQB5.js.map} +0 -0
  132. /package/dist/{websites-6YTT5ZTO.js.map → websites-EH2ULELZ.js.map} +0 -0
  133. /package/dist/{write-audits-to-airtable-IWOO4FFH.js.map → write-audits-to-airtable-LZDYTNX5.js.map} +0 -0
@@ -0,0 +1,2205 @@
1
+ import {
2
+ collectAssetUrls
3
+ } from "./chunk-SL24DCGC.js";
4
+
5
+ // src/cli/commands/blux.ts
6
+ import { readFile, writeFile, mkdir } from "fs/promises";
7
+ import { join, dirname } from "path";
8
+ import { glob } from "tinyglobby";
9
+
10
+ // src/blux/parse.ts
11
+ var hasDisable = (cls) => typeof cls === "string" && cls.split(/\s+/).includes("disable");
12
+ function visibleText(text, style) {
13
+ const s = typeof text === "number" && Number.isFinite(text) ? String(text) : text;
14
+ if (typeof s !== "string" || s.trim() === "") return void 0;
15
+ if (hasDisable(style)) return void 0;
16
+ if (typeof style === "object" && style !== null && hasDisable(style.class)) return void 0;
17
+ return s.trim();
18
+ }
19
+ function asObject(v) {
20
+ if (!v || typeof v !== "object" || Array.isArray(v)) {
21
+ throw new Error("Invalid site.json: expected an object");
22
+ }
23
+ return v;
24
+ }
25
+ function parseBluxSite(input) {
26
+ const j = asObject(input);
27
+ const content = j.content ?? {};
28
+ const styles = j.styles ?? {};
29
+ const nav = j.navigation?.[0]?.items ?? [];
30
+ return {
31
+ meta: {
32
+ name: String(j.name ?? ""),
33
+ domain: String(j.domain ?? ""),
34
+ bluxSiteId: String(j.id ?? "")
35
+ },
36
+ pages: Array.isArray(content.pages) ? content.pages : [],
37
+ feeds: j.feeds ?? {},
38
+ media: j.media ?? {},
39
+ styles,
40
+ nav,
41
+ settings: j.settings ?? {}
42
+ };
43
+ }
44
+
45
+ // src/blux/archetype.ts
46
+ var nonEmpty = (v) => v != null && v !== "" && !(Array.isArray(v) && v.length === 0) && !(typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0);
47
+ function archetype(b) {
48
+ const heading = visibleText(b.title, b._title) !== void 0;
49
+ const text = visibleText(b.body, b._body) !== void 0;
50
+ const media = nonEmpty(b.media?.media);
51
+ const bg = nonEmpty(b.backgroundMedia?.media);
52
+ const kids = Array.isArray(b.items) && b.items.length > 0;
53
+ const cls = nonEmpty(b.class) ? String(b.class) : null;
54
+ if (kids && cls === "slides")
55
+ return { sliceType: "slider", variation: "default", confidence: 0.85 };
56
+ if (bg)
57
+ return { sliceType: "hero", variation: "default", confidence: heading || text ? 0.9 : 0.7 };
58
+ if (kids)
59
+ return { sliceType: "grid", variation: "default", confidence: cls === "grid" ? 0.9 : 0.7 };
60
+ if (media && heading && text)
61
+ return { sliceType: "media_text", variation: "imageRight", confidence: 0.9 };
62
+ if (media && (heading || text))
63
+ return { sliceType: "media_text", variation: "imageRight", confidence: 0.75 };
64
+ if (media) return { sliceType: "media_text", variation: "imageRight", confidence: 0.6 };
65
+ if (heading && text) return { sliceType: "rich_text", variation: "default", confidence: 0.85 };
66
+ if (heading || text) return { sliceType: "rich_text", variation: "default", confidence: 0.6 };
67
+ return { sliceType: "rich_text", variation: "default", confidence: 0.2 };
68
+ }
69
+
70
+ // src/blux/normalize.ts
71
+ function textRole(style) {
72
+ const cls = typeof style === "object" && style !== null ? style.class : style;
73
+ if (typeof cls !== "string") return void 0;
74
+ return cls.split(/\s+/).find((c) => /^text\d+$/.test(c));
75
+ }
76
+ var str = (x) => typeof x === "string" ? x : typeof x === "number" ? String(x) : "";
77
+ function cleanCssValue(x) {
78
+ const s = str(x).trim();
79
+ if (s === "" || s === "px") return "";
80
+ if (!/\s/.test(s) && /px$/.test(s) && !/^-?(\d+(\.\d+)?|\.\d+)px$/.test(s)) return "";
81
+ return s;
82
+ }
83
+ function inlineStyle(style) {
84
+ if (typeof style !== "object" || style === null) return void 0;
85
+ const out = {};
86
+ for (const [k, v] of Object.entries(style)) {
87
+ if (k === "class") continue;
88
+ const cleaned = cleanCssValue(v);
89
+ if (cleaned) out[k] = cleaned;
90
+ }
91
+ return Object.keys(out).length ? out : void 0;
92
+ }
93
+ function parseGoogleFonts(google) {
94
+ if (!google.trim()) return [];
95
+ return google.split("|").map((spec) => {
96
+ const [rawFamily = "", rawWeights = ""] = spec.split(":");
97
+ const family = rawFamily.replace(/\+/g, " ").trim();
98
+ const weights = rawWeights.split(",").map((w) => w.trim()).filter(Boolean).map((w) => w === "regular" ? "400" : w);
99
+ return { family, weights: weights.length ? weights : ["400"] };
100
+ }).filter((f) => f.family);
101
+ }
102
+ function fontFamilyFromStyle(m) {
103
+ const ident = str(m["font-ident"]);
104
+ if (ident.startsWith("T:")) {
105
+ const fam = ident.split(":")[1]?.trim();
106
+ if (fam) return fam;
107
+ }
108
+ return str(m["font-family"]).replace(/['"]/g, "");
109
+ }
110
+ function typekitFontLoads(fontString) {
111
+ const byFamily = /* @__PURE__ */ new Map();
112
+ for (const ident of fontString.split(",").map((s) => s.trim())) {
113
+ if (!ident.startsWith("T:")) continue;
114
+ const [, family = "", variant = ""] = ident.split(":");
115
+ const fam = family.trim();
116
+ if (!fam) continue;
117
+ const v = variant.trim();
118
+ const digit = /^[ni](\d)$/.exec(v);
119
+ const weight = digit ? `${Number(digit[1]) * 100}` : v === "regular" ? "400" : v;
120
+ if (!weight) continue;
121
+ const ws = byFamily.get(fam) ?? [];
122
+ if (!ws.includes(weight)) ws.push(weight);
123
+ byFamily.set(fam, ws);
124
+ }
125
+ return [...byFamily].map(([family, weights]) => ({ family, weights }));
126
+ }
127
+ function mergeFontLoads(base2, extra) {
128
+ const out = base2.map((f) => ({ family: f.family, weights: [...f.weights] }));
129
+ for (const e of extra) {
130
+ const existing = out.find((f) => f.family === e.family);
131
+ if (existing) {
132
+ for (const w of e.weights) if (!existing.weights.includes(w)) existing.weights.push(w);
133
+ } else {
134
+ out.push({ family: e.family, weights: [...e.weights] });
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+ var CONFIDENCE_MIN = 0.5;
140
+ function sectionFromBlock(b, pageUid, diagnostics) {
141
+ const a = archetype(b);
142
+ if (a.confidence < CONFIDENCE_MIN) {
143
+ diagnostics.push({
144
+ kind: "low-confidence-block",
145
+ where: pageUid,
146
+ message: `block mapped to ${a.sliceType} at ${a.confidence}`
147
+ });
148
+ }
149
+ const heading = visibleText(b.title, b._title);
150
+ const body = visibleText(b.body, b._body);
151
+ const headingRole = heading !== void 0 ? textRole(b._title) : void 0;
152
+ const bodyRole = body !== void 0 ? textRole(b._body) : void 0;
153
+ const headingStyle = heading !== void 0 ? inlineStyle(b._title) : void 0;
154
+ const bodyStyle = body !== void 0 ? inlineStyle(b._body) : void 0;
155
+ const block = {};
156
+ for (const [k, v] of Object.entries(b.styles ?? {})) {
157
+ const cleaned = cleanCssValue(v);
158
+ if (cleaned) block[k] = cleaned;
159
+ }
160
+ const presentation = {
161
+ ...headingRole ? { headingRole } : {},
162
+ ...bodyRole ? { bodyRole } : {},
163
+ ...headingStyle ? { headingStyle } : {},
164
+ ...bodyStyle ? { bodyStyle } : {},
165
+ ...Object.keys(block).length ? { block } : {}
166
+ };
167
+ const section = {
168
+ sliceType: a.sliceType,
169
+ variation: a.variation,
170
+ confidence: a.confidence,
171
+ fields: {
172
+ ...heading !== void 0 ? { heading } : {},
173
+ ...body !== void 0 ? { body } : {},
174
+ ...b.media?.media ? { media: b.media.media } : {},
175
+ ...b.backgroundMedia?.media ? { backgroundMedia: b.backgroundMedia.media } : {},
176
+ ...b.ratio ? { ratio: String(b.ratio) } : {},
177
+ ...b.loadEffect ? { anim: String(b.loadEffect) } : {}
178
+ },
179
+ ...Object.keys(presentation).length ? { presentation } : {}
180
+ };
181
+ if (Array.isArray(b.items) && b.items.length > 0) {
182
+ section.children = b.items.map((child) => sectionFromBlock(child, pageUid, diagnostics));
183
+ }
184
+ return section;
185
+ }
186
+ function slugify(s) {
187
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "page";
188
+ }
189
+ function normalizePages(raw) {
190
+ const diagnostics = [];
191
+ const pages = raw.pages.map((p) => {
192
+ const uid = slugify(String(p.title ?? ""));
193
+ return {
194
+ uid,
195
+ title: String(p.title ?? ""),
196
+ description: String(p.description ?? ""),
197
+ sections: (p.items ?? []).map((b) => sectionFromBlock(b, uid, diagnostics))
198
+ };
199
+ });
200
+ return { pages, diagnostics };
201
+ }
202
+ function normalizeTheme(raw) {
203
+ const colors = Object.entries(raw.styles.colors ?? {}).map(([role, value]) => ({
204
+ role,
205
+ value: String(value)
206
+ }));
207
+ const textStyles = [];
208
+ for (const v of Object.values(raw.styles.text ?? {})) {
209
+ const entry = v ?? {};
210
+ const innerKey = Object.keys(entry).find((k) => /^\.text\d+$/.test(k));
211
+ if (!innerKey) continue;
212
+ const m = entry[innerKey] ?? {};
213
+ const transform = cleanCssValue(m["text-transform"]);
214
+ const tracking = cleanCssValue(m["letter-spacing"]);
215
+ const mobileSize = cleanCssValue(m["__media_mobile_font-size"]);
216
+ const mobileLineHeight = cleanCssValue(m["__media_mobile_line-height"]);
217
+ textStyles.push({
218
+ role: innerKey.slice(1),
219
+ // ".text11" -> "text11"
220
+ label: str(entry._label),
221
+ fontFamily: fontFamilyFromStyle(m),
222
+ size: cleanCssValue(m["font-size"]) || "16px",
223
+ weight: typeof m["font-weight"] === "number" ? m["font-weight"] : str(m["font-weight"]) || 400,
224
+ lineHeight: cleanCssValue(m["line-height"]) || "1.5",
225
+ ...transform && transform !== "none" ? { transform } : {},
226
+ ...tracking ? { letterSpacing: tracking } : {},
227
+ ...mobileSize ? { mobileSize } : {},
228
+ ...mobileLineHeight ? { mobileLineHeight } : {}
229
+ });
230
+ }
231
+ const fonts = raw.settings.fonts ?? {};
232
+ const roleFont = (r) => textStyles.find((t) => t.role === r)?.fontFamily ?? "";
233
+ return {
234
+ colors,
235
+ fonts: {
236
+ heading: str(fonts.heading) || roleFont("text0"),
237
+ body: str(fonts.body) || roleFont("text1")
238
+ },
239
+ fontLoad: mergeFontLoads(
240
+ parseGoogleFonts(str(fonts.google)),
241
+ typekitFontLoads(str(fonts.string))
242
+ ),
243
+ textStyles
244
+ };
245
+ }
246
+
247
+ // src/blux/collections.ts
248
+ function singularSlug(name) {
249
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
250
+ return slug.replace(/s$/, "") || "item";
251
+ }
252
+ var RICHTEXT_KEYS = /* @__PURE__ */ new Set(["body", "description"]);
253
+ function fieldType(key, value) {
254
+ if (RICHTEXT_KEYS.has(key)) return "richtext";
255
+ if (value && typeof value === "object" && "media" in value) return "image";
256
+ if (Array.isArray(value)) return "group";
257
+ if (typeof value === "boolean") return "boolean";
258
+ if (typeof value === "number") return "number";
259
+ if (key === "date") return "date";
260
+ if (/^(url|link)/.test(key)) return "link";
261
+ return "text";
262
+ }
263
+ var isStyleKey = (key) => key.startsWith("_");
264
+ function deriveFields(feed) {
265
+ const seen = /* @__PURE__ */ new Map();
266
+ for (const d of feed.fields ?? []) {
267
+ if (d.field) seen.set(d.field, "text");
268
+ }
269
+ for (const item of feed.items ?? []) {
270
+ for (const [key, value] of Object.entries(item)) {
271
+ if (isStyleKey(key)) continue;
272
+ if (!seen.has(key) || seen.get(key) === "text") seen.set(key, fieldType(key, value));
273
+ }
274
+ }
275
+ return [...seen.entries()].map(([key, type]) => ({ key, type }));
276
+ }
277
+ function recordUid(values, i) {
278
+ const title = typeof values.title === "string" ? values.title : "";
279
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
280
+ return slug || `item-${i}`;
281
+ }
282
+ function modelCollections(raw) {
283
+ const out = [];
284
+ for (const feed of Object.values(raw.feeds)) {
285
+ const label = String(feed.name ?? "");
286
+ const items = feed.items ?? [];
287
+ const records = items.map((item, i) => {
288
+ const mediaRefs = [];
289
+ for (const value of Object.values(item)) {
290
+ if (value && typeof value === "object" && typeof value.media === "string") {
291
+ mediaRefs.push(value.media);
292
+ }
293
+ }
294
+ const values = Object.fromEntries(Object.entries(item).filter(([key]) => !isStyleKey(key)));
295
+ return { uid: recordUid(item, i), values, mediaRefs };
296
+ });
297
+ out.push({
298
+ apiId: singularSlug(label),
299
+ label,
300
+ publishRoute: feed.publish ? String(feed.publish) : null,
301
+ fields: deriveFields(feed),
302
+ records
303
+ });
304
+ }
305
+ return out;
306
+ }
307
+
308
+ // src/blux/assemble.ts
309
+ function assembleIR(input) {
310
+ const raw = parseBluxSite(input.siteJson);
311
+ const { pages, diagnostics: pageDiags } = normalizePages(raw);
312
+ const theme = normalizeTheme(raw);
313
+ const collections = modelCollections(raw);
314
+ const urlMap = collectAssetUrls(input.htmls);
315
+ const diagnostics = [...pageDiags];
316
+ const assets = Object.entries(raw.media).map(([id, m]) => {
317
+ const sourceUrl = urlMap.get(id) ?? null;
318
+ if (!sourceUrl) {
319
+ diagnostics.push({
320
+ kind: "unresolved-asset",
321
+ where: id,
322
+ message: `no CDN url for ${m.name ?? id}`
323
+ });
324
+ }
325
+ return {
326
+ id,
327
+ sourceUrl,
328
+ name: String(m.name ?? ""),
329
+ mime: String(m.type ?? ""),
330
+ alt: String(m.name ?? "")
331
+ };
332
+ });
333
+ return {
334
+ meta: raw.meta,
335
+ theme,
336
+ pages,
337
+ collections,
338
+ assets,
339
+ diagnostics
340
+ };
341
+ }
342
+
343
+ // src/blux/emit/plan.ts
344
+ var richText = (html) => ({ __richtext_html: html });
345
+ var assetRef = (id) => ({ __asset_id: id });
346
+
347
+ // src/blux/emit/custom-types.ts
348
+ var FIELD_CONFIG = {
349
+ text: () => ({ type: "Text", config: {} }),
350
+ richtext: () => ({
351
+ type: "StructuredText",
352
+ config: { multi: "paragraph,strong,em,hyperlink,list-item,o-list-item" }
353
+ }),
354
+ image: () => ({ type: "Image", config: { constraint: {}, thumbnails: [] } }),
355
+ group: () => ({ type: "Group", config: { fields: { value: { type: "Text", config: {} } } } }),
356
+ date: () => ({ type: "Date", config: {} }),
357
+ boolean: () => ({ type: "Boolean", config: {} }),
358
+ number: () => ({ type: "Number", config: {} }),
359
+ link: () => ({ type: "Link", config: { allowTargetBlank: true } })
360
+ };
361
+ function buildCustomType(c) {
362
+ const Main = {};
363
+ for (const f of c.fields) {
364
+ const spec = FIELD_CONFIG[f.type]();
365
+ Main[f.key] = { ...spec, config: { ...spec.config, label: f.key } };
366
+ }
367
+ return {
368
+ id: c.apiId,
369
+ label: c.label,
370
+ repeatable: true,
371
+ json: { id: c.apiId, label: c.label, repeatable: true, status: true, json: { Main } }
372
+ };
373
+ }
374
+
375
+ // src/blux/emit/coerce-html.ts
376
+ var BLOCK_RE = /<(h[1-6]|p|div)(\s[^>]*)?>[\s\S]*?<\/\1>/i;
377
+ function coerceHeadingHtml(html, allowed) {
378
+ const m = html.match(BLOCK_RE);
379
+ const block = m ? m[0] : html;
380
+ const tagMatch = block.match(/^<(h[1-6]|p|div)(\s[^>]*)?>/i);
381
+ const tag = tagMatch?.[1]?.toLowerCase();
382
+ if (tag && allowed.includes(tag)) return block;
383
+ const levels = allowed.filter((t) => /^h[1-6]$/.test(t)).map((t) => Number(t[1]));
384
+ const target = tag && tag.startsWith("h") ? `h${levels.reduce((b, l) => Math.abs(l - Number(tag[1])) < Math.abs(b - Number(tag[1])) ? l : b)}` : `h${Math.max(...levels)}`;
385
+ if (!tag) return `<${target}>${block}</${target}>`;
386
+ return block.replace(new RegExp(`^<${tag}`, "i"), `<${target}`).replace(new RegExp(`</${tag}>$`, "i"), `</${target}>`);
387
+ }
388
+ function demoteHeadingsHtml(html) {
389
+ return html.replace(/<(\/?)h[1-6](\s[^>]*)?>/gi, "<$1p$2>");
390
+ }
391
+
392
+ // src/blux/emit/slices.ts
393
+ var HEADING_TAGS = {
394
+ hero: ["h1", "h2"],
395
+ media_text: ["h2", "h3"],
396
+ section_grid: ["h2", "h3"],
397
+ section_grid_item: ["h3", "h4"]
398
+ };
399
+ function rt(html) {
400
+ return html ? richText(html) : void 0;
401
+ }
402
+ function rtHeading(html, slot) {
403
+ return html ? richText(coerceHeadingHtml(html, HEADING_TAGS[slot])) : void 0;
404
+ }
405
+ function rtBody(html) {
406
+ return html ? richText(demoteHeadingsHtml(html)) : void 0;
407
+ }
408
+ function img(id) {
409
+ return id ? assetRef(id) : void 0;
410
+ }
411
+ function compact(o) {
412
+ return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
413
+ }
414
+ function sectionToSlice(s) {
415
+ const f = s.fields;
416
+ switch (s.sliceType) {
417
+ case "hero":
418
+ return {
419
+ slice_type: "hero",
420
+ variation: "default",
421
+ primary: compact({
422
+ heading: rtHeading(f.heading, "hero"),
423
+ body: rtBody(f.body),
424
+ background_image: img(f.backgroundMedia ?? f.media)
425
+ }),
426
+ items: []
427
+ };
428
+ case "media_text":
429
+ return {
430
+ slice_type: "media_text",
431
+ variation: s.variation === "imageLeft" ? "imageLeft" : "imageRight",
432
+ primary: compact({
433
+ heading: rtHeading(f.heading, "media_text"),
434
+ body: rtBody(f.body),
435
+ media: img(f.media)
436
+ }),
437
+ items: []
438
+ };
439
+ case "collection_list":
440
+ return {
441
+ slice_type: "collection_list",
442
+ variation: s.variation === "list" ? "list" : "grid",
443
+ primary: compact({
444
+ heading: rtHeading(f.heading, "section_grid"),
445
+ collection_type: s.collectionRef?.apiId ?? "",
446
+ max_items: 24
447
+ }),
448
+ items: []
449
+ };
450
+ case "grid":
451
+ case "slider":
452
+ return {
453
+ slice_type: "section_grid",
454
+ variation: "default",
455
+ primary: compact({
456
+ heading: rtHeading(f.heading, "section_grid"),
457
+ columns: f.columns ?? 3
458
+ }),
459
+ items: (s.children ?? []).map(
460
+ (c) => compact({
461
+ item_heading: rtHeading(c.fields.heading, "section_grid_item"),
462
+ item_body: rtBody(c.fields.body),
463
+ item_media: img(c.fields.media)
464
+ })
465
+ ).filter((item) => Object.keys(item).length > 0)
466
+ };
467
+ case "rich_text":
468
+ default:
469
+ return {
470
+ slice_type: "rich_text",
471
+ variation: "default",
472
+ primary: compact({ content: rt(f.heading ? `${f.heading}${f.body ?? ""}` : f.body) }),
473
+ items: []
474
+ };
475
+ }
476
+ }
477
+
478
+ // src/blux/emit/flatten.ts
479
+ var isContainer = (s) => s.sliceType === "grid" || s.sliceType === "slider";
480
+ var isFlatLeaf = (s) => (s.sliceType === "media_text" || s.sliceType === "rich_text") && !(s.children ?? []).length;
481
+ function flattenSections(sections) {
482
+ const out = [];
483
+ for (const s of sections) {
484
+ const children = s.children ?? [];
485
+ if (s.sliceType === "hero" && s.fields.media && s.fields.backgroundMedia) {
486
+ const { media, ...heroFields } = s.fields;
487
+ const { children: _heroKids, ...heroSelf } = s;
488
+ out.push({ ...heroSelf, fields: heroFields });
489
+ out.push({
490
+ sliceType: "media_text",
491
+ variation: "imageRight",
492
+ confidence: s.confidence,
493
+ fields: { media }
494
+ });
495
+ out.push(...flattenSections(children));
496
+ continue;
497
+ }
498
+ const keepable = isContainer(s) && !s.fields.media && !s.fields.body;
499
+ if (!children.length) {
500
+ if (isContainer(s) && !keepable) {
501
+ out.push({ ...s, sliceType: s.fields.media ? "media_text" : "rich_text" });
502
+ } else {
503
+ out.push(s);
504
+ }
505
+ } else if (keepable && children.every(isFlatLeaf)) {
506
+ out.push(s);
507
+ } else if (isContainer(s)) {
508
+ const { children: _kids, ...self } = s;
509
+ if (self.fields.media) {
510
+ out.push({ ...self, sliceType: "media_text" });
511
+ } else if (self.fields.heading || self.fields.body) {
512
+ out.push({ ...self, sliceType: "rich_text" });
513
+ }
514
+ out.push(...flattenSections(children));
515
+ } else {
516
+ const { children: _hoisted, ...self } = s;
517
+ out.push(self);
518
+ out.push(...flattenSections(children));
519
+ }
520
+ }
521
+ return out;
522
+ }
523
+
524
+ // src/blux/emit/migration-plan.ts
525
+ var IMAGE_FIELDS = /* @__PURE__ */ new Set(["background_image", "media", "item_media"]);
526
+ var RICHTEXT = /* @__PURE__ */ new Set(["body", "description"]);
527
+ function isEmptyPage(p) {
528
+ return !p.sections.length && !p.title.trim();
529
+ }
530
+ var NON_IMAGE_EXT = /\.(mp4|mov|webm|avi|mp3|wav|pdf|zip)$/i;
531
+ function isImageAsset(a) {
532
+ if (!a) return true;
533
+ if (a.mime) return a.mime.startsWith("image/");
534
+ return !NON_IMAGE_EXT.test(a.name);
535
+ }
536
+ function recordData(rec) {
537
+ const data = {};
538
+ for (const [key, value] of Object.entries(rec.values)) {
539
+ if (value && typeof value === "object" && typeof value.media === "string") {
540
+ data[key] = assetRef(value.media);
541
+ } else if (RICHTEXT.has(key) && typeof value === "string") {
542
+ data[key] = richText(demoteHeadingsHtml(value));
543
+ } else {
544
+ data[key] = value;
545
+ }
546
+ }
547
+ return data;
548
+ }
549
+ function buildMigrationPlan(ir) {
550
+ const diagnostics = [];
551
+ const customTypes = ir.collections.map(buildCustomType);
552
+ const assetById = new Map(ir.assets.map((a) => [a.id, a]));
553
+ const dropNonImages = (rec, keys, where) => {
554
+ for (const [key, val] of Object.entries(rec)) {
555
+ if (keys && !keys.has(key) || !val || typeof val !== "object" || !("__asset_id" in val))
556
+ continue;
557
+ const asset = assetById.get(val.__asset_id);
558
+ if (!isImageAsset(asset)) {
559
+ diagnostics.push({
560
+ kind: "non-image-in-image-field",
561
+ where: `${where}.${key}`,
562
+ message: `${asset?.mime || "unknown mime"} asset dropped from image field`
563
+ });
564
+ delete rec[key];
565
+ }
566
+ }
567
+ };
568
+ const documents = [];
569
+ const stylesManifest = [];
570
+ for (const page of ir.pages) {
571
+ if (isEmptyPage(page)) {
572
+ diagnostics.push({
573
+ kind: "empty-page",
574
+ where: page.uid,
575
+ message: "page has no title and no sections; skipped"
576
+ });
577
+ continue;
578
+ }
579
+ const slices = [];
580
+ const styleEntries = [];
581
+ for (const section of flattenSections(page.sections)) {
582
+ const slice = sectionToSlice(section);
583
+ for (const rec of [slice.primary, ...slice.items]) {
584
+ dropNonImages(rec, IMAGE_FIELDS, `${page.uid}/${slice.slice_type}`);
585
+ }
586
+ const STRUCTURAL = /* @__PURE__ */ new Set(["columns", "collection_type", "max_items"]);
587
+ const hasContent = Object.keys(slice.primary).some((k) => !STRUCTURAL.has(k)) || slice.items.length > 0;
588
+ if (!hasContent) {
589
+ diagnostics.push({
590
+ kind: "empty-slice",
591
+ where: `${page.uid}/${slice.slice_type}`,
592
+ message: "slice has no content after filtering; dropped"
593
+ });
594
+ continue;
595
+ }
596
+ const itemPresentations = (section.children ?? []).filter((c) => c.fields.heading || c.fields.body || c.fields.media).map((c) => c.presentation ?? null);
597
+ const entry = {
598
+ index: slices.length,
599
+ sliceType: slice.slice_type,
600
+ ...section.presentation ? { presentation: section.presentation } : {},
601
+ ...slice.slice_type === "section_grid" && itemPresentations.some(Boolean) ? { items: itemPresentations } : {}
602
+ };
603
+ if (entry.presentation || entry.items) styleEntries.push(entry);
604
+ slices.push(slice);
605
+ }
606
+ stylesManifest.push({ pageUid: page.uid, slices: styleEntries });
607
+ documents.push({
608
+ type: "page",
609
+ uid: page.uid,
610
+ // the page type's title is StructuredText(single heading1)
611
+ data: {
612
+ title: richText(coerceHeadingHtml(page.title || page.uid, ["h1"])),
613
+ slices
614
+ }
615
+ });
616
+ }
617
+ for (const c of ir.collections) {
618
+ for (const rec of c.records) {
619
+ const data = recordData(rec);
620
+ dropNonImages(data, null, `${c.apiId}/${rec.uid}`);
621
+ documents.push({ type: c.apiId, uid: rec.uid, data });
622
+ }
623
+ }
624
+ const assets = ir.assets.filter((a) => a.sourceUrl !== null).map((a) => ({ id: a.id, url: a.sourceUrl, alt: a.alt }));
625
+ return { customTypes, documents, assets, stylesManifest, diagnostics };
626
+ }
627
+
628
+ // src/blux/emit/theme.ts
629
+ function emitThemeCss(theme) {
630
+ const lines = [];
631
+ if (theme.fontLoad.length) {
632
+ const spec = theme.fontLoad.map((f) => `${f.family} ${f.weights.join(",")}`).join("; ");
633
+ lines.push(`/* Fonts to load \u2014 ${spec} */`);
634
+ }
635
+ lines.push("@theme {");
636
+ for (const c of theme.colors) lines.push(` --color-${c.role}: ${c.value};`);
637
+ lines.push(` --font-heading: ${theme.fonts.heading || "sans-serif"};`);
638
+ lines.push(` --font-body: ${theme.fonts.body || "sans-serif"};`);
639
+ for (const t of theme.textStyles) {
640
+ if (t.label) lines.push(` /* ${t.role} \u2014 ${t.label} */`);
641
+ lines.push(` --text-${t.role}: ${t.size};`);
642
+ lines.push(` --text-${t.role}--line-height: ${t.lineHeight};`);
643
+ lines.push(` --text-${t.role}--font-weight: ${t.weight};`);
644
+ if (t.fontFamily) lines.push(` --text-${t.role}--font-family: ${t.fontFamily};`);
645
+ if (t.transform) lines.push(` --text-${t.role}--text-transform: ${t.transform};`);
646
+ if (t.letterSpacing) lines.push(` --text-${t.role}--letter-spacing: ${t.letterSpacing};`);
647
+ if (t.mobileSize) lines.push(` --text-${t.role}--mobile-font-size: ${t.mobileSize};`);
648
+ if (t.mobileLineHeight)
649
+ lines.push(` --text-${t.role}--mobile-line-height: ${t.mobileLineHeight};`);
650
+ }
651
+ lines.push("}");
652
+ return lines.join("\n") + "\n";
653
+ }
654
+ function emitRolesCss(theme) {
655
+ if (!theme.textStyles.length) return "";
656
+ const lines = [
657
+ "/* Role utilities \u2014 one .txt-role-textN per Blux text style, mapping its",
658
+ " @theme vars onto the heading/paragraph it wraps. Generated by blux emit. */"
659
+ ];
660
+ for (const t of theme.textStyles) {
661
+ const r = t.role;
662
+ lines.push(`.txt-role-${r} :is(h1, h2, h3, h4, h5, h6, p) {`);
663
+ if (t.fontFamily) lines.push(` font-family: var(--text-${r}--font-family);`);
664
+ lines.push(
665
+ ` font-size: var(--text-${r});`,
666
+ ` font-weight: var(--text-${r}--font-weight);`,
667
+ ` line-height: var(--text-${r}--line-height);`,
668
+ ` letter-spacing: var(--text-${r}--letter-spacing, normal);`,
669
+ ` text-transform: var(--text-${r}--text-transform, none);`,
670
+ ` margin: 0;`,
671
+ `}`
672
+ );
673
+ }
674
+ return lines.join("\n") + "\n";
675
+ }
676
+
677
+ // src/blux/emit/review.ts
678
+ function buildReviewManifest(ir, opts) {
679
+ const pairs = ir.pages.filter((p) => !isEmptyPage(p)).map((p) => ({
680
+ uid: p.uid,
681
+ converted: `${opts.convertedBase}/${p.uid}`,
682
+ // The home page maps to the site root; other pages to /uid.
683
+ original: p.uid === "home" ? `${opts.bluxBase}/` : `${opts.bluxBase}/${p.uid}`
684
+ }));
685
+ return { pairs, diagnostics: ir.diagnostics };
686
+ }
687
+
688
+ // src/blux/validate.ts
689
+ var DROP_ELEMENTS = /<(script|style|head|noscript|svg|template)[\s\S]*?<\/\1>/gi;
690
+ var COMMENTS = /<!--[\s\S]*?-->/g;
691
+ function decodeEntities(s) {
692
+ return s.replace(/&#(\d+);/g, (_, n) => codePoint(Number(n))).replace(/&#x([0-9a-f]+);/gi, (_, h) => codePoint(parseInt(h, 16))).replace(/&nbsp;/gi, " ").replace(/&(?:amp);/gi, "&").replace(/&(?:lt);/gi, "<").replace(/&(?:gt);/gi, ">").replace(/&(?:quot);/gi, '"').replace(/&(?:apos|#39);/gi, "'").replace(/&[a-z][a-z0-9]*;/gi, " ");
693
+ }
694
+ function codePoint(n) {
695
+ try {
696
+ return String.fromCodePoint(n);
697
+ } catch {
698
+ return " ";
699
+ }
700
+ }
701
+ function normalizeText(s) {
702
+ return decodeEntities(s).toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
703
+ }
704
+ function hasWord(run) {
705
+ return /[a-z0-9]{3,}/.test(run);
706
+ }
707
+ function flattenText(html) {
708
+ const text = html.replace(DROP_ELEMENTS, " ").replace(COMMENTS, " ").replace(/<[^>]+>/g, " ");
709
+ return normalizeText(text);
710
+ }
711
+ function extractTextRuns(html) {
712
+ const stripped = html.replace(DROP_ELEMENTS, " ").replace(COMMENTS, " ");
713
+ const seen = /* @__PURE__ */ new Set();
714
+ for (const piece of stripped.split(/<[^>]+>/)) {
715
+ const run = normalizeText(piece);
716
+ if (hasWord(run)) seen.add(run);
717
+ }
718
+ return [...seen];
719
+ }
720
+ function validateCoverage(exportHtml, renderedHtml) {
721
+ const runs = extractTextRuns(exportHtml);
722
+ const blob = ` ${flattenText(renderedHtml)} `;
723
+ const missing = runs.filter((run) => !blob.includes(` ${run} `));
724
+ const covered = runs.length - missing.length;
725
+ const coveragePct = runs.length === 0 ? 100 : Math.round(covered / runs.length * 100);
726
+ return { total: runs.length, covered, missing, coveragePct };
727
+ }
728
+
729
+ // src/blux/grid/parse-grid.ts
730
+ import { parse as parse2 } from "node-html-parser";
731
+
732
+ // src/blux/grid/token.ts
733
+ var TOKEN_RE = /\bgrid-(\d+|any)(?:-(r|s)(\d+))?\b/;
734
+ function parseGridToken(className) {
735
+ const m = TOKEN_RE.exec(className);
736
+ if (!m) return null;
737
+ const cols = m[1] === "any" ? "any" : Number(m[1]);
738
+ const token = { cols, raw: m[0] };
739
+ if (m[2] === "r") token.ratio = Number(m[3]);
740
+ if (m[2] === "s") token.spacing = Number(m[3]);
741
+ return token;
742
+ }
743
+
744
+ // src/blux/grid/leaf.ts
745
+ import { parse } from "node-html-parser";
746
+ var ROLE_RE = /\btext\d+\b/;
747
+ function textRoleFromClass(className) {
748
+ return ROLE_RE.exec(className)?.[0];
749
+ }
750
+ function headingLevel(el) {
751
+ const m = /^H([1-6])$/.exec(el.tagName ?? "");
752
+ return m ? Number(m[1]) : 2;
753
+ }
754
+ function blockPlainText(html) {
755
+ const BR = "\uE000";
756
+ const text = parse(html.replace(/<br\b[^>]*>/gi, BR)).text;
757
+ return text.replace(/\s+/g, " ").replace(/ *\uE000 */g, "\n").trim();
758
+ }
759
+ function uuidFromUrl(url) {
760
+ const base2 = url.split(/[?#]/)[0] ?? "";
761
+ const file = base2.split("/").pop() ?? "";
762
+ const dot = file.lastIndexOf(".");
763
+ return dot > 0 ? { id: file.slice(0, dot), ext: file.slice(dot + 1) } : { id: file };
764
+ }
765
+ function stripAssetExt(rawId, ext) {
766
+ return ext && rawId.endsWith(`.${ext}`) ? rawId.slice(0, -(ext.length + 1)) : rawId;
767
+ }
768
+ function cssProp(style, prop) {
769
+ const m = new RegExp(`(?:^|;|\\s)${prop}\\s*:\\s*([^;]+)`, "i").exec(style);
770
+ return m?.[1]?.trim();
771
+ }
772
+ var CENTERED = /* @__PURE__ */ new Set(["center", "center center", "50% 50%", "50%"]);
773
+ function readBgSizing(el) {
774
+ const style = el.getAttribute("style") ?? "";
775
+ const out = {};
776
+ const size = cssProp(style, "background-size")?.toLowerCase();
777
+ if (size === "auto" || size === "contain") out.fit = size;
778
+ const pos = cssProp(style, "background-position");
779
+ if (pos && !CENTERED.has(pos.toLowerCase())) out.position = pos;
780
+ return out;
781
+ }
782
+ function readImgSizing(holder) {
783
+ const style = holder.getAttribute("style") ?? "";
784
+ const out = {};
785
+ const w = cssProp(style, "width");
786
+ const wpx = w ? /^(\d+(?:\.\d+)?)px$/i.exec(w) : null;
787
+ if (wpx?.[1]) out.width = Math.round(parseFloat(wpx[1]));
788
+ const ogr = holder.querySelector(".mediaRatio")?.getAttribute("data-og-ratio");
789
+ if (ogr) {
790
+ const n = Number(ogr);
791
+ if (Number.isFinite(n)) out.aspect = Math.round(n * 1e3) / 1e3;
792
+ }
793
+ const fit = cssProp(style, "background-size")?.toLowerCase();
794
+ if (fit === "contain" || fit === "cover") out.fit = fit;
795
+ const mh = cssProp(style, "min-height");
796
+ if (mh) out.minHeight = mh;
797
+ return out;
798
+ }
799
+ function readVideoPlayback(video) {
800
+ const flags = ["controls", "playsinline", "autoplay", "loop", "muted"];
801
+ const pb = {};
802
+ for (const f of flags) if (video.hasAttribute(f)) pb[f] = true;
803
+ return Object.keys(pb).length ? pb : void 0;
804
+ }
805
+ function readVideoAspect(video) {
806
+ let raw;
807
+ let anc = video.parentNode;
808
+ for (let i = 0; i < 3 && anc && !raw; i++) {
809
+ raw = anc.getAttribute?.("data-og-ratio") ?? void 0;
810
+ if (!raw) {
811
+ const mr = anc.querySelector?.(".mediaRatio");
812
+ raw = mr ? cssProp(mr.getAttribute("style") ?? "", "padding-bottom") : void 0;
813
+ }
814
+ anc = anc.parentNode;
815
+ }
816
+ if (!raw) return void 0;
817
+ const n = parseFloat(raw.replace(/%\s*$/, ""));
818
+ return Number.isFinite(n) ? Math.round(n * 1e3) / 1e3 : void 0;
819
+ }
820
+ function mediaFromElement(el) {
821
+ if (el.tagName === "VIDEO") {
822
+ const src = el.getAttribute("src") ?? "";
823
+ const { id, ext } = uuidFromUrl(src);
824
+ if (!id) return null;
825
+ const clean = src.split(/[?#]/)[0] ?? "";
826
+ const slash = clean.lastIndexOf("/");
827
+ const base2 = slash >= 0 ? clean.slice(0, slash + 1) : void 0;
828
+ const aspect = readVideoAspect(el);
829
+ const playback = readVideoPlayback(el);
830
+ return {
831
+ kind: "video",
832
+ assetId: id,
833
+ ...ext ? { ext } : {},
834
+ ...base2 ? { base: base2 } : {},
835
+ ...aspect !== void 0 ? { aspect } : {},
836
+ ...playback ? { playback } : {}
837
+ };
838
+ }
839
+ const img2 = el.classList.contains("camediaload") && el.getAttribute("data-media") ? el : el.querySelector(".camediaload[data-media]");
840
+ if (img2) {
841
+ const rawId = img2.getAttribute("data-media");
842
+ if (rawId) {
843
+ const ext = img2.getAttribute("data-ext") ?? void 0;
844
+ const base2 = img2.getAttribute("data-base") ?? void 0;
845
+ return {
846
+ kind: "image",
847
+ assetId: stripAssetExt(rawId, ext),
848
+ ...ext ? { ext } : {},
849
+ ...base2 ? { base: base2 } : {},
850
+ ...readImgSizing(img2)
851
+ };
852
+ }
853
+ }
854
+ const video = el.querySelector("video");
855
+ if (video) return mediaFromElement(video);
856
+ return null;
857
+ }
858
+
859
+ // src/blux/grid/parse-grid.ts
860
+ var DEFAULT_TOKEN = { cols: 1, raw: "grid-1" };
861
+ var isElement = (n) => n.tagName !== void 0 && n.tagName !== null;
862
+ var hasClass = (el, c) => el.classNames.split(/\s+/).includes(c);
863
+ var isMediaHolder = (el) => hasClass(el, "block-media-holder") || hasClass(el, "camediaload") && !!el.getAttribute("data-media") || el.tagName === "VIDEO";
864
+ var isLeafElement = (el) => hasClass(el, "block-title") || hasClass(el, "block-body") || hasClass(el, "block-subtitle") || isMediaHolder(el);
865
+ var isLeafAnchor = (el) => el.tagName === "A" && collectStructuralChildren(el).length === 0;
866
+ var isStructural = (el) => isLeafElement(el) || el.hasAttribute("data-exec") || // Blux custom-code embed (e.g. map mount)
867
+ hasClass(el, "cagrid") || isLeafAnchor(el) || parseGridToken(el.classNames) !== null;
868
+ function collectStructuralChildren(el) {
869
+ const out = [];
870
+ for (const child of el.childNodes) {
871
+ if (!isElement(child)) continue;
872
+ if (isStructural(child)) out.push(child);
873
+ else out.push(...collectStructuralChildren(child));
874
+ }
875
+ return out;
876
+ }
877
+ function nodeHasText(n) {
878
+ if (n.kind === "heading" || n.kind === "body") return blockPlainText(n.html) !== "";
879
+ if (n.kind === "subtitle") return n.text.trim() !== "";
880
+ return false;
881
+ }
882
+ function isDisabledWithin(caption, holder) {
883
+ let a = caption;
884
+ while (a) {
885
+ if (hasClass(a, "disable")) return true;
886
+ if (a === holder) return false;
887
+ a = a.parentNode;
888
+ }
889
+ return false;
890
+ }
891
+ function parseNode(el) {
892
+ if (hasClass(el, "block-title") && /^H[1-6]$/.test(el.tagName ?? "")) {
893
+ const role = textRoleFromClass(el.classNames);
894
+ return {
895
+ kind: "heading",
896
+ ...role ? { role } : {},
897
+ level: headingLevel(el),
898
+ html: el.innerHTML
899
+ };
900
+ }
901
+ if (hasClass(el, "block-body")) {
902
+ const role = textRoleFromClass(el.classNames);
903
+ return { kind: "body", ...role ? { role } : {}, html: el.innerHTML };
904
+ }
905
+ if (hasClass(el, "block-subtitle")) {
906
+ const role = textRoleFromClass(el.classNames);
907
+ return { kind: "subtitle", ...role ? { role } : {}, text: blockPlainText(el.innerHTML) };
908
+ }
909
+ if (isMediaHolder(el)) {
910
+ const media = mediaFromElement(el);
911
+ if (media) {
912
+ const captions = el.querySelectorAll(".block-title, .block-body, .block-subtitle").filter((c) => !isDisabledWithin(c, el)).map((c) => parseNode(c)).filter(nodeHasText);
913
+ if (captions.length) {
914
+ return { kind: "stack", children: [{ kind: "media", media }, ...captions] };
915
+ }
916
+ return { kind: "media", media };
917
+ }
918
+ }
919
+ if (el.hasAttribute("data-exec")) {
920
+ return { kind: "raw", html: el.outerHTML };
921
+ }
922
+ if (el.tagName === "A") {
923
+ return { kind: "raw", html: el.outerHTML };
924
+ }
925
+ return parseContainer(el);
926
+ }
927
+ function parseContainer(el) {
928
+ const kids = collectStructuralChildren(el);
929
+ const parsed = kids.map((k) => ({ token: parseGridToken(k.classNames), node: parseNode(k) })).filter((p) => !(p.node.kind === "raw" && p.node.html.trim() === ""));
930
+ const isGrid = hasClass(el, "cagrid");
931
+ const tokenCount = parsed.filter((p) => p.token).length;
932
+ if ((isGrid || tokenCount >= 2) && parsed.length > 0) {
933
+ const cells = parsed.map((p) => ({ token: p.token ?? DEFAULT_TOKEN, node: p.node }));
934
+ if (hasClass(el, "caslider")) {
935
+ const cols = Number(el.getAttribute("data-columns"));
936
+ const slider = Number.isInteger(cols) && cols > 0 ? { columns: cols } : {};
937
+ return { kind: "row", cells, slider };
938
+ }
939
+ return { kind: "row", cells };
940
+ }
941
+ const [only] = parsed;
942
+ if (parsed.length === 1 && only) return only.node;
943
+ if (parsed.length === 0) return { kind: "raw", html: el.innerHTML };
944
+ return { kind: "stack", children: parsed.map((p) => p.node) };
945
+ }
946
+ var BAND_ID_RE = /^page-block-(\d+)$/;
947
+ function bandBackground(el) {
948
+ if (!hasClass(el, "camediaload")) return void 0;
949
+ const rawId = el.getAttribute("data-media");
950
+ if (!rawId) return void 0;
951
+ const ext = el.getAttribute("data-ext") ?? void 0;
952
+ const base2 = el.getAttribute("data-base") ?? void 0;
953
+ return {
954
+ kind: "image",
955
+ assetId: stripAssetExt(rawId, ext),
956
+ ...ext ? { ext } : {},
957
+ ...base2 ? { base: base2 } : {},
958
+ // A band background carries its own render sizing (background-size/position);
959
+ // a corner-anchored `auto` accent must not be centered + full-bleed.
960
+ ...readBgSizing(el)
961
+ };
962
+ }
963
+ function parseGridBands(html) {
964
+ const root = parse2(html);
965
+ const content = root.querySelector("#page-content");
966
+ if (!content) return [];
967
+ const bands = [];
968
+ for (const child of content.childNodes) {
969
+ if (!isElement(child)) continue;
970
+ const m = BAND_ID_RE.exec(child.getAttribute("id") ?? "");
971
+ if (!m) continue;
972
+ const idStr = m[1];
973
+ if (idStr === void 0) continue;
974
+ const background = bandBackground(child);
975
+ bands.push({
976
+ index: Number(idStr),
977
+ ...background ? { background } : {},
978
+ root: parseContainer(child)
979
+ });
980
+ }
981
+ return bands;
982
+ }
983
+
984
+ // src/blux/grid/classify-band.ts
985
+ function collectMedia(node) {
986
+ switch (node.kind) {
987
+ case "media":
988
+ return [node.media];
989
+ case "row":
990
+ return node.cells.flatMap((c) => collectMedia(c.node));
991
+ case "stack":
992
+ return node.children.flatMap(collectMedia);
993
+ case "heading":
994
+ case "body":
995
+ case "subtitle":
996
+ case "widget":
997
+ case "raw":
998
+ return [];
999
+ }
1000
+ }
1001
+ function collectText(node) {
1002
+ switch (node.kind) {
1003
+ case "heading":
1004
+ case "body":
1005
+ case "subtitle":
1006
+ return [node];
1007
+ case "row":
1008
+ return node.cells.flatMap((c) => collectText(c.node));
1009
+ case "stack":
1010
+ return node.children.flatMap(collectText);
1011
+ case "media":
1012
+ case "widget":
1013
+ case "raw":
1014
+ return [];
1015
+ }
1016
+ }
1017
+ function collectWidgets(node) {
1018
+ switch (node.kind) {
1019
+ case "widget":
1020
+ return [node.widget];
1021
+ case "row":
1022
+ return node.cells.flatMap((c) => collectWidgets(c.node));
1023
+ case "stack":
1024
+ return node.children.flatMap(collectWidgets);
1025
+ case "heading":
1026
+ case "body":
1027
+ case "subtitle":
1028
+ case "media":
1029
+ case "raw":
1030
+ return [];
1031
+ }
1032
+ }
1033
+ function collectRaws(node) {
1034
+ switch (node.kind) {
1035
+ case "raw":
1036
+ return [node];
1037
+ case "row":
1038
+ return node.cells.flatMap((c) => collectRaws(c.node));
1039
+ case "stack":
1040
+ return node.children.flatMap(collectRaws);
1041
+ case "heading":
1042
+ case "body":
1043
+ case "subtitle":
1044
+ case "media":
1045
+ case "widget":
1046
+ return [];
1047
+ }
1048
+ }
1049
+ function topRowNode(node) {
1050
+ if (node.kind === "row") return node;
1051
+ if (node.kind === "stack" && node.children.length === 1) {
1052
+ const [only] = node.children;
1053
+ if (only && only.kind === "row") return only;
1054
+ }
1055
+ return null;
1056
+ }
1057
+ function isEmptyRaw(node) {
1058
+ if (node.kind !== "raw") return false;
1059
+ const text = node.html.replace(/<[^>]*>/g, "").trim();
1060
+ return text.length === 0;
1061
+ }
1062
+ function base(band) {
1063
+ return { index: band.index, ...band.background ? { background: band.background } : {} };
1064
+ }
1065
+ function nodeText(node) {
1066
+ switch (node.kind) {
1067
+ case "heading":
1068
+ case "body":
1069
+ return blockPlainText(node.html);
1070
+ case "subtitle":
1071
+ return node.text.trim();
1072
+ case "row":
1073
+ case "stack":
1074
+ case "media":
1075
+ case "widget":
1076
+ case "raw":
1077
+ return "";
1078
+ }
1079
+ }
1080
+ function pureCellMedia(cell) {
1081
+ return cell.node.kind === "media" ? cell.node.media : null;
1082
+ }
1083
+ function textRoleMeta(heading, subtitle) {
1084
+ return {
1085
+ ...heading?.kind === "heading" && heading.role ? { headingRole: heading.role } : {},
1086
+ ...heading?.kind === "heading" ? { headingLevel: heading.level } : {},
1087
+ ...subtitle?.kind === "subtitle" && subtitle.role ? { subtitleRole: subtitle.role } : {}
1088
+ };
1089
+ }
1090
+ function cellRatio(cell) {
1091
+ const t = cell.token;
1092
+ if (typeof t.ratio === "number") return t.ratio;
1093
+ if (t.cols === "any") return 50;
1094
+ return Math.round(100 / t.cols);
1095
+ }
1096
+ function carouselSlides(cells) {
1097
+ const out = [];
1098
+ for (const c of cells) {
1099
+ const n = c.node;
1100
+ if (n.kind === "media") {
1101
+ out.push({ media: n.media });
1102
+ continue;
1103
+ }
1104
+ if (n.kind === "stack" && n.children.length === 2) {
1105
+ const [m, h] = n.children;
1106
+ if (m?.kind === "media" && h?.kind === "heading") {
1107
+ out.push({
1108
+ media: m.media,
1109
+ caption: { html: h.html, level: h.level, ...h.role ? { role: h.role } : {} }
1110
+ });
1111
+ continue;
1112
+ }
1113
+ }
1114
+ return null;
1115
+ }
1116
+ return out.length >= 2 ? out : null;
1117
+ }
1118
+ function galleryMedia(cells) {
1119
+ const out = [];
1120
+ for (const c of cells) {
1121
+ const m = pureCellMedia(c);
1122
+ if (!m) return null;
1123
+ out.push(m);
1124
+ }
1125
+ return out.length >= 2 ? out : null;
1126
+ }
1127
+ function rewriteMapMounts(node, isMapMount) {
1128
+ if (isMapMount(node)) return { kind: "widget", widget: { type: "map" } };
1129
+ switch (node.kind) {
1130
+ case "row":
1131
+ return {
1132
+ kind: "row",
1133
+ cells: node.cells.map((c) => ({
1134
+ token: c.token,
1135
+ node: rewriteMapMounts(c.node, isMapMount)
1136
+ })),
1137
+ // Keep the slider marker — dropping it here would silently demote a
1138
+ // Carousel to Grid whenever a map config is present.
1139
+ ...node.slider ? { slider: node.slider } : {}
1140
+ };
1141
+ case "stack":
1142
+ return { kind: "stack", children: node.children.map((n) => rewriteMapMounts(n, isMapMount)) };
1143
+ case "heading":
1144
+ case "body":
1145
+ case "subtitle":
1146
+ case "media":
1147
+ case "widget":
1148
+ case "raw":
1149
+ return node;
1150
+ }
1151
+ }
1152
+ function soleSignificant(node) {
1153
+ let kids;
1154
+ switch (node.kind) {
1155
+ case "row":
1156
+ kids = node.cells.map((c) => c.node);
1157
+ break;
1158
+ case "stack":
1159
+ kids = node.children;
1160
+ break;
1161
+ case "heading":
1162
+ case "body":
1163
+ case "subtitle":
1164
+ case "media":
1165
+ case "widget":
1166
+ case "raw":
1167
+ kids = [node];
1168
+ break;
1169
+ }
1170
+ const significant = kids.filter((n) => !isEmptyRaw(n));
1171
+ return significant.length === 1 && significant[0] ? significant[0] : node;
1172
+ }
1173
+ function classifyBand(band, opts = {}) {
1174
+ const root = opts.isMapMount ? rewriteMapMounts(band.root, opts.isMapMount) : band.root;
1175
+ const widgets = collectWidgets(root);
1176
+ const media = collectMedia(root);
1177
+ const text = collectText(root);
1178
+ const rowNode = topRowNode(root);
1179
+ const row = rowNode ? rowNode.cells : null;
1180
+ const hasSignificantRaw = collectRaws(root).some((n) => !isEmptyRaw(n));
1181
+ const sole = soleSignificant(root);
1182
+ if (sole.kind === "widget" && sole.widget.type === "map") {
1183
+ return { slice: "LocationMap", ...base(band) };
1184
+ }
1185
+ if (media.length === 1 && media[0]?.kind === "video" && text.length === 0 && widgets.length === 0 && row === null && !hasSignificantRaw) {
1186
+ const v = media[0];
1187
+ return { slice: "VideoFeature", ...base(band), media: v };
1188
+ }
1189
+ const headings = text.filter((n) => n.kind === "heading");
1190
+ const subtitles = text.filter((n) => n.kind === "subtitle");
1191
+ const bodies = text.filter((n) => n.kind === "body");
1192
+ if (media.length === 0 && row === null && widgets.length === 0 && !hasSignificantRaw) {
1193
+ if (headings.length === 1 && subtitles.length <= 1 && bodies.length === 0 && !band.background) {
1194
+ const first = headings[0];
1195
+ const sub = subtitles[0];
1196
+ return {
1197
+ slice: "TitleBand",
1198
+ ...base(band),
1199
+ heading: first ? nodeText(first) : "",
1200
+ ...sub ? { subtitle: nodeText(sub) } : {},
1201
+ ...textRoleMeta(first, sub)
1202
+ };
1203
+ }
1204
+ if (headings.length === 0 && subtitles.length === 0 && bodies.length > 0 && !band.background) {
1205
+ return {
1206
+ slice: "RichText",
1207
+ ...base(band),
1208
+ html: bodies.map((b) => b.kind === "body" ? b.html : "").join("\n")
1209
+ };
1210
+ }
1211
+ }
1212
+ if (band.background && headings.length === 1 && subtitles.length <= 1 && bodies.length <= 1 && row === null && media.length === 0 && widgets.length === 0 && !hasSignificantRaw) {
1213
+ const h = headings[0];
1214
+ const sub = subtitles[0];
1215
+ const bod = bodies[0];
1216
+ return {
1217
+ slice: "Hero",
1218
+ ...base(band),
1219
+ ...h ? { heading: nodeText(h) } : {},
1220
+ ...sub ? { subtitle: nodeText(sub) } : {},
1221
+ ...bod && bod.kind === "body" ? { body: bod.html } : {},
1222
+ ...textRoleMeta(h, sub)
1223
+ };
1224
+ }
1225
+ if (rowNode?.slider) {
1226
+ const slides = carouselSlides(rowNode.cells);
1227
+ if (slides) {
1228
+ const spec = { slice: "Carousel", ...base(band), slides };
1229
+ if (rowNode.slider.columns !== void 0) spec.columns = rowNode.slider.columns;
1230
+ return spec;
1231
+ }
1232
+ }
1233
+ if (row) {
1234
+ const gm = galleryMedia(row);
1235
+ if (gm) return { slice: "Gallery", ...base(band), media: gm };
1236
+ }
1237
+ if (media.length === 1 && text.length === 0 && row === null && widgets.length === 0 && !hasSignificantRaw) {
1238
+ const m = media[0];
1239
+ if (m) return { slice: "MediaFull", ...base(band), media: m };
1240
+ }
1241
+ if (row && row.length === 2) {
1242
+ const [c0, c1] = row;
1243
+ if (c0 && c1) {
1244
+ const m0 = pureCellMedia(c0);
1245
+ const m1 = pureCellMedia(c1);
1246
+ const t0 = collectText(c0.node).length > 0;
1247
+ const t1 = collectText(c1.node).length > 0;
1248
+ if (m0 && !m1 && t1) {
1249
+ return {
1250
+ slice: "SplitFeature",
1251
+ ...base(band),
1252
+ media: m0,
1253
+ mediaSide: "left",
1254
+ ratio: cellRatio(c0),
1255
+ text: c1.node
1256
+ };
1257
+ }
1258
+ if (m1 && !m0 && t0) {
1259
+ return {
1260
+ slice: "SplitFeature",
1261
+ ...base(band),
1262
+ media: m1,
1263
+ mediaSide: "right",
1264
+ ratio: cellRatio(c1),
1265
+ text: c0.node
1266
+ };
1267
+ }
1268
+ }
1269
+ }
1270
+ return { slice: "Grid", ...base(band), root };
1271
+ }
1272
+ function classifyBands(bands, opts = {}) {
1273
+ return bands.map((b) => classifyBand(b, opts));
1274
+ }
1275
+
1276
+ // src/blux/grid/extract-map.ts
1277
+ var SCRIPT_RE = /<script\b[^>]*>([\s\S]*?)<\/script>/g;
1278
+ function findScript(html, marker) {
1279
+ for (const m of html.matchAll(SCRIPT_RE)) {
1280
+ const body = m[1];
1281
+ if (body !== void 0 && marker.test(body)) return body;
1282
+ }
1283
+ return null;
1284
+ }
1285
+ function extractStyles(script) {
1286
+ const at = script.indexOf("styles:[");
1287
+ if (at === -1) return [];
1288
+ const start = at + "styles:".length;
1289
+ let depth = 0;
1290
+ for (let i = start; i < script.length; i++) {
1291
+ const ch = script[i];
1292
+ if (ch === "[") depth++;
1293
+ else if (ch === "]") {
1294
+ depth--;
1295
+ if (depth === 0) {
1296
+ try {
1297
+ const raw = script.slice(start, i + 1);
1298
+ const jsonish = raw.replace(/([{,])\s*([A-Za-z_][A-Za-z0-9_.]*)\s*:/g, '$1"$2":');
1299
+ const parsed = JSON.parse(jsonish);
1300
+ return Array.isArray(parsed) ? parsed : [];
1301
+ } catch {
1302
+ return [];
1303
+ }
1304
+ }
1305
+ }
1306
+ }
1307
+ return [];
1308
+ }
1309
+ var LAYER_RE = /(\w+)\s*:\s*new google\.maps\.KmlLayer\(\{([^}]*)\}\)/g;
1310
+ function extractMapConfig(html) {
1311
+ const init = findScript(html, /function initMap\(\)[\s\S]*new google\.maps\.Map/);
1312
+ if (!init) return null;
1313
+ const mountId = /getElementById\(\s*["']([^"']+)["']\s*\)/.exec(init)?.[1];
1314
+ if (!mountId) return null;
1315
+ const layers = [];
1316
+ let mid;
1317
+ for (const m of init.matchAll(LAYER_RE)) {
1318
+ const name = m[1];
1319
+ const args = m[2];
1320
+ if (!name || args === void 0) continue;
1321
+ const layerMid = /[?&]mid=([^&"']+)/.exec(args)?.[1];
1322
+ const lid = /[?&]lid=([^&"']+)/.exec(args)?.[1];
1323
+ if (!layerMid || !lid) continue;
1324
+ mid ??= layerMid;
1325
+ layers.push({
1326
+ name,
1327
+ lid,
1328
+ initiallyVisible: /\bmap\s*:\s*map\b/.test(args),
1329
+ preserveViewport: /preserveViewport\s*:\s*true/.test(args)
1330
+ });
1331
+ }
1332
+ if (!mid || layers.length === 0) return null;
1333
+ const centerM = /center\s*:\s*\{\s*lat\s*:\s*(-?[\d.]+)\s*,\s*lng\s*:\s*(-?[\d.]+)\s*\}/.exec(
1334
+ init
1335
+ );
1336
+ const zoomM = /zoom\s*:\s*(\d+)/.exec(init);
1337
+ const mountTag = new RegExp(`<[^>]*\\bid=["']${mountId}["'][^>]*>`, "i").exec(html)?.[0] ?? "";
1338
+ const height = /height\s*:\s*([\d.]+(?:px|%|vh|vw|em|rem))/i.exec(mountTag)?.[1];
1339
+ const toggles = extractToggles(html);
1340
+ return {
1341
+ mountId,
1342
+ mid,
1343
+ layers,
1344
+ toggles,
1345
+ styles: extractStyles(init),
1346
+ ...centerM?.[1] && centerM[2] ? { center: { lat: Number(centerM[1]), lng: Number(centerM[2]) } } : {},
1347
+ ...zoomM?.[1] ? { zoom: Number(zoomM[1]) } : {},
1348
+ ...height ? { height } : {},
1349
+ ...toggles.length ? { defaultToggle: 0 } : {}
1350
+ };
1351
+ }
1352
+ function makeIsMapMount(config) {
1353
+ const marker = `id="${config.mountId}"`;
1354
+ return (node) => node.kind === "raw" && node.html.includes(marker);
1355
+ }
1356
+ function extractToggles(html) {
1357
+ const labels = [...html.matchAll(/map_icon_text">([^<]*)</g)].map((m) => m[1]).filter((l) => l !== void 0);
1358
+ const click = findScript(html, /clickMap\s*=\s*\{\s*0\s*:/);
1359
+ if (!click || labels.length === 0) return [];
1360
+ const bodyM = /clickMap\s*=\s*(\{[\s\S]*?\}\})\s*;/.exec(click);
1361
+ if (!bodyM?.[1]) return [];
1362
+ const groups = [];
1363
+ for (const g of bodyM[1].matchAll(/(\d+)\s*:\s*function\s*\(onoff\)\s*\{([^}]*)\}/g)) {
1364
+ const idx = Number(g[1]);
1365
+ const body = g[2] ?? "";
1366
+ const layerNames = [
1367
+ ...new Set(
1368
+ [...body.matchAll(/mapLayers\.(\w+)\./g)].map((m) => m[1]).filter((n) => n !== void 0)
1369
+ )
1370
+ ];
1371
+ const label = labels[idx];
1372
+ if (label === void 0) return [];
1373
+ groups[idx] = { label, layers: layerNames, panelIndex: idx };
1374
+ }
1375
+ return groups.length === labels.length && groups.every((g) => g !== void 0) ? groups : [];
1376
+ }
1377
+
1378
+ // src/blux/emit/block-styles.ts
1379
+ function blockStylesByIndex(siteJson) {
1380
+ const out = /* @__PURE__ */ new Map();
1381
+ const blocks = siteJson?.content?.pages?.[0]?.items;
1382
+ if (!Array.isArray(blocks)) return out;
1383
+ blocks.forEach((b, i) => {
1384
+ const styles = b?.styles ?? {};
1385
+ const cleaned = {};
1386
+ for (const [k, v] of Object.entries(styles)) {
1387
+ const c = cleanCssValue(v);
1388
+ if (c) cleaned[k] = c;
1389
+ }
1390
+ if (Object.keys(cleaned).length) out.set(i, cleaned);
1391
+ });
1392
+ return out;
1393
+ }
1394
+
1395
+ // src/blux/emit/grid-slice.ts
1396
+ function stripTags(html) {
1397
+ return html.replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();
1398
+ }
1399
+ function sliceSpecToPlanSlice(spec) {
1400
+ switch (spec.slice) {
1401
+ case "Hero":
1402
+ return {
1403
+ slice_type: "hero",
1404
+ variation: "band",
1405
+ items: [],
1406
+ primary: {
1407
+ band: spec.index,
1408
+ ...spec.heading ? { heading: spec.heading } : {},
1409
+ ...spec.subtitle ? { subtitle: spec.subtitle } : {},
1410
+ ...spec.body ? { body: stripTags(spec.body) } : {}
1411
+ }
1412
+ };
1413
+ case "TitleBand":
1414
+ return {
1415
+ slice_type: "title_band",
1416
+ variation: "default",
1417
+ items: [],
1418
+ primary: {
1419
+ band: spec.index,
1420
+ heading: spec.heading,
1421
+ ...spec.subtitle ? { subtitle: spec.subtitle } : {}
1422
+ }
1423
+ };
1424
+ case "RichText":
1425
+ return {
1426
+ slice_type: "rich_text",
1427
+ variation: "default",
1428
+ items: [],
1429
+ primary: { content: richText(spec.html), band: spec.index }
1430
+ };
1431
+ case "SplitFeature":
1432
+ return {
1433
+ slice_type: "split_feature",
1434
+ variation: "default",
1435
+ items: [],
1436
+ primary: { band: spec.index }
1437
+ };
1438
+ case "Gallery":
1439
+ return {
1440
+ slice_type: "gallery",
1441
+ variation: "default",
1442
+ items: [],
1443
+ primary: { band: spec.index }
1444
+ };
1445
+ case "Carousel":
1446
+ return {
1447
+ slice_type: "carousel",
1448
+ variation: "default",
1449
+ items: spec.slides.map(
1450
+ (s) => s.caption ? { caption: blockPlainText(s.caption.html) } : {}
1451
+ ),
1452
+ primary: { band: spec.index }
1453
+ };
1454
+ case "MediaFull":
1455
+ case "VideoFeature":
1456
+ return {
1457
+ slice_type: "media_full",
1458
+ variation: "default",
1459
+ items: [],
1460
+ primary: { band: spec.index }
1461
+ };
1462
+ case "LocationMap":
1463
+ return {
1464
+ slice_type: "location_map",
1465
+ variation: "default",
1466
+ items: [],
1467
+ primary: { band: spec.index }
1468
+ };
1469
+ case "Grid":
1470
+ return {
1471
+ slice_type: "grid_band",
1472
+ variation: "default",
1473
+ items: [],
1474
+ primary: { band: spec.index }
1475
+ };
1476
+ }
1477
+ }
1478
+
1479
+ // src/blux/emit/grid-plan.ts
1480
+ function mediaCdnUrl(m) {
1481
+ return m.base ? `${m.base}${m.assetId}${m.ext ? `.${m.ext}` : ""}` : null;
1482
+ }
1483
+ function mediaUrl(m, sourceUrlById) {
1484
+ return mediaCdnUrl(m) ?? sourceUrlById.get(m.assetId) ?? null;
1485
+ }
1486
+ function collectPlanAssets(specs, resolve, diagnostics) {
1487
+ const byId = /* @__PURE__ */ new Map();
1488
+ const seen = /* @__PURE__ */ new Set();
1489
+ const add = (m) => {
1490
+ if (seen.has(m.assetId)) return;
1491
+ seen.add(m.assetId);
1492
+ const asset = resolve(m);
1493
+ if (asset) byId.set(m.assetId, asset);
1494
+ else
1495
+ diagnostics?.push({
1496
+ kind: "unresolved-asset",
1497
+ where: m.assetId,
1498
+ message: `media ${m.assetId} has no CDN base nor IR source url \u2014 not uploaded`
1499
+ });
1500
+ };
1501
+ for (const spec of specs) {
1502
+ if (spec.background) add(spec.background);
1503
+ switch (spec.slice) {
1504
+ case "Gallery":
1505
+ spec.media.forEach(add);
1506
+ break;
1507
+ case "Carousel":
1508
+ spec.slides.forEach((s) => add(s.media));
1509
+ break;
1510
+ case "MediaFull":
1511
+ case "VideoFeature":
1512
+ add(spec.media);
1513
+ break;
1514
+ case "SplitFeature":
1515
+ add(spec.media);
1516
+ collectMedia(spec.text).forEach(add);
1517
+ break;
1518
+ case "Grid":
1519
+ collectMedia(spec.root).forEach(add);
1520
+ break;
1521
+ default:
1522
+ break;
1523
+ }
1524
+ }
1525
+ return [...byId.values()];
1526
+ }
1527
+ function buildGridPlan(specs, ir) {
1528
+ const page = ir.pages[0];
1529
+ const uid = page?.uid ?? "home";
1530
+ const title = page?.title ?? uid;
1531
+ const doc = {
1532
+ type: "page",
1533
+ uid,
1534
+ data: { title: richText(`<h1>${title}</h1>`), slices: specs.map(sliceSpecToPlanSlice) }
1535
+ };
1536
+ const assetById = new Map(ir.assets.map((a) => [a.id, a]));
1537
+ const sourceUrlById = new Map(ir.assets.map((a) => [a.id, a.sourceUrl]));
1538
+ const resolve = (m) => {
1539
+ const asset = assetById.get(m.assetId);
1540
+ const url = mediaUrl(m, sourceUrlById);
1541
+ return url ? { id: m.assetId, url, alt: asset?.alt ?? "" } : null;
1542
+ };
1543
+ const diagnostics = [...ir.diagnostics ?? []];
1544
+ const assets = collectPlanAssets(specs, resolve, diagnostics);
1545
+ const customTypes = ir.collections.map(buildCustomType);
1546
+ return { customTypes, documents: [doc], assets, stylesManifest: [], diagnostics };
1547
+ }
1548
+
1549
+ // src/blux/emit/presentation.ts
1550
+ function renderToken(t) {
1551
+ return {
1552
+ cols: t.cols,
1553
+ ...t.ratio !== void 0 ? { ratio: t.ratio } : {},
1554
+ ...t.spacing !== void 0 ? { spacing: t.spacing } : {}
1555
+ };
1556
+ }
1557
+ function renderNode(node, resolve) {
1558
+ switch (node.kind) {
1559
+ case "row": {
1560
+ const cells = [];
1561
+ for (const c of node.cells) {
1562
+ const rn = renderNode(c.node, resolve);
1563
+ if (rn) cells.push({ token: renderToken(c.token), node: rn });
1564
+ }
1565
+ return { kind: "row", cells };
1566
+ }
1567
+ case "stack": {
1568
+ const children = [];
1569
+ for (const c of node.children) {
1570
+ const rn = renderNode(c, resolve);
1571
+ if (rn) children.push(rn);
1572
+ }
1573
+ return { kind: "stack", children };
1574
+ }
1575
+ case "heading":
1576
+ return {
1577
+ kind: "heading",
1578
+ level: node.level,
1579
+ html: node.html,
1580
+ ...node.role ? { role: node.role } : {}
1581
+ };
1582
+ case "body":
1583
+ return { kind: "body", html: node.html, ...node.role ? { role: node.role } : {} };
1584
+ case "subtitle":
1585
+ return { kind: "subtitle", text: node.text, ...node.role ? { role: node.role } : {} };
1586
+ case "media": {
1587
+ const m = resolve(node.media);
1588
+ return m ? { kind: "media", media: m } : null;
1589
+ }
1590
+ case "raw":
1591
+ return { kind: "raw", html: node.html };
1592
+ case "widget":
1593
+ return { kind: "widget", widget: node.widget };
1594
+ }
1595
+ }
1596
+ function hasMapWidget(node) {
1597
+ if (node.kind === "widget") return node.widget.type === "map";
1598
+ if (node.kind === "row") return node.cells.some((c) => hasMapWidget(c.node));
1599
+ if (node.kind === "stack") return node.children.some(hasMapWidget);
1600
+ return false;
1601
+ }
1602
+ function buildPresentation(specs, deps) {
1603
+ const bands = {};
1604
+ for (const spec of specs) {
1605
+ const bp = {};
1606
+ const style = deps.styleFor(spec.index);
1607
+ if (style) bp.style = style;
1608
+ if (spec.background) {
1609
+ const bg = deps.resolveMedia(spec.background);
1610
+ if (bg) bp.background = bg;
1611
+ }
1612
+ switch (spec.slice) {
1613
+ case "Hero":
1614
+ case "TitleBand": {
1615
+ const meta = {
1616
+ ...spec.headingRole ? { headingRole: spec.headingRole } : {},
1617
+ ...spec.headingLevel !== void 0 ? { headingLevel: spec.headingLevel } : {},
1618
+ ...spec.subtitleRole ? { subtitleRole: spec.subtitleRole } : {}
1619
+ };
1620
+ if (Object.keys(meta).length) bp.text = meta;
1621
+ break;
1622
+ }
1623
+ case "RichText":
1624
+ break;
1625
+ // content is in the page doc
1626
+ case "Gallery": {
1627
+ const g = spec.media.map(deps.resolveMedia).filter((m) => m !== null);
1628
+ if (g.length) bp.gallery = g;
1629
+ break;
1630
+ }
1631
+ case "Carousel": {
1632
+ const slides = [];
1633
+ for (const s of spec.slides) {
1634
+ const media = deps.resolveMedia(s.media);
1635
+ if (!media) break;
1636
+ const slide = {
1637
+ media
1638
+ };
1639
+ if (s.caption) {
1640
+ const caption = { level: s.caption.level };
1641
+ if (s.caption.role !== void 0) caption.role = s.caption.role;
1642
+ slide.caption = caption;
1643
+ }
1644
+ slides.push(slide);
1645
+ }
1646
+ if (slides.length > 0) {
1647
+ bp.carousel = spec.columns !== void 0 ? { slides, columns: spec.columns } : { slides };
1648
+ }
1649
+ break;
1650
+ }
1651
+ case "MediaFull":
1652
+ case "VideoFeature": {
1653
+ const m = deps.resolveMedia(spec.media);
1654
+ if (m) bp.media = m;
1655
+ break;
1656
+ }
1657
+ case "SplitFeature": {
1658
+ const media = deps.resolveMedia(spec.media);
1659
+ const text = renderNode(spec.text, deps.resolveMedia);
1660
+ if (media && text) bp.split = { mediaSide: spec.mediaSide, ratio: spec.ratio, media, text };
1661
+ break;
1662
+ }
1663
+ case "LocationMap":
1664
+ if (deps.map) bp.map = deps.map;
1665
+ break;
1666
+ case "Grid": {
1667
+ const tree = renderNode(spec.root, deps.resolveMedia);
1668
+ if (tree) bp.tree = tree;
1669
+ if (deps.map && hasMapWidget(spec.root)) bp.map = deps.map;
1670
+ break;
1671
+ }
1672
+ }
1673
+ bands[String(spec.index)] = bp;
1674
+ }
1675
+ return { bands };
1676
+ }
1677
+
1678
+ // src/blux/emit/convert.ts
1679
+ function mapRenderFromConfig(c) {
1680
+ return {
1681
+ mid: c.mid,
1682
+ layers: c.layers,
1683
+ toggles: c.toggles,
1684
+ styles: c.styles,
1685
+ ...c.center ? { center: c.center } : {},
1686
+ ...c.zoom !== void 0 ? { zoom: c.zoom } : {},
1687
+ ...c.height ? { height: c.height } : {},
1688
+ ...c.defaultToggle !== void 0 ? { defaultToggle: c.defaultToggle } : {}
1689
+ };
1690
+ }
1691
+ function convertExport({
1692
+ html,
1693
+ siteJson
1694
+ }) {
1695
+ const bands = parseGridBands(html);
1696
+ const mapConfig = extractMapConfig(html);
1697
+ const specs = classifyBands(bands, mapConfig ? { isMapMount: makeIsMapMount(mapConfig) } : {});
1698
+ const ir = assembleIR({ siteJson, htmls: [html] });
1699
+ const assetsById = new Map(ir.assets.map((a) => [a.id, a]));
1700
+ const sourceUrlById = new Map(ir.assets.map((a) => [a.id, a.sourceUrl]));
1701
+ const styles = blockStylesByIndex(siteJson);
1702
+ const deps = {
1703
+ resolveMedia: (m) => {
1704
+ const url = mediaUrl(m, sourceUrlById);
1705
+ if (!url) return null;
1706
+ const alt = assetsById.get(m.assetId)?.alt;
1707
+ const rm = {
1708
+ kind: m.kind,
1709
+ url,
1710
+ ...alt ? { alt } : {},
1711
+ ...m.width !== void 0 ? { width: m.width } : {},
1712
+ ...m.aspect !== void 0 ? { aspect: m.aspect } : {},
1713
+ ...m.fit ? { fit: m.fit } : {},
1714
+ ...m.position ? { position: m.position } : {},
1715
+ ...m.minHeight ? { minHeight: m.minHeight } : {},
1716
+ ...m.playback ? { playback: m.playback } : {}
1717
+ };
1718
+ return rm;
1719
+ },
1720
+ styleFor: (i) => styles.get(i),
1721
+ map: mapConfig ? mapRenderFromConfig(mapConfig) : null
1722
+ };
1723
+ const plan = buildGridPlan(specs, ir);
1724
+ const presentation = buildPresentation(specs, deps);
1725
+ return { bands, specs, ir, mapConfig, plan, presentation };
1726
+ }
1727
+
1728
+ // src/blux/emit/validate-layout.ts
1729
+ function tokKey(t) {
1730
+ return `${t.cols}${t.ratio !== void 0 ? `r${t.ratio}` : ""}${t.spacing !== void 0 ? `s${t.spacing}` : ""}`;
1731
+ }
1732
+ function sigOf(node) {
1733
+ switch (node.kind) {
1734
+ case "row":
1735
+ return `row[${node.cells.map((c) => `${tokKey(c.token)}:${sigOf(c.node)}`).join(",")}]`;
1736
+ case "stack":
1737
+ return `stack[${node.children.map(sigOf).join(",")}]`;
1738
+ case "heading":
1739
+ return `h${node.level}`;
1740
+ case "body":
1741
+ return "body";
1742
+ case "subtitle":
1743
+ return "subtitle";
1744
+ case "media":
1745
+ return `media:${node.media.kind}`;
1746
+ case "widget":
1747
+ return `widget:${node.widget.type}`;
1748
+ case "raw":
1749
+ return "raw";
1750
+ }
1751
+ }
1752
+ function sourceLabel(spec) {
1753
+ switch (spec.slice) {
1754
+ case "Grid":
1755
+ return sigOf(spec.root);
1756
+ case "Gallery":
1757
+ return `gallery(${spec.media.length})`;
1758
+ case "Carousel":
1759
+ return `carousel(${spec.slides.length})`;
1760
+ case "MediaFull":
1761
+ return "media_full";
1762
+ case "VideoFeature":
1763
+ return "video";
1764
+ case "SplitFeature":
1765
+ return `split(${spec.mediaSide},${spec.ratio})`;
1766
+ case "LocationMap":
1767
+ return "location_map";
1768
+ case "Hero":
1769
+ return "hero";
1770
+ case "TitleBand":
1771
+ return "title_band";
1772
+ case "RichText":
1773
+ return "rich_text";
1774
+ }
1775
+ }
1776
+ function validateLayout(specs, presentation) {
1777
+ const findings = [];
1778
+ const rows = [];
1779
+ const manifestKeys = Object.keys(presentation.bands);
1780
+ let gridBands = 0;
1781
+ if (specs.length !== manifestKeys.length) {
1782
+ findings.push({ kind: "band-count", specs: specs.length, manifest: manifestKeys.length });
1783
+ }
1784
+ for (const spec of specs) {
1785
+ const source = sourceLabel(spec);
1786
+ const bp = presentation.bands[String(spec.index)];
1787
+ if (!bp) {
1788
+ findings.push({ kind: "band-missing", band: spec.index });
1789
+ rows.push({ band: spec.index, slice: spec.slice, source, converted: "\u2205", ok: false });
1790
+ continue;
1791
+ }
1792
+ const before = findings.length;
1793
+ let converted = source;
1794
+ if (spec.background && !bp.background) {
1795
+ findings.push({ kind: "media-dropped", band: spec.index, where: "background" });
1796
+ }
1797
+ switch (spec.slice) {
1798
+ case "Grid": {
1799
+ gridBands++;
1800
+ converted = bp.tree ? sigOf(bp.tree) : "\u2205";
1801
+ if (source !== converted) {
1802
+ findings.push({
1803
+ kind: "tree-drift",
1804
+ band: spec.index,
1805
+ expected: source,
1806
+ actual: converted
1807
+ });
1808
+ }
1809
+ if (hasMapWidget(spec.root) && !bp.map) {
1810
+ findings.push({ kind: "map-missing", band: spec.index });
1811
+ }
1812
+ break;
1813
+ }
1814
+ case "Gallery": {
1815
+ const got = bp.gallery?.length ?? 0;
1816
+ converted = `gallery(${got})`;
1817
+ if (got < spec.media.length) {
1818
+ findings.push({
1819
+ kind: "media-dropped",
1820
+ band: spec.index,
1821
+ where: `gallery ${got}/${spec.media.length}`
1822
+ });
1823
+ }
1824
+ break;
1825
+ }
1826
+ case "Carousel": {
1827
+ const got = bp.carousel?.slides.length ?? 0;
1828
+ converted = `carousel(${got})`;
1829
+ if (got < spec.slides.length) {
1830
+ findings.push({
1831
+ kind: "media-dropped",
1832
+ band: spec.index,
1833
+ where: `carousel ${got}/${spec.slides.length}`
1834
+ });
1835
+ }
1836
+ break;
1837
+ }
1838
+ case "MediaFull":
1839
+ case "VideoFeature": {
1840
+ if (!bp.media) {
1841
+ converted = "\u2205";
1842
+ findings.push({ kind: "media-dropped", band: spec.index, where: "media" });
1843
+ }
1844
+ break;
1845
+ }
1846
+ case "SplitFeature": {
1847
+ if (!bp.split) {
1848
+ converted = "\u2205";
1849
+ findings.push({ kind: "media-dropped", band: spec.index, where: "split" });
1850
+ } else {
1851
+ converted = `split(${bp.split.mediaSide},${bp.split.ratio})`;
1852
+ if (converted !== source) {
1853
+ findings.push({
1854
+ kind: "tree-drift",
1855
+ band: spec.index,
1856
+ expected: source,
1857
+ actual: converted
1858
+ });
1859
+ }
1860
+ const expectedText = sigOf(spec.text);
1861
+ const actualText = sigOf(bp.split.text);
1862
+ if (expectedText !== actualText) {
1863
+ findings.push({
1864
+ kind: "tree-drift",
1865
+ band: spec.index,
1866
+ expected: `split.text ${expectedText}`,
1867
+ actual: `split.text ${actualText}`
1868
+ });
1869
+ }
1870
+ }
1871
+ break;
1872
+ }
1873
+ case "LocationMap": {
1874
+ if (!bp.map) {
1875
+ converted = "\u2205";
1876
+ findings.push({ kind: "map-missing", band: spec.index });
1877
+ }
1878
+ break;
1879
+ }
1880
+ case "Hero":
1881
+ case "TitleBand":
1882
+ case "RichText":
1883
+ break;
1884
+ // text lives in the page doc (gated by grid-slice.test.ts); nothing manifest-carried to lose beyond background
1885
+ default: {
1886
+ const _exhaustive = spec;
1887
+ throw new Error(`validateLayout: unhandled slice ${_exhaustive.slice}`);
1888
+ }
1889
+ }
1890
+ rows.push({
1891
+ band: spec.index,
1892
+ slice: spec.slice,
1893
+ source,
1894
+ converted,
1895
+ ok: findings.length === before
1896
+ });
1897
+ }
1898
+ return { bands: specs.length, gridBands, faithful: findings.length === 0, findings, rows };
1899
+ }
1900
+ function fmtFinding(f) {
1901
+ switch (f.kind) {
1902
+ case "band-count":
1903
+ return ` band count mismatch: ${f.specs} source bands vs ${f.manifest} manifest bands`;
1904
+ case "band-missing":
1905
+ return ` band ${f.band}: no manifest entry`;
1906
+ case "tree-drift":
1907
+ return ` band ${f.band}: grid tree drift
1908
+ expected ${f.expected}
1909
+ actual ${f.actual}`;
1910
+ case "media-dropped":
1911
+ return ` band ${f.band}: media dropped (${f.where})`;
1912
+ case "map-missing":
1913
+ return ` band ${f.band}: map config missing`;
1914
+ }
1915
+ }
1916
+ function formatLayoutReport(r) {
1917
+ const headline = `layout fidelity: ${r.faithful ? "FAITHFUL" : `${r.findings.length} finding(s)`} \u2014 ${r.bands} bands (${r.gridBands} grid-tree checked)`;
1918
+ const table = r.rows.map((x) => {
1919
+ const tag = x.ok ? "ok" : "!!";
1920
+ const sig = x.ok ? x.source : `${x.source} -> ${x.converted}`;
1921
+ return ` band ${String(x.band).padStart(2)} ${x.slice.padEnd(13)} ${tag} ${sig}`;
1922
+ });
1923
+ const detail = r.findings.length ? ["findings:", ...r.findings.map(fmtFinding)] : [];
1924
+ return [headline, ...table, ...detail].join("\n");
1925
+ }
1926
+
1927
+ // src/blux/emit/rewrite-manifest.ts
1928
+ var swap = (m, map) => {
1929
+ const url = map.get(m.url);
1930
+ return url ? { ...m, url } : m;
1931
+ };
1932
+ function walkNode(node, map) {
1933
+ switch (node.kind) {
1934
+ case "row":
1935
+ return {
1936
+ kind: "row",
1937
+ cells: node.cells.map((c) => ({ token: c.token, node: walkNode(c.node, map) }))
1938
+ };
1939
+ case "stack":
1940
+ return { kind: "stack", children: node.children.map((c) => walkNode(c, map)) };
1941
+ case "media":
1942
+ return { kind: "media", media: swap(node.media, map) };
1943
+ default:
1944
+ return node;
1945
+ }
1946
+ }
1947
+ function walkBand(bp, map) {
1948
+ const out = { ...bp };
1949
+ if (bp.background) out.background = swap(bp.background, map);
1950
+ if (bp.media) out.media = swap(bp.media, map);
1951
+ if (bp.gallery) out.gallery = bp.gallery.map((m) => swap(m, map));
1952
+ if (bp.carousel)
1953
+ out.carousel = {
1954
+ ...bp.carousel,
1955
+ slides: bp.carousel.slides.map((s) => ({ ...s, media: swap(s.media, map) }))
1956
+ };
1957
+ if (bp.tree) out.tree = walkNode(bp.tree, map);
1958
+ if (bp.split)
1959
+ out.split = {
1960
+ ...bp.split,
1961
+ media: swap(bp.split.media, map),
1962
+ text: walkNode(bp.split.text, map)
1963
+ };
1964
+ return out;
1965
+ }
1966
+ function rewriteManifestUrls(manifest, urlMap) {
1967
+ const bands = {};
1968
+ for (const [k, bp] of Object.entries(manifest.bands)) bands[k] = walkBand(bp, urlMap);
1969
+ return { bands };
1970
+ }
1971
+
1972
+ // src/cli/commands/blux.ts
1973
+ async function runBluxCommand(action, dir, opts) {
1974
+ if (action === "emit") {
1975
+ if (!dir) return { output: "blux emit needs a Blux export directory.", code: 1 };
1976
+ let siteJson;
1977
+ try {
1978
+ siteJson = JSON.parse(await readFile(join(dir, "site.json"), "utf-8"));
1979
+ } catch (err) {
1980
+ return {
1981
+ output: `could not read site.json in ${dir}: ${err.message}`,
1982
+ code: 1
1983
+ };
1984
+ }
1985
+ const out = opts.out ?? join(dir, "blux-out");
1986
+ const htmlPaths = (await glob(["**/*.html"], { cwd: dir, absolute: true, ignore: ["blux-out/**"] })).sort();
1987
+ const htmls = await Promise.all(htmlPaths.map((p) => readFile(p, "utf-8")));
1988
+ const ir = assembleIR({ siteJson, htmls });
1989
+ let probeLine = "";
1990
+ if (opts.probe) {
1991
+ const { probeAssetUrls } = await import("./probe-DJAMLVTT.js");
1992
+ const used = /* @__PURE__ */ new Set();
1993
+ const collect = (v) => {
1994
+ if (!v || typeof v !== "object") return;
1995
+ if ("__asset_id" in v) used.add(v.__asset_id);
1996
+ else if (Array.isArray(v)) v.forEach(collect);
1997
+ else Object.values(v).forEach(collect);
1998
+ };
1999
+ buildMigrationPlan(ir).documents.forEach((d) => collect(d.data));
2000
+ const targets = ir.assets.filter((a) => used.has(a.id) && !a.sourceUrl);
2001
+ const probed = await probeAssetUrls(targets, ir.meta.bluxSiteId, opts.fetchImpl ?? fetch);
2002
+ let hits = 0;
2003
+ for (const a of ir.assets) {
2004
+ const url = probed.get(a.id);
2005
+ if (url) {
2006
+ a.sourceUrl = url;
2007
+ hits++;
2008
+ }
2009
+ }
2010
+ ir.diagnostics = ir.diagnostics.filter(
2011
+ (d) => !(d.kind === "unresolved-asset" && probed.get(d.where))
2012
+ );
2013
+ probeLine = `probe resolved ${hits}/${targets.length} used assets`;
2014
+ }
2015
+ const plan = buildMigrationPlan(ir);
2016
+ const manifest = buildReviewManifest(ir, {
2017
+ convertedBase: opts.convertedBase ?? "http://localhost:5173",
2018
+ bluxBase: opts.bluxBase ?? `https://${ir.meta.domain}`
2019
+ });
2020
+ await mkdir(join(out, "customtypes"), { recursive: true });
2021
+ await writeFile(join(out, "ir.json"), JSON.stringify(ir, null, 2));
2022
+ await writeFile(join(out, "migration-plan.json"), JSON.stringify(plan, null, 2));
2023
+ const rolesCss = emitRolesCss(ir.theme);
2024
+ await writeFile(
2025
+ join(out, "theme.css"),
2026
+ emitThemeCss(ir.theme) + (rolesCss ? "\n" + rolesCss : "")
2027
+ );
2028
+ await writeFile(join(out, "review-manifest.json"), JSON.stringify(manifest, null, 2));
2029
+ await writeFile(
2030
+ join(out, "styles-manifest.json"),
2031
+ JSON.stringify(plan.stylesManifest, null, 2)
2032
+ );
2033
+ for (const ct of plan.customTypes) {
2034
+ await writeFile(join(out, "customtypes", `${ct.id}.json`), JSON.stringify(ct.json, null, 2));
2035
+ }
2036
+ const resolved = ir.assets.filter((a) => a.sourceUrl !== null).length;
2037
+ const diagnostics = [...ir.diagnostics, ...plan.diagnostics];
2038
+ const lines = [
2039
+ `site: ${ir.meta.name} (${ir.meta.domain})`,
2040
+ ...probeLine ? [probeLine] : [],
2041
+ `pages: ${ir.pages.length} | custom types: ${plan.customTypes.length} | documents: ${plan.documents.length} | assets: ${resolved}/${ir.assets.length} resolved`,
2042
+ `diagnostics: ${diagnostics.length}`,
2043
+ ...diagnostics.map((d) => ` - [${d.kind}] ${d.where}: ${d.message}`),
2044
+ `wrote ${out}`
2045
+ ];
2046
+ return { output: lines.join("\n"), code: 0 };
2047
+ }
2048
+ if (action === "migrate") {
2049
+ if (!dir) {
2050
+ return {
2051
+ output: "blux migrate needs an emitted output directory (or a plan .json path).",
2052
+ code: 1
2053
+ };
2054
+ }
2055
+ if (!process.env.PRISMIC_REPOSITORY_NAME || !process.env.PRISMIC_WRITE_TOKEN) {
2056
+ return {
2057
+ output: "Set PRISMIC_REPOSITORY_NAME and PRISMIC_WRITE_TOKEN to run a live migration.",
2058
+ code: 1
2059
+ };
2060
+ }
2061
+ const planPath = dir.endsWith(".json") ? dir : join(dir, "migration-plan.json");
2062
+ const plan = JSON.parse(await readFile(planPath, "utf-8"));
2063
+ const { pushCustomTypes, runMigration } = await import("./run-migration-LJNNZWIV.js");
2064
+ const pushed = await pushCustomTypes(plan.customTypes);
2065
+ const r = await runMigration(plan, (line) => process.stderr.write(`${line}
2066
+ `));
2067
+ const missing = r.missingAssets.length ? `
2068
+ WARNING missing assets: ${r.missingAssets.join(", ")}` : "";
2069
+ let manifestNote = "";
2070
+ const manifestPath = join(dirname(planPath), "blux-presentation.json");
2071
+ try {
2072
+ const manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
2073
+ const rewritten = rewriteManifestUrls(manifest, r.assetUrlByCdn);
2074
+ await writeFile(manifestPath, JSON.stringify(rewritten, null, 2) + "\n");
2075
+ manifestNote = "\nmanifest media rewritten to Prismic urls";
2076
+ } catch {
2077
+ }
2078
+ return {
2079
+ output: `custom types pushed: ${pushed.join(", ") || "none"}
2080
+ assets: ${r.assetsUploaded} uploaded, ${r.assetsReused} reused | documents: ${r.docsCreated} created, ${r.docsUpdated} updated \u2192 ${process.env.PRISMIC_REPOSITORY_NAME} (publish the migration release in the dashboard)` + missing + manifestNote,
2081
+ code: 0
2082
+ };
2083
+ }
2084
+ if (action === "validate") {
2085
+ if (!dir) return { output: "blux validate needs a Blux export directory.", code: 1 };
2086
+ let exportHtml;
2087
+ try {
2088
+ exportHtml = await readFile(join(dir, "index.html"), "utf-8");
2089
+ } catch (err) {
2090
+ return { output: `could not read index.html in ${dir}: ${err.message}`, code: 1 };
2091
+ }
2092
+ let rendered = null;
2093
+ if (opts.against) {
2094
+ try {
2095
+ if (/^https?:\/\//.test(opts.against)) {
2096
+ const res = await (opts.fetchImpl ?? fetch)(opts.against);
2097
+ if (!res.ok) {
2098
+ return {
2099
+ output: `could not fetch --against ${opts.against}: HTTP ${res.status}`,
2100
+ code: 1
2101
+ };
2102
+ }
2103
+ rendered = await res.text();
2104
+ } else {
2105
+ rendered = await readFile(opts.against, "utf-8");
2106
+ }
2107
+ } catch (err) {
2108
+ return {
2109
+ output: `could not read --against ${opts.against}: ${err.message}`,
2110
+ code: 1
2111
+ };
2112
+ }
2113
+ }
2114
+ let siteJson;
2115
+ try {
2116
+ siteJson = JSON.parse(await readFile(join(dir, "site.json"), "utf-8"));
2117
+ } catch (err) {
2118
+ return { output: `could not read site.json in ${dir}: ${err.message}`, code: 1 };
2119
+ }
2120
+ const { specs, presentation } = convertExport({ html: exportHtml, siteJson });
2121
+ const layout = validateLayout(specs, presentation);
2122
+ const lines = [formatLayoutReport(layout)];
2123
+ if (rendered !== null) {
2124
+ const report = validateCoverage(exportHtml, rendered);
2125
+ lines.push(
2126
+ "",
2127
+ `content coverage: ${report.covered}/${report.total} runs (${report.coveragePct}%)`,
2128
+ ...report.missing.length ? [
2129
+ "missing runs \u2014 export text absent from the render:",
2130
+ ...report.missing.map((m) => ` - ${m}`)
2131
+ ] : ["all export text runs present in the render"]
2132
+ );
2133
+ }
2134
+ return { output: lines.join("\n"), code: layout.faithful ? 0 : 1 };
2135
+ }
2136
+ if (action === "grid") {
2137
+ if (!dir) return { output: "blux grid needs a Blux export directory.", code: 1 };
2138
+ let html;
2139
+ try {
2140
+ html = await readFile(join(dir, "index.html"), "utf-8");
2141
+ } catch (err) {
2142
+ return {
2143
+ output: `could not read index.html in ${dir}: ${err.message}`,
2144
+ code: 1
2145
+ };
2146
+ }
2147
+ const bands = parseGridBands(html);
2148
+ const outDir = opts.out ?? join(dir, "blux-out");
2149
+ await mkdir(outDir, { recursive: true });
2150
+ await writeFile(join(outDir, "grid-tree.json"), JSON.stringify(bands, null, 2));
2151
+ const mapConfig = extractMapConfig(html);
2152
+ if (mapConfig) {
2153
+ await writeFile(
2154
+ join(outDir, "map-config.json"),
2155
+ JSON.stringify(mapConfig, null, 2) + "\n",
2156
+ "utf-8"
2157
+ );
2158
+ }
2159
+ return {
2160
+ output: `Parsed ${bands.length} bands \u2192 ${join(outDir, "grid-tree.json")}` + (mapConfig ? ", map config extracted" : ""),
2161
+ code: 0
2162
+ };
2163
+ }
2164
+ if (action === "convert") {
2165
+ if (!dir) return { output: "blux convert needs a Blux export directory.", code: 1 };
2166
+ let html;
2167
+ let siteJson;
2168
+ try {
2169
+ html = await readFile(join(dir, "index.html"), "utf-8");
2170
+ siteJson = JSON.parse(await readFile(join(dir, "site.json"), "utf-8"));
2171
+ } catch (err) {
2172
+ return { output: `could not read export in ${dir}: ${err.message}`, code: 1 };
2173
+ }
2174
+ const { bands, mapConfig, plan, presentation, ir, specs } = convertExport({ html, siteJson });
2175
+ const outDir = opts.out ?? join(dir, "blux-out");
2176
+ await mkdir(outDir, { recursive: true });
2177
+ await writeFile(join(outDir, "migration-plan.json"), JSON.stringify(plan, null, 2));
2178
+ await writeFile(
2179
+ join(outDir, "blux-presentation.json"),
2180
+ JSON.stringify(presentation, null, 2) + "\n"
2181
+ );
2182
+ await writeFile(
2183
+ join(outDir, "theme.css"),
2184
+ emitThemeCss(ir.theme) + "\n" + emitRolesCss(ir.theme)
2185
+ );
2186
+ if (mapConfig) {
2187
+ await writeFile(join(outDir, "map-config.json"), JSON.stringify(mapConfig, null, 2) + "\n");
2188
+ }
2189
+ const layout = validateLayout(specs, presentation);
2190
+ await writeFile(join(outDir, "layout-report.json"), JSON.stringify(layout, null, 2) + "\n");
2191
+ const sliceCount = plan.documents[0]?.data.slices?.length ?? 0;
2192
+ return {
2193
+ output: `Converted ${bands.length} bands \u2192 ${outDir} (${Object.keys(presentation.bands).length} manifest bands, ${sliceCount} slices` + (mapConfig ? ", map config extracted" : "") + ")\n" + formatLayoutReport(layout),
2194
+ code: 0
2195
+ };
2196
+ }
2197
+ return {
2198
+ output: `unknown blux action '${action}'. Use: emit, migrate, validate, grid, convert.`,
2199
+ code: 1
2200
+ };
2201
+ }
2202
+ export {
2203
+ runBluxCommand
2204
+ };
2205
+ //# sourceMappingURL=blux-FKZYMKLF.js.map