@xtrable-ltd/nanoesis 0.1.8 → 0.1.10

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 (31) hide show
  1. package/dist/adapter-azure-blob.d.ts +7 -1
  2. package/dist/adapter-azure-blob.js +2 -2
  3. package/dist/{chunk-J6VYOB47.js → chunk-UZQ7UP2B.js} +1678 -1298
  4. package/dist/editor-api.d.ts +39 -2
  5. package/dist/editor-api.js +393 -4
  6. package/dist/index.d.ts +371 -165
  7. package/dist/index.js +21 -1
  8. package/editor/assets/MigrationsPane-BAHPPSXP.css +1 -0
  9. package/editor/assets/MigrationsPane-_FGonx4-.js +4 -0
  10. package/editor/assets/{TemplatesPane-B4_sg2u5.css → TemplatesPane-CiLiMCc8.css} +1 -1
  11. package/editor/assets/{TemplatesPane-4IAoeX4-.js → TemplatesPane-Z6Bn69Hb.js} +204 -202
  12. package/editor/assets/{cssMode-BhSmGQp_.js → cssMode-dkQrIPWx.js} +1 -1
  13. package/editor/assets/{freemarker2-Z1jVSRUs.js → freemarker2-DEqcFFWa.js} +1 -1
  14. package/editor/assets/{handlebars-C3kew8-P.js → handlebars-C6ojANWr.js} +1 -1
  15. package/editor/assets/{html-RVg2mWQY.js → html-BmiAmVUD.js} +1 -1
  16. package/editor/assets/{htmlMode-ljHXud-Y.js → htmlMode-BBmUqToI.js} +1 -1
  17. package/editor/assets/{index-R39CtpUa.css → index-DEz8GUII.css} +1 -1
  18. package/editor/assets/index-LtCzUHAw.js +138 -0
  19. package/editor/assets/{javascript-CAr3NHzi.js → javascript-Cxm2TfJy.js} +1 -1
  20. package/editor/assets/{jsonMode-Cq4fxtNe.js → jsonMode-CW5012Hx.js} +1 -1
  21. package/editor/assets/{liquid-DeSAzZOT.js → liquid-DrS7ilHv.js} +1 -1
  22. package/editor/assets/{mdx-C4Ynnq4H.js → mdx-CwdSU5o1.js} +1 -1
  23. package/editor/assets/{python-rTSPH-tA.js → python-CALCR0yC.js} +1 -1
  24. package/editor/assets/{razor-B-0Do2A9.js → razor-SVCo2LoM.js} +1 -1
  25. package/editor/assets/{tsMode-m4eOcwoz.js → tsMode-CzXfTR_Q.js} +1 -1
  26. package/editor/assets/{typescript-CdvinFng.js → typescript-CP0Ovrv7.js} +1 -1
  27. package/editor/assets/{xml-DuAYX2gv.js → xml-B2yqloTa.js} +1 -1
  28. package/editor/assets/{yaml-BWplD8Hf.js → yaml-DTLJhzgY.js} +1 -1
  29. package/editor/index.html +2 -2
  30. package/package.json +1 -1
  31. package/editor/assets/index-CLoI_HF2.js +0 -134
@@ -154,1451 +154,1793 @@ function copy(bytes) {
154
154
  return bytes.slice();
155
155
  }
156
156
 
157
- // ../engine/src/store/content-index.ts
158
- var RESERVED_PREFIX = ".nanoesis/";
159
- var INDEX_KEY = `${RESERVED_PREFIX}index.json`;
160
- var BACKUP_RING_SIZE = 3;
161
- function backupKey(slot) {
162
- return `${RESERVED_PREFIX}index.bak.${slot}`;
157
+ // ../engine/src/html/dom.ts
158
+ import { parse, parseFragment, serialize } from "parse5";
159
+ function isElement(node) {
160
+ return "tagName" in node;
163
161
  }
164
- function emptyIndex() {
165
- return freezeIndex(0, []);
162
+ function isTextNode(node) {
163
+ return node.nodeName === "#text";
166
164
  }
167
- async function loadIndex(store) {
168
- const live = parseIndex(await store.get(INDEX_KEY));
169
- if (live !== void 0) return live;
170
- let best;
171
- for (let slot = 0; slot < BACKUP_RING_SIZE; slot += 1) {
172
- const candidate = parseIndex(await store.get(backupKey(slot)));
173
- if (candidate !== void 0 && (best === void 0 || candidate.version > best.version)) {
174
- best = candidate;
175
- }
176
- }
177
- return best ?? emptyIndex();
165
+ function getAttribute(el, name) {
166
+ return el.attrs.find((attr) => attr.name === name)?.value;
178
167
  }
179
- async function saveIndex(store, prev, nextKeys) {
180
- await store.put(backupKey(prev.version % BACKUP_RING_SIZE), serialize(prev));
181
- const next = freezeIndex(prev.version + 1, nextKeys);
182
- await store.put(INDEX_KEY, serialize(next));
183
- return next;
168
+ function hasAttribute(el, name) {
169
+ return el.attrs.some((attr) => attr.name === name);
184
170
  }
185
- async function reconcileIndex(store, actualKeys) {
186
- const prev = await loadIndex(store);
187
- const keys = [...new Set(actualKeys.filter((key) => !key.startsWith(RESERVED_PREFIX)))].sort();
188
- const prevKeys = new Set(prev.keys);
189
- const nextKeys = new Set(keys);
190
- const added = keys.filter((key) => !prevKeys.has(key));
191
- const removed = prev.keys.filter((key) => !nextKeys.has(key));
192
- if (added.length === 0 && removed.length === 0) return { index: prev, added, removed };
193
- return { index: await saveIndex(store, prev, keys), added, removed };
171
+ function setAttribute(el, name, value) {
172
+ const existing = el.attrs.find((attr) => attr.name === name);
173
+ if (existing) existing.value = value;
174
+ else el.attrs.push({ name, value });
194
175
  }
195
- function freezeIndex(version, keys) {
196
- const sorted = [...new Set(keys)].sort();
197
- return { version, keys: sorted, checksum: checksumOf(version, sorted) };
176
+ function removeAttribute(el, name) {
177
+ el.attrs = el.attrs.filter((attr) => attr.name !== name);
198
178
  }
199
- function serialize(index) {
200
- return new TextEncoder().encode(`${JSON.stringify(index, null, 2)}
201
- `);
179
+ function parseFragmentNodes(html) {
180
+ return parseFragment(html).childNodes;
202
181
  }
203
- function parseIndex(bytes) {
204
- if (bytes === void 0) return void 0;
205
- let raw;
206
- try {
207
- raw = JSON.parse(new TextDecoder().decode(bytes));
208
- } catch {
209
- return void 0;
210
- }
211
- if (typeof raw !== "object" || raw === null) return void 0;
212
- const { version, keys, checksum } = raw;
213
- if (typeof version !== "number" || typeof checksum !== "string" || !Array.isArray(keys)) {
214
- return void 0;
215
- }
216
- const stringKeys = keys.filter((key) => typeof key === "string");
217
- if (stringKeys.length !== keys.length) return void 0;
218
- const rebuilt = freezeIndex(version, stringKeys);
219
- return rebuilt.checksum === checksum ? rebuilt : void 0;
182
+ function isFullDocument(html) {
183
+ const start = html.trimStart().toLowerCase();
184
+ return start.startsWith("<!doctype") || start.startsWith("<html");
220
185
  }
221
- function checksumOf(version, sortedKeys) {
222
- const text = `${version}
223
- ${sortedKeys.join("\n")}`;
224
- let hash = 2166136261;
225
- for (let i = 0; i < text.length; i += 1) {
226
- hash ^= text.charCodeAt(i);
227
- hash = Math.imul(hash, 16777619);
228
- }
229
- return (hash >>> 0).toString(16).padStart(8, "0");
186
+ function parseNodes(html) {
187
+ return isFullDocument(html) ? parse(html).childNodes : parseFragment(html).childNodes;
188
+ }
189
+ function serializeNodes(nodes) {
190
+ const fragment = parseFragment("");
191
+ fragment.childNodes = nodes;
192
+ return serialize(fragment);
230
193
  }
231
194
 
232
- // ../engine/src/store/indexed-store.ts
233
- var IndexedStore = class {
234
- constructor(store) {
235
- this.store = store;
236
- }
237
- store;
238
- index;
239
- loading;
240
- /**
241
- * Per-instance mutex (DESIGN §11d): every mutation (`write`/`delete`/`rename`/`reconcile`)
242
- * runs through {@link serializeMutation}, which chains onto the previous mutation's
243
- * completion. Without it, two concurrent mutations both read the cached `this.index`,
244
- * each compute their "next" key set from that stale snapshot, and the second
245
- * `saveIndex` clobbers the first the file writes land in blob, but the index keeps
246
- * stale references or loses fresh ones (the "I deleted it and it's still there" or
247
- * "I added it and it didn't appear" bugs surfaced dogfooding the marketing site,
248
- * 2026-05-28). Reads (`list`/`exists`/`readBytes`) are *not* serialised; an
249
- * in-flight mutation simply means a reader sees the pre-mutation index, eventually
250
- * consistent and safe. Crashes inside `work()` release the lock so a single failure
251
- * does not deadlock subsequent operations.
252
- */
253
- mutationLock = Promise.resolve();
254
- /** The index, loaded once on first need and cached (mutations replace the cached copy). */
255
- async loaded() {
256
- if (this.index !== void 0) return this.index;
257
- this.loading ??= loadIndex(this.store);
258
- this.index = await this.loading;
259
- return this.index;
260
- }
261
- serializeMutation(work) {
262
- const previous = this.mutationLock;
263
- let release;
264
- this.mutationLock = new Promise((resolve) => {
265
- release = resolve;
266
- });
267
- return (async () => {
268
- try {
269
- await previous;
270
- return await work();
271
- } finally {
272
- release();
273
- }
274
- })();
275
- }
276
- async list(dir) {
277
- return childrenOf((await this.loaded()).keys, dir);
278
- }
279
- async readText(path) {
280
- return new TextDecoder().decode(await this.readBytes(path));
281
- }
282
- async readBytes(path) {
283
- const bytes = await this.store.get(path);
284
- if (bytes === void 0) throw new Error(`No such file in content source: ${path}`);
285
- return bytes;
286
- }
287
- async exists(path) {
288
- return pathExists((await this.loaded()).keys, path);
289
- }
290
- /**
291
- * Create or overwrite `key`. The index is rewritten only when `key` is new (an
292
- * overwrite leaves the key set unchanged, so editing an item is a single `put`).
293
- */
294
- write(key, bytes) {
295
- return this.serializeMutation(async () => {
296
- const target = guarded(normalize(key));
297
- await this.store.put(target, bytes);
298
- const index = await this.loaded();
299
- if (!index.keys.includes(target)) {
300
- this.index = await saveIndex(this.store, index, [...index.keys, target]);
301
- }
302
- });
303
- }
304
- /**
305
- * Delete a file, or a whole directory subtree (every key under `key/`). Idempotent: a
306
- * path the index does not know is still deleted from the store (clearing an orphan),
307
- * and deleting nothing is a no-op.
308
- */
309
- delete(key) {
310
- return this.serializeMutation(async () => {
311
- const target = guarded(normalize(key));
312
- const index = await this.loaded();
313
- const prefix = `${target}/`;
314
- const removed = index.keys.filter((k) => k === target || k.startsWith(prefix));
315
- if (removed.length === 0) {
316
- await this.store.delete(target);
317
- return;
318
- }
319
- await Promise.all(removed.map((k) => this.store.delete(k)));
320
- const remaining = index.keys.filter((k) => k !== target && !k.startsWith(prefix));
321
- this.index = await saveIndex(this.store, index, remaining);
322
- });
323
- }
324
- /**
325
- * Move/rename a file, or a whole directory subtree (every key under `from/` is remapped
326
- * under `to/`). Clobbering an existing destination, or renaming a missing path, are
327
- * returned as data (`ok: false`), not thrown (CLAUDE §2), the same contract the host's
328
- * `/api/rename` enforces. (Mutating the reserved namespace is a programmer error and
329
- * still throws.)
330
- */
331
- rename(from, to) {
332
- return this.serializeMutation(async () => {
333
- const source = guarded(normalize(from));
334
- const dest = guarded(normalize(to));
335
- if (source === dest) return { ok: true };
336
- const index = await this.loaded();
337
- const sourcePrefix = `${source}/`;
338
- const affected = index.keys.filter((k) => k === source || k.startsWith(sourcePrefix));
339
- if (affected.length === 0) return { ok: false, reason: "missing" };
340
- const destPrefix = `${dest}/`;
341
- if (index.keys.some((k) => k === dest || k.startsWith(destPrefix))) {
342
- return { ok: false, reason: "exists" };
343
- }
344
- const moves = affected.map((k) => ({
345
- from: k,
346
- to: k === source ? dest : dest + k.slice(source.length)
347
- }));
348
- for (const move of moves) {
349
- const bytes = await this.store.get(move.from);
350
- if (bytes === void 0) continue;
351
- await this.store.put(move.to, bytes);
352
- await this.store.delete(move.from);
353
- }
354
- const movedFrom = new Set(moves.map((move) => move.from));
355
- const next = index.keys.filter((k) => !movedFrom.has(k)).concat(moves.map((m) => m.to));
356
- this.index = await saveIndex(this.store, index, next);
357
- return { ok: true };
358
- });
359
- }
360
- /**
361
- * Rebuild this store's index from the *actual* keys the underlying store holds (DESIGN
362
- * §11d), recovering files that arrived by a path that bypassed the index. A
363
- * {@link BlobStore} cannot enumerate itself, so the caller supplies the real key set
364
- * from an adapter that can (e.g. `BlobContainer.list`). Unlike calling
365
- * {@link reconcileIndex} on a fresh store, this also refreshes the cached in-memory
366
- * index, so this live instance sees the recovered files immediately.
367
- */
368
- reconcile(actualKeys) {
369
- return this.serializeMutation(async () => {
370
- const result = await reconcileIndex(this.store, actualKeys);
371
- this.index = result.index;
372
- return result;
373
- });
374
- }
195
+ // ../engine/src/template/registry.ts
196
+ var FIELD_TYPES = {
197
+ image: { type: "image", valueKind: "asset", control: "image", multiline: false },
198
+ file: { type: "file", valueKind: "asset", control: "file", multiline: false },
199
+ url: { type: "url", valueKind: "url", control: "url", multiline: false },
200
+ email: { type: "email", valueKind: "text", control: "email", multiline: false },
201
+ phone: { type: "phone", valueKind: "text", control: "phone", multiline: false },
202
+ date: { type: "date", valueKind: "text", control: "date", multiline: false },
203
+ time: { type: "time", valueKind: "text", control: "time", multiline: false },
204
+ code: { type: "code", valueKind: "text", control: "code", multiline: true },
205
+ richtext: { type: "richtext", valueKind: "html", control: "richtext", multiline: true },
206
+ authors: { type: "authors", valueKind: "authors", control: "authors", multiline: false },
207
+ text: { type: "text", valueKind: "text", control: "text", multiline: true },
208
+ shorttext: { type: "shorttext", valueKind: "text", control: "shorttext", multiline: false }
375
209
  };
376
- function guarded(key) {
377
- if (key === "" || key.startsWith(RESERVED_PREFIX)) {
378
- throw new Error(`Refusing to mutate a reserved key: ${key === "" ? "(root)" : key}`);
379
- }
380
- return key;
210
+ function isFieldType(value) {
211
+ return Object.prototype.hasOwnProperty.call(FIELD_TYPES, value);
381
212
  }
382
- function normalize(path) {
383
- return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
384
- }
385
- function childrenOf(keys, dir) {
386
- const base = normalize(dir);
387
- const prefix = base === "" ? "" : `${base}/`;
388
- const entries = /* @__PURE__ */ new Map();
389
- for (const key of keys) {
390
- if (prefix !== "" && !key.startsWith(prefix)) continue;
391
- const rest = key.slice(prefix.length);
392
- if (rest === "") continue;
393
- const slash = rest.indexOf("/");
394
- if (slash === -1) entries.set(rest, "file");
395
- else entries.set(rest.slice(0, slash), "dir");
396
- }
397
- return [...entries].map(([name, kind]) => ({ name, kind }));
398
- }
399
- function pathExists(keys, path) {
400
- const target = normalize(path);
401
- if (target === "") return true;
402
- if (keys.includes(target)) return true;
403
- const prefix = `${target}/`;
404
- return keys.some((key) => key.startsWith(prefix));
213
+ function valueKindOf(type) {
214
+ return FIELD_TYPES[type].valueKind;
405
215
  }
406
216
 
407
- // ../engine/src/content/source.ts
408
- function normalizePath(path) {
409
- return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
410
- }
411
- var InMemoryContentSource = class {
412
- files;
413
- constructor(files) {
414
- this.files = new Map(Object.entries(files).map(([key, value]) => [normalizePath(key), value]));
415
- }
416
- async readText(path) {
417
- const value = this.files.get(normalizePath(path));
418
- if (value === void 0) throw new Error(`No such file in content source: ${path}`);
419
- return typeof value === "string" ? value : new TextDecoder().decode(value);
420
- }
421
- async readBytes(path) {
422
- const value = this.files.get(normalizePath(path));
423
- if (value === void 0) throw new Error(`No such file in content source: ${path}`);
424
- return typeof value === "string" ? new TextEncoder().encode(value) : value;
217
+ // ../engine/src/template/inference.ts
218
+ var SHORT_TEXT_ELEMENTS = /* @__PURE__ */ new Set([
219
+ "h1",
220
+ "h2",
221
+ "h3",
222
+ "h4",
223
+ "h5",
224
+ "h6",
225
+ "title",
226
+ "label",
227
+ "th",
228
+ "caption",
229
+ "figcaption",
230
+ "legend",
231
+ "summary",
232
+ "option",
233
+ "dt",
234
+ "button"
235
+ ]);
236
+ var CODE_ELEMENTS = /* @__PURE__ */ new Set(["code", "pre", "kbd", "samp"]);
237
+ var PLAIN_TEXT_ELEMENTS = /* @__PURE__ */ new Set([
238
+ "p",
239
+ "span",
240
+ "li",
241
+ "td",
242
+ "dd",
243
+ "strong",
244
+ "em",
245
+ "b",
246
+ "i",
247
+ "small",
248
+ "time",
249
+ "a",
250
+ "figcaption",
251
+ "div",
252
+ "article",
253
+ "section",
254
+ "main",
255
+ "aside",
256
+ "blockquote"
257
+ ]);
258
+ function inferControl(ctx) {
259
+ if (ctx.annotation !== void 0 && isFieldType(ctx.annotation)) {
260
+ return ctx.annotation;
425
261
  }
426
- async exists(path) {
427
- const target = normalizePath(path);
428
- if (target === "") return true;
429
- if (this.files.has(target)) return true;
430
- const prefix = `${target}/`;
431
- for (const key of this.files.keys()) {
432
- if (key.startsWith(prefix)) return true;
262
+ if (ctx.attribute !== void 0) {
263
+ const attr = ctx.attribute;
264
+ const prefix = (ctx.valuePrefix ?? "").toLowerCase();
265
+ if (attr === "href") {
266
+ if (prefix.endsWith("mailto:")) return "email";
267
+ if (prefix.endsWith("tel:")) return "phone";
268
+ if (ctx.wholeValue) return ctx.download === true ? "file" : "url";
433
269
  }
434
- return false;
435
- }
436
- async list(dir) {
437
- const base = normalizePath(dir);
438
- const prefix = base === "" ? "" : `${base}/`;
439
- const entries = /* @__PURE__ */ new Map();
440
- for (const key of this.files.keys()) {
441
- if (prefix !== "" && !key.startsWith(prefix)) continue;
442
- const rest = key.slice(prefix.length);
443
- if (rest === "") continue;
444
- const slash = rest.indexOf("/");
445
- if (slash === -1) entries.set(rest, "file");
446
- else entries.set(rest.slice(0, slash), "dir");
270
+ if (ctx.wholeValue && attr === "src") {
271
+ if (ctx.tag === "img") return "image";
272
+ return "file";
447
273
  }
448
- return [...entries].map(([name, kind]) => ({ name, kind }));
449
- }
450
- };
451
-
452
- // ../engine/src/url/redirects.ts
453
- var DEFAULT_STATUS = 301;
454
- var OUTPUT_PATH = "_redirects";
455
- function isRule(value) {
456
- if (typeof value !== "object" || value === null) return false;
457
- const record = value;
458
- return typeof record.from === "string" && typeof record.to === "string";
459
- }
460
- function parseRedirects(raw) {
461
- if (!Array.isArray(raw)) return [];
462
- const rules = [];
463
- for (const entry of raw) {
464
- if (!isRule(entry)) continue;
465
- const status = entry.status;
466
- rules.push(
467
- typeof status === "number" && Number.isInteger(status) ? { from: entry.from, to: entry.to, status } : { from: entry.from, to: entry.to }
468
- );
469
- }
470
- return rules;
471
- }
472
- function resolveTarget(from, exact) {
473
- const seen = /* @__PURE__ */ new Set([from]);
474
- let target = exact.get(from).to;
475
- while (exact.has(target) && !seen.has(target)) {
476
- seen.add(target);
477
- target = exact.get(target).to;
274
+ if (ctx.wholeValue && ctx.tag === "source" && attr === "srcset") return "file";
275
+ if (attr === "alt" || attr === "title") return "shorttext";
276
+ return "shorttext";
478
277
  }
479
- return target;
278
+ if (CODE_ELEMENTS.has(ctx.tag)) return "code";
279
+ if (SHORT_TEXT_ELEMENTS.has(ctx.tag)) return "shorttext";
280
+ if (PLAIN_TEXT_ELEMENTS.has(ctx.tag)) return "text";
281
+ return "shorttext";
480
282
  }
481
- function buildRedirects(rules, liveUrls) {
482
- const exact = /* @__PURE__ */ new Map();
483
- const wildcard = /* @__PURE__ */ new Map();
484
- for (const rule of rules) {
485
- (rule.from.includes("*") ? wildcard : exact).set(rule.from, rule);
486
- }
487
- const resolved = [];
488
- for (const [from, rule] of exact) {
489
- if (liveUrls.has(from)) continue;
490
- const to = resolveTarget(from, exact);
491
- if (to === from) continue;
492
- resolved.push(rule.status !== void 0 ? { from, to, status: rule.status } : { from, to });
283
+
284
+ // ../engine/src/template/token.ts
285
+ var TOKEN = /\{([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\}/g;
286
+ function findTokens(input) {
287
+ const out = [];
288
+ for (const match of input.matchAll(TOKEN)) {
289
+ const expr = match[1] ?? "";
290
+ const path = expr.split(".");
291
+ out.push({ raw: match[0], name: path[0] ?? "", path, dotted: path.length > 1 });
493
292
  }
494
- resolved.push(...wildcard.values());
495
- if (resolved.length === 0) return void 0;
496
- resolved.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
497
- const contents = resolved.map((rule) => `${rule.from} ${rule.to} ${rule.status ?? DEFAULT_STATUS}
498
- `).join("");
499
- return { path: OUTPUT_PATH, contents };
293
+ return out;
500
294
  }
501
-
502
- // ../engine/src/content/loader.ts
503
- var SORT_FILE = "_sort.json";
504
- var REDIRECTS_FILE = "_redirects.json";
505
- var SITE_CONFIG_FILE = "_site.json";
506
- var ASSETS_DIR = "assets";
507
- var ITEM_EXT = ".json";
508
- var DEFAULT_DIRS = {
509
- content: "content",
510
- templates: "templates",
511
- components: "components",
512
- /** Static passthrough, copied verbatim to the published root (DESIGN §8). The
513
- * engine never reads it; named here so hosts/editor share one source of truth. */
514
- public: "public"
515
- };
516
- function join(...parts) {
517
- return parts.filter((part) => part !== "").join("/");
295
+ function wholeValueToken(input) {
296
+ const trimmed = input.trim();
297
+ const tokens = findTokens(trimmed);
298
+ if (tokens.length === 1 && tokens[0]?.raw === trimmed) return tokens[0];
299
+ return null;
518
300
  }
519
- function stripBom(text) {
520
- return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
301
+ function substituteTokens(input, resolve) {
302
+ return input.replace(/\{([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\}/g, (raw, expr) => {
303
+ const path = expr.split(".");
304
+ return resolve({ raw, name: path[0] ?? "", path, dotted: path.length > 1 });
305
+ });
521
306
  }
522
- async function loadContentTree(source, contentDir = DEFAULT_DIRS.content) {
523
- return loadDir(source, contentDir, "", "");
307
+ function literalPrefix(input) {
308
+ const brace = input.indexOf("{");
309
+ return (brace === -1 ? input : input.slice(0, brace)).toLowerCase();
524
310
  }
525
- async function loadDir(source, dirPath, slug, treePath) {
526
- const [entries, sort] = await Promise.all([source.list(dirPath), readSort(source, dirPath)]);
527
- const childMap = /* @__PURE__ */ new Map();
528
- for (const entry of entries) {
529
- if (entry.kind === "file") {
530
- if (entry.name === SORT_FILE || entry.name === REDIRECTS_FILE || entry.name === SITE_CONFIG_FILE || !entry.name.endsWith(ITEM_EXT))
531
- continue;
532
- const childSlug = entry.name.slice(0, -ITEM_EXT.length);
533
- childMap.set(
534
- childSlug,
535
- await loadItem(source, join(dirPath, entry.name), childSlug, treePath)
536
- );
537
- } else {
538
- if (entry.name === ASSETS_DIR) continue;
539
- const childPath = join(treePath, entry.name);
540
- childMap.set(
541
- entry.name,
542
- await loadDir(source, join(dirPath, entry.name), entry.name, childPath)
543
- );
544
- }
545
- }
546
- const name = sort.name ?? (slug === "" ? "" : humanize(slug));
311
+
312
+ // ../engine/src/template/fields.ts
313
+ function lengthConstraints(el) {
314
+ const min = parseLength(getAttribute(el, "data-minlength"));
315
+ const max = parseLength(getAttribute(el, "data-maxlength"));
547
316
  return {
548
- kind: "dir",
549
- slug,
550
- name,
551
- path: treePath,
552
- children: orderChildren(childMap, sort.order),
553
- ...sort.collection !== void 0 && { collection: sort.collection },
554
- ...sort.defaultTemplate !== void 0 && { defaultTemplate: sort.defaultTemplate }
317
+ ...min !== void 0 && { minLength: min },
318
+ ...max !== void 0 && { maxLength: max }
555
319
  };
556
320
  }
557
- async function loadItem(source, filePath, slug, parentPath2) {
558
- const raw = await source.readText(filePath);
559
- try {
560
- const item = parseContentItem(JSON.parse(raw));
561
- return { kind: "item", slug, path: join(parentPath2, slug), item };
562
- } catch (error) {
563
- throw new Error(`Failed to load ${filePath}: ${error.message}`);
564
- }
565
- }
566
- async function readSort(source, dirPath) {
567
- const path = join(dirPath, SORT_FILE);
568
- if (!await source.exists(path)) return { order: [] };
569
- try {
570
- return parseSortFile(JSON.parse(await source.readText(path)));
571
- } catch {
572
- return { order: [] };
573
- }
321
+ function parseLength(raw) {
322
+ if (raw === void 0) return void 0;
323
+ const value = Number(raw);
324
+ return Number.isInteger(value) && value >= 0 ? value : void 0;
574
325
  }
575
- async function loadRedirects(source, contentDir = DEFAULT_DIRS.content) {
576
- const path = join(contentDir, REDIRECTS_FILE);
577
- if (!await source.exists(path)) return [];
578
- try {
579
- return parseRedirects(JSON.parse(stripBom(await source.readText(path))));
580
- } catch {
581
- return [];
582
- }
326
+ function deriveFields(template, components = /* @__PURE__ */ new Map()) {
327
+ return deriveFromSource(template, components, /* @__PURE__ */ new Set());
583
328
  }
584
- async function loadSiteConfig(source, contentDir = DEFAULT_DIRS.content) {
585
- const path = join(contentDir, SITE_CONFIG_FILE);
586
- if (!await source.exists(path)) return {};
587
- try {
588
- const raw = JSON.parse(stripBom(await source.readText(path)));
589
- const baseUrl = typeof raw.baseUrl === "string" ? raw.baseUrl.trim() : "";
590
- return baseUrl !== "" ? { baseUrl } : {};
591
- } catch {
592
- return {};
593
- }
329
+ function componentPropTypes(tag, components, seen) {
330
+ if (seen.has(tag)) return /* @__PURE__ */ new Map();
331
+ const source = components.get(tag);
332
+ if (source === void 0) return /* @__PURE__ */ new Map();
333
+ const fields = deriveFromSource(source, components, new Set(seen).add(tag));
334
+ return new Map(fields.map((field) => [field.name, field.type]));
594
335
  }
595
- function orderChildren(map, order) {
596
- const out = [];
597
- const used = /* @__PURE__ */ new Set();
598
- for (const slug of order) {
599
- const node = map.get(slug);
600
- if (node !== void 0) {
601
- out.push(node);
602
- used.add(slug);
336
+ function deriveFromSource(source, components, seen) {
337
+ const order = [];
338
+ const fields = /* @__PURE__ */ new Map();
339
+ const ensure = (name) => {
340
+ const existing = fields.get(name);
341
+ if (existing !== void 0) return existing;
342
+ const created = {
343
+ name,
344
+ type: "shorttext",
345
+ label: humanize(name),
346
+ required: false,
347
+ multiline: false
348
+ };
349
+ fields.set(name, created);
350
+ order.push(name);
351
+ return created;
352
+ };
353
+ const record = (name, type) => {
354
+ if (fields.has(name)) return;
355
+ const field = ensure(name);
356
+ field.type = type;
357
+ field.multiline = FIELD_TYPES[type].multiline;
358
+ };
359
+ const applyAnnotations = (el) => {
360
+ const named = getAttribute(el, "data-field") ?? firstTokenName(el);
361
+ if (named === void 0) return;
362
+ const field = ensure(named);
363
+ if (hasAttribute(el, "data-required")) field.required = true;
364
+ const help = getAttribute(el, "data-help");
365
+ if (help !== void 0) field.help = help;
366
+ const label = getAttribute(el, "data-label");
367
+ if (label !== void 0) field.label = label;
368
+ const { minLength, maxLength } = lengthConstraints(el);
369
+ if (minLength !== void 0) field.minLength = minLength;
370
+ if (maxLength !== void 0) field.maxLength = maxLength;
371
+ };
372
+ const recordFreeText = (text) => {
373
+ const whole = wholeValueToken(text);
374
+ for (const token of findTokens(text)) {
375
+ if (token.dotted) continue;
376
+ record(
377
+ token.name,
378
+ inferControl({
379
+ tag: "",
380
+ wholeValue: whole?.name === token.name
381
+ })
382
+ );
383
+ }
384
+ };
385
+ const visit = (nodes) => {
386
+ for (const node of nodes) {
387
+ if (!isElement(node)) {
388
+ if (isTextNode(node)) recordFreeText(node.value);
389
+ continue;
390
+ }
391
+ const eachField = getAttribute(node, "data-each");
392
+ if (eachField !== void 0) {
393
+ const field = ensure(eachField);
394
+ field.type = "url";
395
+ field.multiple = true;
396
+ const pickFrom = getAttribute(node, "data-pick-from");
397
+ if (pickFrom !== void 0) field.pickFrom = pickFrom;
398
+ applyAnnotations(node);
399
+ continue;
400
+ }
401
+ if (hasAttribute(node, "data-each-in")) continue;
402
+ const annotation = getAttribute(node, "data-type");
403
+ const propTypes = components.has(node.tagName) ? componentPropTypes(node.tagName, components, seen) : void 0;
404
+ for (const attr of node.attrs) {
405
+ const whole = wholeValueToken(attr.value);
406
+ for (const token of findTokens(attr.value)) {
407
+ if (token.dotted) continue;
408
+ const isWhole = whole?.name === token.name;
409
+ const propType = isWhole ? propTypes?.get(attr.name) : void 0;
410
+ record(
411
+ token.name,
412
+ propType ?? inferControl({
413
+ tag: node.tagName,
414
+ attribute: attr.name,
415
+ wholeValue: isWhole,
416
+ valuePrefix: literalPrefix(attr.value),
417
+ download: hasAttribute(node, "download"),
418
+ ...annotation !== void 0 && { annotation }
419
+ })
420
+ );
421
+ }
422
+ }
423
+ for (const child of node.childNodes) {
424
+ if (!isTextNode(child)) continue;
425
+ const whole = wholeValueToken(child.value);
426
+ for (const token of findTokens(child.value)) {
427
+ if (token.dotted) continue;
428
+ record(
429
+ token.name,
430
+ inferControl({
431
+ tag: node.tagName,
432
+ wholeValue: whole?.name === token.name,
433
+ ...annotation !== void 0 && { annotation }
434
+ })
435
+ );
436
+ }
437
+ }
438
+ applyAnnotations(node);
439
+ visit(node.childNodes);
603
440
  }
441
+ };
442
+ visit(parseNodes(source));
443
+ return order.map((name) => freeze(fields.get(name)));
444
+ }
445
+ function firstTokenName(el) {
446
+ for (const attr of el.attrs) {
447
+ const token = findTokens(attr.value).find((candidate) => !candidate.dotted);
448
+ if (token !== void 0) return token.name;
604
449
  }
605
- for (const [slug, node] of map) {
606
- if (!used.has(slug)) out.push(node);
450
+ for (const child of el.childNodes) {
451
+ if (isTextNode(child)) {
452
+ const token = findTokens(child.value).find((candidate) => !candidate.dotted);
453
+ if (token !== void 0) return token.name;
454
+ }
607
455
  }
608
- return out;
456
+ return void 0;
609
457
  }
610
- function loadComponents(source, componentsDir = DEFAULT_DIRS.components) {
611
- return loadComponentFiles(source, componentsDir, "html");
458
+ function freeze(field) {
459
+ return {
460
+ name: field.name,
461
+ type: field.type,
462
+ label: field.label,
463
+ required: field.required,
464
+ multiline: field.multiline,
465
+ ...field.help !== void 0 && { help: field.help },
466
+ ...field.minLength !== void 0 && { minLength: field.minLength },
467
+ ...field.maxLength !== void 0 && { maxLength: field.maxLength },
468
+ ...field.multiple !== void 0 && { multiple: field.multiple },
469
+ ...field.pickFrom !== void 0 && { pickFrom: field.pickFrom }
470
+ };
612
471
  }
613
- function loadComponentStyles(source, componentsDir = DEFAULT_DIRS.components) {
614
- return loadComponentFiles(source, componentsDir, "css");
472
+
473
+ // ../engine/src/template/stamp.ts
474
+ function detectStamp(oldSource, newSource, components = /* @__PURE__ */ new Map()) {
475
+ const oldFields = new Set(deriveFields(oldSource, components).map((f) => f.name));
476
+ const newFields = new Set(deriveFields(newSource, components).map((f) => f.name));
477
+ const removed = [];
478
+ const added = [];
479
+ for (const name of oldFields) {
480
+ if (!newFields.has(name)) removed.push(name);
481
+ }
482
+ for (const name of newFields) {
483
+ if (!oldFields.has(name)) added.push(name);
484
+ }
485
+ removed.sort();
486
+ added.sort();
487
+ return { destructive: removed.length > 0, removedTokens: removed, addedTokens: added };
615
488
  }
616
- function loadComponentScripts(source, componentsDir = DEFAULT_DIRS.components) {
617
- return loadComponentFiles(source, componentsDir, "js");
489
+
490
+ // ../engine/src/template/versions.ts
491
+ var VERSION_PATTERN = /^(.+)@v([1-9]\d*)$/;
492
+ function parseVersionedName(name) {
493
+ const match = VERSION_PATTERN.exec(name);
494
+ if (!match) return null;
495
+ const base = match[1];
496
+ if (VERSION_PATTERN.test(base)) return null;
497
+ return { base, version: Number.parseInt(match[2], 10) };
498
+ }
499
+ function isVersionedTemplateName(name) {
500
+ return parseVersionedName(name) !== null;
501
+ }
502
+ function baseTemplateName(name) {
503
+ const parsed = parseVersionedName(name);
504
+ return parsed ? parsed.base : name;
505
+ }
506
+ function versionNumber(name) {
507
+ const parsed = parseVersionedName(name);
508
+ return parsed ? parsed.version : null;
509
+ }
510
+ function snapshotName(base, version) {
511
+ if (!Number.isInteger(version) || version < 1) {
512
+ throw new Error(`Snapshot version must be a positive integer, got ${version}`);
513
+ }
514
+ if (isVersionedTemplateName(base)) {
515
+ throw new Error(`snapshotName base "${base}" is already versioned`);
516
+ }
517
+ return `${base}@v${version}`;
518
+ }
519
+ function nextVersionNumber(existingNames) {
520
+ let max = 0;
521
+ for (const name of existingNames) {
522
+ const n = versionNumber(name);
523
+ if (n !== null && n > max) max = n;
524
+ }
525
+ return max + 1;
526
+ }
527
+ function isReservedVersionedPath(path) {
528
+ if (!path.endsWith(".html")) return false;
529
+ if (!(path.startsWith("templates/") || path.startsWith("components/"))) return false;
530
+ const stem = path.slice(0, -".html".length);
531
+ const lastSlash = stem.lastIndexOf("/");
532
+ const baseName = stem.slice(lastSlash + 1);
533
+ return isVersionedTemplateName(baseName);
618
534
  }
619
- async function loadComponentFiles(source, componentsDir, extension) {
620
- const suffix = `.${extension}`;
621
- const map = /* @__PURE__ */ new Map();
622
- const walk = async (dir) => {
623
- if (!await source.exists(dir)) return;
624
- for (const entry of await source.list(dir)) {
625
- const full = join(dir, entry.name);
626
- if (entry.kind === "dir") {
627
- await walk(full);
628
- } else if (entry.name.endsWith(suffix)) {
629
- const key = entry.name.slice(0, -suffix.length).toLowerCase();
630
- map.set(key, await source.readText(full));
631
- }
535
+
536
+ // ../engine/src/store/content-index.ts
537
+ var RESERVED_PREFIX = ".nanoesis/";
538
+ var INDEX_KEY = `${RESERVED_PREFIX}index.json`;
539
+ var BACKUP_RING_SIZE = 3;
540
+ function backupKey(slot) {
541
+ return `${RESERVED_PREFIX}index.bak.${slot}`;
542
+ }
543
+ function emptyIndex() {
544
+ return freezeIndex(0, []);
545
+ }
546
+ async function loadIndex(store) {
547
+ const live = parseIndex(await store.get(INDEX_KEY));
548
+ if (live !== void 0) return live;
549
+ let best;
550
+ for (let slot = 0; slot < BACKUP_RING_SIZE; slot += 1) {
551
+ const candidate = parseIndex(await store.get(backupKey(slot)));
552
+ if (candidate !== void 0 && (best === void 0 || candidate.version > best.version)) {
553
+ best = candidate;
632
554
  }
633
- };
634
- await walk(componentsDir);
635
- return map;
555
+ }
556
+ return best ?? emptyIndex();
636
557
  }
637
- async function loadTemplate(source, name, templatesDir = DEFAULT_DIRS.templates) {
638
- return source.readText(join(templatesDir, `${name}.html`));
558
+ async function saveIndex(store, prev, nextKeys) {
559
+ await store.put(backupKey(prev.version % BACKUP_RING_SIZE), serialize2(prev));
560
+ const next = freezeIndex(prev.version + 1, nextKeys);
561
+ await store.put(INDEX_KEY, serialize2(next));
562
+ return next;
639
563
  }
640
- function loadTemplateStyle(source, name, templatesDir = DEFAULT_DIRS.templates) {
641
- return tryReadText(source, join(templatesDir, `${name}.css`));
564
+ async function reconcileIndex(store, actualKeys) {
565
+ const prev = await loadIndex(store);
566
+ const keys = [...new Set(actualKeys.filter((key) => !key.startsWith(RESERVED_PREFIX)))].sort();
567
+ const prevKeys = new Set(prev.keys);
568
+ const nextKeys = new Set(keys);
569
+ const added = keys.filter((key) => !prevKeys.has(key));
570
+ const removed = prev.keys.filter((key) => !nextKeys.has(key));
571
+ if (added.length === 0 && removed.length === 0) return { index: prev, added, removed };
572
+ return { index: await saveIndex(store, prev, keys), added, removed };
642
573
  }
643
- function loadTemplateScript(source, name, templatesDir = DEFAULT_DIRS.templates) {
644
- return tryReadText(source, join(templatesDir, `${name}.js`));
574
+ function freezeIndex(version, keys) {
575
+ const sorted = [...new Set(keys)].sort();
576
+ return { version, keys: sorted, checksum: checksumOf(version, sorted) };
645
577
  }
646
- var DOCUMENT_SHELL = "document";
647
- function loadDocumentShell(source, templatesDir = DEFAULT_DIRS.templates) {
648
- return tryReadText(source, join(templatesDir, `${DOCUMENT_SHELL}.html`));
578
+ function serialize2(index) {
579
+ return new TextEncoder().encode(`${JSON.stringify(index, null, 2)}
580
+ `);
649
581
  }
650
- async function tryReadText(source, path) {
582
+ function parseIndex(bytes) {
583
+ if (bytes === void 0) return void 0;
584
+ let raw;
651
585
  try {
652
- return await source.readText(path);
586
+ raw = JSON.parse(new TextDecoder().decode(bytes));
653
587
  } catch {
654
588
  return void 0;
655
589
  }
590
+ if (typeof raw !== "object" || raw === null) return void 0;
591
+ const { version, keys, checksum } = raw;
592
+ if (typeof version !== "number" || typeof checksum !== "string" || !Array.isArray(keys)) {
593
+ return void 0;
594
+ }
595
+ const stringKeys = keys.filter((key) => typeof key === "string");
596
+ if (stringKeys.length !== keys.length) return void 0;
597
+ const rebuilt = freezeIndex(version, stringKeys);
598
+ return rebuilt.checksum === checksum ? rebuilt : void 0;
656
599
  }
657
-
658
- // ../engine/src/template/registry.ts
659
- var FIELD_TYPES = {
660
- image: { type: "image", valueKind: "asset", control: "image", multiline: false },
661
- file: { type: "file", valueKind: "asset", control: "file", multiline: false },
662
- url: { type: "url", valueKind: "url", control: "url", multiline: false },
663
- email: { type: "email", valueKind: "text", control: "email", multiline: false },
664
- phone: { type: "phone", valueKind: "text", control: "phone", multiline: false },
665
- date: { type: "date", valueKind: "text", control: "date", multiline: false },
666
- time: { type: "time", valueKind: "text", control: "time", multiline: false },
667
- code: { type: "code", valueKind: "text", control: "code", multiline: true },
668
- richtext: { type: "richtext", valueKind: "html", control: "richtext", multiline: true },
669
- authors: { type: "authors", valueKind: "authors", control: "authors", multiline: false },
670
- text: { type: "text", valueKind: "text", control: "text", multiline: true },
671
- shorttext: { type: "shorttext", valueKind: "text", control: "shorttext", multiline: false }
672
- };
673
- function isFieldType(value) {
674
- return Object.prototype.hasOwnProperty.call(FIELD_TYPES, value);
675
- }
676
- function valueKindOf(type) {
677
- return FIELD_TYPES[type].valueKind;
600
+ function checksumOf(version, sortedKeys) {
601
+ const text = `${version}
602
+ ${sortedKeys.join("\n")}`;
603
+ let hash = 2166136261;
604
+ for (let i = 0; i < text.length; i += 1) {
605
+ hash ^= text.charCodeAt(i);
606
+ hash = Math.imul(hash, 16777619);
607
+ }
608
+ return (hash >>> 0).toString(16).padStart(8, "0");
678
609
  }
679
610
 
680
- // ../engine/src/template/inference.ts
681
- var SHORT_TEXT_ELEMENTS = /* @__PURE__ */ new Set([
682
- "h1",
683
- "h2",
684
- "h3",
685
- "h4",
686
- "h5",
687
- "h6",
688
- "title",
689
- "label",
690
- "th",
691
- "caption",
692
- "figcaption",
693
- "legend",
694
- "summary",
695
- "option",
696
- "dt",
697
- "button"
698
- ]);
699
- var CODE_ELEMENTS = /* @__PURE__ */ new Set(["code", "pre", "kbd", "samp"]);
700
- var PLAIN_TEXT_ELEMENTS = /* @__PURE__ */ new Set([
701
- "p",
702
- "span",
703
- "li",
704
- "td",
705
- "dd",
706
- "strong",
707
- "em",
708
- "b",
709
- "i",
710
- "small",
711
- "time",
712
- "a",
713
- "figcaption",
714
- "div",
715
- "article",
716
- "section",
717
- "main",
718
- "aside",
719
- "blockquote"
720
- ]);
721
- function inferControl(ctx) {
722
- if (ctx.annotation !== void 0 && isFieldType(ctx.annotation)) {
723
- return ctx.annotation;
611
+ // ../engine/src/store/indexed-store.ts
612
+ var IndexedStore = class {
613
+ constructor(store) {
614
+ this.store = store;
724
615
  }
725
- if (ctx.attribute !== void 0) {
726
- const attr = ctx.attribute;
727
- const prefix = (ctx.valuePrefix ?? "").toLowerCase();
728
- if (attr === "href") {
729
- if (prefix.endsWith("mailto:")) return "email";
730
- if (prefix.endsWith("tel:")) return "phone";
731
- if (ctx.wholeValue) return ctx.download === true ? "file" : "url";
732
- }
733
- if (ctx.wholeValue && attr === "src") {
734
- if (ctx.tag === "img") return "image";
735
- return "file";
736
- }
737
- if (ctx.wholeValue && ctx.tag === "source" && attr === "srcset") return "file";
738
- if (attr === "alt" || attr === "title") return "shorttext";
739
- return "shorttext";
616
+ store;
617
+ index;
618
+ loading;
619
+ /**
620
+ * Per-instance mutex (DESIGN §11d): every mutation (`write`/`delete`/`rename`/`reconcile`)
621
+ * runs through {@link serializeMutation}, which chains onto the previous mutation's
622
+ * completion. Without it, two concurrent mutations both read the cached `this.index`,
623
+ * each compute their "next" key set from that stale snapshot, and the second
624
+ * `saveIndex` clobbers the first the file writes land in blob, but the index keeps
625
+ * stale references or loses fresh ones (the "I deleted it and it's still there" or
626
+ * "I added it and it didn't appear" bugs surfaced dogfooding the marketing site,
627
+ * 2026-05-28). Reads (`list`/`exists`/`readBytes`) are *not* serialised; an
628
+ * in-flight mutation simply means a reader sees the pre-mutation index, eventually
629
+ * consistent and safe. Crashes inside `work()` release the lock so a single failure
630
+ * does not deadlock subsequent operations.
631
+ */
632
+ mutationLock = Promise.resolve();
633
+ /** The index, loaded once on first need and cached (mutations replace the cached copy). */
634
+ async loaded() {
635
+ if (this.index !== void 0) return this.index;
636
+ this.loading ??= loadIndex(this.store);
637
+ this.index = await this.loading;
638
+ return this.index;
740
639
  }
741
- if (CODE_ELEMENTS.has(ctx.tag)) return "code";
742
- if (SHORT_TEXT_ELEMENTS.has(ctx.tag)) return "shorttext";
743
- if (PLAIN_TEXT_ELEMENTS.has(ctx.tag)) return "text";
744
- return "shorttext";
745
- }
746
-
747
- // ../engine/src/template/token.ts
748
- var TOKEN = /\{([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\}/g;
749
- function findTokens(input) {
750
- const out = [];
751
- for (const match of input.matchAll(TOKEN)) {
752
- const expr = match[1] ?? "";
753
- const path = expr.split(".");
754
- out.push({ raw: match[0], name: path[0] ?? "", path, dotted: path.length > 1 });
640
+ serializeMutation(work) {
641
+ const previous = this.mutationLock;
642
+ let release;
643
+ this.mutationLock = new Promise((resolve) => {
644
+ release = resolve;
645
+ });
646
+ return (async () => {
647
+ try {
648
+ await previous;
649
+ return await work();
650
+ } finally {
651
+ release();
652
+ }
653
+ })();
755
654
  }
756
- return out;
757
- }
758
- function wholeValueToken(input) {
759
- const trimmed = input.trim();
760
- const tokens = findTokens(trimmed);
761
- if (tokens.length === 1 && tokens[0]?.raw === trimmed) return tokens[0];
762
- return null;
763
- }
764
- function substituteTokens(input, resolve) {
765
- return input.replace(/\{([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\}/g, (raw, expr) => {
766
- const path = expr.split(".");
767
- return resolve({ raw, name: path[0] ?? "", path, dotted: path.length > 1 });
768
- });
769
- }
770
- function literalPrefix(input) {
771
- const brace = input.indexOf("{");
772
- return (brace === -1 ? input : input.slice(0, brace)).toLowerCase();
773
- }
774
-
775
- // ../engine/src/html/escape.ts
776
- function escapeHtmlText(value) {
777
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
778
- }
779
- function escapeHtmlAttribute(value) {
780
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
781
- }
782
- function escapeJsonStringContent(value) {
783
- const literal = JSON.stringify(value);
784
- return literal.slice(1, -1);
785
- }
786
- function sanitizeUrl(url) {
787
- let cleaned = "";
788
- for (const ch of url) {
789
- const code = ch.codePointAt(0);
790
- if (code !== void 0 && code > 32) cleaned += ch;
655
+ async list(dir) {
656
+ return childrenOf((await this.loaded()).keys, dir);
791
657
  }
792
- const scheme = cleaned.toLowerCase();
793
- if (scheme.startsWith("javascript:") || scheme.startsWith("vbscript:")) {
794
- return "#";
658
+ async readText(path) {
659
+ return new TextDecoder().decode(await this.readBytes(path));
795
660
  }
796
- return url;
797
- }
798
-
799
- // ../engine/src/authors/authors.ts
800
- function toAuthorRefs(value) {
801
- if (!Array.isArray(value)) return [];
802
- const refs = [];
803
- for (const entry of value) {
804
- if (entry === null || typeof entry !== "object") continue;
805
- const record = entry;
806
- const name = typeof record.name === "string" ? record.name : "";
807
- if (name.trim() === "") continue;
808
- refs.push({
809
- name,
810
- ...typeof record.user === "string" && record.user !== "" && { user: record.user },
811
- ...typeof record.href === "string" && record.href !== "" && { href: record.href }
661
+ async readBytes(path) {
662
+ const bytes = await this.store.get(path);
663
+ if (bytes === void 0) throw new Error(`No such file in content source: ${path}`);
664
+ return bytes;
665
+ }
666
+ async exists(path) {
667
+ return pathExists((await this.loaded()).keys, path);
668
+ }
669
+ /**
670
+ * Create or overwrite `key`. The index is rewritten only when `key` is new (an
671
+ * overwrite leaves the key set unchanged, so editing an item is a single `put`).
672
+ *
673
+ * **Auto-stamp** (PROPOSAL §4.1): a destructive write to a template or component
674
+ * path (one that removes a token from the existing source) snapshots the prior
675
+ * bytes to `<base>@v<N+1>.html` before the new bytes land. Order is
676
+ * **current first, snapshot second** (§16.3): if the snapshot copy fails after
677
+ * the current write succeeded, the site stays consistent — only the migration
678
+ * UX's left-pane comparison is unavailable for items affected by this stamp,
679
+ * surfaced as `templates.stamp-incomplete`.
680
+ *
681
+ * Direct writes to a reserved-version path are refused (snapshots are
682
+ * immutable; the user retires them via `delete`, not by overwriting them).
683
+ */
684
+ write(key, bytes) {
685
+ return this.serializeMutation(async () => {
686
+ const target = guarded(normalize(key));
687
+ if (isReservedVersionedPath(target)) {
688
+ throw new Error(
689
+ `Refusing to write to a reserved-version path: ${target} (snapshots are immutable; retire via delete)`
690
+ );
691
+ }
692
+ const stampCandidate = stampTargetOf(target);
693
+ const previousBytes = stampCandidate !== void 0 ? await this.store.get(target) : void 0;
694
+ let stampDecision;
695
+ if (stampCandidate !== void 0 && previousBytes !== void 0) {
696
+ const oldSource = new TextDecoder().decode(previousBytes);
697
+ const newSource = new TextDecoder().decode(bytes);
698
+ const decision = detectStamp(oldSource, newSource);
699
+ if (decision.destructive) {
700
+ const index2 = await this.loaded();
701
+ const siblings = index2.keys.filter((k) => k.startsWith(`${stampCandidate.dir}/`) && k.endsWith(".html")).map((k) => k.slice(stampCandidate.dir.length + 1, -".html".length));
702
+ const version = nextVersionNumber(siblings);
703
+ stampDecision = {
704
+ snapshotPath: `${stampCandidate.dir}/${stampCandidate.name}@v${version}.html`,
705
+ bytes: previousBytes,
706
+ version
707
+ };
708
+ }
709
+ }
710
+ await this.store.put(target, bytes);
711
+ const index = await this.loaded();
712
+ let nextIndex = index;
713
+ if (!nextIndex.keys.includes(target)) {
714
+ nextIndex = await saveIndex(this.store, nextIndex, [...nextIndex.keys, target]);
715
+ }
716
+ let stamped;
717
+ let stampIncomplete;
718
+ if (stampDecision !== void 0) {
719
+ try {
720
+ await this.store.put(stampDecision.snapshotPath, stampDecision.bytes);
721
+ if (!nextIndex.keys.includes(stampDecision.snapshotPath)) {
722
+ nextIndex = await saveIndex(this.store, nextIndex, [
723
+ ...nextIndex.keys,
724
+ stampDecision.snapshotPath
725
+ ]);
726
+ }
727
+ stamped = {
728
+ name: stampCandidate.name,
729
+ version: stampDecision.version,
730
+ snapshotPath: stampDecision.snapshotPath
731
+ };
732
+ } catch {
733
+ stampIncomplete = true;
734
+ }
735
+ }
736
+ this.index = nextIndex;
737
+ return {
738
+ ...stamped !== void 0 && { stamped },
739
+ ...stampIncomplete === true && { stampIncomplete: true }
740
+ };
741
+ });
742
+ }
743
+ /**
744
+ * Manually stamp a snapshot of the current `<kind>/<name>.html` to the next available
745
+ * `<name>@v<N+1>.html` slot. Uses the same internal `this.store.put` pathway the
746
+ * auto-stamp uses (so the public `write`'s reserved-version refusal doesn't apply),
747
+ * and runs under the mutation mutex so a concurrent destructive auto-stamp can't
748
+ * race with a manual stamp.
749
+ */
750
+ stamp(name, kind) {
751
+ return this.serializeMutation(async () => {
752
+ if (isVersionedTemplateName(name)) {
753
+ throw new Error(`stamp: cannot stamp an already-versioned name (${name})`);
754
+ }
755
+ const dir = kind === "template" ? "templates" : "components";
756
+ const currentPath = `${dir}/${name}.html`;
757
+ const bytes = await this.store.get(currentPath);
758
+ if (bytes === void 0) {
759
+ throw new Error(`stamp: ${currentPath} does not exist (nothing to snapshot)`);
760
+ }
761
+ const index = await this.loaded();
762
+ const siblings = index.keys.filter((k) => k.startsWith(`${dir}/`) && k.endsWith(".html")).map((k) => k.slice(dir.length + 1, -".html".length));
763
+ const version = nextVersionNumber(siblings);
764
+ const snapshotPath = `${dir}/${name}@v${version}.html`;
765
+ await this.store.put(snapshotPath, bytes);
766
+ if (!index.keys.includes(snapshotPath)) {
767
+ this.index = await saveIndex(this.store, index, [...index.keys, snapshotPath]);
768
+ } else {
769
+ this.index = index;
770
+ }
771
+ return { name, version, snapshotPath };
772
+ });
773
+ }
774
+ /**
775
+ * Delete a file, or a whole directory subtree (every key under `key/`). Idempotent: a
776
+ * path the index does not know is still deleted from the store (clearing an orphan),
777
+ * and deleting nothing is a no-op.
778
+ */
779
+ delete(key) {
780
+ return this.serializeMutation(async () => {
781
+ const target = guarded(normalize(key));
782
+ const index = await this.loaded();
783
+ const prefix = `${target}/`;
784
+ const removed = index.keys.filter((k) => k === target || k.startsWith(prefix));
785
+ if (removed.length === 0) {
786
+ await this.store.delete(target);
787
+ return;
788
+ }
789
+ await Promise.all(removed.map((k) => this.store.delete(k)));
790
+ const remaining = index.keys.filter((k) => k !== target && !k.startsWith(prefix));
791
+ this.index = await saveIndex(this.store, index, remaining);
792
+ });
793
+ }
794
+ /**
795
+ * Move/rename a file, or a whole directory subtree (every key under `from/` is remapped
796
+ * under `to/`). Clobbering an existing destination, or renaming a missing path, are
797
+ * returned as data (`ok: false`), not thrown (CLAUDE §2), the same contract the host's
798
+ * `/api/rename` enforces. (Mutating the reserved namespace is a programmer error and
799
+ * still throws.)
800
+ */
801
+ rename(from, to) {
802
+ return this.serializeMutation(async () => {
803
+ const source = guarded(normalize(from));
804
+ const dest = guarded(normalize(to));
805
+ if (source === dest) return { ok: true };
806
+ if (isReservedVersionedPath(source) || isReservedVersionedPath(dest)) {
807
+ return { ok: false, reason: "reserved-version" };
808
+ }
809
+ const index = await this.loaded();
810
+ const sourcePrefix = `${source}/`;
811
+ const affected = index.keys.filter((k) => k === source || k.startsWith(sourcePrefix));
812
+ if (affected.length === 0) return { ok: false, reason: "missing" };
813
+ const destPrefix = `${dest}/`;
814
+ if (index.keys.some((k) => k === dest || k.startsWith(destPrefix))) {
815
+ return { ok: false, reason: "exists" };
816
+ }
817
+ const moves = affected.map((k) => ({
818
+ from: k,
819
+ to: k === source ? dest : dest + k.slice(source.length)
820
+ }));
821
+ for (const move of moves) {
822
+ const bytes = await this.store.get(move.from);
823
+ if (bytes === void 0) continue;
824
+ await this.store.put(move.to, bytes);
825
+ await this.store.delete(move.from);
826
+ }
827
+ const movedFrom = new Set(moves.map((move) => move.from));
828
+ const next = index.keys.filter((k) => !movedFrom.has(k)).concat(moves.map((m) => m.to));
829
+ this.index = await saveIndex(this.store, index, next);
830
+ return { ok: true };
831
+ });
832
+ }
833
+ /**
834
+ * Rebuild this store's index from the *actual* keys the underlying store holds (DESIGN
835
+ * §11d), recovering files that arrived by a path that bypassed the index. A
836
+ * {@link BlobStore} cannot enumerate itself, so the caller supplies the real key set
837
+ * from an adapter that can (e.g. `BlobContainer.list`). Unlike calling
838
+ * {@link reconcileIndex} on a fresh store, this also refreshes the cached in-memory
839
+ * index, so this live instance sees the recovered files immediately.
840
+ */
841
+ reconcile(actualKeys) {
842
+ return this.serializeMutation(async () => {
843
+ const result = await reconcileIndex(this.store, actualKeys);
844
+ this.index = result.index;
845
+ return result;
812
846
  });
813
847
  }
814
- return refs;
815
- }
816
- function joinAuthors(names) {
817
- if (names.length === 0) return "";
818
- if (names.length === 1) return names[0] ?? "";
819
- const last = names[names.length - 1] ?? "";
820
- const head = names.slice(0, -1).join(", ");
821
- return `${head} and ${last}`;
822
- }
823
- function renderAuthors(value, directory) {
824
- const names = toAuthorRefs(value).map((ref) => {
825
- const resolved = ref.user !== void 0 ? directory?.(ref.user) : void 0;
826
- return escapeHtmlText(resolved?.displayName ?? ref.name);
827
- });
828
- return joinAuthors(names);
829
- }
830
-
831
- // ../engine/src/html/dom.ts
832
- import { parse, parseFragment, serialize as serialize2 } from "parse5";
833
- function isElement(node) {
834
- return "tagName" in node;
835
- }
836
- function isTextNode(node) {
837
- return node.nodeName === "#text";
838
- }
839
- function getAttribute(el, name) {
840
- return el.attrs.find((attr) => attr.name === name)?.value;
841
- }
842
- function hasAttribute(el, name) {
843
- return el.attrs.some((attr) => attr.name === name);
844
- }
845
- function setAttribute(el, name, value) {
846
- const existing = el.attrs.find((attr) => attr.name === name);
847
- if (existing) existing.value = value;
848
- else el.attrs.push({ name, value });
849
- }
850
- function removeAttribute(el, name) {
851
- el.attrs = el.attrs.filter((attr) => attr.name !== name);
852
- }
853
- function parseFragmentNodes(html) {
854
- return parseFragment(html).childNodes;
855
- }
856
- function isFullDocument(html) {
857
- const start = html.trimStart().toLowerCase();
858
- return start.startsWith("<!doctype") || start.startsWith("<html");
859
- }
860
- function parseNodes(html) {
861
- return isFullDocument(html) ? parse(html).childNodes : parseFragment(html).childNodes;
862
- }
863
- function serializeNodes(nodes) {
864
- const fragment = parseFragment("");
865
- fragment.childNodes = nodes;
866
- return serialize2(fragment);
867
- }
868
-
869
- // ../engine/src/media/media.ts
870
- var DEFAULT_WIDTHS = [400, 800, 1200, 1600];
871
- var MIME = {
872
- avif: "image/avif",
873
- webp: "image/webp",
874
- jpeg: "image/jpeg",
875
- png: "image/png"
876
848
  };
877
- function extOf(format) {
878
- return format === "jpeg" ? "jpg" : format;
879
- }
880
- function fallbackFormatFor(assetPath) {
881
- const ext = assetPath.slice(assetPath.lastIndexOf(".") + 1).toLowerCase();
882
- if (ext === "png") return "png";
883
- if (ext === "webp") return "webp";
884
- return "jpeg";
849
+ function stampTargetOf(target) {
850
+ if (!target.endsWith(".html")) return void 0;
851
+ const dir = target.startsWith("templates/") ? "templates" : target.startsWith("components/") ? "components" : void 0;
852
+ if (dir === void 0) return void 0;
853
+ const stem = target.slice(dir.length + 1, -".html".length);
854
+ if (stem === "" || stem.includes("@v")) return void 0;
855
+ return { dir, name: stem };
885
856
  }
886
- function contentHash(bytes) {
887
- let hash = 2166136261;
888
- for (let i = 0; i < bytes.length; i += 1) {
889
- hash ^= bytes[i] ?? 0;
890
- hash = Math.imul(hash, 16777619);
857
+ function guarded(key) {
858
+ if (key === "" || key.startsWith(RESERVED_PREFIX)) {
859
+ throw new Error(`Refusing to mutate a reserved key: ${key === "" ? "(root)" : key}`);
891
860
  }
892
- return (hash >>> 0).toString(36);
861
+ return key;
893
862
  }
894
- async function processImage(input, assetPath, encoder) {
895
- const fallbackFormat = fallbackFormatFor(assetPath);
896
- const formats = [.../* @__PURE__ */ new Set(["avif", "webp", fallbackFormat])];
897
- const encoded = await encoder.encode(input, { widths: DEFAULT_WIDTHS, formats });
898
- const hash = contentHash(input);
899
- const base = assetPath.slice(
900
- 0,
901
- assetPath.lastIndexOf(".") >= 0 ? assetPath.lastIndexOf(".") : assetPath.length
902
- );
903
- const artifacts = [];
904
- const urlOf = (variant) => {
905
- const path = `${base}.${hash}-${variant.width}.${extOf(variant.format)}`;
906
- artifacts.push({ path, contents: variant.bytes });
907
- return `/${path}`;
908
- };
909
- const byFormat = /* @__PURE__ */ new Map();
910
- for (const variant of [...encoded.variants].sort((a, b) => a.width - b.width)) {
911
- const url = urlOf(variant);
912
- const list = byFormat.get(variant.format) ?? [];
913
- list.push({ url, width: variant.width });
914
- byFormat.set(variant.format, list);
863
+ function normalize(path) {
864
+ return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
865
+ }
866
+ function childrenOf(keys, dir) {
867
+ const base = normalize(dir);
868
+ const prefix = base === "" ? "" : `${base}/`;
869
+ const entries = /* @__PURE__ */ new Map();
870
+ for (const key of keys) {
871
+ if (prefix !== "" && !key.startsWith(prefix)) continue;
872
+ const rest = key.slice(prefix.length);
873
+ if (rest === "") continue;
874
+ const slash = rest.indexOf("/");
875
+ if (slash === -1) entries.set(rest, "file");
876
+ else entries.set(rest.slice(0, slash), "dir");
915
877
  }
916
- const srcsetOf = (format) => (byFormat.get(format) ?? []).map((entry) => `${entry.url} ${entry.width}w`).join(", ");
917
- const sources = ["avif", "webp"].filter((format) => byFormat.has(format)).map((format) => ({ format, mime: MIME[format], srcset: srcsetOf(format) }));
918
- const fallbackList = byFormat.get(fallbackFormat) ?? [];
919
- const fallbackSrc = fallbackList[fallbackList.length - 1]?.url ?? `/${assetPath}`;
920
- const info = {
921
- width: encoded.sourceWidth,
922
- height: encoded.sourceHeight,
923
- sources,
924
- fallbackSrc,
925
- ...encoded.blurDataUri !== void 0 && { blurDataUri: encoded.blurDataUri }
926
- };
927
- return { artifacts, info };
878
+ return [...entries].map(([name, kind]) => ({ name, kind })).sort((a, b) => a.name.localeCompare(b.name));
928
879
  }
929
- function buildPictureMarkup(info, extraAttrs) {
930
- const sources = info.sources.map(
931
- (source) => `<source type="${source.mime}" srcset="${escapeHtmlAttribute(source.srcset)}">`
932
- ).join("");
933
- const carried = extraAttrs.filter(([name]) => name !== "src" && name !== "width" && name !== "height").map(([name, value]) => ` ${name}="${escapeHtmlAttribute(value)}"`).join("");
934
- const img = `<img src="${escapeHtmlAttribute(info.fallbackSrc)}" width="${info.width}" height="${info.height}" loading="lazy" decoding="async"${carried}>`;
935
- return `<picture>${sources}${img}</picture>`;
880
+ function pathExists(keys, path) {
881
+ const target = normalize(path);
882
+ if (target === "") return true;
883
+ if (keys.includes(target)) return true;
884
+ const prefix = `${target}/`;
885
+ return keys.some((key) => key.startsWith(prefix));
936
886
  }
937
887
 
938
- // ../engine/src/url/references.ts
939
- var REFERENCE_PREFIX = "ref:";
940
- function referenceTarget(value) {
941
- return value.startsWith(REFERENCE_PREFIX) ? value.slice(REFERENCE_PREFIX.length) : void 0;
888
+ // ../engine/src/content/source.ts
889
+ function normalizePath(path) {
890
+ return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
942
891
  }
943
- function collectHtmlReferences(html) {
944
- const found = /* @__PURE__ */ new Set();
945
- const visit = (nodes) => {
946
- for (const node of nodes) {
947
- if (!isElement(node)) continue;
948
- for (const attr of node.attrs) {
949
- const target = referenceTarget(attr.value);
950
- if (target !== void 0) found.add(target);
951
- }
952
- visit(node.childNodes);
892
+ var InMemoryContentSource = class {
893
+ files;
894
+ constructor(files) {
895
+ this.files = new Map(Object.entries(files).map(([key, value]) => [normalizePath(key), value]));
896
+ }
897
+ async readText(path) {
898
+ const value = this.files.get(normalizePath(path));
899
+ if (value === void 0) throw new Error(`No such file in content source: ${path}`);
900
+ return typeof value === "string" ? value : new TextDecoder().decode(value);
901
+ }
902
+ async readBytes(path) {
903
+ const value = this.files.get(normalizePath(path));
904
+ if (value === void 0) throw new Error(`No such file in content source: ${path}`);
905
+ return typeof value === "string" ? new TextEncoder().encode(value) : value;
906
+ }
907
+ async exists(path) {
908
+ const target = normalizePath(path);
909
+ if (target === "") return true;
910
+ if (this.files.has(target)) return true;
911
+ const prefix = `${target}/`;
912
+ for (const key of this.files.keys()) {
913
+ if (key.startsWith(prefix)) return true;
953
914
  }
954
- };
955
- visit(parseNodes(html));
956
- return [...found].sort();
957
- }
915
+ return false;
916
+ }
917
+ async list(dir) {
918
+ const base = normalizePath(dir);
919
+ const prefix = base === "" ? "" : `${base}/`;
920
+ const entries = /* @__PURE__ */ new Map();
921
+ for (const key of this.files.keys()) {
922
+ if (prefix !== "" && !key.startsWith(prefix)) continue;
923
+ const rest = key.slice(prefix.length);
924
+ if (rest === "") continue;
925
+ const slash = rest.indexOf("/");
926
+ if (slash === -1) entries.set(rest, "file");
927
+ else entries.set(rest.slice(0, slash), "dir");
928
+ }
929
+ return [...entries].map(([name, kind]) => ({ name, kind })).sort((a, b) => a.name.localeCompare(b.name));
930
+ }
931
+ };
958
932
 
959
- // ../engine/src/compile/compiler.ts
960
- var LOOP_ATTRS = ["data-each-in", "data-each", "data-sort", "data-limit", "data-pick-from"];
961
- var AUTHORING_ATTRS = ["data-type", "data-field", "data-required", "data-help", "data-label"];
962
- function compileTemplate(input) {
963
- const ctx = {
964
- components: input.components,
965
- usedComponents: /* @__PURE__ */ new Set(),
966
- ...input.context !== void 0 && { context: input.context },
967
- ...input.media !== void 0 && { media: input.media },
968
- ...input.authorDirectory !== void 0 && { authorDirectory: input.authorDirectory },
969
- ...input.componentStyles !== void 0 && { componentStyles: input.componentStyles }
970
- };
971
- const body = transformNodes(parseNodes(input.template), input.scope, ctx);
972
- const out = input.document === void 0 ? body : fillSlots(transformNodes(parseNodes(input.document), input.scope, ctx), body);
973
- injectPageAssets(out, input, ctx);
974
- return serializeNodes(out);
933
+ // ../engine/src/url/redirects.ts
934
+ var DEFAULT_STATUS = 301;
935
+ var OUTPUT_PATH = "_redirects";
936
+ function isRule(value) {
937
+ if (typeof value !== "object" || value === null) return false;
938
+ const record = value;
939
+ return typeof record.from === "string" && typeof record.to === "string";
975
940
  }
976
- function injectPageAssets(nodes, input, ctx) {
977
- const style = input.templateStyle?.trim();
978
- if (style) {
979
- const styleNode = parseFragmentNodes(`<style>
980
- ${style}
981
- </style>`);
982
- const head = findElement(nodes, "head");
983
- if (head) head.childNodes.push(...styleNode);
984
- else nodes.unshift(...styleNode);
985
- }
986
- const scripts = [];
987
- const templateScript = input.templateScript?.trim();
988
- if (templateScript) scripts.push(templateScript);
989
- for (const tag of [...ctx.usedComponents].sort()) {
990
- const js = input.componentScripts?.get(tag)?.trim();
991
- if (js) scripts.push(js);
941
+ function parseRedirects(raw) {
942
+ if (!Array.isArray(raw)) return [];
943
+ const rules = [];
944
+ for (const entry of raw) {
945
+ if (!isRule(entry)) continue;
946
+ const status = entry.status;
947
+ rules.push(
948
+ typeof status === "number" && Number.isInteger(status) ? { from: entry.from, to: entry.to, status } : { from: entry.from, to: entry.to }
949
+ );
992
950
  }
993
- if (scripts.length > 0) {
994
- const scriptNodes = scripts.flatMap((js) => parseFragmentNodes(`<script>
995
- ${js}
996
- </script>`));
997
- const body = findElement(nodes, "body");
998
- if (body) body.childNodes.push(...scriptNodes);
999
- else nodes.push(...scriptNodes);
951
+ return rules;
952
+ }
953
+ function resolveTarget(from, exact) {
954
+ const seen = /* @__PURE__ */ new Set([from]);
955
+ let target = exact.get(from).to;
956
+ while (exact.has(target) && !seen.has(target)) {
957
+ seen.add(target);
958
+ target = exact.get(target).to;
1000
959
  }
960
+ return target;
1001
961
  }
1002
- function findElement(nodes, tagName) {
1003
- for (const node of nodes) {
1004
- if (!isElement(node)) continue;
1005
- if (node.tagName === tagName) return node;
1006
- const found = findElement(node.childNodes, tagName);
1007
- if (found !== void 0) return found;
962
+ function buildRedirects(rules, liveUrls) {
963
+ const exact = /* @__PURE__ */ new Map();
964
+ const wildcard = /* @__PURE__ */ new Map();
965
+ for (const rule of rules) {
966
+ (rule.from.includes("*") ? wildcard : exact).set(rule.from, rule);
1008
967
  }
1009
- return void 0;
968
+ const resolved = [];
969
+ for (const [from, rule] of exact) {
970
+ if (liveUrls.has(from)) continue;
971
+ const to = resolveTarget(from, exact);
972
+ if (to === from) continue;
973
+ resolved.push(rule.status !== void 0 ? { from, to, status: rule.status } : { from, to });
974
+ }
975
+ resolved.push(...wildcard.values());
976
+ if (resolved.length === 0) return void 0;
977
+ resolved.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
978
+ const contents = resolved.map((rule) => `${rule.from} ${rule.to} ${rule.status ?? DEFAULT_STATUS}
979
+ `).join("");
980
+ return { path: OUTPUT_PATH, contents };
1010
981
  }
1011
- function isBoundItem(value) {
1012
- return typeof value === "object" && value !== null && !Array.isArray(value) && "url" in value;
982
+
983
+ // ../engine/src/content/loader.ts
984
+ var SORT_FILE = "_sort.json";
985
+ var REDIRECTS_FILE = "_redirects.json";
986
+ var SITE_CONFIG_FILE = "_site.json";
987
+ var ASSETS_DIR = "assets";
988
+ var ITEM_EXT = ".json";
989
+ var DEFAULT_DIRS = {
990
+ content: "content",
991
+ templates: "templates",
992
+ components: "components",
993
+ /** Static passthrough, copied verbatim to the published root (DESIGN §8). The
994
+ * engine never reads it; named here so hosts/editor share one source of truth. */
995
+ public: "public"
996
+ };
997
+ function join(...parts) {
998
+ return parts.filter((part) => part !== "").join("/");
1013
999
  }
1014
- function resolveToString(value) {
1015
- if (value === void 0 || value === null) return "";
1016
- if (Array.isArray(value)) {
1017
- return value.map((entry) => entry === null ? "" : String(entry)).join(", ");
1018
- }
1019
- return String(value);
1000
+ function stripBom(text) {
1001
+ return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
1020
1002
  }
1021
- function scopeResolver(scope) {
1022
- return (token) => {
1023
- const head = scope[token.name];
1024
- if (isBoundItem(head)) {
1025
- if (token.path.length === 1) return head.url;
1026
- const field = head.fields[token.path[1] ?? ""];
1027
- return isBoundItem(field) ? field.url : resolveToString(field);
1028
- }
1029
- if (token.dotted) return "";
1030
- return resolveToString(head);
1031
- };
1003
+ async function loadContentTree(source, contentDir = DEFAULT_DIRS.content) {
1004
+ return loadDir(source, contentDir, "", "");
1032
1005
  }
1033
- function transformNodes(nodes, scope, ctx) {
1034
- const out = [];
1035
- for (const node of nodes) {
1036
- if (isElement(node)) {
1037
- out.push(...transformElement(node, scope, ctx));
1038
- } else if (isTextNode(node)) {
1039
- node.value = substituteTokens(node.value, scopeResolver(scope));
1040
- out.push(node);
1006
+ async function loadDir(source, dirPath, slug, treePath) {
1007
+ const [entries, sort] = await Promise.all([source.list(dirPath), readSort(source, dirPath)]);
1008
+ const childMap = /* @__PURE__ */ new Map();
1009
+ for (const entry of entries) {
1010
+ if (entry.kind === "file") {
1011
+ if (entry.name === SORT_FILE || entry.name === REDIRECTS_FILE || entry.name === SITE_CONFIG_FILE || !entry.name.endsWith(ITEM_EXT))
1012
+ continue;
1013
+ const childSlug = entry.name.slice(0, -ITEM_EXT.length);
1014
+ childMap.set(
1015
+ childSlug,
1016
+ await loadItem(source, join(dirPath, entry.name), childSlug, treePath)
1017
+ );
1041
1018
  } else {
1042
- out.push(node);
1019
+ if (entry.name === ASSETS_DIR) continue;
1020
+ const childPath = join(treePath, entry.name);
1021
+ childMap.set(
1022
+ entry.name,
1023
+ await loadDir(source, join(dirPath, entry.name), entry.name, childPath)
1024
+ );
1043
1025
  }
1044
1026
  }
1045
- return out;
1027
+ const name = sort.name ?? (slug === "" ? "" : humanize(slug));
1028
+ return {
1029
+ kind: "dir",
1030
+ slug,
1031
+ name,
1032
+ path: treePath,
1033
+ children: orderChildren(childMap, sort.order),
1034
+ ...sort.collection !== void 0 && { collection: sort.collection },
1035
+ ...sort.defaultTemplate !== void 0 && { defaultTemplate: sort.defaultTemplate }
1036
+ };
1046
1037
  }
1047
- function transformElement(el, scope, ctx) {
1048
- if (hasAttribute(el, "data-each-in") || hasAttribute(el, "data-each")) {
1049
- return expandLoop(el, scope, ctx);
1050
- }
1051
- if (ctx.components.has(el.tagName)) {
1052
- return expandComponent(el, scope, ctx);
1053
- }
1054
- substituteAttributes(el, scope, ctx);
1055
- const annotation = getAttribute(el, "data-type");
1056
- for (const attr of AUTHORING_ATTRS) removeAttribute(el, attr);
1057
- if (el.tagName === "img" && ctx.media !== void 0) {
1058
- const info = ctx.media.image(getAttribute(el, "src") ?? "");
1059
- if (info !== void 0) {
1060
- const attrs = el.attrs.map((attr) => [attr.name, attr.value]);
1061
- return parseFragmentNodes(buildPictureMarkup(info, attrs));
1062
- }
1063
- }
1064
- if (isJsonLd(el)) {
1065
- substituteRawText(el, scope, "json");
1066
- return [el];
1038
+ async function loadItem(source, filePath, slug, parentPath2) {
1039
+ const raw = await source.readText(filePath);
1040
+ try {
1041
+ const item = parseContentItem(JSON.parse(raw));
1042
+ return { kind: "item", slug, path: join(parentPath2, slug), item };
1043
+ } catch (error) {
1044
+ throw new Error(`Failed to load ${filePath}: ${error.message}`);
1067
1045
  }
1068
- if (el.tagName === "script" || el.tagName === "style") {
1069
- substituteRawText(el, scope, "raw");
1070
- return [el];
1046
+ }
1047
+ async function readSort(source, dirPath) {
1048
+ const path = join(dirPath, SORT_FILE);
1049
+ if (!await source.exists(path)) return { order: [] };
1050
+ try {
1051
+ return parseSortFile(JSON.parse(await source.readText(path)));
1052
+ } catch {
1053
+ return { order: [] };
1071
1054
  }
1072
- const field = wholeValueFieldToken(el, annotation);
1073
- if (field?.kind === "html") {
1074
- const injected = parseFragmentNodes(resolveToString(scope[field.token.name]));
1075
- el.childNodes = transformRichMedia(injected, ctx);
1076
- return [el];
1055
+ }
1056
+ async function loadRedirects(source, contentDir = DEFAULT_DIRS.content) {
1057
+ const path = join(contentDir, REDIRECTS_FILE);
1058
+ if (!await source.exists(path)) return [];
1059
+ try {
1060
+ return parseRedirects(JSON.parse(stripBom(await source.readText(path))));
1061
+ } catch {
1062
+ return [];
1077
1063
  }
1078
- if (field?.kind === "authors") {
1079
- el.childNodes = parseFragmentNodes(
1080
- renderAuthors(scope[field.token.name], ctx.authorDirectory)
1081
- );
1082
- return [el];
1064
+ }
1065
+ async function loadSiteConfig(source, contentDir = DEFAULT_DIRS.content) {
1066
+ const path = join(contentDir, SITE_CONFIG_FILE);
1067
+ if (!await source.exists(path)) return {};
1068
+ try {
1069
+ const raw = JSON.parse(stripBom(await source.readText(path)));
1070
+ const baseUrl = typeof raw.baseUrl === "string" ? raw.baseUrl.trim() : "";
1071
+ return baseUrl !== "" ? { baseUrl } : {};
1072
+ } catch {
1073
+ return {};
1083
1074
  }
1084
- el.childNodes = transformNodes(el.childNodes, scope, ctx);
1085
- return [el];
1086
1075
  }
1087
- function transformRichMedia(nodes, ctx) {
1076
+ function orderChildren(map, order) {
1088
1077
  const out = [];
1089
- for (const node of nodes) {
1090
- if (!isElement(node)) {
1078
+ const used = /* @__PURE__ */ new Set();
1079
+ for (const slug of order) {
1080
+ const node = map.get(slug);
1081
+ if (node !== void 0) {
1091
1082
  out.push(node);
1092
- continue;
1093
- }
1094
- node.childNodes = transformRichMedia(node.childNodes, ctx);
1095
- if (node.tagName === "img") {
1096
- const src = getAttribute(node, "src");
1097
- const info = src !== void 0 ? ctx.media?.image(src) : void 0;
1098
- if (info !== void 0) {
1099
- const attrs = node.attrs.map((attr) => [attr.name, attr.value]);
1100
- out.push(...parseFragmentNodes(buildPictureMarkup(info, attrs)));
1101
- continue;
1102
- }
1103
- if (src !== void 0) setAttribute(node, "src", resolveMediaUrl(src, ctx));
1083
+ used.add(slug);
1104
1084
  }
1105
- const href = getAttribute(node, "href");
1106
- if (href !== void 0) setAttribute(node, "href", resolveMediaUrl(href, ctx));
1107
- out.push(node);
1085
+ }
1086
+ for (const [slug, node] of map) {
1087
+ if (!used.has(slug)) out.push(node);
1108
1088
  }
1109
1089
  return out;
1110
1090
  }
1111
- function resolveMediaUrl(value, ctx) {
1112
- const referenced = resolveReference(value, ctx);
1113
- if (referenced !== void 0) return sanitizeUrl(referenced);
1114
- return sanitizeUrl(ctx.media?.file(value) ?? value);
1115
- }
1116
- function resolveReference(value, ctx) {
1117
- const path = referenceTarget(value);
1118
- if (path === void 0) return void 0;
1119
- return ctx.context?.reference(path)?.url ?? "#";
1091
+ function loadComponents(source, componentsDir = DEFAULT_DIRS.components) {
1092
+ return loadComponentFiles(source, componentsDir, "html");
1120
1093
  }
1121
- function expandLoop(el, scope, ctx) {
1122
- if (ctx.context === void 0) return [];
1123
- const eachIn = getAttribute(el, "data-each-in");
1124
- let items;
1125
- if (eachIn !== void 0) {
1126
- const limitText = getAttribute(el, "data-limit");
1127
- const limit = limitText !== void 0 ? Number.parseInt(limitText, 10) : void 0;
1128
- const sort = getAttribute(el, "data-sort");
1129
- items = ctx.context.collection(eachIn, {
1130
- ...sort !== void 0 && { sort },
1131
- ...limit !== void 0 && !Number.isNaN(limit) && { limit }
1132
- });
1133
- } else {
1134
- const fieldName = getAttribute(el, "data-each") ?? "";
1135
- const value = scope[fieldName];
1136
- const paths = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : typeof value === "string" ? [value] : [];
1137
- items = paths.map((path) => ctx.context?.reference(path)).filter((bound) => bound !== void 0);
1138
- }
1139
- const out = [];
1140
- for (const item of items) {
1141
- const clone = cloneNode(el);
1142
- for (const attr of LOOP_ATTRS) removeAttribute(clone, attr);
1143
- out.push(...transformElement(clone, { ...scope, item }, ctx));
1144
- }
1145
- return out;
1094
+ function loadComponentStyles(source, componentsDir = DEFAULT_DIRS.components) {
1095
+ return loadComponentFiles(source, componentsDir, "css");
1146
1096
  }
1147
- function cloneNode(node) {
1148
- if (isElement(node)) {
1149
- const clone = {
1150
- ...node,
1151
- attrs: node.attrs.map((attr) => ({ ...attr })),
1152
- childNodes: []
1153
- };
1154
- clone.childNodes = node.childNodes.map(cloneNode);
1155
- return clone;
1156
- }
1157
- return { ...node };
1097
+ function loadComponentScripts(source, componentsDir = DEFAULT_DIRS.components) {
1098
+ return loadComponentFiles(source, componentsDir, "js");
1158
1099
  }
1159
- function substituteAttributes(el, scope, ctx) {
1160
- for (const attr of el.attrs) {
1161
- if (findTokens(attr.value).length === 0) {
1162
- const referenced2 = resolveReference(attr.value, ctx);
1163
- if (referenced2 !== void 0) attr.value = sanitizeUrl(referenced2);
1164
- continue;
1100
+ async function loadComponentFiles(source, componentsDir, extension) {
1101
+ const suffix = `.${extension}`;
1102
+ const map = /* @__PURE__ */ new Map();
1103
+ const walk = async (dir) => {
1104
+ if (!await source.exists(dir)) return;
1105
+ for (const entry of await source.list(dir)) {
1106
+ const full = join(dir, entry.name);
1107
+ if (entry.kind === "dir") {
1108
+ await walk(full);
1109
+ } else if (entry.name.endsWith(suffix)) {
1110
+ const key = entry.name.slice(0, -suffix.length).toLowerCase();
1111
+ map.set(key, await source.readText(full));
1112
+ }
1165
1113
  }
1166
- const annotation = getAttribute(el, "data-type");
1167
- const whole = wholeValueToken(attr.value) !== null;
1168
- const type = inferControl({
1169
- tag: el.tagName,
1170
- attribute: attr.name,
1171
- wholeValue: whole,
1172
- valuePrefix: literalPrefix(attr.value),
1173
- download: hasAttribute(el, "download"),
1174
- ...annotation !== void 0 && { annotation }
1175
- });
1176
- let value = substituteTokens(attr.value, scopeResolver(scope));
1177
- const referenced = resolveReference(value, ctx);
1178
- if (referenced !== void 0) value = referenced;
1179
- const fileUrl = ctx.media?.file(value);
1180
- if (fileUrl !== void 0) value = fileUrl;
1181
- if (whole && valueKindOf(type) === "url") value = sanitizeUrl(value);
1182
- attr.value = value;
1183
- }
1114
+ };
1115
+ await walk(componentsDir);
1116
+ return map;
1117
+ }
1118
+ async function loadTemplate(source, name, templatesDir = DEFAULT_DIRS.templates) {
1119
+ return source.readText(join(templatesDir, `${name}.html`));
1120
+ }
1121
+ function loadTemplateStyle(source, name, templatesDir = DEFAULT_DIRS.templates) {
1122
+ return tryReadText(source, join(templatesDir, `${name}.css`));
1184
1123
  }
1185
- function substituteRawText(el, scope, mode) {
1186
- const resolve = scopeResolver(scope);
1187
- for (const child of el.childNodes) {
1188
- if (!isTextNode(child)) continue;
1189
- child.value = substituteTokens(child.value, (token) => {
1190
- const text = resolve(token);
1191
- return mode === "json" ? escapeJsonStringContent(text) : text;
1192
- });
1124
+ function loadTemplateScript(source, name, templatesDir = DEFAULT_DIRS.templates) {
1125
+ return tryReadText(source, join(templatesDir, `${name}.js`));
1126
+ }
1127
+ var DOCUMENT_SHELL = "document";
1128
+ function loadDocumentShell(source, templatesDir = DEFAULT_DIRS.templates) {
1129
+ return tryReadText(source, join(templatesDir, `${DOCUMENT_SHELL}.html`));
1130
+ }
1131
+ async function tryReadText(source, path) {
1132
+ try {
1133
+ return await source.readText(path);
1134
+ } catch {
1135
+ return void 0;
1193
1136
  }
1194
1137
  }
1195
- function wholeValueFieldToken(el, annotation) {
1138
+
1139
+ // ../engine/src/url/references.ts
1140
+ var REFERENCE_PREFIX = "ref:";
1141
+ function referenceTarget(value) {
1142
+ return value.startsWith(REFERENCE_PREFIX) ? value.slice(REFERENCE_PREFIX.length) : void 0;
1143
+ }
1144
+ function collectHtmlReferences(html) {
1145
+ const found = /* @__PURE__ */ new Set();
1146
+ const visit = (nodes) => {
1147
+ for (const node of nodes) {
1148
+ if (!isElement(node)) continue;
1149
+ for (const attr of node.attrs) {
1150
+ const target = referenceTarget(attr.value);
1151
+ if (target !== void 0) found.add(target);
1152
+ }
1153
+ visit(node.childNodes);
1154
+ }
1155
+ };
1156
+ visit(parseNodes(html));
1157
+ return [...found].sort();
1158
+ }
1159
+
1160
+ // ../engine/src/template/analyze.ts
1161
+ function analyzeTemplate(source, components = /* @__PURE__ */ new Map()) {
1162
+ const requiredFields = /* @__PURE__ */ new Set();
1163
+ const queryLoopPaths = [];
1164
+ const curatedLoopFields = [];
1165
+ const imageFields = /* @__PURE__ */ new Set();
1166
+ const fileFields = /* @__PURE__ */ new Set();
1167
+ const richTextFields = /* @__PURE__ */ new Set();
1168
+ const constraints = /* @__PURE__ */ new Map();
1169
+ const references = /* @__PURE__ */ new Set();
1170
+ const visit = (nodes) => {
1171
+ for (const node of nodes) {
1172
+ if (!isElement(node)) continue;
1173
+ const richText = richTextFieldName(node);
1174
+ if (richText !== void 0) richTextFields.add(richText);
1175
+ const eachIn = getAttribute(node, "data-each-in");
1176
+ if (eachIn !== void 0) {
1177
+ queryLoopPaths.push(eachIn);
1178
+ continue;
1179
+ }
1180
+ const each = getAttribute(node, "data-each");
1181
+ if (each !== void 0) curatedLoopFields.push(each);
1182
+ if (hasAttribute(node, "data-required")) {
1183
+ const field = annotatedFieldName(node);
1184
+ if (field !== void 0) requiredFields.add(field);
1185
+ }
1186
+ const bounds = lengthConstraints(node);
1187
+ if (bounds.minLength !== void 0 || bounds.maxLength !== void 0) {
1188
+ const field = annotatedFieldName(node);
1189
+ if (field !== void 0) constraints.set(field, bounds);
1190
+ }
1191
+ const annotation = getAttribute(node, "data-type");
1192
+ const propTypes = components.has(node.tagName) ? componentPropTypes(node.tagName, components, /* @__PURE__ */ new Set()) : void 0;
1193
+ for (const attr of node.attrs) {
1194
+ const reference = referenceTarget(attr.value);
1195
+ if (reference !== void 0) references.add(reference);
1196
+ const token = wholeValueToken(attr.value);
1197
+ if (token === null || token.dotted) continue;
1198
+ const type = propTypes?.get(attr.name) ?? inferControl({
1199
+ tag: node.tagName,
1200
+ attribute: attr.name,
1201
+ wholeValue: true,
1202
+ valuePrefix: literalPrefix(attr.value),
1203
+ download: hasAttribute(node, "download"),
1204
+ ...annotation !== void 0 && { annotation }
1205
+ });
1206
+ if (type === "image") imageFields.add(token.name);
1207
+ else if (type === "file") fileFields.add(token.name);
1208
+ else if (type === "richtext") richTextFields.add(token.name);
1209
+ }
1210
+ visit(node.childNodes);
1211
+ }
1212
+ };
1213
+ visit(parseNodes(source));
1214
+ return {
1215
+ requiredFields: [...requiredFields],
1216
+ queryLoopPaths,
1217
+ curatedLoopFields,
1218
+ imageFields: [...imageFields],
1219
+ fileFields: [...fileFields],
1220
+ richTextFields: [...richTextFields],
1221
+ constraints,
1222
+ references: [...references].sort()
1223
+ };
1224
+ }
1225
+ function richTextFieldName(el) {
1196
1226
  const meaningful = el.childNodes.filter(
1197
1227
  (child) => !(isTextNode(child) && child.value.trim() === "")
1198
1228
  );
1199
1229
  const only = meaningful[0];
1200
- if (meaningful.length !== 1 || only === void 0 || !isTextNode(only)) return null;
1230
+ if (meaningful.length !== 1 || only === void 0 || !isTextNode(only)) return void 0;
1201
1231
  const token = wholeValueToken(only.value);
1202
- if (token === null || token.dotted) return null;
1232
+ if (token === null || token.dotted) return void 0;
1233
+ const annotation = getAttribute(el, "data-type");
1203
1234
  const type = inferControl({
1204
1235
  tag: el.tagName,
1205
1236
  wholeValue: true,
1206
1237
  ...annotation !== void 0 && { annotation }
1207
1238
  });
1208
- return { token, kind: valueKindOf(type) };
1209
- }
1210
- function isJsonLd(el) {
1211
- return el.tagName === "script" && getAttribute(el, "type")?.toLowerCase() === "application/ld+json";
1239
+ return valueKindOf(type) === "html" ? token.name : void 0;
1212
1240
  }
1213
- function expandComponent(el, scope, ctx) {
1214
- const source = ctx.components.get(el.tagName);
1215
- if (source === void 0) return [el];
1216
- const resolve = scopeResolver(scope);
1217
- const propScope = {};
1241
+ function annotatedFieldName(el) {
1242
+ const explicit = getAttribute(el, "data-field");
1243
+ if (explicit !== void 0) return explicit;
1218
1244
  for (const attr of el.attrs) {
1219
- propScope[attr.name] = substituteTokens(attr.value, resolve);
1220
- }
1221
- const slotChildren = transformNodes(el.childNodes, scope, ctx);
1222
- const body = transformNodes(parseNodes(source), propScope, ctx);
1223
- const result = fillSlots(body, slotChildren);
1224
- ctx.usedComponents.add(el.tagName);
1225
- const css = ctx.componentStyles?.get(el.tagName)?.trim();
1226
- if (css) {
1227
- const root = result.find(isElement);
1228
- if (root !== void 0) {
1229
- const styleNode = parseFragmentNodes(`<style>@scope {
1230
- ${css}
1231
- }</style>`);
1232
- root.childNodes = [...styleNode, ...root.childNodes];
1233
- }
1245
+ const token = findTokens(attr.value).find((candidate) => !candidate.dotted);
1246
+ if (token !== void 0) return token.name;
1234
1247
  }
1235
- return result;
1236
- }
1237
- function fillSlots(body, slotChildren) {
1238
- const named = /* @__PURE__ */ new Map();
1239
- const defaultSlot = [];
1240
- for (const child of slotChildren) {
1241
- const slotName = isElement(child) ? getAttribute(child, "slot") : void 0;
1242
- if (slotName !== void 0) {
1243
- if (isElement(child)) removeAttribute(child, "slot");
1244
- const group = named.get(slotName) ?? [];
1245
- group.push(child);
1246
- named.set(slotName, group);
1247
- } else {
1248
- defaultSlot.push(child);
1248
+ for (const child of el.childNodes) {
1249
+ if (isTextNode(child)) {
1250
+ const token = findTokens(child.value).find((candidate) => !candidate.dotted);
1251
+ if (token !== void 0) return token.name;
1249
1252
  }
1250
1253
  }
1251
- return replaceSlots(body, named, defaultSlot);
1254
+ return void 0;
1252
1255
  }
1253
- function replaceSlots(nodes, named, defaultSlot) {
1254
- const out = [];
1255
- for (const node of nodes) {
1256
- if (isElement(node) && node.tagName === "slot") {
1257
- const name = getAttribute(node, "name");
1258
- const provided = name !== void 0 ? named.get(name) : defaultSlot;
1259
- if (provided && provided.length > 0) {
1260
- out.push(...provided);
1261
- } else {
1262
- out.push(...replaceSlots(node.childNodes, named, defaultSlot));
1256
+
1257
+ // ../engine/src/migrations/migrations.ts
1258
+ async function pendingMigrations(store) {
1259
+ const components = await loadComponents(store).catch(() => /* @__PURE__ */ new Map());
1260
+ const items = await collectItems(store);
1261
+ const templateFieldsCache = /* @__PURE__ */ new Map();
1262
+ const requiredFieldsCache = /* @__PURE__ */ new Map();
1263
+ const snapshotFieldsCache = /* @__PURE__ */ new Map();
1264
+ const fieldsFor = async (templateName) => {
1265
+ const cached = templateFieldsCache.get(templateName);
1266
+ if (cached !== void 0) return cached;
1267
+ const html = await readTemplateOrNull(store, templateName);
1268
+ const fields = new Set(html === null ? [] : deriveFields(html, components).map((f) => f.name));
1269
+ templateFieldsCache.set(templateName, fields);
1270
+ return fields;
1271
+ };
1272
+ const requiredFor = async (templateName) => {
1273
+ const cached = requiredFieldsCache.get(templateName);
1274
+ if (cached !== void 0) return cached;
1275
+ const html = await readTemplateOrNull(store, templateName);
1276
+ let required = [];
1277
+ if (html !== null) {
1278
+ try {
1279
+ required = analyzeTemplate(html, components).requiredFields;
1280
+ } catch {
1281
+ required = [];
1263
1282
  }
1264
- } else if (isElement(node)) {
1265
- node.childNodes = replaceSlots(node.childNodes, named, defaultSlot);
1266
- out.push(node);
1267
- } else {
1268
- out.push(node);
1269
1283
  }
1284
+ requiredFieldsCache.set(templateName, required);
1285
+ return required;
1286
+ };
1287
+ const out = [];
1288
+ const byTemplate = /* @__PURE__ */ new Map();
1289
+ for (const item of items) {
1290
+ const expected = await fieldsFor(item.template);
1291
+ const itemFieldNames = Object.keys(item.fields);
1292
+ const orphans = itemFieldNames.filter((name) => !expected.has(name));
1293
+ const required = await requiredFor(item.template);
1294
+ const missing = required.filter((name) => {
1295
+ const value = item.fields[name];
1296
+ return value === void 0 || value === null || value === "";
1297
+ });
1298
+ if (orphans.length === 0 && missing.length === 0) continue;
1299
+ const base = baseTemplateName(item.template);
1300
+ const bestFit = isVersionedTemplateName(item.template) ? null : await pickBestFitSnapshot(store, base, itemFieldNames, snapshotFieldsCache, components);
1301
+ const finding2 = {
1302
+ path: item.path,
1303
+ boundTemplate: item.template,
1304
+ currentTemplate: base,
1305
+ orphanFields: orphans.sort(),
1306
+ missingRequiredFields: [...missing].sort(),
1307
+ bestFitSnapshot: bestFit
1308
+ };
1309
+ out.push(finding2);
1310
+ const bucket = byTemplate.get(base);
1311
+ if (bucket === void 0) byTemplate.set(base, [finding2]);
1312
+ else bucket.push(finding2);
1270
1313
  }
1271
- return out;
1314
+ return { items: out, byTemplate };
1272
1315
  }
1273
-
1274
- // ../engine/src/aggregate/aggregate.ts
1275
- function joinUrl(baseUrl, sitePath) {
1276
- return baseUrl.replace(/\/+$/, "") + sitePath;
1316
+ async function bestFitSnapshot(store, base, itemFieldNames) {
1317
+ const components = await loadComponents(store).catch(() => /* @__PURE__ */ new Map());
1318
+ return pickBestFitSnapshot(store, base, itemFieldNames, /* @__PURE__ */ new Map(), components);
1277
1319
  }
1278
- function textContent(html) {
1279
- const parts = [];
1280
- const visit = (nodes) => {
1281
- for (const node of nodes) {
1282
- if (isTextNode(node)) parts.push(node.value);
1283
- else if (isElement(node)) visit(node.childNodes);
1320
+ async function applyMigration(store, itemPath, resolution) {
1321
+ const text = await store.readText(itemPath);
1322
+ const raw = JSON.parse(text);
1323
+ if (typeof raw !== "object" || raw === null) {
1324
+ throw new Error(`applyMigration: ${itemPath} is not a JSON object`);
1325
+ }
1326
+ const obj = raw;
1327
+ const fields = typeof obj["fields"] === "object" && obj["fields"] !== null ? { ...obj["fields"] } : {};
1328
+ for (const [from, to] of Object.entries(resolution.rename)) {
1329
+ if (Object.prototype.hasOwnProperty.call(fields, from)) {
1330
+ fields[to] = fields[from];
1331
+ delete fields[from];
1284
1332
  }
1285
- };
1286
- visit(parseFragmentNodes(html));
1287
- return parts.join(" ").replace(/\s+/g, " ").trim();
1288
- }
1289
- function buildSitemap(entries, baseUrl) {
1290
- const urls = entries.map((entry) => {
1291
- const lastmod = entry.updated ?? entry.published;
1292
- const mod = lastmod !== void 0 ? `
1293
- <lastmod>${escapeHtmlText(lastmod)}</lastmod>` : "";
1294
- return ` <url>
1295
- <loc>${escapeHtmlText(joinUrl(baseUrl, entry.url))}</loc>${mod}
1296
- </url>`;
1297
- }).join("\n");
1298
- const contents = `<?xml version="1.0" encoding="UTF-8"?>
1299
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1300
- ${urls}
1301
- </urlset>
1302
- `;
1303
- return { path: "sitemap.xml", contents };
1333
+ }
1334
+ for (const name of resolution.drop) delete fields[name];
1335
+ for (const [name, value] of Object.entries(resolution.fill)) {
1336
+ if (value !== "") fields[name] = value;
1337
+ }
1338
+ obj["fields"] = Object.fromEntries(Object.entries(fields).sort(([a], [b]) => a.localeCompare(b)));
1339
+ await store.write(itemPath, new TextEncoder().encode(`${JSON.stringify(obj, null, 2)}
1340
+ `));
1304
1341
  }
1305
- function buildRss(entries, options) {
1306
- const items = [...entries].sort(
1307
- (a, b) => String(b.published ?? "").localeCompare(String(a.published ?? ""))
1308
- );
1309
- const lastBuild = items[0]?.published;
1310
- const rfc822 = (iso) => new Date(iso).toUTCString();
1311
- const itemXml = items.map((entry) => {
1312
- const link = joinUrl(options.baseUrl, entry.url);
1313
- const pub = entry.published !== void 0 ? `
1314
- <pubDate>${escapeHtmlText(rfc822(entry.published))}</pubDate>` : "";
1315
- const desc = entry.summary !== void 0 ? `
1316
- <description>${escapeHtmlText(entry.summary)}</description>` : "";
1317
- return ` <item>
1318
- <title>${escapeHtmlText(entry.title)}</title>
1319
- <link>${escapeHtmlText(link)}</link>
1320
- <guid>${escapeHtmlText(link)}</guid>${pub}${desc}
1321
- </item>`;
1322
- }).join("\n");
1323
- const built = lastBuild !== void 0 ? `
1324
- <lastBuildDate>${escapeHtmlText(rfc822(lastBuild))}</lastBuildDate>` : "";
1325
- const contents = `<?xml version="1.0" encoding="UTF-8"?>
1326
- <rss version="2.0">
1327
- <channel>
1328
- <title>${escapeHtmlText(options.title)}</title>
1329
- <link>${escapeHtmlText(options.baseUrl)}</link>
1330
- <description>${escapeHtmlText(options.description)}</description>${built}
1331
- ${itemXml}
1332
- </channel>
1333
- </rss>
1334
- `;
1335
- return { path: "feed.xml", contents };
1342
+ async function collectItems(store, dir = "content") {
1343
+ const out = [];
1344
+ let entries;
1345
+ try {
1346
+ entries = await store.list(dir);
1347
+ } catch {
1348
+ return out;
1349
+ }
1350
+ for (const entry of entries) {
1351
+ const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
1352
+ if (entry.kind === "dir") {
1353
+ out.push(...await collectItems(store, path));
1354
+ continue;
1355
+ }
1356
+ if (!entry.name.endsWith(".json") || entry.name.startsWith("_")) continue;
1357
+ let data;
1358
+ try {
1359
+ data = JSON.parse(await store.readText(path));
1360
+ } catch {
1361
+ continue;
1362
+ }
1363
+ if (typeof data !== "object" || data === null) continue;
1364
+ const obj = data;
1365
+ const template = obj["template"];
1366
+ if (typeof template !== "string") continue;
1367
+ const fields = obj["fields"];
1368
+ out.push({
1369
+ path,
1370
+ template,
1371
+ fields: typeof fields === "object" && fields !== null ? fields : {}
1372
+ });
1373
+ }
1374
+ return out;
1336
1375
  }
1337
- function buildContentIndex(entries) {
1338
- const docs = entries.map((entry) => ({
1339
- url: entry.url,
1340
- title: entry.title,
1341
- ...entry.published !== void 0 && { published: entry.published },
1342
- ...entry.summary !== void 0 && { summary: entry.summary }
1343
- }));
1344
- return { path: "content.json", contents: JSON.stringify(docs) };
1376
+ async function readTemplateOrNull(store, templateName) {
1377
+ try {
1378
+ return await store.readText(`templates/${templateName}.html`);
1379
+ } catch {
1380
+ return null;
1381
+ }
1345
1382
  }
1346
-
1347
- // ../engine/src/concurrency.ts
1348
- async function mapWithConcurrency(items, limit, fn) {
1349
- const results = new Array(items.length);
1350
- const workers = Math.max(1, Math.min(Math.floor(limit), items.length));
1351
- let next = 0;
1352
- const run = async () => {
1353
- while (next < items.length) {
1354
- const index = next++;
1355
- results[index] = await fn(items[index], index);
1383
+ async function pickBestFitSnapshot(store, base, itemFieldNames, cache, components) {
1384
+ let entries;
1385
+ try {
1386
+ entries = await store.list("templates");
1387
+ } catch {
1388
+ return null;
1389
+ }
1390
+ const itemSet = new Set(itemFieldNames);
1391
+ let bestVersion = 0;
1392
+ let bestName = null;
1393
+ for (const entry of entries) {
1394
+ if (entry.kind !== "file" || !entry.name.endsWith(".html")) continue;
1395
+ const stem = entry.name.slice(0, -".html".length);
1396
+ if (baseTemplateName(stem) !== base || !isVersionedTemplateName(stem)) continue;
1397
+ const version = versionNumber(stem);
1398
+ if (version === null) continue;
1399
+ let tokens = cache.get(stem);
1400
+ if (tokens === void 0) {
1401
+ try {
1402
+ const html = await store.readText(`templates/${entry.name}`);
1403
+ tokens = new Set(deriveFields(html, components).map((f) => f.name));
1404
+ } catch {
1405
+ tokens = /* @__PURE__ */ new Set();
1406
+ }
1407
+ cache.set(stem, tokens);
1356
1408
  }
1357
- };
1358
- await Promise.all(Array.from({ length: workers }, () => run()));
1359
- return results;
1409
+ const covers = [...itemSet].every((name) => tokens.has(name));
1410
+ if (covers && version > bestVersion) {
1411
+ bestVersion = version;
1412
+ bestName = stem;
1413
+ }
1414
+ }
1415
+ return bestName;
1360
1416
  }
1361
1417
 
1362
- // ../engine/src/template/fields.ts
1363
- function lengthConstraints(el) {
1364
- const min = parseLength(getAttribute(el, "data-minlength"));
1365
- const max = parseLength(getAttribute(el, "data-maxlength"));
1366
- return {
1367
- ...min !== void 0 && { minLength: min },
1368
- ...max !== void 0 && { maxLength: max }
1369
- };
1418
+ // ../engine/src/html/escape.ts
1419
+ function escapeHtmlText(value) {
1420
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1370
1421
  }
1371
- function parseLength(raw) {
1372
- if (raw === void 0) return void 0;
1373
- const value = Number(raw);
1374
- return Number.isInteger(value) && value >= 0 ? value : void 0;
1422
+ function escapeHtmlAttribute(value) {
1423
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1375
1424
  }
1376
- function deriveFields(template, components = /* @__PURE__ */ new Map()) {
1377
- return deriveFromSource(template, components, /* @__PURE__ */ new Set());
1425
+ function escapeJsonStringContent(value) {
1426
+ const literal = JSON.stringify(value);
1427
+ return literal.slice(1, -1);
1378
1428
  }
1379
- function componentPropTypes(tag, components, seen) {
1380
- if (seen.has(tag)) return /* @__PURE__ */ new Map();
1381
- const source = components.get(tag);
1382
- if (source === void 0) return /* @__PURE__ */ new Map();
1383
- const fields = deriveFromSource(source, components, new Set(seen).add(tag));
1384
- return new Map(fields.map((field) => [field.name, field.type]));
1429
+ function sanitizeUrl(url) {
1430
+ let cleaned = "";
1431
+ for (const ch of url) {
1432
+ const code = ch.codePointAt(0);
1433
+ if (code !== void 0 && code > 32) cleaned += ch;
1434
+ }
1435
+ const scheme = cleaned.toLowerCase();
1436
+ if (scheme.startsWith("javascript:") || scheme.startsWith("vbscript:")) {
1437
+ return "#";
1438
+ }
1439
+ return url;
1385
1440
  }
1386
- function deriveFromSource(source, components, seen) {
1387
- const order = [];
1388
- const fields = /* @__PURE__ */ new Map();
1389
- const ensure = (name) => {
1390
- const existing = fields.get(name);
1391
- if (existing !== void 0) return existing;
1392
- const created = {
1441
+
1442
+ // ../engine/src/authors/authors.ts
1443
+ function toAuthorRefs(value) {
1444
+ if (!Array.isArray(value)) return [];
1445
+ const refs = [];
1446
+ for (const entry of value) {
1447
+ if (entry === null || typeof entry !== "object") continue;
1448
+ const record = entry;
1449
+ const name = typeof record.name === "string" ? record.name : "";
1450
+ if (name.trim() === "") continue;
1451
+ refs.push({
1393
1452
  name,
1394
- type: "shorttext",
1395
- label: humanize(name),
1396
- required: false,
1397
- multiline: false
1398
- };
1399
- fields.set(name, created);
1400
- order.push(name);
1401
- return created;
1402
- };
1403
- const record = (name, type) => {
1404
- if (fields.has(name)) return;
1405
- const field = ensure(name);
1406
- field.type = type;
1407
- field.multiline = FIELD_TYPES[type].multiline;
1408
- };
1409
- const applyAnnotations = (el) => {
1410
- const named = getAttribute(el, "data-field") ?? firstTokenName(el);
1411
- if (named === void 0) return;
1412
- const field = ensure(named);
1413
- if (hasAttribute(el, "data-required")) field.required = true;
1414
- const help = getAttribute(el, "data-help");
1415
- if (help !== void 0) field.help = help;
1416
- const label = getAttribute(el, "data-label");
1417
- if (label !== void 0) field.label = label;
1418
- const { minLength, maxLength } = lengthConstraints(el);
1419
- if (minLength !== void 0) field.minLength = minLength;
1420
- if (maxLength !== void 0) field.maxLength = maxLength;
1453
+ ...typeof record.user === "string" && record.user !== "" && { user: record.user },
1454
+ ...typeof record.href === "string" && record.href !== "" && { href: record.href }
1455
+ });
1456
+ }
1457
+ return refs;
1458
+ }
1459
+ function joinAuthors(names) {
1460
+ if (names.length === 0) return "";
1461
+ if (names.length === 1) return names[0] ?? "";
1462
+ const last = names[names.length - 1] ?? "";
1463
+ const head = names.slice(0, -1).join(", ");
1464
+ return `${head} and ${last}`;
1465
+ }
1466
+ function renderAuthors(value, directory) {
1467
+ const names = toAuthorRefs(value).map((ref) => {
1468
+ const resolved = ref.user !== void 0 ? directory?.(ref.user) : void 0;
1469
+ return escapeHtmlText(resolved?.displayName ?? ref.name);
1470
+ });
1471
+ return joinAuthors(names);
1472
+ }
1473
+
1474
+ // ../engine/src/media/media.ts
1475
+ var DEFAULT_WIDTHS = [400, 800, 1200, 1600];
1476
+ var MIME = {
1477
+ avif: "image/avif",
1478
+ webp: "image/webp",
1479
+ jpeg: "image/jpeg",
1480
+ png: "image/png"
1481
+ };
1482
+ function extOf(format) {
1483
+ return format === "jpeg" ? "jpg" : format;
1484
+ }
1485
+ function fallbackFormatFor(assetPath) {
1486
+ const ext = assetPath.slice(assetPath.lastIndexOf(".") + 1).toLowerCase();
1487
+ if (ext === "png") return "png";
1488
+ if (ext === "webp") return "webp";
1489
+ return "jpeg";
1490
+ }
1491
+ function contentHash(bytes) {
1492
+ let hash = 2166136261;
1493
+ for (let i = 0; i < bytes.length; i += 1) {
1494
+ hash ^= bytes[i] ?? 0;
1495
+ hash = Math.imul(hash, 16777619);
1496
+ }
1497
+ return (hash >>> 0).toString(36);
1498
+ }
1499
+ async function processImage(input, assetPath, encoder) {
1500
+ const fallbackFormat = fallbackFormatFor(assetPath);
1501
+ const formats = [.../* @__PURE__ */ new Set(["avif", "webp", fallbackFormat])];
1502
+ const encoded = await encoder.encode(input, { widths: DEFAULT_WIDTHS, formats });
1503
+ const hash = contentHash(input);
1504
+ const base = assetPath.slice(
1505
+ 0,
1506
+ assetPath.lastIndexOf(".") >= 0 ? assetPath.lastIndexOf(".") : assetPath.length
1507
+ );
1508
+ const artifacts = [];
1509
+ const urlOf = (variant) => {
1510
+ const path = `${base}.${hash}-${variant.width}.${extOf(variant.format)}`;
1511
+ artifacts.push({ path, contents: variant.bytes });
1512
+ return `/${path}`;
1421
1513
  };
1422
- const visit = (nodes) => {
1423
- for (const node of nodes) {
1424
- if (!isElement(node)) {
1425
- if (isTextNode(node)) continue;
1426
- continue;
1427
- }
1428
- const eachField = getAttribute(node, "data-each");
1429
- if (eachField !== void 0) {
1430
- const field = ensure(eachField);
1431
- field.type = "url";
1432
- field.multiple = true;
1433
- const pickFrom = getAttribute(node, "data-pick-from");
1434
- if (pickFrom !== void 0) field.pickFrom = pickFrom;
1435
- applyAnnotations(node);
1436
- continue;
1437
- }
1438
- if (hasAttribute(node, "data-each-in")) continue;
1439
- const annotation = getAttribute(node, "data-type");
1440
- const propTypes = components.has(node.tagName) ? componentPropTypes(node.tagName, components, seen) : void 0;
1441
- for (const attr of node.attrs) {
1442
- const whole = wholeValueToken(attr.value);
1443
- for (const token of findTokens(attr.value)) {
1444
- if (token.dotted) continue;
1445
- const isWhole = whole?.name === token.name;
1446
- const propType = isWhole ? propTypes?.get(attr.name) : void 0;
1447
- record(
1448
- token.name,
1449
- propType ?? inferControl({
1450
- tag: node.tagName,
1451
- attribute: attr.name,
1452
- wholeValue: isWhole,
1453
- valuePrefix: literalPrefix(attr.value),
1454
- download: hasAttribute(node, "download"),
1455
- ...annotation !== void 0 && { annotation }
1456
- })
1457
- );
1458
- }
1459
- }
1460
- for (const child of node.childNodes) {
1461
- if (!isTextNode(child)) continue;
1462
- const whole = wholeValueToken(child.value);
1463
- for (const token of findTokens(child.value)) {
1464
- if (token.dotted) continue;
1465
- record(
1466
- token.name,
1467
- inferControl({
1468
- tag: node.tagName,
1469
- wholeValue: whole?.name === token.name,
1470
- ...annotation !== void 0 && { annotation }
1471
- })
1472
- );
1473
- }
1474
- }
1475
- applyAnnotations(node);
1476
- visit(node.childNodes);
1477
- }
1514
+ const byFormat = /* @__PURE__ */ new Map();
1515
+ for (const variant of [...encoded.variants].sort((a, b) => a.width - b.width)) {
1516
+ const url = urlOf(variant);
1517
+ const list = byFormat.get(variant.format) ?? [];
1518
+ list.push({ url, width: variant.width });
1519
+ byFormat.set(variant.format, list);
1520
+ }
1521
+ const srcsetOf = (format) => (byFormat.get(format) ?? []).map((entry) => `${entry.url} ${entry.width}w`).join(", ");
1522
+ const sources = ["avif", "webp"].filter((format) => byFormat.has(format)).map((format) => ({ format, mime: MIME[format], srcset: srcsetOf(format) }));
1523
+ const fallbackList = byFormat.get(fallbackFormat) ?? [];
1524
+ const fallbackSrc = fallbackList[fallbackList.length - 1]?.url ?? `/${assetPath}`;
1525
+ const info = {
1526
+ width: encoded.sourceWidth,
1527
+ height: encoded.sourceHeight,
1528
+ sources,
1529
+ fallbackSrc,
1530
+ ...encoded.blurDataUri !== void 0 && { blurDataUri: encoded.blurDataUri }
1478
1531
  };
1479
- visit(parseNodes(source));
1480
- return order.map((name) => freeze(fields.get(name)));
1532
+ return { artifacts, info };
1481
1533
  }
1482
- function firstTokenName(el) {
1483
- for (const attr of el.attrs) {
1484
- const token = findTokens(attr.value).find((candidate) => !candidate.dotted);
1485
- if (token !== void 0) return token.name;
1534
+ function buildPictureMarkup(info, extraAttrs) {
1535
+ const sources = info.sources.map(
1536
+ (source) => `<source type="${source.mime}" srcset="${escapeHtmlAttribute(source.srcset)}">`
1537
+ ).join("");
1538
+ const carried = extraAttrs.filter(([name]) => name !== "src" && name !== "width" && name !== "height").map(([name, value]) => ` ${name}="${escapeHtmlAttribute(value)}"`).join("");
1539
+ const img = `<img src="${escapeHtmlAttribute(info.fallbackSrc)}" width="${info.width}" height="${info.height}" loading="lazy" decoding="async"${carried}>`;
1540
+ return `<picture>${sources}${img}</picture>`;
1541
+ }
1542
+
1543
+ // ../engine/src/compile/compiler.ts
1544
+ var LOOP_ATTRS = ["data-each-in", "data-each", "data-sort", "data-limit", "data-pick-from"];
1545
+ var AUTHORING_ATTRS = ["data-type", "data-field", "data-required", "data-help", "data-label"];
1546
+ function compileTemplate(input) {
1547
+ const ctx = {
1548
+ components: input.components,
1549
+ usedComponents: /* @__PURE__ */ new Set(),
1550
+ ...input.context !== void 0 && { context: input.context },
1551
+ ...input.media !== void 0 && { media: input.media },
1552
+ ...input.authorDirectory !== void 0 && { authorDirectory: input.authorDirectory },
1553
+ ...input.componentStyles !== void 0 && { componentStyles: input.componentStyles }
1554
+ };
1555
+ const body = transformNodes(parseNodes(input.template), input.scope, ctx);
1556
+ const out = input.document === void 0 ? body : fillSlots(transformNodes(parseNodes(input.document), input.scope, ctx), body);
1557
+ injectPageAssets(out, input, ctx);
1558
+ return serializeNodes(out);
1559
+ }
1560
+ function injectPageAssets(nodes, input, ctx) {
1561
+ const style = input.templateStyle?.trim();
1562
+ if (style) {
1563
+ const styleNode = parseFragmentNodes(`<style>
1564
+ ${style}
1565
+ </style>`);
1566
+ const head = findElement(nodes, "head");
1567
+ if (head) head.childNodes.push(...styleNode);
1568
+ else nodes.unshift(...styleNode);
1486
1569
  }
1487
- for (const child of el.childNodes) {
1488
- if (isTextNode(child)) {
1489
- const token = findTokens(child.value).find((candidate) => !candidate.dotted);
1490
- if (token !== void 0) return token.name;
1491
- }
1570
+ const scripts = [];
1571
+ const templateScript = input.templateScript?.trim();
1572
+ if (templateScript) scripts.push(templateScript);
1573
+ for (const tag of [...ctx.usedComponents].sort()) {
1574
+ const js = input.componentScripts?.get(tag)?.trim();
1575
+ if (js) scripts.push(js);
1576
+ }
1577
+ if (scripts.length > 0) {
1578
+ const scriptNodes = scripts.flatMap((js) => parseFragmentNodes(`<script>
1579
+ ${js}
1580
+ </script>`));
1581
+ const body = findElement(nodes, "body");
1582
+ if (body) body.childNodes.push(...scriptNodes);
1583
+ else nodes.push(...scriptNodes);
1584
+ }
1585
+ }
1586
+ function findElement(nodes, tagName) {
1587
+ for (const node of nodes) {
1588
+ if (!isElement(node)) continue;
1589
+ if (node.tagName === tagName) return node;
1590
+ const found = findElement(node.childNodes, tagName);
1591
+ if (found !== void 0) return found;
1492
1592
  }
1493
1593
  return void 0;
1494
1594
  }
1495
- function freeze(field) {
1496
- return {
1497
- name: field.name,
1498
- type: field.type,
1499
- label: field.label,
1500
- required: field.required,
1501
- multiline: field.multiline,
1502
- ...field.help !== void 0 && { help: field.help },
1503
- ...field.minLength !== void 0 && { minLength: field.minLength },
1504
- ...field.maxLength !== void 0 && { maxLength: field.maxLength },
1505
- ...field.multiple !== void 0 && { multiple: field.multiple },
1506
- ...field.pickFrom !== void 0 && { pickFrom: field.pickFrom }
1595
+ function isBoundItem(value) {
1596
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "url" in value;
1597
+ }
1598
+ function resolveToString(value) {
1599
+ if (value === void 0 || value === null) return "";
1600
+ if (Array.isArray(value)) {
1601
+ return value.map((entry) => entry === null ? "" : String(entry)).join(", ");
1602
+ }
1603
+ return String(value);
1604
+ }
1605
+ function scopeResolver(scope) {
1606
+ return (token) => {
1607
+ const head = scope[token.name];
1608
+ if (isBoundItem(head)) {
1609
+ if (token.path.length === 1) return head.url;
1610
+ const field = head.fields[token.path[1] ?? ""];
1611
+ return isBoundItem(field) ? field.url : resolveToString(field);
1612
+ }
1613
+ if (token.dotted) return "";
1614
+ return resolveToString(head);
1507
1615
  };
1508
1616
  }
1509
-
1510
- // ../engine/src/template/analyze.ts
1511
- function analyzeTemplate(source, components = /* @__PURE__ */ new Map()) {
1512
- const requiredFields = /* @__PURE__ */ new Set();
1513
- const queryLoopPaths = [];
1514
- const curatedLoopFields = [];
1515
- const imageFields = /* @__PURE__ */ new Set();
1516
- const fileFields = /* @__PURE__ */ new Set();
1517
- const richTextFields = /* @__PURE__ */ new Set();
1518
- const constraints = /* @__PURE__ */ new Map();
1519
- const references = /* @__PURE__ */ new Set();
1520
- const visit = (nodes) => {
1521
- for (const node of nodes) {
1522
- if (!isElement(node)) continue;
1523
- const richText = richTextFieldName(node);
1524
- if (richText !== void 0) richTextFields.add(richText);
1525
- const eachIn = getAttribute(node, "data-each-in");
1526
- if (eachIn !== void 0) queryLoopPaths.push(eachIn);
1527
- const each = getAttribute(node, "data-each");
1528
- if (each !== void 0) curatedLoopFields.push(each);
1529
- if (hasAttribute(node, "data-required")) {
1530
- const field = annotatedFieldName(node);
1531
- if (field !== void 0) requiredFields.add(field);
1532
- }
1533
- const bounds = lengthConstraints(node);
1534
- if (bounds.minLength !== void 0 || bounds.maxLength !== void 0) {
1535
- const field = annotatedFieldName(node);
1536
- if (field !== void 0) constraints.set(field, bounds);
1537
- }
1538
- const annotation = getAttribute(node, "data-type");
1539
- const propTypes = components.has(node.tagName) ? componentPropTypes(node.tagName, components, /* @__PURE__ */ new Set()) : void 0;
1540
- for (const attr of node.attrs) {
1541
- const reference = referenceTarget(attr.value);
1542
- if (reference !== void 0) references.add(reference);
1543
- const token = wholeValueToken(attr.value);
1544
- if (token === null || token.dotted) continue;
1545
- const type = propTypes?.get(attr.name) ?? inferControl({
1546
- tag: node.tagName,
1547
- attribute: attr.name,
1548
- wholeValue: true,
1549
- valuePrefix: literalPrefix(attr.value),
1550
- download: hasAttribute(node, "download"),
1551
- ...annotation !== void 0 && { annotation }
1552
- });
1553
- if (type === "image") imageFields.add(token.name);
1554
- else if (type === "file") fileFields.add(token.name);
1555
- else if (type === "richtext") richTextFields.add(token.name);
1617
+ function transformNodes(nodes, scope, ctx) {
1618
+ const out = [];
1619
+ for (const node of nodes) {
1620
+ if (isElement(node)) {
1621
+ out.push(...transformElement(node, scope, ctx));
1622
+ } else if (isTextNode(node)) {
1623
+ node.value = substituteTokens(node.value, scopeResolver(scope));
1624
+ out.push(node);
1625
+ } else {
1626
+ out.push(node);
1627
+ }
1628
+ }
1629
+ return out;
1630
+ }
1631
+ function transformElement(el, scope, ctx) {
1632
+ if (hasAttribute(el, "data-each-in") || hasAttribute(el, "data-each")) {
1633
+ return expandLoop(el, scope, ctx);
1634
+ }
1635
+ if (ctx.components.has(el.tagName)) {
1636
+ return expandComponent(el, scope, ctx);
1637
+ }
1638
+ substituteAttributes(el, scope, ctx);
1639
+ const annotation = getAttribute(el, "data-type");
1640
+ for (const attr of AUTHORING_ATTRS) removeAttribute(el, attr);
1641
+ if (el.tagName === "img" && ctx.media !== void 0) {
1642
+ const info = ctx.media.image(getAttribute(el, "src") ?? "");
1643
+ if (info !== void 0) {
1644
+ const attrs = el.attrs.map((attr) => [attr.name, attr.value]);
1645
+ return parseFragmentNodes(buildPictureMarkup(info, attrs));
1646
+ }
1647
+ }
1648
+ if (isJsonLd(el)) {
1649
+ substituteRawText(el, scope, "json");
1650
+ return [el];
1651
+ }
1652
+ if (el.tagName === "script" || el.tagName === "style") {
1653
+ substituteRawText(el, scope, "raw");
1654
+ return [el];
1655
+ }
1656
+ const field = wholeValueFieldToken(el, annotation);
1657
+ if (field?.kind === "html") {
1658
+ const injected = parseFragmentNodes(resolveToString(scope[field.token.name]));
1659
+ el.childNodes = transformRichMedia(injected, ctx);
1660
+ return [el];
1661
+ }
1662
+ if (field?.kind === "authors") {
1663
+ el.childNodes = parseFragmentNodes(
1664
+ renderAuthors(scope[field.token.name], ctx.authorDirectory)
1665
+ );
1666
+ return [el];
1667
+ }
1668
+ el.childNodes = transformNodes(el.childNodes, scope, ctx);
1669
+ return [el];
1670
+ }
1671
+ function transformRichMedia(nodes, ctx) {
1672
+ const out = [];
1673
+ for (const node of nodes) {
1674
+ if (!isElement(node)) {
1675
+ out.push(node);
1676
+ continue;
1677
+ }
1678
+ node.childNodes = transformRichMedia(node.childNodes, ctx);
1679
+ if (node.tagName === "img") {
1680
+ const src = getAttribute(node, "src");
1681
+ const info = src !== void 0 ? ctx.media?.image(src) : void 0;
1682
+ if (info !== void 0) {
1683
+ const attrs = node.attrs.map((attr) => [attr.name, attr.value]);
1684
+ out.push(...parseFragmentNodes(buildPictureMarkup(info, attrs)));
1685
+ continue;
1556
1686
  }
1557
- visit(node.childNodes);
1687
+ if (src !== void 0) setAttribute(node, "src", resolveMediaUrl(src, ctx));
1688
+ }
1689
+ const href = getAttribute(node, "href");
1690
+ if (href !== void 0) setAttribute(node, "href", resolveMediaUrl(href, ctx));
1691
+ out.push(node);
1692
+ }
1693
+ return out;
1694
+ }
1695
+ function resolveMediaUrl(value, ctx) {
1696
+ const referenced = resolveReference(value, ctx);
1697
+ if (referenced !== void 0) return sanitizeUrl(referenced);
1698
+ return sanitizeUrl(ctx.media?.file(value) ?? value);
1699
+ }
1700
+ function resolveReference(value, ctx) {
1701
+ const path = referenceTarget(value);
1702
+ if (path === void 0) return void 0;
1703
+ return ctx.context?.reference(path)?.url ?? "#";
1704
+ }
1705
+ function expandLoop(el, scope, ctx) {
1706
+ if (ctx.context === void 0) return [];
1707
+ const eachIn = getAttribute(el, "data-each-in");
1708
+ let items;
1709
+ if (eachIn !== void 0) {
1710
+ const limitText = getAttribute(el, "data-limit");
1711
+ const limit = limitText !== void 0 ? Number.parseInt(limitText, 10) : void 0;
1712
+ const sort = getAttribute(el, "data-sort");
1713
+ items = ctx.context.collection(eachIn, {
1714
+ ...sort !== void 0 && { sort },
1715
+ ...limit !== void 0 && !Number.isNaN(limit) && { limit }
1716
+ });
1717
+ } else {
1718
+ const fieldName = getAttribute(el, "data-each") ?? "";
1719
+ const value = scope[fieldName];
1720
+ const paths = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : typeof value === "string" ? [value] : [];
1721
+ items = paths.map((path) => ctx.context?.reference(path)).filter((bound) => bound !== void 0);
1722
+ }
1723
+ const out = [];
1724
+ for (const item of items) {
1725
+ const clone = cloneNode(el);
1726
+ for (const attr of LOOP_ATTRS) removeAttribute(clone, attr);
1727
+ out.push(...transformElement(clone, { ...scope, item }, ctx));
1728
+ }
1729
+ return out;
1730
+ }
1731
+ function cloneNode(node) {
1732
+ if (isElement(node)) {
1733
+ const clone = {
1734
+ ...node,
1735
+ attrs: node.attrs.map((attr) => ({ ...attr })),
1736
+ childNodes: []
1737
+ };
1738
+ clone.childNodes = node.childNodes.map(cloneNode);
1739
+ return clone;
1740
+ }
1741
+ return { ...node };
1742
+ }
1743
+ function substituteAttributes(el, scope, ctx) {
1744
+ for (const attr of el.attrs) {
1745
+ if (findTokens(attr.value).length === 0) {
1746
+ const referenced2 = resolveReference(attr.value, ctx);
1747
+ if (referenced2 !== void 0) attr.value = sanitizeUrl(referenced2);
1748
+ continue;
1558
1749
  }
1559
- };
1560
- visit(parseNodes(source));
1561
- return {
1562
- requiredFields: [...requiredFields],
1563
- queryLoopPaths,
1564
- curatedLoopFields,
1565
- imageFields: [...imageFields],
1566
- fileFields: [...fileFields],
1567
- richTextFields: [...richTextFields],
1568
- constraints,
1569
- references: [...references].sort()
1570
- };
1750
+ const annotation = getAttribute(el, "data-type");
1751
+ const whole = wholeValueToken(attr.value) !== null;
1752
+ const type = inferControl({
1753
+ tag: el.tagName,
1754
+ attribute: attr.name,
1755
+ wholeValue: whole,
1756
+ valuePrefix: literalPrefix(attr.value),
1757
+ download: hasAttribute(el, "download"),
1758
+ ...annotation !== void 0 && { annotation }
1759
+ });
1760
+ let value = substituteTokens(attr.value, scopeResolver(scope));
1761
+ const referenced = resolveReference(value, ctx);
1762
+ if (referenced !== void 0) value = referenced;
1763
+ const fileUrl = ctx.media?.file(value);
1764
+ if (fileUrl !== void 0) value = fileUrl;
1765
+ if (whole && valueKindOf(type) === "url") value = sanitizeUrl(value);
1766
+ attr.value = value;
1767
+ }
1571
1768
  }
1572
- function richTextFieldName(el) {
1769
+ function substituteRawText(el, scope, mode) {
1770
+ const resolve = scopeResolver(scope);
1771
+ for (const child of el.childNodes) {
1772
+ if (!isTextNode(child)) continue;
1773
+ child.value = substituteTokens(child.value, (token) => {
1774
+ const text = resolve(token);
1775
+ return mode === "json" ? escapeJsonStringContent(text) : text;
1776
+ });
1777
+ }
1778
+ }
1779
+ function wholeValueFieldToken(el, annotation) {
1573
1780
  const meaningful = el.childNodes.filter(
1574
1781
  (child) => !(isTextNode(child) && child.value.trim() === "")
1575
1782
  );
1576
1783
  const only = meaningful[0];
1577
- if (meaningful.length !== 1 || only === void 0 || !isTextNode(only)) return void 0;
1784
+ if (meaningful.length !== 1 || only === void 0 || !isTextNode(only)) return null;
1578
1785
  const token = wholeValueToken(only.value);
1579
- if (token === null || token.dotted) return void 0;
1580
- const annotation = getAttribute(el, "data-type");
1786
+ if (token === null || token.dotted) return null;
1581
1787
  const type = inferControl({
1582
1788
  tag: el.tagName,
1583
1789
  wholeValue: true,
1584
1790
  ...annotation !== void 0 && { annotation }
1585
1791
  });
1586
- return valueKindOf(type) === "html" ? token.name : void 0;
1792
+ return { token, kind: valueKindOf(type) };
1587
1793
  }
1588
- function annotatedFieldName(el) {
1589
- const explicit = getAttribute(el, "data-field");
1590
- if (explicit !== void 0) return explicit;
1794
+ function isJsonLd(el) {
1795
+ return el.tagName === "script" && getAttribute(el, "type")?.toLowerCase() === "application/ld+json";
1796
+ }
1797
+ function expandComponent(el, scope, ctx) {
1798
+ const source = ctx.components.get(el.tagName);
1799
+ if (source === void 0) return [el];
1800
+ const resolve = scopeResolver(scope);
1801
+ const propScope = {};
1591
1802
  for (const attr of el.attrs) {
1592
- const token = findTokens(attr.value).find((candidate) => !candidate.dotted);
1593
- if (token !== void 0) return token.name;
1803
+ propScope[attr.name] = substituteTokens(attr.value, resolve);
1594
1804
  }
1595
- for (const child of el.childNodes) {
1596
- if (isTextNode(child)) {
1597
- const token = findTokens(child.value).find((candidate) => !candidate.dotted);
1598
- if (token !== void 0) return token.name;
1805
+ const slotChildren = transformNodes(el.childNodes, scope, ctx);
1806
+ const body = transformNodes(parseNodes(source), propScope, ctx);
1807
+ const result = fillSlots(body, slotChildren);
1808
+ ctx.usedComponents.add(el.tagName);
1809
+ const css = ctx.componentStyles?.get(el.tagName)?.trim();
1810
+ if (css) {
1811
+ const root = result.find(isElement);
1812
+ if (root !== void 0) {
1813
+ const styleNode = parseFragmentNodes(`<style>@scope {
1814
+ ${css}
1815
+ }</style>`);
1816
+ root.childNodes = [...styleNode, ...root.childNodes];
1599
1817
  }
1600
1818
  }
1601
- return void 0;
1819
+ return result;
1820
+ }
1821
+ function fillSlots(body, slotChildren) {
1822
+ const named = /* @__PURE__ */ new Map();
1823
+ const defaultSlot = [];
1824
+ for (const child of slotChildren) {
1825
+ const slotName = isElement(child) ? getAttribute(child, "slot") : void 0;
1826
+ if (slotName !== void 0) {
1827
+ if (isElement(child)) removeAttribute(child, "slot");
1828
+ const group = named.get(slotName) ?? [];
1829
+ group.push(child);
1830
+ named.set(slotName, group);
1831
+ } else {
1832
+ defaultSlot.push(child);
1833
+ }
1834
+ }
1835
+ return replaceSlots(body, named, defaultSlot);
1836
+ }
1837
+ function replaceSlots(nodes, named, defaultSlot) {
1838
+ const out = [];
1839
+ for (const node of nodes) {
1840
+ if (isElement(node) && node.tagName === "slot") {
1841
+ const name = getAttribute(node, "name");
1842
+ const provided = name !== void 0 ? named.get(name) : defaultSlot;
1843
+ if (provided && provided.length > 0) {
1844
+ out.push(...provided);
1845
+ } else {
1846
+ out.push(...replaceSlots(node.childNodes, named, defaultSlot));
1847
+ }
1848
+ } else if (isElement(node)) {
1849
+ node.childNodes = replaceSlots(node.childNodes, named, defaultSlot);
1850
+ out.push(node);
1851
+ } else {
1852
+ out.push(node);
1853
+ }
1854
+ }
1855
+ return out;
1856
+ }
1857
+
1858
+ // ../engine/src/aggregate/aggregate.ts
1859
+ function joinUrl(baseUrl, sitePath) {
1860
+ return baseUrl.replace(/\/+$/, "") + sitePath;
1861
+ }
1862
+ function textContent(html) {
1863
+ const parts = [];
1864
+ const visit = (nodes) => {
1865
+ for (const node of nodes) {
1866
+ if (isTextNode(node)) parts.push(node.value);
1867
+ else if (isElement(node)) visit(node.childNodes);
1868
+ }
1869
+ };
1870
+ visit(parseFragmentNodes(html));
1871
+ return parts.join(" ").replace(/\s+/g, " ").trim();
1872
+ }
1873
+ function buildSitemap(entries, baseUrl) {
1874
+ const urls = entries.map((entry) => {
1875
+ const lastmod = entry.updated ?? entry.published;
1876
+ const mod = lastmod !== void 0 ? `
1877
+ <lastmod>${escapeHtmlText(lastmod)}</lastmod>` : "";
1878
+ return ` <url>
1879
+ <loc>${escapeHtmlText(joinUrl(baseUrl, entry.url))}</loc>${mod}
1880
+ </url>`;
1881
+ }).join("\n");
1882
+ const contents = `<?xml version="1.0" encoding="UTF-8"?>
1883
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1884
+ ${urls}
1885
+ </urlset>
1886
+ `;
1887
+ return { path: "sitemap.xml", contents };
1888
+ }
1889
+ function buildRss(entries, options) {
1890
+ const items = [...entries].sort(
1891
+ (a, b) => String(b.published ?? "").localeCompare(String(a.published ?? ""))
1892
+ );
1893
+ const lastBuild = items[0]?.published;
1894
+ const rfc822 = (iso) => new Date(iso).toUTCString();
1895
+ const itemXml = items.map((entry) => {
1896
+ const link = joinUrl(options.baseUrl, entry.url);
1897
+ const pub = entry.published !== void 0 ? `
1898
+ <pubDate>${escapeHtmlText(rfc822(entry.published))}</pubDate>` : "";
1899
+ const desc = entry.summary !== void 0 ? `
1900
+ <description>${escapeHtmlText(entry.summary)}</description>` : "";
1901
+ return ` <item>
1902
+ <title>${escapeHtmlText(entry.title)}</title>
1903
+ <link>${escapeHtmlText(link)}</link>
1904
+ <guid>${escapeHtmlText(link)}</guid>${pub}${desc}
1905
+ </item>`;
1906
+ }).join("\n");
1907
+ const built = lastBuild !== void 0 ? `
1908
+ <lastBuildDate>${escapeHtmlText(rfc822(lastBuild))}</lastBuildDate>` : "";
1909
+ const contents = `<?xml version="1.0" encoding="UTF-8"?>
1910
+ <rss version="2.0">
1911
+ <channel>
1912
+ <title>${escapeHtmlText(options.title)}</title>
1913
+ <link>${escapeHtmlText(options.baseUrl)}</link>
1914
+ <description>${escapeHtmlText(options.description)}</description>${built}
1915
+ ${itemXml}
1916
+ </channel>
1917
+ </rss>
1918
+ `;
1919
+ return { path: "feed.xml", contents };
1920
+ }
1921
+ function buildContentIndex(entries) {
1922
+ const docs = entries.map((entry) => ({
1923
+ url: entry.url,
1924
+ title: entry.title,
1925
+ ...entry.published !== void 0 && { published: entry.published },
1926
+ ...entry.summary !== void 0 && { summary: entry.summary }
1927
+ }));
1928
+ return { path: "content.json", contents: JSON.stringify(docs) };
1929
+ }
1930
+
1931
+ // ../engine/src/concurrency.ts
1932
+ async function mapWithConcurrency(items, limit, fn) {
1933
+ const results = new Array(items.length);
1934
+ const workers = Math.max(1, Math.min(Math.floor(limit), items.length));
1935
+ let next = 0;
1936
+ const run = async () => {
1937
+ while (next < items.length) {
1938
+ const index = next++;
1939
+ results[index] = await fn(items[index], index);
1940
+ }
1941
+ };
1942
+ await Promise.all(Array.from({ length: workers }, () => run()));
1943
+ return results;
1602
1944
  }
1603
1945
 
1604
1946
  // ../engine/src/url/urls.ts
@@ -2269,18 +2611,32 @@ async function validateSite(source) {
2269
2611
  }
2270
2612
  };
2271
2613
  const analysisCache = /* @__PURE__ */ new Map();
2614
+ const sourceCache = /* @__PURE__ */ new Map();
2272
2615
  const analyze = async (name) => {
2273
2616
  const cached = analysisCache.get(name);
2274
2617
  if (cached !== void 0 || analysisCache.has(name)) return cached ?? null;
2275
2618
  let result;
2276
2619
  try {
2277
- result = analyzeTemplate(await loadTemplate(source, name), components);
2620
+ const html = await loadTemplate(source, name);
2621
+ sourceCache.set(name, html);
2622
+ result = analyzeTemplate(html, components);
2278
2623
  } catch {
2279
2624
  result = null;
2280
2625
  }
2281
2626
  analysisCache.set(name, result);
2282
2627
  return result;
2283
2628
  };
2629
+ const expectedFieldsCache = /* @__PURE__ */ new Map();
2630
+ const expectedFieldsFor = (templateName) => {
2631
+ const cached = expectedFieldsCache.get(templateName);
2632
+ if (cached !== void 0) return cached;
2633
+ const html = sourceCache.get(templateName);
2634
+ const fields = new Set(
2635
+ html !== void 0 ? deriveFields(html, components).map((f) => f.name) : []
2636
+ );
2637
+ expectedFieldsCache.set(templateName, fields);
2638
+ return fields;
2639
+ };
2284
2640
  const urlOwners = /* @__PURE__ */ new Map();
2285
2641
  for (const { node, templateName } of published) {
2286
2642
  const owners = urlOwners.get(urlForItem(node.path)) ?? [];
@@ -2297,14 +2653,28 @@ async function validateSite(source) {
2297
2653
  }
2298
2654
  const analysis = await analyze(templateName);
2299
2655
  if (analysis === null) {
2656
+ const snapshotHint = isVersionedTemplateName(templateName) ? ` (the snapshot may have been deleted while this item still pins to it \u2014 restore it, rebind the item to current via the Migrations workspace, or delete this item).` : "";
2300
2657
  add(
2301
2658
  "error",
2302
2659
  "template-missing",
2303
- `"${node.path}" uses template "${templateName}", which does not exist.`,
2660
+ `"${node.path}" uses template "${templateName}", which does not exist.${snapshotHint}`,
2304
2661
  node.path
2305
2662
  );
2306
2663
  continue;
2307
2664
  }
2665
+ if (!isVersionedTemplateName(templateName)) {
2666
+ const expectedFields = expectedFieldsFor(templateName);
2667
+ const itemFieldNames = Object.keys(node.item.fields);
2668
+ const orphans = itemFieldNames.filter((name) => !expectedFields.has(name));
2669
+ if (orphans.length > 0) {
2670
+ add(
2671
+ "warning",
2672
+ "content.schema-drift",
2673
+ `"${node.path}" has field${orphans.length === 1 ? "" : "s"} not rendered by template "${templateName}": ${orphans.map((o) => `"${o}"`).join(", ")}. Open the Migrations workspace to drop, rename, or keep them.`,
2674
+ node.path
2675
+ );
2676
+ }
2677
+ }
2308
2678
  const scope = buildScope2(node.item);
2309
2679
  for (const field of analysis.requiredFields) {
2310
2680
  if (isEmpty(scope[field])) {
@@ -2590,10 +2960,10 @@ function createDiagnosticRegistry() {
2590
2960
  repairs.set(repair.id, repair);
2591
2961
  },
2592
2962
  runAll,
2593
- async runRepair(id, deps) {
2963
+ async runRepair(id, deps, args = {}) {
2594
2964
  const repair = repairs.get(id);
2595
2965
  if (repair === void 0) throw new Error(`Unknown repair id: ${id}`);
2596
- await repair.run(deps);
2966
+ await repair.run(deps, args);
2597
2967
  return runAll(deps);
2598
2968
  }
2599
2969
  };
@@ -2733,6 +3103,20 @@ export {
2733
3103
  parseContentItem,
2734
3104
  parseSortFile,
2735
3105
  InMemoryBlobStore,
3106
+ FIELD_TYPES,
3107
+ isFieldType,
3108
+ valueKindOf,
3109
+ inferControl,
3110
+ findTokens,
3111
+ wholeValueToken,
3112
+ deriveFields,
3113
+ detectStamp,
3114
+ isVersionedTemplateName,
3115
+ baseTemplateName,
3116
+ versionNumber,
3117
+ snapshotName,
3118
+ nextVersionNumber,
3119
+ isReservedVersionedPath,
2736
3120
  RESERVED_PREFIX,
2737
3121
  emptyIndex,
2738
3122
  loadIndex,
@@ -2752,12 +3136,10 @@ export {
2752
3136
  loadTemplate,
2753
3137
  DOCUMENT_SHELL,
2754
3138
  loadDocumentShell,
2755
- FIELD_TYPES,
2756
- isFieldType,
2757
- valueKindOf,
2758
- inferControl,
2759
- findTokens,
2760
- wholeValueToken,
3139
+ analyzeTemplate,
3140
+ pendingMigrations,
3141
+ bestFitSnapshot,
3142
+ applyMigration,
2761
3143
  escapeHtmlText,
2762
3144
  escapeHtmlAttribute,
2763
3145
  escapeJsonStringContent,
@@ -2773,8 +3155,6 @@ export {
2773
3155
  buildSitemap,
2774
3156
  buildRss,
2775
3157
  buildContentIndex,
2776
- deriveFields,
2777
- analyzeTemplate,
2778
3158
  outputPathForItem,
2779
3159
  urlForItem,
2780
3160
  compileSite,