@xtrable-ltd/nanoesis 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/adapter-azure-blob.d.ts +97 -0
  4. package/dist/adapter-azure-blob.js +127 -0
  5. package/dist/adapter-cloudflare.d.ts +28 -0
  6. package/dist/adapter-cloudflare.js +32 -0
  7. package/dist/adapter-fs.d.ts +38 -0
  8. package/dist/adapter-fs.js +54 -0
  9. package/dist/adapter-local-jwt.d.ts +205 -0
  10. package/dist/adapter-local-jwt.js +550 -0
  11. package/dist/adapter-sharp.d.ts +11 -0
  12. package/dist/adapter-sharp.js +39 -0
  13. package/dist/adapter-shell.d.ts +48 -0
  14. package/dist/adapter-shell.js +56 -0
  15. package/dist/adapter-trusted-header.d.ts +43 -0
  16. package/dist/adapter-trusted-header.js +21 -0
  17. package/dist/chunk-G2UEZTYC.js +2541 -0
  18. package/dist/editor-api.d.ts +198 -0
  19. package/dist/editor-api.js +592 -0
  20. package/dist/editor.d.ts +13 -0
  21. package/dist/editor.js +6 -0
  22. package/dist/index.d.ts +1238 -0
  23. package/dist/index.js +124 -0
  24. package/editor/assets/TemplatesPane-5qsDAK_B.js +792 -0
  25. package/editor/assets/TemplatesPane-B4_sg2u5.css +1 -0
  26. package/editor/assets/abap-BrgZPUOV.js +6 -0
  27. package/editor/assets/apex-DyP6w7ZV.js +6 -0
  28. package/editor/assets/azcli-BaLxmfj-.js +6 -0
  29. package/editor/assets/bat-CFOPXBzS.js +6 -0
  30. package/editor/assets/bicep-BfEKNvv3.js +7 -0
  31. package/editor/assets/cameligo-BFG1Mk7z.js +6 -0
  32. package/editor/assets/clojure-DTECt2xU.js +6 -0
  33. package/editor/assets/codicon-DCmgc-ay.ttf +0 -0
  34. package/editor/assets/coffee-CDGzqUPQ.js +6 -0
  35. package/editor/assets/cpp-CLLBncYj.js +6 -0
  36. package/editor/assets/csharp-dUCx_-0o.js +6 -0
  37. package/editor/assets/csp-5Rap-vPy.js +6 -0
  38. package/editor/assets/css-D3h14YRZ.js +8 -0
  39. package/editor/assets/css.worker-DaIe3gwK.js +84 -0
  40. package/editor/assets/cssMode-CGp4MIjR.js +9 -0
  41. package/editor/assets/cypher-DrQuvNYM.js +6 -0
  42. package/editor/assets/dart-CFKIUWau.js +6 -0
  43. package/editor/assets/dockerfile-Zznr-cwX.js +6 -0
  44. package/editor/assets/ecl-Ce3n6wWz.js +6 -0
  45. package/editor/assets/editor.worker-BCzxt1at.js +12 -0
  46. package/editor/assets/elixir-deUWdS0T.js +6 -0
  47. package/editor/assets/flow9-i9-g7ZhI.js +6 -0
  48. package/editor/assets/freemarker2-CJkwxmPv.js +8 -0
  49. package/editor/assets/fsharp-CzKuDChf.js +6 -0
  50. package/editor/assets/go-Cphgjts3.js +6 -0
  51. package/editor/assets/graphql-Cg7bfA9N.js +6 -0
  52. package/editor/assets/handlebars-CKb5i2nM.js +6 -0
  53. package/editor/assets/hcl-0cvrggvQ.js +6 -0
  54. package/editor/assets/html-DyMbQx0w.js +6 -0
  55. package/editor/assets/html.worker-CKrFyw_2.js +461 -0
  56. package/editor/assets/htmlMode-DVPeqtn-.js +9 -0
  57. package/editor/assets/index-CbuWEnUB.css +7 -0
  58. package/editor/assets/index-DJmSgobK.js +129 -0
  59. package/editor/assets/ini-Drc7WvVn.js +6 -0
  60. package/editor/assets/java-B_fMsGYe.js +6 -0
  61. package/editor/assets/javascript-Bp1Qh9wR.js +6 -0
  62. package/editor/assets/json.worker-B7c_PmGb.js +49 -0
  63. package/editor/assets/jsonMode-FLEeVtx7.js +15 -0
  64. package/editor/assets/julia-Bqgm2twL.js +6 -0
  65. package/editor/assets/kotlin-BSkB5QuD.js +6 -0
  66. package/editor/assets/less-BsTHnhdd.js +7 -0
  67. package/editor/assets/lexon-YWi4-JPR.js +6 -0
  68. package/editor/assets/liquid-Bh8c534t.js +6 -0
  69. package/editor/assets/lua-nf6ki56Z.js +6 -0
  70. package/editor/assets/m3-Cpb6xl2v.js +6 -0
  71. package/editor/assets/markdown-DSZPf7rp.js +6 -0
  72. package/editor/assets/mdx-BUbo8M9l.js +6 -0
  73. package/editor/assets/mips-B_c3zf-v.js +6 -0
  74. package/editor/assets/msdax-rUNN04Wq.js +6 -0
  75. package/editor/assets/mysql-DDwshQtU.js +6 -0
  76. package/editor/assets/nanoesis-logo-CgieIWPg.png +0 -0
  77. package/editor/assets/objective-c-B5zXfXm9.js +6 -0
  78. package/editor/assets/pascal-CXOwvkN_.js +6 -0
  79. package/editor/assets/pascaligo-Bc-ZgV77.js +6 -0
  80. package/editor/assets/perl-CwNk8-XU.js +6 -0
  81. package/editor/assets/pgsql-tGk8EFnU.js +6 -0
  82. package/editor/assets/php-CpIb_Oan.js +6 -0
  83. package/editor/assets/pla-B03wrqEc.js +6 -0
  84. package/editor/assets/postiats-BKlk5iyT.js +6 -0
  85. package/editor/assets/powerquery-Bhzvs7bI.js +6 -0
  86. package/editor/assets/powershell-Dd3NCNK9.js +6 -0
  87. package/editor/assets/protobuf-COyEY5Pt.js +7 -0
  88. package/editor/assets/pug-BaJupSGV.js +6 -0
  89. package/editor/assets/python-CuJlk8g3.js +6 -0
  90. package/editor/assets/qsharp-DXyYeYxl.js +6 -0
  91. package/editor/assets/r-CdQndTaG.js +6 -0
  92. package/editor/assets/razor-CuQT_1Ku.js +6 -0
  93. package/editor/assets/redis-CVwtpugi.js +6 -0
  94. package/editor/assets/redshift-25W9uPmb.js +6 -0
  95. package/editor/assets/restructuredtext-DfzH4Xui.js +6 -0
  96. package/editor/assets/ruby-Cp1zYvxS.js +6 -0
  97. package/editor/assets/rust-D5C2fndG.js +6 -0
  98. package/editor/assets/sb-CDntyWJ8.js +6 -0
  99. package/editor/assets/scala-BoFRg7Ot.js +6 -0
  100. package/editor/assets/scheme-Bio4gycK.js +6 -0
  101. package/editor/assets/scss-4Ik7cdeQ.js +8 -0
  102. package/editor/assets/shell-CX-rkNHf.js +6 -0
  103. package/editor/assets/solidity-Tw7wswEv.js +6 -0
  104. package/editor/assets/sophia-C5WLch3f.js +6 -0
  105. package/editor/assets/sparql-DHaeiCBh.js +6 -0
  106. package/editor/assets/sql-CCSDG5nI.js +6 -0
  107. package/editor/assets/st-pnP8ivHi.js +6 -0
  108. package/editor/assets/swift-DwJ7jVG9.js +8 -0
  109. package/editor/assets/systemverilog-B9Xyijhd.js +6 -0
  110. package/editor/assets/tcl-DnHyzjbg.js +6 -0
  111. package/editor/assets/ts.worker-BhkL8olL.js +51334 -0
  112. package/editor/assets/tsMode-CT2HUNtN.js +16 -0
  113. package/editor/assets/twig-CPajHgWi.js +6 -0
  114. package/editor/assets/typescript-CtMx97cn.js +6 -0
  115. package/editor/assets/typespec-D-MeaMDU.js +6 -0
  116. package/editor/assets/vb-DgyLZaXg.js +6 -0
  117. package/editor/assets/wgsl-BIv9DU6q.js +303 -0
  118. package/editor/assets/xml-CyfpINj_.js +6 -0
  119. package/editor/assets/yaml-BBWmgfMA.js +6 -0
  120. package/editor/config.json +3 -0
  121. package/editor/index.html +28 -0
  122. package/package.json +85 -0
@@ -0,0 +1,2541 @@
1
+ // ../engine/src/slug.ts
2
+ function slugify(input) {
3
+ return input.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
4
+ }
5
+
6
+ // ../engine/src/text.ts
7
+ function humanize(name) {
8
+ const spaced = name.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim().replace(/\s+/g, " ");
9
+ if (spaced.length === 0) return "";
10
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
11
+ }
12
+
13
+ // ../engine/src/mime.ts
14
+ var CONTENT_TYPES = {
15
+ html: "text/html; charset=utf-8",
16
+ json: "application/json; charset=utf-8",
17
+ js: "text/javascript; charset=utf-8",
18
+ mjs: "text/javascript; charset=utf-8",
19
+ css: "text/css; charset=utf-8",
20
+ txt: "text/plain; charset=utf-8",
21
+ xml: "application/xml; charset=utf-8",
22
+ svg: "image/svg+xml",
23
+ png: "image/png",
24
+ jpg: "image/jpeg",
25
+ jpeg: "image/jpeg",
26
+ webp: "image/webp",
27
+ avif: "image/avif",
28
+ gif: "image/gif",
29
+ ico: "image/x-icon",
30
+ woff: "font/woff",
31
+ woff2: "font/woff2",
32
+ map: "application/json; charset=utf-8"
33
+ };
34
+ function contentTypeFor(path) {
35
+ const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
36
+ return CONTENT_TYPES[ext] ?? "application/octet-stream";
37
+ }
38
+
39
+ // ../engine/src/content/item.ts
40
+ var ContentParseError = class extends Error {
41
+ constructor(message) {
42
+ super(message);
43
+ this.name = "ContentParseError";
44
+ }
45
+ };
46
+ function isPlainObject(value) {
47
+ return typeof value === "object" && value !== null && !Array.isArray(value);
48
+ }
49
+ function isFieldPrimitive(value) {
50
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
51
+ }
52
+ function isFlatRecord(value) {
53
+ return isPlainObject(value) && Object.values(value).every(isFieldPrimitive);
54
+ }
55
+ function parseFieldValue(key, value) {
56
+ if (isFieldPrimitive(value)) return value;
57
+ if (Array.isArray(value)) {
58
+ if (value.every(isFieldPrimitive)) return value;
59
+ if (value.every(isFlatRecord)) return value;
60
+ throw new ContentParseError(
61
+ `Field "${key}" array must hold only primitives or only flat records (no nesting).`
62
+ );
63
+ }
64
+ throw new ContentParseError(`Field "${key}" must be a primitive or an array, not a bare object.`);
65
+ }
66
+ function parseOptionalString(raw, key) {
67
+ const value = raw[key];
68
+ if (value === void 0) return void 0;
69
+ if (typeof value !== "string") {
70
+ throw new ContentParseError(`"${key}" must be a string when present.`);
71
+ }
72
+ return value;
73
+ }
74
+ function parseContentItem(raw) {
75
+ if (!isPlainObject(raw)) {
76
+ throw new ContentParseError("A content item must be a JSON object.");
77
+ }
78
+ if (typeof raw.title !== "string") {
79
+ throw new ContentParseError('A content item requires a string "title".');
80
+ }
81
+ let isPublished = false;
82
+ if (raw.isPublished !== void 0) {
83
+ if (typeof raw.isPublished !== "boolean") {
84
+ throw new ContentParseError(
85
+ `"isPublished" must be a boolean when present, got ${JSON.stringify(raw.isPublished)}.`
86
+ );
87
+ }
88
+ isPublished = raw.isPublished;
89
+ }
90
+ let fields = {};
91
+ if (raw.fields !== void 0) {
92
+ if (!isPlainObject(raw.fields)) {
93
+ throw new ContentParseError('"fields" must be an object.');
94
+ }
95
+ fields = Object.fromEntries(
96
+ Object.entries(raw.fields).map(([key, value]) => [key, parseFieldValue(key, value)])
97
+ );
98
+ }
99
+ const template = parseOptionalString(raw, "template");
100
+ const created = parseOptionalString(raw, "created");
101
+ const published = parseOptionalString(raw, "published");
102
+ const updated = parseOptionalString(raw, "updated");
103
+ return {
104
+ title: raw.title,
105
+ isPublished,
106
+ fields,
107
+ ...template !== void 0 && { template },
108
+ ...created !== void 0 && { created },
109
+ ...published !== void 0 && { published },
110
+ ...updated !== void 0 && { updated }
111
+ };
112
+ }
113
+
114
+ // ../engine/src/content/sort.ts
115
+ function isPlainObject2(value) {
116
+ return typeof value === "object" && value !== null && !Array.isArray(value);
117
+ }
118
+ function parseSortFile(raw) {
119
+ if (!isPlainObject2(raw)) return { order: [] };
120
+ const order = Array.isArray(raw.order) ? raw.order.filter((entry) => typeof entry === "string") : [];
121
+ const name = typeof raw.name === "string" ? raw.name : void 0;
122
+ const defaultTemplate = typeof raw.defaultTemplate === "string" ? raw.defaultTemplate : void 0;
123
+ const collection = isPlainObject2(raw.collection) ? { recursive: raw.collection.recursive === true } : void 0;
124
+ return {
125
+ order,
126
+ ...name !== void 0 && { name },
127
+ ...defaultTemplate !== void 0 && { defaultTemplate },
128
+ ...collection !== void 0 && { collection }
129
+ };
130
+ }
131
+
132
+ // ../engine/src/store/blob-store.ts
133
+ var InMemoryBlobStore = class {
134
+ blobs = /* @__PURE__ */ new Map();
135
+ constructor(initial) {
136
+ if (initial === void 0) return;
137
+ const encoder = new TextEncoder();
138
+ for (const [key, value] of Object.entries(initial)) {
139
+ this.blobs.set(key, copy(typeof value === "string" ? encoder.encode(value) : value));
140
+ }
141
+ }
142
+ async get(key) {
143
+ const value = this.blobs.get(key);
144
+ return value === void 0 ? void 0 : copy(value);
145
+ }
146
+ async put(key, bytes) {
147
+ this.blobs.set(key, copy(bytes));
148
+ }
149
+ async delete(key) {
150
+ this.blobs.delete(key);
151
+ }
152
+ };
153
+ function copy(bytes) {
154
+ return bytes.slice();
155
+ }
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}`;
163
+ }
164
+ function emptyIndex() {
165
+ return freezeIndex(0, []);
166
+ }
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();
178
+ }
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;
184
+ }
185
+ function freezeIndex(version, keys) {
186
+ const sorted = [...new Set(keys)].sort();
187
+ return { version, keys: sorted, checksum: checksumOf(version, sorted) };
188
+ }
189
+ function serialize(index) {
190
+ return new TextEncoder().encode(`${JSON.stringify(index, null, 2)}
191
+ `);
192
+ }
193
+ function parseIndex(bytes) {
194
+ if (bytes === void 0) return void 0;
195
+ let raw;
196
+ try {
197
+ raw = JSON.parse(new TextDecoder().decode(bytes));
198
+ } catch {
199
+ return void 0;
200
+ }
201
+ if (typeof raw !== "object" || raw === null) return void 0;
202
+ const { version, keys, checksum } = raw;
203
+ if (typeof version !== "number" || typeof checksum !== "string" || !Array.isArray(keys)) {
204
+ return void 0;
205
+ }
206
+ const stringKeys = keys.filter((key) => typeof key === "string");
207
+ if (stringKeys.length !== keys.length) return void 0;
208
+ const rebuilt = freezeIndex(version, stringKeys);
209
+ return rebuilt.checksum === checksum ? rebuilt : void 0;
210
+ }
211
+ function checksumOf(version, sortedKeys) {
212
+ const text = `${version}
213
+ ${sortedKeys.join("\n")}`;
214
+ let hash = 2166136261;
215
+ for (let i = 0; i < text.length; i += 1) {
216
+ hash ^= text.charCodeAt(i);
217
+ hash = Math.imul(hash, 16777619);
218
+ }
219
+ return (hash >>> 0).toString(16).padStart(8, "0");
220
+ }
221
+
222
+ // ../engine/src/store/indexed-store.ts
223
+ var IndexedStore = class {
224
+ constructor(store) {
225
+ this.store = store;
226
+ }
227
+ store;
228
+ index;
229
+ loading;
230
+ /** The index, loaded once on first need and cached (mutations replace the cached copy). */
231
+ async loaded() {
232
+ if (this.index !== void 0) return this.index;
233
+ this.loading ??= loadIndex(this.store);
234
+ this.index = await this.loading;
235
+ return this.index;
236
+ }
237
+ async list(dir) {
238
+ return childrenOf((await this.loaded()).keys, dir);
239
+ }
240
+ async readText(path) {
241
+ return new TextDecoder().decode(await this.readBytes(path));
242
+ }
243
+ async readBytes(path) {
244
+ const bytes = await this.store.get(path);
245
+ if (bytes === void 0) throw new Error(`No such file in content source: ${path}`);
246
+ return bytes;
247
+ }
248
+ async exists(path) {
249
+ return pathExists((await this.loaded()).keys, path);
250
+ }
251
+ /**
252
+ * Create or overwrite `key`. The index is rewritten only when `key` is new (an
253
+ * overwrite leaves the key set unchanged, so editing an item is a single `put`).
254
+ */
255
+ async write(key, bytes) {
256
+ const target = guarded(normalize(key));
257
+ await this.store.put(target, bytes);
258
+ const index = await this.loaded();
259
+ if (!index.keys.includes(target)) {
260
+ this.index = await saveIndex(this.store, index, [...index.keys, target]);
261
+ }
262
+ }
263
+ /**
264
+ * Delete a file, or a whole directory subtree (every key under `key/`). Idempotent: a
265
+ * path the index does not know is still deleted from the store (clearing an orphan),
266
+ * and deleting nothing is a no-op.
267
+ */
268
+ async delete(key) {
269
+ const target = guarded(normalize(key));
270
+ const index = await this.loaded();
271
+ const prefix = `${target}/`;
272
+ const removed = index.keys.filter((k) => k === target || k.startsWith(prefix));
273
+ if (removed.length === 0) {
274
+ await this.store.delete(target);
275
+ return;
276
+ }
277
+ await Promise.all(removed.map((k) => this.store.delete(k)));
278
+ const remaining = index.keys.filter((k) => k !== target && !k.startsWith(prefix));
279
+ this.index = await saveIndex(this.store, index, remaining);
280
+ }
281
+ /**
282
+ * Move/rename a file, or a whole directory subtree (every key under `from/` is remapped
283
+ * under `to/`). Clobbering an existing destination, or renaming a missing path, are
284
+ * returned as data (`ok: false`), not thrown (CLAUDE §2), the same contract the host's
285
+ * `/api/rename` enforces. (Mutating the reserved namespace is a programmer error and
286
+ * still throws.)
287
+ */
288
+ async rename(from, to) {
289
+ const source = guarded(normalize(from));
290
+ const dest = guarded(normalize(to));
291
+ if (source === dest) return { ok: true };
292
+ const index = await this.loaded();
293
+ const sourcePrefix = `${source}/`;
294
+ const affected = index.keys.filter((k) => k === source || k.startsWith(sourcePrefix));
295
+ if (affected.length === 0) return { ok: false, reason: "missing" };
296
+ const destPrefix = `${dest}/`;
297
+ if (index.keys.some((k) => k === dest || k.startsWith(destPrefix))) {
298
+ return { ok: false, reason: "exists" };
299
+ }
300
+ const moves = affected.map((k) => ({
301
+ from: k,
302
+ to: k === source ? dest : dest + k.slice(source.length)
303
+ }));
304
+ for (const move of moves) {
305
+ const bytes = await this.store.get(move.from);
306
+ if (bytes === void 0) continue;
307
+ await this.store.put(move.to, bytes);
308
+ await this.store.delete(move.from);
309
+ }
310
+ const movedFrom = new Set(moves.map((move) => move.from));
311
+ const next = index.keys.filter((k) => !movedFrom.has(k)).concat(moves.map((m) => m.to));
312
+ this.index = await saveIndex(this.store, index, next);
313
+ return { ok: true };
314
+ }
315
+ };
316
+ function guarded(key) {
317
+ if (key === "" || key.startsWith(RESERVED_PREFIX)) {
318
+ throw new Error(`Refusing to mutate a reserved key: ${key === "" ? "(root)" : key}`);
319
+ }
320
+ return key;
321
+ }
322
+ function normalize(path) {
323
+ return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
324
+ }
325
+ function childrenOf(keys, dir) {
326
+ const base = normalize(dir);
327
+ const prefix = base === "" ? "" : `${base}/`;
328
+ const entries = /* @__PURE__ */ new Map();
329
+ for (const key of keys) {
330
+ if (prefix !== "" && !key.startsWith(prefix)) continue;
331
+ const rest = key.slice(prefix.length);
332
+ if (rest === "") continue;
333
+ const slash = rest.indexOf("/");
334
+ if (slash === -1) entries.set(rest, "file");
335
+ else entries.set(rest.slice(0, slash), "dir");
336
+ }
337
+ return [...entries].map(([name, kind]) => ({ name, kind }));
338
+ }
339
+ function pathExists(keys, path) {
340
+ const target = normalize(path);
341
+ if (target === "") return true;
342
+ if (keys.includes(target)) return true;
343
+ const prefix = `${target}/`;
344
+ return keys.some((key) => key.startsWith(prefix));
345
+ }
346
+
347
+ // ../engine/src/content/source.ts
348
+ function normalizePath(path) {
349
+ return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
350
+ }
351
+ var InMemoryContentSource = class {
352
+ files;
353
+ constructor(files) {
354
+ this.files = new Map(Object.entries(files).map(([key, value]) => [normalizePath(key), value]));
355
+ }
356
+ async readText(path) {
357
+ const value = this.files.get(normalizePath(path));
358
+ if (value === void 0) throw new Error(`No such file in content source: ${path}`);
359
+ return typeof value === "string" ? value : new TextDecoder().decode(value);
360
+ }
361
+ async readBytes(path) {
362
+ const value = this.files.get(normalizePath(path));
363
+ if (value === void 0) throw new Error(`No such file in content source: ${path}`);
364
+ return typeof value === "string" ? new TextEncoder().encode(value) : value;
365
+ }
366
+ async exists(path) {
367
+ const target = normalizePath(path);
368
+ if (target === "") return true;
369
+ if (this.files.has(target)) return true;
370
+ const prefix = `${target}/`;
371
+ for (const key of this.files.keys()) {
372
+ if (key.startsWith(prefix)) return true;
373
+ }
374
+ return false;
375
+ }
376
+ async list(dir) {
377
+ const base = normalizePath(dir);
378
+ const prefix = base === "" ? "" : `${base}/`;
379
+ const entries = /* @__PURE__ */ new Map();
380
+ for (const key of this.files.keys()) {
381
+ if (prefix !== "" && !key.startsWith(prefix)) continue;
382
+ const rest = key.slice(prefix.length);
383
+ if (rest === "") continue;
384
+ const slash = rest.indexOf("/");
385
+ if (slash === -1) entries.set(rest, "file");
386
+ else entries.set(rest.slice(0, slash), "dir");
387
+ }
388
+ return [...entries].map(([name, kind]) => ({ name, kind }));
389
+ }
390
+ };
391
+
392
+ // ../engine/src/url/redirects.ts
393
+ var DEFAULT_STATUS = 301;
394
+ var OUTPUT_PATH = "_redirects";
395
+ function isRule(value) {
396
+ if (typeof value !== "object" || value === null) return false;
397
+ const record = value;
398
+ return typeof record.from === "string" && typeof record.to === "string";
399
+ }
400
+ function parseRedirects(raw) {
401
+ if (!Array.isArray(raw)) return [];
402
+ const rules = [];
403
+ for (const entry of raw) {
404
+ if (!isRule(entry)) continue;
405
+ const status = entry.status;
406
+ rules.push(
407
+ typeof status === "number" && Number.isInteger(status) ? { from: entry.from, to: entry.to, status } : { from: entry.from, to: entry.to }
408
+ );
409
+ }
410
+ return rules;
411
+ }
412
+ function resolveTarget(from, exact) {
413
+ const seen = /* @__PURE__ */ new Set([from]);
414
+ let target = exact.get(from).to;
415
+ while (exact.has(target) && !seen.has(target)) {
416
+ seen.add(target);
417
+ target = exact.get(target).to;
418
+ }
419
+ return target;
420
+ }
421
+ function buildRedirects(rules, liveUrls) {
422
+ const exact = /* @__PURE__ */ new Map();
423
+ const wildcard = /* @__PURE__ */ new Map();
424
+ for (const rule of rules) {
425
+ (rule.from.includes("*") ? wildcard : exact).set(rule.from, rule);
426
+ }
427
+ const resolved = [];
428
+ for (const [from, rule] of exact) {
429
+ if (liveUrls.has(from)) continue;
430
+ const to = resolveTarget(from, exact);
431
+ if (to === from) continue;
432
+ resolved.push(rule.status !== void 0 ? { from, to, status: rule.status } : { from, to });
433
+ }
434
+ resolved.push(...wildcard.values());
435
+ if (resolved.length === 0) return void 0;
436
+ resolved.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
437
+ const contents = resolved.map((rule) => `${rule.from} ${rule.to} ${rule.status ?? DEFAULT_STATUS}
438
+ `).join("");
439
+ return { path: OUTPUT_PATH, contents };
440
+ }
441
+
442
+ // ../engine/src/content/loader.ts
443
+ var SORT_FILE = "_sort.json";
444
+ var REDIRECTS_FILE = "_redirects.json";
445
+ var SITE_CONFIG_FILE = "_site.json";
446
+ var ASSETS_DIR = "assets";
447
+ var ITEM_EXT = ".json";
448
+ var DEFAULT_DIRS = {
449
+ content: "content",
450
+ templates: "templates",
451
+ components: "components",
452
+ /** Static passthrough, copied verbatim to the published root (DESIGN §8). The
453
+ * engine never reads it; named here so hosts/editor share one source of truth. */
454
+ public: "public"
455
+ };
456
+ function join(...parts) {
457
+ return parts.filter((part) => part !== "").join("/");
458
+ }
459
+ function stripBom(text) {
460
+ return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
461
+ }
462
+ async function loadContentTree(source, contentDir = DEFAULT_DIRS.content) {
463
+ return loadDir(source, contentDir, "", "");
464
+ }
465
+ async function loadDir(source, dirPath, slug, treePath) {
466
+ const [entries, sort] = await Promise.all([source.list(dirPath), readSort(source, dirPath)]);
467
+ const childMap = /* @__PURE__ */ new Map();
468
+ for (const entry of entries) {
469
+ if (entry.kind === "file") {
470
+ if (entry.name === SORT_FILE || entry.name === REDIRECTS_FILE || entry.name === SITE_CONFIG_FILE || !entry.name.endsWith(ITEM_EXT))
471
+ continue;
472
+ const childSlug = entry.name.slice(0, -ITEM_EXT.length);
473
+ childMap.set(
474
+ childSlug,
475
+ await loadItem(source, join(dirPath, entry.name), childSlug, treePath)
476
+ );
477
+ } else {
478
+ if (entry.name === ASSETS_DIR) continue;
479
+ const childPath = join(treePath, entry.name);
480
+ childMap.set(
481
+ entry.name,
482
+ await loadDir(source, join(dirPath, entry.name), entry.name, childPath)
483
+ );
484
+ }
485
+ }
486
+ const name = sort.name ?? (slug === "" ? "" : humanize(slug));
487
+ return {
488
+ kind: "dir",
489
+ slug,
490
+ name,
491
+ path: treePath,
492
+ children: orderChildren(childMap, sort.order),
493
+ ...sort.collection !== void 0 && { collection: sort.collection },
494
+ ...sort.defaultTemplate !== void 0 && { defaultTemplate: sort.defaultTemplate }
495
+ };
496
+ }
497
+ async function loadItem(source, filePath, slug, parentPath2) {
498
+ const raw = await source.readText(filePath);
499
+ try {
500
+ const item = parseContentItem(JSON.parse(raw));
501
+ return { kind: "item", slug, path: join(parentPath2, slug), item };
502
+ } catch (error) {
503
+ throw new Error(`Failed to load ${filePath}: ${error.message}`);
504
+ }
505
+ }
506
+ async function readSort(source, dirPath) {
507
+ const path = join(dirPath, SORT_FILE);
508
+ if (!await source.exists(path)) return { order: [] };
509
+ try {
510
+ return parseSortFile(JSON.parse(await source.readText(path)));
511
+ } catch {
512
+ return { order: [] };
513
+ }
514
+ }
515
+ async function loadRedirects(source, contentDir = DEFAULT_DIRS.content) {
516
+ const path = join(contentDir, REDIRECTS_FILE);
517
+ if (!await source.exists(path)) return [];
518
+ try {
519
+ return parseRedirects(JSON.parse(stripBom(await source.readText(path))));
520
+ } catch {
521
+ return [];
522
+ }
523
+ }
524
+ async function loadSiteConfig(source, contentDir = DEFAULT_DIRS.content) {
525
+ const path = join(contentDir, SITE_CONFIG_FILE);
526
+ if (!await source.exists(path)) return {};
527
+ try {
528
+ const raw = JSON.parse(stripBom(await source.readText(path)));
529
+ const baseUrl = typeof raw.baseUrl === "string" ? raw.baseUrl.trim() : "";
530
+ return baseUrl !== "" ? { baseUrl } : {};
531
+ } catch {
532
+ return {};
533
+ }
534
+ }
535
+ function orderChildren(map, order) {
536
+ const out = [];
537
+ const used = /* @__PURE__ */ new Set();
538
+ for (const slug of order) {
539
+ const node = map.get(slug);
540
+ if (node !== void 0) {
541
+ out.push(node);
542
+ used.add(slug);
543
+ }
544
+ }
545
+ for (const [slug, node] of map) {
546
+ if (!used.has(slug)) out.push(node);
547
+ }
548
+ return out;
549
+ }
550
+ function loadComponents(source, componentsDir = DEFAULT_DIRS.components) {
551
+ return loadComponentFiles(source, componentsDir, "html");
552
+ }
553
+ function loadComponentStyles(source, componentsDir = DEFAULT_DIRS.components) {
554
+ return loadComponentFiles(source, componentsDir, "css");
555
+ }
556
+ function loadComponentScripts(source, componentsDir = DEFAULT_DIRS.components) {
557
+ return loadComponentFiles(source, componentsDir, "js");
558
+ }
559
+ async function loadComponentFiles(source, componentsDir, extension) {
560
+ const suffix = `.${extension}`;
561
+ const map = /* @__PURE__ */ new Map();
562
+ const walk = async (dir) => {
563
+ if (!await source.exists(dir)) return;
564
+ for (const entry of await source.list(dir)) {
565
+ const full = join(dir, entry.name);
566
+ if (entry.kind === "dir") {
567
+ await walk(full);
568
+ } else if (entry.name.endsWith(suffix)) {
569
+ const key = entry.name.slice(0, -suffix.length).toLowerCase();
570
+ map.set(key, await source.readText(full));
571
+ }
572
+ }
573
+ };
574
+ await walk(componentsDir);
575
+ return map;
576
+ }
577
+ async function loadTemplate(source, name, templatesDir = DEFAULT_DIRS.templates) {
578
+ return source.readText(join(templatesDir, `${name}.html`));
579
+ }
580
+ function loadTemplateStyle(source, name, templatesDir = DEFAULT_DIRS.templates) {
581
+ return tryReadText(source, join(templatesDir, `${name}.css`));
582
+ }
583
+ function loadTemplateScript(source, name, templatesDir = DEFAULT_DIRS.templates) {
584
+ return tryReadText(source, join(templatesDir, `${name}.js`));
585
+ }
586
+ var DOCUMENT_SHELL = "document";
587
+ function loadDocumentShell(source, templatesDir = DEFAULT_DIRS.templates) {
588
+ return tryReadText(source, join(templatesDir, `${DOCUMENT_SHELL}.html`));
589
+ }
590
+ async function tryReadText(source, path) {
591
+ try {
592
+ return await source.readText(path);
593
+ } catch {
594
+ return void 0;
595
+ }
596
+ }
597
+
598
+ // ../engine/src/template/registry.ts
599
+ var FIELD_TYPES = {
600
+ image: { type: "image", valueKind: "asset", control: "image", multiline: false },
601
+ file: { type: "file", valueKind: "asset", control: "file", multiline: false },
602
+ url: { type: "url", valueKind: "url", control: "url", multiline: false },
603
+ email: { type: "email", valueKind: "text", control: "email", multiline: false },
604
+ phone: { type: "phone", valueKind: "text", control: "phone", multiline: false },
605
+ date: { type: "date", valueKind: "text", control: "date", multiline: false },
606
+ time: { type: "time", valueKind: "text", control: "time", multiline: false },
607
+ code: { type: "code", valueKind: "text", control: "code", multiline: true },
608
+ richtext: { type: "richtext", valueKind: "html", control: "richtext", multiline: true },
609
+ authors: { type: "authors", valueKind: "authors", control: "authors", multiline: false },
610
+ text: { type: "text", valueKind: "text", control: "text", multiline: true },
611
+ shorttext: { type: "shorttext", valueKind: "text", control: "shorttext", multiline: false }
612
+ };
613
+ function isFieldType(value) {
614
+ return Object.prototype.hasOwnProperty.call(FIELD_TYPES, value);
615
+ }
616
+ function valueKindOf(type) {
617
+ return FIELD_TYPES[type].valueKind;
618
+ }
619
+
620
+ // ../engine/src/template/inference.ts
621
+ var SHORT_TEXT_ELEMENTS = /* @__PURE__ */ new Set([
622
+ "h1",
623
+ "h2",
624
+ "h3",
625
+ "h4",
626
+ "h5",
627
+ "h6",
628
+ "title",
629
+ "label",
630
+ "th",
631
+ "caption",
632
+ "figcaption",
633
+ "legend",
634
+ "summary",
635
+ "option",
636
+ "dt",
637
+ "button"
638
+ ]);
639
+ var CODE_ELEMENTS = /* @__PURE__ */ new Set(["code", "pre", "kbd", "samp"]);
640
+ var PLAIN_TEXT_ELEMENTS = /* @__PURE__ */ new Set([
641
+ "p",
642
+ "span",
643
+ "li",
644
+ "td",
645
+ "dd",
646
+ "strong",
647
+ "em",
648
+ "b",
649
+ "i",
650
+ "small",
651
+ "time",
652
+ "a",
653
+ "figcaption",
654
+ "div",
655
+ "article",
656
+ "section",
657
+ "main",
658
+ "aside",
659
+ "blockquote"
660
+ ]);
661
+ function inferControl(ctx) {
662
+ if (ctx.annotation !== void 0 && isFieldType(ctx.annotation)) {
663
+ return ctx.annotation;
664
+ }
665
+ if (ctx.attribute !== void 0) {
666
+ const attr = ctx.attribute;
667
+ const prefix = (ctx.valuePrefix ?? "").toLowerCase();
668
+ if (attr === "href") {
669
+ if (prefix.endsWith("mailto:")) return "email";
670
+ if (prefix.endsWith("tel:")) return "phone";
671
+ if (ctx.wholeValue) return ctx.download === true ? "file" : "url";
672
+ }
673
+ if (ctx.wholeValue && attr === "src") {
674
+ if (ctx.tag === "img") return "image";
675
+ return "file";
676
+ }
677
+ if (ctx.wholeValue && ctx.tag === "source" && attr === "srcset") return "file";
678
+ if (attr === "alt" || attr === "title") return "shorttext";
679
+ return "shorttext";
680
+ }
681
+ if (CODE_ELEMENTS.has(ctx.tag)) return "code";
682
+ if (SHORT_TEXT_ELEMENTS.has(ctx.tag)) return "shorttext";
683
+ if (PLAIN_TEXT_ELEMENTS.has(ctx.tag)) return "text";
684
+ return "shorttext";
685
+ }
686
+
687
+ // ../engine/src/template/token.ts
688
+ var TOKEN = /\{([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\}/g;
689
+ function findTokens(input) {
690
+ const out = [];
691
+ for (const match of input.matchAll(TOKEN)) {
692
+ const expr = match[1] ?? "";
693
+ const path = expr.split(".");
694
+ out.push({ raw: match[0], name: path[0] ?? "", path, dotted: path.length > 1 });
695
+ }
696
+ return out;
697
+ }
698
+ function wholeValueToken(input) {
699
+ const trimmed = input.trim();
700
+ const tokens = findTokens(trimmed);
701
+ if (tokens.length === 1 && tokens[0]?.raw === trimmed) return tokens[0];
702
+ return null;
703
+ }
704
+ function substituteTokens(input, resolve) {
705
+ return input.replace(/\{([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\}/g, (raw, expr) => {
706
+ const path = expr.split(".");
707
+ return resolve({ raw, name: path[0] ?? "", path, dotted: path.length > 1 });
708
+ });
709
+ }
710
+ function literalPrefix(input) {
711
+ const brace = input.indexOf("{");
712
+ return (brace === -1 ? input : input.slice(0, brace)).toLowerCase();
713
+ }
714
+
715
+ // ../engine/src/html/escape.ts
716
+ function escapeHtmlText(value) {
717
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
718
+ }
719
+ function escapeHtmlAttribute(value) {
720
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
721
+ }
722
+ function escapeJsonStringContent(value) {
723
+ const literal = JSON.stringify(value);
724
+ return literal.slice(1, -1);
725
+ }
726
+ function sanitizeUrl(url) {
727
+ let cleaned = "";
728
+ for (const ch of url) {
729
+ const code = ch.codePointAt(0);
730
+ if (code !== void 0 && code > 32) cleaned += ch;
731
+ }
732
+ const scheme = cleaned.toLowerCase();
733
+ if (scheme.startsWith("javascript:") || scheme.startsWith("vbscript:")) {
734
+ return "#";
735
+ }
736
+ return url;
737
+ }
738
+
739
+ // ../engine/src/authors/authors.ts
740
+ function toAuthorRefs(value) {
741
+ if (!Array.isArray(value)) return [];
742
+ const refs = [];
743
+ for (const entry of value) {
744
+ if (entry === null || typeof entry !== "object") continue;
745
+ const record = entry;
746
+ const name = typeof record.name === "string" ? record.name : "";
747
+ if (name.trim() === "") continue;
748
+ refs.push({
749
+ name,
750
+ ...typeof record.user === "string" && record.user !== "" && { user: record.user },
751
+ ...typeof record.href === "string" && record.href !== "" && { href: record.href }
752
+ });
753
+ }
754
+ return refs;
755
+ }
756
+ function joinAuthors(names) {
757
+ if (names.length === 0) return "";
758
+ if (names.length === 1) return names[0] ?? "";
759
+ const last = names[names.length - 1] ?? "";
760
+ const head = names.slice(0, -1).join(", ");
761
+ return `${head} and ${last}`;
762
+ }
763
+ function renderAuthors(value, directory) {
764
+ const names = toAuthorRefs(value).map((ref) => {
765
+ const resolved = ref.user !== void 0 ? directory?.(ref.user) : void 0;
766
+ return escapeHtmlText(resolved?.displayName ?? ref.name);
767
+ });
768
+ return joinAuthors(names);
769
+ }
770
+
771
+ // ../engine/src/html/dom.ts
772
+ import { parse, parseFragment, serialize as serialize2 } from "parse5";
773
+ function isElement(node) {
774
+ return "tagName" in node;
775
+ }
776
+ function isTextNode(node) {
777
+ return node.nodeName === "#text";
778
+ }
779
+ function getAttribute(el, name) {
780
+ return el.attrs.find((attr) => attr.name === name)?.value;
781
+ }
782
+ function hasAttribute(el, name) {
783
+ return el.attrs.some((attr) => attr.name === name);
784
+ }
785
+ function setAttribute(el, name, value) {
786
+ const existing = el.attrs.find((attr) => attr.name === name);
787
+ if (existing) existing.value = value;
788
+ else el.attrs.push({ name, value });
789
+ }
790
+ function removeAttribute(el, name) {
791
+ el.attrs = el.attrs.filter((attr) => attr.name !== name);
792
+ }
793
+ function parseFragmentNodes(html) {
794
+ return parseFragment(html).childNodes;
795
+ }
796
+ function isFullDocument(html) {
797
+ const start = html.trimStart().toLowerCase();
798
+ return start.startsWith("<!doctype") || start.startsWith("<html");
799
+ }
800
+ function parseNodes(html) {
801
+ return isFullDocument(html) ? parse(html).childNodes : parseFragment(html).childNodes;
802
+ }
803
+ function serializeNodes(nodes) {
804
+ const fragment = parseFragment("");
805
+ fragment.childNodes = nodes;
806
+ return serialize2(fragment);
807
+ }
808
+
809
+ // ../engine/src/media/media.ts
810
+ var DEFAULT_WIDTHS = [400, 800, 1200, 1600];
811
+ var MIME = {
812
+ avif: "image/avif",
813
+ webp: "image/webp",
814
+ jpeg: "image/jpeg",
815
+ png: "image/png"
816
+ };
817
+ function extOf(format) {
818
+ return format === "jpeg" ? "jpg" : format;
819
+ }
820
+ function fallbackFormatFor(assetPath) {
821
+ const ext = assetPath.slice(assetPath.lastIndexOf(".") + 1).toLowerCase();
822
+ if (ext === "png") return "png";
823
+ if (ext === "webp") return "webp";
824
+ return "jpeg";
825
+ }
826
+ function contentHash(bytes) {
827
+ let hash = 2166136261;
828
+ for (let i = 0; i < bytes.length; i += 1) {
829
+ hash ^= bytes[i] ?? 0;
830
+ hash = Math.imul(hash, 16777619);
831
+ }
832
+ return (hash >>> 0).toString(36);
833
+ }
834
+ async function processImage(input, assetPath, encoder) {
835
+ const fallbackFormat = fallbackFormatFor(assetPath);
836
+ const formats = [.../* @__PURE__ */ new Set(["avif", "webp", fallbackFormat])];
837
+ const encoded = await encoder.encode(input, { widths: DEFAULT_WIDTHS, formats });
838
+ const hash = contentHash(input);
839
+ const base = assetPath.slice(
840
+ 0,
841
+ assetPath.lastIndexOf(".") >= 0 ? assetPath.lastIndexOf(".") : assetPath.length
842
+ );
843
+ const artifacts = [];
844
+ const urlOf = (variant) => {
845
+ const path = `${base}.${hash}-${variant.width}.${extOf(variant.format)}`;
846
+ artifacts.push({ path, contents: variant.bytes });
847
+ return `/${path}`;
848
+ };
849
+ const byFormat = /* @__PURE__ */ new Map();
850
+ for (const variant of [...encoded.variants].sort((a, b) => a.width - b.width)) {
851
+ const url = urlOf(variant);
852
+ const list = byFormat.get(variant.format) ?? [];
853
+ list.push({ url, width: variant.width });
854
+ byFormat.set(variant.format, list);
855
+ }
856
+ const srcsetOf = (format) => (byFormat.get(format) ?? []).map((entry) => `${entry.url} ${entry.width}w`).join(", ");
857
+ const sources = ["avif", "webp"].filter((format) => byFormat.has(format)).map((format) => ({ format, mime: MIME[format], srcset: srcsetOf(format) }));
858
+ const fallbackList = byFormat.get(fallbackFormat) ?? [];
859
+ const fallbackSrc = fallbackList[fallbackList.length - 1]?.url ?? `/${assetPath}`;
860
+ const info = {
861
+ width: encoded.sourceWidth,
862
+ height: encoded.sourceHeight,
863
+ sources,
864
+ fallbackSrc,
865
+ ...encoded.blurDataUri !== void 0 && { blurDataUri: encoded.blurDataUri }
866
+ };
867
+ return { artifacts, info };
868
+ }
869
+ function buildPictureMarkup(info, extraAttrs) {
870
+ const sources = info.sources.map(
871
+ (source) => `<source type="${source.mime}" srcset="${escapeHtmlAttribute(source.srcset)}">`
872
+ ).join("");
873
+ const carried = extraAttrs.filter(([name]) => name !== "src" && name !== "width" && name !== "height").map(([name, value]) => ` ${name}="${escapeHtmlAttribute(value)}"`).join("");
874
+ const img = `<img src="${escapeHtmlAttribute(info.fallbackSrc)}" width="${info.width}" height="${info.height}" loading="lazy" decoding="async"${carried}>`;
875
+ return `<picture>${sources}${img}</picture>`;
876
+ }
877
+
878
+ // ../engine/src/url/references.ts
879
+ var REFERENCE_PREFIX = "ref:";
880
+ function referenceTarget(value) {
881
+ return value.startsWith(REFERENCE_PREFIX) ? value.slice(REFERENCE_PREFIX.length) : void 0;
882
+ }
883
+ function collectHtmlReferences(html) {
884
+ const found = /* @__PURE__ */ new Set();
885
+ const visit = (nodes) => {
886
+ for (const node of nodes) {
887
+ if (!isElement(node)) continue;
888
+ for (const attr of node.attrs) {
889
+ const target = referenceTarget(attr.value);
890
+ if (target !== void 0) found.add(target);
891
+ }
892
+ visit(node.childNodes);
893
+ }
894
+ };
895
+ visit(parseNodes(html));
896
+ return [...found].sort();
897
+ }
898
+
899
+ // ../engine/src/compile/compiler.ts
900
+ var LOOP_ATTRS = ["data-each-in", "data-each", "data-sort", "data-limit", "data-pick-from"];
901
+ var AUTHORING_ATTRS = ["data-type", "data-field", "data-required", "data-help", "data-label"];
902
+ function compileTemplate(input) {
903
+ const ctx = {
904
+ components: input.components,
905
+ usedComponents: /* @__PURE__ */ new Set(),
906
+ ...input.context !== void 0 && { context: input.context },
907
+ ...input.media !== void 0 && { media: input.media },
908
+ ...input.authorDirectory !== void 0 && { authorDirectory: input.authorDirectory },
909
+ ...input.componentStyles !== void 0 && { componentStyles: input.componentStyles }
910
+ };
911
+ const body = transformNodes(parseNodes(input.template), input.scope, ctx);
912
+ const out = input.document === void 0 ? body : fillSlots(transformNodes(parseNodes(input.document), input.scope, ctx), body);
913
+ injectPageAssets(out, input, ctx);
914
+ return serializeNodes(out);
915
+ }
916
+ function injectPageAssets(nodes, input, ctx) {
917
+ const style = input.templateStyle?.trim();
918
+ if (style) {
919
+ const styleNode = parseFragmentNodes(`<style>
920
+ ${style}
921
+ </style>`);
922
+ const head = findElement(nodes, "head");
923
+ if (head) head.childNodes.push(...styleNode);
924
+ else nodes.unshift(...styleNode);
925
+ }
926
+ const scripts = [];
927
+ const templateScript = input.templateScript?.trim();
928
+ if (templateScript) scripts.push(templateScript);
929
+ for (const tag of [...ctx.usedComponents].sort()) {
930
+ const js = input.componentScripts?.get(tag)?.trim();
931
+ if (js) scripts.push(js);
932
+ }
933
+ if (scripts.length > 0) {
934
+ const scriptNodes = scripts.flatMap((js) => parseFragmentNodes(`<script>
935
+ ${js}
936
+ </script>`));
937
+ const body = findElement(nodes, "body");
938
+ if (body) body.childNodes.push(...scriptNodes);
939
+ else nodes.push(...scriptNodes);
940
+ }
941
+ }
942
+ function findElement(nodes, tagName) {
943
+ for (const node of nodes) {
944
+ if (!isElement(node)) continue;
945
+ if (node.tagName === tagName) return node;
946
+ const found = findElement(node.childNodes, tagName);
947
+ if (found !== void 0) return found;
948
+ }
949
+ return void 0;
950
+ }
951
+ function isBoundItem(value) {
952
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "url" in value;
953
+ }
954
+ function resolveToString(value) {
955
+ if (value === void 0 || value === null) return "";
956
+ if (Array.isArray(value)) {
957
+ return value.map((entry) => entry === null ? "" : String(entry)).join(", ");
958
+ }
959
+ return String(value);
960
+ }
961
+ function scopeResolver(scope) {
962
+ return (token) => {
963
+ const head = scope[token.name];
964
+ if (isBoundItem(head)) {
965
+ if (token.path.length === 1) return head.url;
966
+ const field = head.fields[token.path[1] ?? ""];
967
+ return isBoundItem(field) ? field.url : resolveToString(field);
968
+ }
969
+ if (token.dotted) return "";
970
+ return resolveToString(head);
971
+ };
972
+ }
973
+ function transformNodes(nodes, scope, ctx) {
974
+ const out = [];
975
+ for (const node of nodes) {
976
+ if (isElement(node)) {
977
+ out.push(...transformElement(node, scope, ctx));
978
+ } else if (isTextNode(node)) {
979
+ node.value = substituteTokens(node.value, scopeResolver(scope));
980
+ out.push(node);
981
+ } else {
982
+ out.push(node);
983
+ }
984
+ }
985
+ return out;
986
+ }
987
+ function transformElement(el, scope, ctx) {
988
+ if (hasAttribute(el, "data-each-in") || hasAttribute(el, "data-each")) {
989
+ return expandLoop(el, scope, ctx);
990
+ }
991
+ if (ctx.components.has(el.tagName)) {
992
+ return expandComponent(el, scope, ctx);
993
+ }
994
+ substituteAttributes(el, scope, ctx);
995
+ const annotation = getAttribute(el, "data-type");
996
+ for (const attr of AUTHORING_ATTRS) removeAttribute(el, attr);
997
+ if (el.tagName === "img" && ctx.media !== void 0) {
998
+ const info = ctx.media.image(getAttribute(el, "src") ?? "");
999
+ if (info !== void 0) {
1000
+ const attrs = el.attrs.map((attr) => [attr.name, attr.value]);
1001
+ return parseFragmentNodes(buildPictureMarkup(info, attrs));
1002
+ }
1003
+ }
1004
+ if (isJsonLd(el)) {
1005
+ substituteRawText(el, scope, "json");
1006
+ return [el];
1007
+ }
1008
+ if (el.tagName === "script" || el.tagName === "style") {
1009
+ substituteRawText(el, scope, "raw");
1010
+ return [el];
1011
+ }
1012
+ const field = wholeValueFieldToken(el, annotation);
1013
+ if (field?.kind === "html") {
1014
+ const injected = parseFragmentNodes(resolveToString(scope[field.token.name]));
1015
+ el.childNodes = transformRichMedia(injected, ctx);
1016
+ return [el];
1017
+ }
1018
+ if (field?.kind === "authors") {
1019
+ el.childNodes = parseFragmentNodes(
1020
+ renderAuthors(scope[field.token.name], ctx.authorDirectory)
1021
+ );
1022
+ return [el];
1023
+ }
1024
+ el.childNodes = transformNodes(el.childNodes, scope, ctx);
1025
+ return [el];
1026
+ }
1027
+ function transformRichMedia(nodes, ctx) {
1028
+ const out = [];
1029
+ for (const node of nodes) {
1030
+ if (!isElement(node)) {
1031
+ out.push(node);
1032
+ continue;
1033
+ }
1034
+ node.childNodes = transformRichMedia(node.childNodes, ctx);
1035
+ if (node.tagName === "img") {
1036
+ const src = getAttribute(node, "src");
1037
+ const info = src !== void 0 ? ctx.media?.image(src) : void 0;
1038
+ if (info !== void 0) {
1039
+ const attrs = node.attrs.map((attr) => [attr.name, attr.value]);
1040
+ out.push(...parseFragmentNodes(buildPictureMarkup(info, attrs)));
1041
+ continue;
1042
+ }
1043
+ if (src !== void 0) setAttribute(node, "src", resolveMediaUrl(src, ctx));
1044
+ }
1045
+ const href = getAttribute(node, "href");
1046
+ if (href !== void 0) setAttribute(node, "href", resolveMediaUrl(href, ctx));
1047
+ out.push(node);
1048
+ }
1049
+ return out;
1050
+ }
1051
+ function resolveMediaUrl(value, ctx) {
1052
+ const referenced = resolveReference(value, ctx);
1053
+ if (referenced !== void 0) return sanitizeUrl(referenced);
1054
+ return sanitizeUrl(ctx.media?.file(value) ?? value);
1055
+ }
1056
+ function resolveReference(value, ctx) {
1057
+ const path = referenceTarget(value);
1058
+ if (path === void 0) return void 0;
1059
+ return ctx.context?.reference(path)?.url ?? "#";
1060
+ }
1061
+ function expandLoop(el, scope, ctx) {
1062
+ if (ctx.context === void 0) return [];
1063
+ const eachIn = getAttribute(el, "data-each-in");
1064
+ let items;
1065
+ if (eachIn !== void 0) {
1066
+ const limitText = getAttribute(el, "data-limit");
1067
+ const limit = limitText !== void 0 ? Number.parseInt(limitText, 10) : void 0;
1068
+ const sort = getAttribute(el, "data-sort");
1069
+ items = ctx.context.collection(eachIn, {
1070
+ ...sort !== void 0 && { sort },
1071
+ ...limit !== void 0 && !Number.isNaN(limit) && { limit }
1072
+ });
1073
+ } else {
1074
+ const fieldName = getAttribute(el, "data-each") ?? "";
1075
+ const value = scope[fieldName];
1076
+ const paths = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : typeof value === "string" ? [value] : [];
1077
+ items = paths.map((path) => ctx.context?.reference(path)).filter((bound) => bound !== void 0);
1078
+ }
1079
+ const out = [];
1080
+ for (const item of items) {
1081
+ const clone = cloneNode(el);
1082
+ for (const attr of LOOP_ATTRS) removeAttribute(clone, attr);
1083
+ out.push(...transformElement(clone, { ...scope, item }, ctx));
1084
+ }
1085
+ return out;
1086
+ }
1087
+ function cloneNode(node) {
1088
+ if (isElement(node)) {
1089
+ const clone = {
1090
+ ...node,
1091
+ attrs: node.attrs.map((attr) => ({ ...attr })),
1092
+ childNodes: []
1093
+ };
1094
+ clone.childNodes = node.childNodes.map(cloneNode);
1095
+ return clone;
1096
+ }
1097
+ return { ...node };
1098
+ }
1099
+ function substituteAttributes(el, scope, ctx) {
1100
+ for (const attr of el.attrs) {
1101
+ if (findTokens(attr.value).length === 0) {
1102
+ const referenced2 = resolveReference(attr.value, ctx);
1103
+ if (referenced2 !== void 0) attr.value = sanitizeUrl(referenced2);
1104
+ continue;
1105
+ }
1106
+ const annotation = getAttribute(el, "data-type");
1107
+ const whole = wholeValueToken(attr.value) !== null;
1108
+ const type = inferControl({
1109
+ tag: el.tagName,
1110
+ attribute: attr.name,
1111
+ wholeValue: whole,
1112
+ valuePrefix: literalPrefix(attr.value),
1113
+ download: hasAttribute(el, "download"),
1114
+ ...annotation !== void 0 && { annotation }
1115
+ });
1116
+ let value = substituteTokens(attr.value, scopeResolver(scope));
1117
+ const referenced = resolveReference(value, ctx);
1118
+ if (referenced !== void 0) value = referenced;
1119
+ const fileUrl = ctx.media?.file(value);
1120
+ if (fileUrl !== void 0) value = fileUrl;
1121
+ if (whole && valueKindOf(type) === "url") value = sanitizeUrl(value);
1122
+ attr.value = value;
1123
+ }
1124
+ }
1125
+ function substituteRawText(el, scope, mode) {
1126
+ const resolve = scopeResolver(scope);
1127
+ for (const child of el.childNodes) {
1128
+ if (!isTextNode(child)) continue;
1129
+ child.value = substituteTokens(child.value, (token) => {
1130
+ const text = resolve(token);
1131
+ return mode === "json" ? escapeJsonStringContent(text) : text;
1132
+ });
1133
+ }
1134
+ }
1135
+ function wholeValueFieldToken(el, annotation) {
1136
+ const meaningful = el.childNodes.filter(
1137
+ (child) => !(isTextNode(child) && child.value.trim() === "")
1138
+ );
1139
+ const only = meaningful[0];
1140
+ if (meaningful.length !== 1 || only === void 0 || !isTextNode(only)) return null;
1141
+ const token = wholeValueToken(only.value);
1142
+ if (token === null || token.dotted) return null;
1143
+ const type = inferControl({
1144
+ tag: el.tagName,
1145
+ wholeValue: true,
1146
+ ...annotation !== void 0 && { annotation }
1147
+ });
1148
+ return { token, kind: valueKindOf(type) };
1149
+ }
1150
+ function isJsonLd(el) {
1151
+ return el.tagName === "script" && getAttribute(el, "type")?.toLowerCase() === "application/ld+json";
1152
+ }
1153
+ function expandComponent(el, scope, ctx) {
1154
+ const source = ctx.components.get(el.tagName);
1155
+ if (source === void 0) return [el];
1156
+ const resolve = scopeResolver(scope);
1157
+ const propScope = {};
1158
+ for (const attr of el.attrs) {
1159
+ propScope[attr.name] = substituteTokens(attr.value, resolve);
1160
+ }
1161
+ const slotChildren = transformNodes(el.childNodes, scope, ctx);
1162
+ const body = transformNodes(parseNodes(source), propScope, ctx);
1163
+ const result = fillSlots(body, slotChildren);
1164
+ ctx.usedComponents.add(el.tagName);
1165
+ const css = ctx.componentStyles?.get(el.tagName)?.trim();
1166
+ if (css) {
1167
+ const root = result.find(isElement);
1168
+ if (root !== void 0) {
1169
+ const styleNode = parseFragmentNodes(`<style>@scope {
1170
+ ${css}
1171
+ }</style>`);
1172
+ root.childNodes = [...styleNode, ...root.childNodes];
1173
+ }
1174
+ }
1175
+ return result;
1176
+ }
1177
+ function fillSlots(body, slotChildren) {
1178
+ const named = /* @__PURE__ */ new Map();
1179
+ const defaultSlot = [];
1180
+ for (const child of slotChildren) {
1181
+ const slotName = isElement(child) ? getAttribute(child, "slot") : void 0;
1182
+ if (slotName !== void 0) {
1183
+ if (isElement(child)) removeAttribute(child, "slot");
1184
+ const group = named.get(slotName) ?? [];
1185
+ group.push(child);
1186
+ named.set(slotName, group);
1187
+ } else {
1188
+ defaultSlot.push(child);
1189
+ }
1190
+ }
1191
+ return replaceSlots(body, named, defaultSlot);
1192
+ }
1193
+ function replaceSlots(nodes, named, defaultSlot) {
1194
+ const out = [];
1195
+ for (const node of nodes) {
1196
+ if (isElement(node) && node.tagName === "slot") {
1197
+ const name = getAttribute(node, "name");
1198
+ const provided = name !== void 0 ? named.get(name) : defaultSlot;
1199
+ if (provided && provided.length > 0) {
1200
+ out.push(...provided);
1201
+ } else {
1202
+ out.push(...replaceSlots(node.childNodes, named, defaultSlot));
1203
+ }
1204
+ } else if (isElement(node)) {
1205
+ node.childNodes = replaceSlots(node.childNodes, named, defaultSlot);
1206
+ out.push(node);
1207
+ } else {
1208
+ out.push(node);
1209
+ }
1210
+ }
1211
+ return out;
1212
+ }
1213
+
1214
+ // ../engine/src/aggregate/aggregate.ts
1215
+ function joinUrl(baseUrl, sitePath) {
1216
+ return baseUrl.replace(/\/+$/, "") + sitePath;
1217
+ }
1218
+ function textContent(html) {
1219
+ const parts = [];
1220
+ const visit = (nodes) => {
1221
+ for (const node of nodes) {
1222
+ if (isTextNode(node)) parts.push(node.value);
1223
+ else if (isElement(node)) visit(node.childNodes);
1224
+ }
1225
+ };
1226
+ visit(parseFragmentNodes(html));
1227
+ return parts.join(" ").replace(/\s+/g, " ").trim();
1228
+ }
1229
+ function buildSitemap(entries, baseUrl) {
1230
+ const urls = entries.map((entry) => {
1231
+ const lastmod = entry.updated ?? entry.published;
1232
+ const mod = lastmod !== void 0 ? `
1233
+ <lastmod>${escapeHtmlText(lastmod)}</lastmod>` : "";
1234
+ return ` <url>
1235
+ <loc>${escapeHtmlText(joinUrl(baseUrl, entry.url))}</loc>${mod}
1236
+ </url>`;
1237
+ }).join("\n");
1238
+ const contents = `<?xml version="1.0" encoding="UTF-8"?>
1239
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1240
+ ${urls}
1241
+ </urlset>
1242
+ `;
1243
+ return { path: "sitemap.xml", contents };
1244
+ }
1245
+ function buildRss(entries, options) {
1246
+ const items = [...entries].sort(
1247
+ (a, b) => String(b.published ?? "").localeCompare(String(a.published ?? ""))
1248
+ );
1249
+ const lastBuild = items[0]?.published;
1250
+ const rfc822 = (iso) => new Date(iso).toUTCString();
1251
+ const itemXml = items.map((entry) => {
1252
+ const link = joinUrl(options.baseUrl, entry.url);
1253
+ const pub = entry.published !== void 0 ? `
1254
+ <pubDate>${escapeHtmlText(rfc822(entry.published))}</pubDate>` : "";
1255
+ const desc = entry.summary !== void 0 ? `
1256
+ <description>${escapeHtmlText(entry.summary)}</description>` : "";
1257
+ return ` <item>
1258
+ <title>${escapeHtmlText(entry.title)}</title>
1259
+ <link>${escapeHtmlText(link)}</link>
1260
+ <guid>${escapeHtmlText(link)}</guid>${pub}${desc}
1261
+ </item>`;
1262
+ }).join("\n");
1263
+ const built = lastBuild !== void 0 ? `
1264
+ <lastBuildDate>${escapeHtmlText(rfc822(lastBuild))}</lastBuildDate>` : "";
1265
+ const contents = `<?xml version="1.0" encoding="UTF-8"?>
1266
+ <rss version="2.0">
1267
+ <channel>
1268
+ <title>${escapeHtmlText(options.title)}</title>
1269
+ <link>${escapeHtmlText(options.baseUrl)}</link>
1270
+ <description>${escapeHtmlText(options.description)}</description>${built}
1271
+ ${itemXml}
1272
+ </channel>
1273
+ </rss>
1274
+ `;
1275
+ return { path: "feed.xml", contents };
1276
+ }
1277
+ function buildContentIndex(entries) {
1278
+ const docs = entries.map((entry) => ({
1279
+ url: entry.url,
1280
+ title: entry.title,
1281
+ ...entry.published !== void 0 && { published: entry.published },
1282
+ ...entry.summary !== void 0 && { summary: entry.summary }
1283
+ }));
1284
+ return { path: "content.json", contents: JSON.stringify(docs) };
1285
+ }
1286
+
1287
+ // ../engine/src/concurrency.ts
1288
+ async function mapWithConcurrency(items, limit, fn) {
1289
+ const results = new Array(items.length);
1290
+ const workers = Math.max(1, Math.min(Math.floor(limit), items.length));
1291
+ let next = 0;
1292
+ const run = async () => {
1293
+ while (next < items.length) {
1294
+ const index = next++;
1295
+ results[index] = await fn(items[index], index);
1296
+ }
1297
+ };
1298
+ await Promise.all(Array.from({ length: workers }, () => run()));
1299
+ return results;
1300
+ }
1301
+
1302
+ // ../engine/src/template/fields.ts
1303
+ function lengthConstraints(el) {
1304
+ const min = parseLength(getAttribute(el, "data-minlength"));
1305
+ const max = parseLength(getAttribute(el, "data-maxlength"));
1306
+ return {
1307
+ ...min !== void 0 && { minLength: min },
1308
+ ...max !== void 0 && { maxLength: max }
1309
+ };
1310
+ }
1311
+ function parseLength(raw) {
1312
+ if (raw === void 0) return void 0;
1313
+ const value = Number(raw);
1314
+ return Number.isInteger(value) && value >= 0 ? value : void 0;
1315
+ }
1316
+ function deriveFields(template, components = /* @__PURE__ */ new Map()) {
1317
+ return deriveFromSource(template, components, /* @__PURE__ */ new Set());
1318
+ }
1319
+ function componentPropTypes(tag, components, seen) {
1320
+ if (seen.has(tag)) return /* @__PURE__ */ new Map();
1321
+ const source = components.get(tag);
1322
+ if (source === void 0) return /* @__PURE__ */ new Map();
1323
+ const fields = deriveFromSource(source, components, new Set(seen).add(tag));
1324
+ return new Map(fields.map((field) => [field.name, field.type]));
1325
+ }
1326
+ function deriveFromSource(source, components, seen) {
1327
+ const order = [];
1328
+ const fields = /* @__PURE__ */ new Map();
1329
+ const ensure = (name) => {
1330
+ const existing = fields.get(name);
1331
+ if (existing !== void 0) return existing;
1332
+ const created = {
1333
+ name,
1334
+ type: "shorttext",
1335
+ label: humanize(name),
1336
+ required: false,
1337
+ multiline: false
1338
+ };
1339
+ fields.set(name, created);
1340
+ order.push(name);
1341
+ return created;
1342
+ };
1343
+ const record = (name, type) => {
1344
+ if (fields.has(name)) return;
1345
+ const field = ensure(name);
1346
+ field.type = type;
1347
+ field.multiline = FIELD_TYPES[type].multiline;
1348
+ };
1349
+ const applyAnnotations = (el) => {
1350
+ const named = getAttribute(el, "data-field") ?? firstTokenName(el);
1351
+ if (named === void 0) return;
1352
+ const field = ensure(named);
1353
+ if (hasAttribute(el, "data-required")) field.required = true;
1354
+ const help = getAttribute(el, "data-help");
1355
+ if (help !== void 0) field.help = help;
1356
+ const label = getAttribute(el, "data-label");
1357
+ if (label !== void 0) field.label = label;
1358
+ const { minLength, maxLength } = lengthConstraints(el);
1359
+ if (minLength !== void 0) field.minLength = minLength;
1360
+ if (maxLength !== void 0) field.maxLength = maxLength;
1361
+ };
1362
+ const visit = (nodes) => {
1363
+ for (const node of nodes) {
1364
+ if (!isElement(node)) {
1365
+ if (isTextNode(node)) continue;
1366
+ continue;
1367
+ }
1368
+ const eachField = getAttribute(node, "data-each");
1369
+ if (eachField !== void 0) {
1370
+ const field = ensure(eachField);
1371
+ field.type = "url";
1372
+ field.multiple = true;
1373
+ const pickFrom = getAttribute(node, "data-pick-from");
1374
+ if (pickFrom !== void 0) field.pickFrom = pickFrom;
1375
+ applyAnnotations(node);
1376
+ continue;
1377
+ }
1378
+ if (hasAttribute(node, "data-each-in")) continue;
1379
+ const annotation = getAttribute(node, "data-type");
1380
+ const propTypes = components.has(node.tagName) ? componentPropTypes(node.tagName, components, seen) : void 0;
1381
+ for (const attr of node.attrs) {
1382
+ const whole = wholeValueToken(attr.value);
1383
+ for (const token of findTokens(attr.value)) {
1384
+ if (token.dotted) continue;
1385
+ const isWhole = whole?.name === token.name;
1386
+ const propType = isWhole ? propTypes?.get(attr.name) : void 0;
1387
+ record(
1388
+ token.name,
1389
+ propType ?? inferControl({
1390
+ tag: node.tagName,
1391
+ attribute: attr.name,
1392
+ wholeValue: isWhole,
1393
+ valuePrefix: literalPrefix(attr.value),
1394
+ download: hasAttribute(node, "download"),
1395
+ ...annotation !== void 0 && { annotation }
1396
+ })
1397
+ );
1398
+ }
1399
+ }
1400
+ for (const child of node.childNodes) {
1401
+ if (!isTextNode(child)) continue;
1402
+ const whole = wholeValueToken(child.value);
1403
+ for (const token of findTokens(child.value)) {
1404
+ if (token.dotted) continue;
1405
+ record(
1406
+ token.name,
1407
+ inferControl({
1408
+ tag: node.tagName,
1409
+ wholeValue: whole?.name === token.name,
1410
+ ...annotation !== void 0 && { annotation }
1411
+ })
1412
+ );
1413
+ }
1414
+ }
1415
+ applyAnnotations(node);
1416
+ visit(node.childNodes);
1417
+ }
1418
+ };
1419
+ visit(parseNodes(source));
1420
+ return order.map((name) => freeze(fields.get(name)));
1421
+ }
1422
+ function firstTokenName(el) {
1423
+ for (const attr of el.attrs) {
1424
+ const token = findTokens(attr.value).find((candidate) => !candidate.dotted);
1425
+ if (token !== void 0) return token.name;
1426
+ }
1427
+ for (const child of el.childNodes) {
1428
+ if (isTextNode(child)) {
1429
+ const token = findTokens(child.value).find((candidate) => !candidate.dotted);
1430
+ if (token !== void 0) return token.name;
1431
+ }
1432
+ }
1433
+ return void 0;
1434
+ }
1435
+ function freeze(field) {
1436
+ return {
1437
+ name: field.name,
1438
+ type: field.type,
1439
+ label: field.label,
1440
+ required: field.required,
1441
+ multiline: field.multiline,
1442
+ ...field.help !== void 0 && { help: field.help },
1443
+ ...field.minLength !== void 0 && { minLength: field.minLength },
1444
+ ...field.maxLength !== void 0 && { maxLength: field.maxLength },
1445
+ ...field.multiple !== void 0 && { multiple: field.multiple },
1446
+ ...field.pickFrom !== void 0 && { pickFrom: field.pickFrom }
1447
+ };
1448
+ }
1449
+
1450
+ // ../engine/src/template/analyze.ts
1451
+ function analyzeTemplate(source, components = /* @__PURE__ */ new Map()) {
1452
+ const requiredFields = /* @__PURE__ */ new Set();
1453
+ const queryLoopPaths = [];
1454
+ const curatedLoopFields = [];
1455
+ const imageFields = /* @__PURE__ */ new Set();
1456
+ const fileFields = /* @__PURE__ */ new Set();
1457
+ const richTextFields = /* @__PURE__ */ new Set();
1458
+ const constraints = /* @__PURE__ */ new Map();
1459
+ const references = /* @__PURE__ */ new Set();
1460
+ const visit = (nodes) => {
1461
+ for (const node of nodes) {
1462
+ if (!isElement(node)) continue;
1463
+ const richText = richTextFieldName(node);
1464
+ if (richText !== void 0) richTextFields.add(richText);
1465
+ const eachIn = getAttribute(node, "data-each-in");
1466
+ if (eachIn !== void 0) queryLoopPaths.push(eachIn);
1467
+ const each = getAttribute(node, "data-each");
1468
+ if (each !== void 0) curatedLoopFields.push(each);
1469
+ if (hasAttribute(node, "data-required")) {
1470
+ const field = annotatedFieldName(node);
1471
+ if (field !== void 0) requiredFields.add(field);
1472
+ }
1473
+ const bounds = lengthConstraints(node);
1474
+ if (bounds.minLength !== void 0 || bounds.maxLength !== void 0) {
1475
+ const field = annotatedFieldName(node);
1476
+ if (field !== void 0) constraints.set(field, bounds);
1477
+ }
1478
+ const annotation = getAttribute(node, "data-type");
1479
+ const propTypes = components.has(node.tagName) ? componentPropTypes(node.tagName, components, /* @__PURE__ */ new Set()) : void 0;
1480
+ for (const attr of node.attrs) {
1481
+ const reference = referenceTarget(attr.value);
1482
+ if (reference !== void 0) references.add(reference);
1483
+ const token = wholeValueToken(attr.value);
1484
+ if (token === null || token.dotted) continue;
1485
+ const type = propTypes?.get(attr.name) ?? inferControl({
1486
+ tag: node.tagName,
1487
+ attribute: attr.name,
1488
+ wholeValue: true,
1489
+ valuePrefix: literalPrefix(attr.value),
1490
+ download: hasAttribute(node, "download"),
1491
+ ...annotation !== void 0 && { annotation }
1492
+ });
1493
+ if (type === "image") imageFields.add(token.name);
1494
+ else if (type === "file") fileFields.add(token.name);
1495
+ else if (type === "richtext") richTextFields.add(token.name);
1496
+ }
1497
+ visit(node.childNodes);
1498
+ }
1499
+ };
1500
+ visit(parseNodes(source));
1501
+ return {
1502
+ requiredFields: [...requiredFields],
1503
+ queryLoopPaths,
1504
+ curatedLoopFields,
1505
+ imageFields: [...imageFields],
1506
+ fileFields: [...fileFields],
1507
+ richTextFields: [...richTextFields],
1508
+ constraints,
1509
+ references: [...references].sort()
1510
+ };
1511
+ }
1512
+ function richTextFieldName(el) {
1513
+ const meaningful = el.childNodes.filter(
1514
+ (child) => !(isTextNode(child) && child.value.trim() === "")
1515
+ );
1516
+ const only = meaningful[0];
1517
+ if (meaningful.length !== 1 || only === void 0 || !isTextNode(only)) return void 0;
1518
+ const token = wholeValueToken(only.value);
1519
+ if (token === null || token.dotted) return void 0;
1520
+ const annotation = getAttribute(el, "data-type");
1521
+ const type = inferControl({
1522
+ tag: el.tagName,
1523
+ wholeValue: true,
1524
+ ...annotation !== void 0 && { annotation }
1525
+ });
1526
+ return valueKindOf(type) === "html" ? token.name : void 0;
1527
+ }
1528
+ function annotatedFieldName(el) {
1529
+ const explicit = getAttribute(el, "data-field");
1530
+ if (explicit !== void 0) return explicit;
1531
+ for (const attr of el.attrs) {
1532
+ const token = findTokens(attr.value).find((candidate) => !candidate.dotted);
1533
+ if (token !== void 0) return token.name;
1534
+ }
1535
+ for (const child of el.childNodes) {
1536
+ if (isTextNode(child)) {
1537
+ const token = findTokens(child.value).find((candidate) => !candidate.dotted);
1538
+ if (token !== void 0) return token.name;
1539
+ }
1540
+ }
1541
+ return void 0;
1542
+ }
1543
+
1544
+ // ../engine/src/url/urls.ts
1545
+ function segmentsOf(contentPath) {
1546
+ return contentPath.split("/").filter((segment) => segment !== "");
1547
+ }
1548
+ function outputPathForItem(contentPath) {
1549
+ const segments = segmentsOf(contentPath);
1550
+ if (segments[segments.length - 1] === "index") {
1551
+ return [...segments.slice(0, -1), "index.html"].join("/");
1552
+ }
1553
+ return [...segments, "index.html"].join("/");
1554
+ }
1555
+ function urlForItem(contentPath) {
1556
+ const segments = segmentsOf(contentPath);
1557
+ const dirSegments = segments[segments.length - 1] === "index" ? segments.slice(0, -1) : segments;
1558
+ return dirSegments.length === 0 ? "/" : `/${dirSegments.join("/")}/`;
1559
+ }
1560
+
1561
+ // ../engine/src/compile/site.ts
1562
+ var DEFAULT_IMAGE_CONCURRENCY = 4;
1563
+ async function compileSite(source, options = {}) {
1564
+ const [
1565
+ tree,
1566
+ components,
1567
+ componentStyles,
1568
+ componentScripts,
1569
+ documentShell,
1570
+ redirects,
1571
+ siteConfig
1572
+ ] = await Promise.all([
1573
+ loadContentTree(source),
1574
+ loadComponents(source),
1575
+ loadComponentStyles(source),
1576
+ loadComponentScripts(source),
1577
+ loadDocumentShell(source),
1578
+ loadRedirects(source),
1579
+ loadSiteConfig(source)
1580
+ ]);
1581
+ const context = buildResolveContext(tree);
1582
+ const baseUrl = options.baseUrl ?? siteConfig.baseUrl;
1583
+ const templateCache = /* @__PURE__ */ new Map();
1584
+ const getTemplate = async (name) => {
1585
+ const cached = templateCache.get(name);
1586
+ if (cached !== void 0) return cached;
1587
+ const loaded = await loadTemplate(source, name);
1588
+ templateCache.set(name, loaded);
1589
+ return loaded;
1590
+ };
1591
+ const styleCache = /* @__PURE__ */ new Map();
1592
+ const scriptCache = /* @__PURE__ */ new Map();
1593
+ const getStyle = async (name) => {
1594
+ if (!styleCache.has(name)) styleCache.set(name, await loadTemplateStyle(source, name));
1595
+ return styleCache.get(name);
1596
+ };
1597
+ const getScript = async (name) => {
1598
+ if (!scriptCache.has(name)) scriptCache.set(name, await loadTemplateScript(source, name));
1599
+ return scriptCache.get(name);
1600
+ };
1601
+ const analysisCache = /* @__PURE__ */ new Map();
1602
+ const getAnalysis = (name, templateSource) => {
1603
+ const cached = analysisCache.get(name);
1604
+ if (cached !== void 0) return cached;
1605
+ const analysis = analyzeTemplate(templateSource, components);
1606
+ analysisCache.set(name, analysis);
1607
+ return analysis;
1608
+ };
1609
+ const artifacts = [];
1610
+ const entries = [];
1611
+ const walk = async (dir, inheritedTemplate) => {
1612
+ const dirTemplate = dir.defaultTemplate ?? inheritedTemplate;
1613
+ for (const child of dir.children) {
1614
+ if (child.kind === "dir") {
1615
+ await walk(child, dirTemplate);
1616
+ continue;
1617
+ }
1618
+ if (!child.item.isPublished) continue;
1619
+ const templateName = child.item.template ?? dirTemplate;
1620
+ if (templateName === void 0) {
1621
+ throw new Error(
1622
+ `Item "${child.path}" has no template (no per-item template and no folder default).`
1623
+ );
1624
+ }
1625
+ const template = await getTemplate(templateName);
1626
+ const analysis = getAnalysis(templateName, template);
1627
+ const { media, mediaArtifacts } = await collectMedia(
1628
+ source,
1629
+ child,
1630
+ analysis,
1631
+ options.imageEncoder,
1632
+ options.imageConcurrency ?? DEFAULT_IMAGE_CONCURRENCY
1633
+ );
1634
+ artifacts.push(...mediaArtifacts);
1635
+ const templateStyle = await getStyle(templateName);
1636
+ const templateScript = await getScript(templateName);
1637
+ const contents = compileTemplate({
1638
+ template,
1639
+ scope: buildScope(child.item),
1640
+ components,
1641
+ context,
1642
+ media,
1643
+ componentStyles,
1644
+ componentScripts,
1645
+ ...options.authorDirectory !== void 0 && { authorDirectory: options.authorDirectory },
1646
+ ...documentShell !== void 0 && { document: documentShell },
1647
+ ...templateStyle !== void 0 && { templateStyle },
1648
+ ...templateScript !== void 0 && { templateScript }
1649
+ });
1650
+ artifacts.push({ path: outputPathForItem(child.path), contents });
1651
+ entries.push(toEntry(child));
1652
+ }
1653
+ };
1654
+ await walk(tree, void 0);
1655
+ const redirectArtifact = buildRedirects(redirects, new Set(entries.map((entry) => entry.url)));
1656
+ if (redirectArtifact !== void 0) artifacts.push(redirectArtifact);
1657
+ if (options.contentIndex !== false) artifacts.push(buildContentIndex(entries));
1658
+ if (baseUrl !== void 0) {
1659
+ artifacts.push(buildSitemap(entries, baseUrl));
1660
+ const rss = options.rss;
1661
+ if (rss !== void 0) {
1662
+ const feed = entries.filter((entry) => inCollection(entry, rss.collection));
1663
+ artifacts.push(
1664
+ buildRss(feed, {
1665
+ baseUrl,
1666
+ title: rss.title,
1667
+ description: rss.description ?? rss.title,
1668
+ collection: rss.collection
1669
+ })
1670
+ );
1671
+ }
1672
+ }
1673
+ return artifacts;
1674
+ }
1675
+ async function compilePage(source, itemPath, options = {}) {
1676
+ const [tree, components, componentStyles, componentScripts] = await Promise.all([
1677
+ loadContentTree(source),
1678
+ loadComponents(source),
1679
+ loadComponentStyles(source),
1680
+ loadComponentScripts(source)
1681
+ ]);
1682
+ const found = findItem(tree, itemPath, void 0);
1683
+ if (found === void 0) return void 0;
1684
+ const item = options.draft ?? found.node.item;
1685
+ const templateName = item.template ?? found.dirTemplate;
1686
+ if (templateName === void 0) return void 0;
1687
+ const [template, templateStyle, templateScript, documentShell] = await Promise.all([
1688
+ loadTemplate(source, templateName),
1689
+ loadTemplateStyle(source, templateName),
1690
+ loadTemplateScript(source, templateName),
1691
+ loadDocumentShell(source)
1692
+ ]);
1693
+ const analysis = analyzeTemplate(template, components);
1694
+ const node = { ...found.node, item };
1695
+ let media;
1696
+ let mediaArtifacts = [];
1697
+ if (options.media !== void 0) {
1698
+ media = options.media;
1699
+ } else {
1700
+ const collected = await collectMedia(
1701
+ source,
1702
+ node,
1703
+ analysis,
1704
+ options.imageEncoder,
1705
+ DEFAULT_IMAGE_CONCURRENCY
1706
+ );
1707
+ media = collected.media;
1708
+ mediaArtifacts = collected.mediaArtifacts;
1709
+ }
1710
+ const html = compileTemplate({
1711
+ template,
1712
+ scope: buildScope(item),
1713
+ components,
1714
+ context: buildResolveContext(tree),
1715
+ media,
1716
+ componentStyles,
1717
+ componentScripts,
1718
+ ...options.authorDirectory !== void 0 && { authorDirectory: options.authorDirectory },
1719
+ ...documentShell !== void 0 && { document: documentShell },
1720
+ ...templateStyle !== void 0 && { templateStyle },
1721
+ ...templateScript !== void 0 && { templateScript }
1722
+ });
1723
+ return { html, mediaArtifacts };
1724
+ }
1725
+ function findItem(dir, path, inherited) {
1726
+ const dirTemplate = dir.defaultTemplate ?? inherited;
1727
+ for (const child of dir.children) {
1728
+ if (child.kind === "item") {
1729
+ if (child.path === path) return { node: child, dirTemplate };
1730
+ } else {
1731
+ const found = findItem(child, path, dirTemplate);
1732
+ if (found !== void 0) return found;
1733
+ }
1734
+ }
1735
+ return void 0;
1736
+ }
1737
+ function toEntry(node) {
1738
+ const fields = node.item.fields;
1739
+ return {
1740
+ path: node.path,
1741
+ slug: node.slug,
1742
+ url: urlForItem(node.path),
1743
+ title: node.item.title,
1744
+ ...node.item.published !== void 0 && { published: node.item.published },
1745
+ ...node.item.updated !== void 0 && { updated: node.item.updated },
1746
+ ...typeof fields.summary === "string" && { summary: fields.summary }
1747
+ };
1748
+ }
1749
+ function inCollection(entry, collectionPath) {
1750
+ if (entry.slug === "index") return false;
1751
+ const collection = collectionPath.replace(/^\/+/, "").replace(/\/+$/, "");
1752
+ if (collection === "") return true;
1753
+ return entry.path === collection || entry.path.startsWith(`${collection}/`);
1754
+ }
1755
+ async function collectMedia(source, node, analysis, encoder, imageConcurrency) {
1756
+ const itemDir = parentPath(node.path);
1757
+ const fields = node.item.fields;
1758
+ const images = /* @__PURE__ */ new Map();
1759
+ const files = /* @__PURE__ */ new Map();
1760
+ const mediaArtifacts = [];
1761
+ for (const field of analysis.fileFields) {
1762
+ for (const ref of assetRefs(fields[field])) {
1763
+ const published = joinAsset(itemDir, ref);
1764
+ const bytes = await tryReadBytes(source, `content/${published}`);
1765
+ if (bytes === void 0) continue;
1766
+ mediaArtifacts.push({ path: published, contents: bytes });
1767
+ files.set(ref, `/${published}`);
1768
+ }
1769
+ }
1770
+ if (encoder !== void 0) {
1771
+ const imageRefs = /* @__PURE__ */ new Set();
1772
+ for (const field of analysis.imageFields) {
1773
+ for (const ref of assetRefs(fields[field])) imageRefs.add(ref);
1774
+ }
1775
+ for (const field of analysis.richTextFields) {
1776
+ const value = fields[field];
1777
+ if (typeof value === "string") {
1778
+ for (const ref of inlineImageRefs(value)) imageRefs.add(ref);
1779
+ }
1780
+ }
1781
+ const encoded = await mapWithConcurrency([...imageRefs], imageConcurrency, async (ref) => {
1782
+ const published = joinAsset(itemDir, ref);
1783
+ const bytes = await tryReadBytes(source, `content/${published}`);
1784
+ if (bytes === void 0) return void 0;
1785
+ return { ref, ...await processImage(bytes, published, encoder) };
1786
+ });
1787
+ for (const result of encoded) {
1788
+ if (result === void 0) continue;
1789
+ mediaArtifacts.push(...result.artifacts);
1790
+ images.set(result.ref, result.info);
1791
+ }
1792
+ }
1793
+ return {
1794
+ media: { image: (ref) => images.get(ref), file: (ref) => files.get(ref) },
1795
+ mediaArtifacts
1796
+ };
1797
+ }
1798
+ async function tryReadBytes(source, path) {
1799
+ try {
1800
+ return await source.readBytes(path);
1801
+ } catch {
1802
+ return void 0;
1803
+ }
1804
+ }
1805
+ function parentPath(path) {
1806
+ const slash = path.lastIndexOf("/");
1807
+ return slash === -1 ? "" : path.slice(0, slash);
1808
+ }
1809
+ function joinAsset(dir, ref) {
1810
+ return [dir, ref.replace(/^\/+/, "")].filter((part) => part !== "").join("/");
1811
+ }
1812
+ function inlineImageRefs(html) {
1813
+ const refs = [];
1814
+ const walk = (nodes) => {
1815
+ for (const node of nodes) {
1816
+ if (!isElement(node)) continue;
1817
+ if (node.tagName === "img") {
1818
+ const src = getAttribute(node, "src");
1819
+ if (src !== void 0 && isAssetRef(src)) refs.push(src);
1820
+ }
1821
+ walk(node.childNodes);
1822
+ }
1823
+ };
1824
+ walk(parseFragmentNodes(html));
1825
+ return refs;
1826
+ }
1827
+ function isAssetRef(value) {
1828
+ if (value === "" || value.startsWith("/") || value.startsWith("data:")) return false;
1829
+ return !/^[a-z][a-z0-9+.-]*:/i.test(value);
1830
+ }
1831
+ function assetRefs(value) {
1832
+ if (typeof value === "string") return [value];
1833
+ if (Array.isArray(value))
1834
+ return value.filter((entry) => typeof entry === "string");
1835
+ return [];
1836
+ }
1837
+ function buildScope(item) {
1838
+ return {
1839
+ ...item.fields,
1840
+ title: item.title,
1841
+ ...item.created !== void 0 && { created: item.created },
1842
+ ...item.published !== void 0 && { published: item.published },
1843
+ ...item.updated !== void 0 && { updated: item.updated }
1844
+ };
1845
+ }
1846
+ function indexTree(tree) {
1847
+ const items = /* @__PURE__ */ new Map();
1848
+ const dirs = /* @__PURE__ */ new Map();
1849
+ const walk = (dir) => {
1850
+ dirs.set(dir.path, dir);
1851
+ for (const child of dir.children) {
1852
+ if (child.kind === "item") items.set(child.path, child);
1853
+ else walk(child);
1854
+ }
1855
+ };
1856
+ walk(tree);
1857
+ return { items, dirs };
1858
+ }
1859
+ function normalizeCollectionPath(path) {
1860
+ return path.replace(/^\/+/, "").replace(/\/+$/, "");
1861
+ }
1862
+ function buildResolveContext(tree) {
1863
+ const { items, dirs } = indexTree(tree);
1864
+ const bound = (node) => ({
1865
+ url: urlForItem(node.path),
1866
+ fields: buildScope(node.item)
1867
+ });
1868
+ return {
1869
+ collection(path, query) {
1870
+ const dir = dirs.get(normalizeCollectionPath(path));
1871
+ if (dir === void 0) return [];
1872
+ const recursive = dir.collection?.recursive ?? false;
1873
+ const collected = [];
1874
+ const gather = (current) => {
1875
+ for (const child of current.children) {
1876
+ if (child.kind === "item") {
1877
+ if (child.slug !== "index" && child.item.isPublished) collected.push(child);
1878
+ } else if (recursive) {
1879
+ gather(child);
1880
+ }
1881
+ }
1882
+ };
1883
+ gather(dir);
1884
+ const sorted = sortItems(collected, query.sort);
1885
+ const limited = query.limit !== void 0 ? sorted.slice(0, query.limit) : sorted;
1886
+ return limited.map(bound);
1887
+ },
1888
+ reference(path) {
1889
+ const node = items.get(normalizeCollectionPath(path));
1890
+ if (node === void 0 || !node.item.isPublished) return void 0;
1891
+ return bound(node);
1892
+ }
1893
+ };
1894
+ }
1895
+ function sortItems(items, sort) {
1896
+ if (sort === void 0) return [...items];
1897
+ const [field = "published", direction] = sort.trim().split(/\s+/);
1898
+ const keyOf = (node) => {
1899
+ const value = buildScope(node.item)[field];
1900
+ return value === void 0 || value === null ? "" : String(value);
1901
+ };
1902
+ const sorted = [...items].sort((a, b) => keyOf(a).localeCompare(keyOf(b)));
1903
+ return direction === "desc" ? sorted.reverse() : sorted;
1904
+ }
1905
+
1906
+ // ../engine/src/reference/reference.ts
1907
+ var FIELD_TYPE_DOCS = [
1908
+ {
1909
+ type: "shorttext",
1910
+ summary: "A single line of plain text, the default for headings and any token not inferred as something more specific.",
1911
+ example: "<h1>{title}</h1>"
1912
+ },
1913
+ {
1914
+ type: "text",
1915
+ summary: "Multi-line plain text with no formatting, inferred for paragraphs and block elements.",
1916
+ example: "<p>{summary}</p>"
1917
+ },
1918
+ {
1919
+ type: "richtext",
1920
+ summary: "Formatted HTML from the rich-text editor (headings, links, lists, inline images). Opt-in, never inferred, so a plain block never becomes a surprise editor.",
1921
+ example: '<div data-type="richtext">{body}</div>'
1922
+ },
1923
+ {
1924
+ type: "code",
1925
+ summary: "A monospaced code block, inferred inside <pre>, <code>, <kbd>, and <samp>.",
1926
+ example: "<pre>{snippet}</pre>"
1927
+ },
1928
+ {
1929
+ type: "image",
1930
+ summary: "An image asset. On publish it runs the media pipeline (responsive <picture> variants). Inferred from <img src>.",
1931
+ example: '<img src="{hero}" alt="{caption}">'
1932
+ },
1933
+ {
1934
+ type: "file",
1935
+ summary: "A downloadable file asset (PDF, zip\u2026). Inferred from a non-image src, or an <a href> marked download.",
1936
+ example: '<a href="{brochure}" download>Download the brochure</a>'
1937
+ },
1938
+ {
1939
+ type: "url",
1940
+ summary: "A link target, an internal page reference or an external URL. Inferred from <a href>.",
1941
+ example: '<a href="{cta_link}">{cta_label}</a>'
1942
+ },
1943
+ {
1944
+ type: "email",
1945
+ summary: "An email address. Inferred from an href that starts with mailto:.",
1946
+ example: '<a href="mailto:{email}">{email}</a>'
1947
+ },
1948
+ {
1949
+ type: "phone",
1950
+ summary: "A phone number. Inferred from an href that starts with tel:.",
1951
+ example: '<a href="tel:{phone}">{phone}</a>'
1952
+ },
1953
+ {
1954
+ type: "date",
1955
+ summary: "A calendar date, shown as a date picker. Opt-in via data-type, never inferred.",
1956
+ example: '<time datetime="{published}" data-type="date">{published}</time>'
1957
+ },
1958
+ {
1959
+ type: "time",
1960
+ summary: "A time of day, shown as a time picker. Opt-in via data-type, never inferred.",
1961
+ example: '<span data-type="time">{doors_open}</span>'
1962
+ },
1963
+ {
1964
+ type: "authors",
1965
+ summary: "A byline: an ordered list of authors the editor builds from your users\u2019 display names (or typed-in guests), rendered as one grammatical line, \u201CA\u201D, \u201CA and B\u201D, \u201CA, B and C\u201D (no Oxford comma). Opt-in via data-type, never inferred. A display-name change updates every past byline.",
1966
+ example: '<p class="byline" data-type="authors">{authors}</p>'
1967
+ }
1968
+ ];
1969
+ function buildAuthoringReference(context = {}) {
1970
+ return {
1971
+ title: "nanoesis authoring reference",
1972
+ intro: "Everything you can put in a template. A template is ordinary HTML with {tokens}; the set of tokens is the editor form, \u201Cthe template is the schema\u201D. This reference is generated from the engine, so it always matches the version you are running.",
1973
+ sections: [
1974
+ tokensSection(),
1975
+ fieldTypesSection(),
1976
+ annotationsSection(),
1977
+ loopsSection(context),
1978
+ componentsSection(context),
1979
+ shellSection()
1980
+ ]
1981
+ };
1982
+ }
1983
+ function tokensSection() {
1984
+ return {
1985
+ title: "Tokens",
1986
+ intro: "Wrap a field name in braces to print it. Each distinct token becomes one field in the editor; where the token sits decides the field\u2019s type.",
1987
+ entries: [
1988
+ {
1989
+ name: "{field}",
1990
+ summary: "Print a page field, the name in the braces is the field name.",
1991
+ example: "<h1>{title}</h1>\n<p>{summary}</p>"
1992
+ },
1993
+ {
1994
+ name: "{title}, {published}, {created}, {updated}",
1995
+ summary: "Built-in fields every page has. {title} names the page in the tree; the others are ISO timestamps you can format or print."
1996
+ },
1997
+ {
1998
+ name: "{item} and {item.field}",
1999
+ summary: "Inside a loop, the current item: {item} is its URL, and {item.title} (or any field) is its content.",
2000
+ example: '<a href="{item}">{item.title}</a>'
2001
+ }
2002
+ ]
2003
+ };
2004
+ }
2005
+ function fieldTypesSection() {
2006
+ return {
2007
+ title: "Field types",
2008
+ intro: "A token\u2019s control type is inferred from where it sits; the non-obvious ones (rich text, date, time, code) are opt-in with data-type. One entry per type, with the snippet that produces it.",
2009
+ entries: FIELD_TYPE_DOCS.map(({ type, summary, example }) => ({
2010
+ name: type,
2011
+ summary,
2012
+ example,
2013
+ ...FIELD_TYPES[type].multiline && { detail: "Multi-line." }
2014
+ }))
2015
+ };
2016
+ }
2017
+ function annotationsSection() {
2018
+ return {
2019
+ title: "Field annotations",
2020
+ intro: "Optional data-* attributes that refine a field. Put them on the element holding the token; use data-field to name the field when the element has none.",
2021
+ entries: [
2022
+ {
2023
+ name: "data-field",
2024
+ summary: "Says which field an element\u2019s annotations apply to. They default to the element\u2019s first token (attributes before text); use data-field to target a different one, e.g. make the link text required, not the URL. (Not needed when the element holds a single token.)",
2025
+ example: '<a href="{url}" data-field="label" data-required>{label}</a>'
2026
+ },
2027
+ {
2028
+ name: "data-label",
2029
+ summary: "Set the label shown above the field in the editor (defaults to a humanised name).",
2030
+ example: '<p data-label="Short summary">{summary}</p>'
2031
+ },
2032
+ {
2033
+ name: "data-help",
2034
+ summary: "Add a line of helper text beneath the field in the editor.",
2035
+ example: '<p data-help="One or two sentences.">{summary}</p>'
2036
+ },
2037
+ {
2038
+ name: "data-required",
2039
+ summary: "Mark the field mandatory. Publishing is blocked until every page fills it.",
2040
+ example: "<p data-required>{summary}</p>"
2041
+ },
2042
+ {
2043
+ name: "data-minlength / data-maxlength",
2044
+ summary: "Warn (it does not block) when the value is shorter or longer than N characters, e.g. a 50-160 character SEO meta description.",
2045
+ example: '<p data-minlength="50" data-maxlength="160">{summary}</p>'
2046
+ },
2047
+ {
2048
+ name: "data-type",
2049
+ summary: "Force the control type, overriding inference, the escape hatch for rich text, date, time, and code.",
2050
+ example: '<div data-type="richtext">{body}</div>'
2051
+ }
2052
+ ]
2053
+ };
2054
+ }
2055
+ function loopsSection(context) {
2056
+ const entries = [
2057
+ {
2058
+ name: "Automatic loop, data-each-in",
2059
+ summary: "Repeat an element once per published page in a folder (and its subfolders), filling {item.\u2026} tokens automatically. Point it at a folder path.",
2060
+ example: '<ul>\n <li data-each-in="/blog" data-sort="published desc" data-limit="5">\n <a href="{item}">{item.title}</a>\n </li>\n</ul>'
2061
+ },
2062
+ {
2063
+ name: "data-sort",
2064
+ summary: 'Order an automatic loop by a field and direction, "field asc" or "field desc".',
2065
+ example: '<li data-each-in="/blog" data-sort="published desc">\u2026</li>'
2066
+ },
2067
+ {
2068
+ name: "data-limit",
2069
+ summary: "Take at most N items (after sorting), e.g. a \u201Clatest 3\u201D list.",
2070
+ example: '<li data-each-in="/blog" data-limit="3">\u2026</li>'
2071
+ },
2072
+ {
2073
+ name: "Curated loop, data-each",
2074
+ summary: "Repeat over a hand-picked, ordered list of references stored in a field; the author chooses the items in the editor.",
2075
+ example: '<div data-each="featured" data-pick-from="/products">\n <a href="{item}"><img src="{item.image}" alt="">{item.title}</a>\n</div>'
2076
+ },
2077
+ {
2078
+ name: "data-pick-from",
2079
+ summary: "Scope a curated loop\u2019s reference picker to one folder, so the author only sees the right pages.",
2080
+ example: '<div data-each="featured" data-pick-from="/products">\u2026</div>'
2081
+ }
2082
+ ];
2083
+ const collections = sortedUnique(context.collections);
2084
+ if (collections.length > 0) {
2085
+ entries.push({
2086
+ name: "Your collections",
2087
+ summary: "Folders in this site you can loop over or pick from:",
2088
+ detail: collections.join(", ")
2089
+ });
2090
+ }
2091
+ return {
2092
+ title: "Loops",
2093
+ intro: "Two ways to repeat content: automatic (every page in a folder) and curated (a chosen list).",
2094
+ entries
2095
+ };
2096
+ }
2097
+ function componentsSection(context) {
2098
+ const entries = [
2099
+ {
2100
+ name: "Using a component",
2101
+ summary: "A file in components/ named card.html becomes the tag <card>. Pass props as attributes; the component\u2019s own tokens are typed by how it uses them.",
2102
+ example: '<card title="{name}" image="{photo}"></card>'
2103
+ },
2104
+ {
2105
+ name: "Slots",
2106
+ summary: "A <slot> inside a component renders whatever children a usage wraps around it.",
2107
+ example: '<!-- components/panel.html -->\n<section class="panel"><slot></slot></section>'
2108
+ }
2109
+ ];
2110
+ const components = sortedUnique(context.components);
2111
+ if (components.length > 0) {
2112
+ entries.push({
2113
+ name: "Your components",
2114
+ summary: "Components available in this site:",
2115
+ detail: components.map((tag) => `<${tag}>`).join(", ")
2116
+ });
2117
+ }
2118
+ return {
2119
+ title: "Components",
2120
+ intro: "Reusable fragments with their own scoped CSS and JS, used as custom tags.",
2121
+ entries
2122
+ };
2123
+ }
2124
+ function shellSection() {
2125
+ return {
2126
+ title: "The document shell",
2127
+ intro: "templates/document.html wraps every page, the <head> and the chrome around <slot></slot>. Tokens here (meta description, Open Graph image\u2026) become each page\u2019s Meta & SEO fields, so annotations like data-required and data-maxlength on them apply to every page.",
2128
+ entries: []
2129
+ };
2130
+ }
2131
+ function sortedUnique(values) {
2132
+ return [...new Set(values ?? [])].sort();
2133
+ }
2134
+ function renderReferenceMarkdown(reference) {
2135
+ const lines = [`# ${reference.title}`, "", reference.intro, ""];
2136
+ for (const section of reference.sections) {
2137
+ lines.push(`## ${section.title}`, "", section.intro, "");
2138
+ for (const entry of section.entries) {
2139
+ lines.push(`### ${entry.name}`, "", entry.summary, "");
2140
+ if (entry.detail !== void 0) lines.push(`*${entry.detail}*`, "");
2141
+ if (entry.example !== void 0) lines.push("```html", entry.example, "```", "");
2142
+ }
2143
+ }
2144
+ return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}
2145
+ `;
2146
+ }
2147
+
2148
+ // ../engine/src/validate/gate.ts
2149
+ async function validateSite(source) {
2150
+ const [tree, components, documentShell] = await Promise.all([
2151
+ loadContentTree(source),
2152
+ loadComponents(source),
2153
+ loadDocumentShell(source)
2154
+ ]);
2155
+ const { items, dirs, published } = indexTree2(tree);
2156
+ let shellAnalysis = null;
2157
+ if (documentShell !== void 0) {
2158
+ try {
2159
+ shellAnalysis = analyzeTemplate(documentShell, components);
2160
+ } catch {
2161
+ shellAnalysis = null;
2162
+ }
2163
+ }
2164
+ const shellRequiredFields = shellAnalysis?.requiredFields ?? [];
2165
+ const shellConstraints = shellAnalysis?.constraints ?? /* @__PURE__ */ new Map();
2166
+ const diagnostics = [];
2167
+ const add = (severity, code, message, path) => {
2168
+ diagnostics.push({ severity, code, message, ...path !== void 0 && { path } });
2169
+ };
2170
+ const reportRef = (ref, owner, where) => {
2171
+ const target = items.get(stripSlashes(ref));
2172
+ if (target === void 0) {
2173
+ add(
2174
+ "error",
2175
+ "reference-missing",
2176
+ `"${owner}" references "${ref}" (${where}), which does not exist.`,
2177
+ owner
2178
+ );
2179
+ } else if (!target.item.isPublished) {
2180
+ add(
2181
+ "error",
2182
+ "reference-unpublished",
2183
+ `"${owner}" references "${ref}" (${where}), which is not published.`,
2184
+ owner
2185
+ );
2186
+ }
2187
+ };
2188
+ const checkLengths = (bounds, scope, path) => {
2189
+ for (const [field, { minLength, maxLength }] of bounds) {
2190
+ const value = scope[field];
2191
+ if (typeof value !== "string" || value === "") continue;
2192
+ const { length } = value;
2193
+ if (minLength !== void 0 && length < minLength) {
2194
+ add(
2195
+ "warning",
2196
+ "value-too-short",
2197
+ `"${path}" field "${field}" is ${length} characters; the minimum is ${minLength}.`,
2198
+ path
2199
+ );
2200
+ }
2201
+ if (maxLength !== void 0 && length > maxLength) {
2202
+ add(
2203
+ "warning",
2204
+ "value-too-long",
2205
+ `"${path}" field "${field}" is ${length} characters; the maximum is ${maxLength}.`,
2206
+ path
2207
+ );
2208
+ }
2209
+ }
2210
+ };
2211
+ const analysisCache = /* @__PURE__ */ new Map();
2212
+ const analyze = async (name) => {
2213
+ const cached = analysisCache.get(name);
2214
+ if (cached !== void 0 || analysisCache.has(name)) return cached ?? null;
2215
+ let result;
2216
+ try {
2217
+ result = analyzeTemplate(await loadTemplate(source, name), components);
2218
+ } catch {
2219
+ result = null;
2220
+ }
2221
+ analysisCache.set(name, result);
2222
+ return result;
2223
+ };
2224
+ const urlOwners = /* @__PURE__ */ new Map();
2225
+ for (const { node, templateName } of published) {
2226
+ const owners = urlOwners.get(urlForItem(node.path)) ?? [];
2227
+ owners.push(node.path);
2228
+ urlOwners.set(urlForItem(node.path), owners);
2229
+ if (templateName === void 0) {
2230
+ add(
2231
+ "error",
2232
+ "no-template",
2233
+ `"${node.path}" has no template (no per-item or folder default).`,
2234
+ node.path
2235
+ );
2236
+ continue;
2237
+ }
2238
+ const analysis = await analyze(templateName);
2239
+ if (analysis === null) {
2240
+ add(
2241
+ "error",
2242
+ "template-missing",
2243
+ `"${node.path}" uses template "${templateName}", which does not exist.`,
2244
+ node.path
2245
+ );
2246
+ continue;
2247
+ }
2248
+ const scope = buildScope2(node.item);
2249
+ for (const field of analysis.requiredFields) {
2250
+ if (isEmpty(scope[field])) {
2251
+ add(
2252
+ "error",
2253
+ "required-empty",
2254
+ `"${node.path}" is missing required field "${field}".`,
2255
+ node.path
2256
+ );
2257
+ }
2258
+ }
2259
+ for (const field of shellRequiredFields) {
2260
+ if (isEmpty(scope[field])) {
2261
+ add(
2262
+ "error",
2263
+ "required-empty",
2264
+ `"${node.path}" is missing required meta field "${field}" (document shell).`,
2265
+ node.path
2266
+ );
2267
+ }
2268
+ }
2269
+ checkLengths(analysis.constraints, scope, node.path);
2270
+ checkLengths(shellConstraints, scope, node.path);
2271
+ for (const loopPath of analysis.queryLoopPaths) {
2272
+ if (!dirs.has(stripSlashes(loopPath))) {
2273
+ add(
2274
+ "error",
2275
+ "loop-target-missing",
2276
+ `"${node.path}" loops over "${loopPath}", which is not a directory.`,
2277
+ node.path
2278
+ );
2279
+ }
2280
+ }
2281
+ for (const field of analysis.curatedLoopFields) {
2282
+ for (const ref of asPaths(node.item.fields[field])) {
2283
+ reportRef(ref, node.path, `field "${field}"`);
2284
+ }
2285
+ }
2286
+ for (const [field, value] of Object.entries(scope)) {
2287
+ if (typeof value !== "string") continue;
2288
+ const direct = referenceTarget(value);
2289
+ if (direct !== void 0) reportRef(direct, node.path, `field "${field}"`);
2290
+ if (analysis.richTextFields.includes(field)) {
2291
+ for (const ref of collectHtmlReferences(value))
2292
+ reportRef(ref, node.path, `field "${field}"`);
2293
+ }
2294
+ }
2295
+ }
2296
+ const templatesDir = DEFAULT_DIRS.templates;
2297
+ if (shellAnalysis !== null) {
2298
+ for (const ref of shellAnalysis.references) {
2299
+ reportRef(ref, `${templatesDir}/${DOCUMENT_SHELL}.html`, "link");
2300
+ }
2301
+ }
2302
+ for (const [name, analysis] of analysisCache) {
2303
+ if (analysis === null) continue;
2304
+ for (const ref of analysis.references) reportRef(ref, `${templatesDir}/${name}.html`, "link");
2305
+ }
2306
+ for (const [tag, html] of components) {
2307
+ for (const ref of collectHtmlReferences(html)) {
2308
+ reportRef(ref, `${DEFAULT_DIRS.components}/${tag}.html`, "link");
2309
+ }
2310
+ }
2311
+ for (const [url, owners] of urlOwners) {
2312
+ if (owners.length > 1) {
2313
+ add(
2314
+ "error",
2315
+ "url-collision",
2316
+ `URL ${url} is produced by multiple items: ${owners.join(", ")}.`
2317
+ );
2318
+ }
2319
+ }
2320
+ const tagToPaths = /* @__PURE__ */ new Map();
2321
+ for (const relative of await listComponentFiles(source)) {
2322
+ const file = relative.slice(relative.lastIndexOf("/") + 1);
2323
+ const tag = file.slice(0, -".html".length).toLowerCase();
2324
+ const owners = tagToPaths.get(tag) ?? [];
2325
+ owners.push(`${DEFAULT_DIRS.components}/${relative}`);
2326
+ tagToPaths.set(tag, owners);
2327
+ }
2328
+ for (const [tag, paths] of [...tagToPaths].sort(([a], [b]) => a.localeCompare(b))) {
2329
+ if (paths.length > 1) {
2330
+ add(
2331
+ "error",
2332
+ "component-tag-collision",
2333
+ `Multiple components resolve to <${tag}>: ${paths.join(", ")}.`
2334
+ );
2335
+ }
2336
+ }
2337
+ const errors = diagnostics.filter((d) => d.severity === "error");
2338
+ const warnings = diagnostics.filter((d) => d.severity === "warning");
2339
+ return { diagnostics, errors, warnings, ok: errors.length === 0 };
2340
+ }
2341
+ function indexTree2(tree) {
2342
+ const items = /* @__PURE__ */ new Map();
2343
+ const dirs = /* @__PURE__ */ new Map();
2344
+ const published = [];
2345
+ const walk = (dir, inheritedTemplate) => {
2346
+ dirs.set(dir.path, dir);
2347
+ const dirTemplate = dir.defaultTemplate ?? inheritedTemplate;
2348
+ for (const child of dir.children) {
2349
+ if (child.kind === "dir") {
2350
+ walk(child, dirTemplate);
2351
+ continue;
2352
+ }
2353
+ items.set(child.path, child);
2354
+ if (child.item.isPublished) {
2355
+ published.push({ node: child, templateName: child.item.template ?? dirTemplate });
2356
+ }
2357
+ }
2358
+ };
2359
+ walk(tree, void 0);
2360
+ return { items, dirs, published };
2361
+ }
2362
+ function buildScope2(item) {
2363
+ return {
2364
+ ...item.fields,
2365
+ title: item.title,
2366
+ ...item.created !== void 0 && { created: item.created },
2367
+ ...item.published !== void 0 && { published: item.published },
2368
+ ...item.updated !== void 0 && { updated: item.updated }
2369
+ };
2370
+ }
2371
+ function isEmpty(value) {
2372
+ if (value === void 0 || value === null || value === "") return true;
2373
+ if (Array.isArray(value)) return value.length === 0;
2374
+ return false;
2375
+ }
2376
+ function asPaths(value) {
2377
+ if (typeof value === "string") return [value];
2378
+ if (Array.isArray(value))
2379
+ return value.filter((entry) => typeof entry === "string");
2380
+ return [];
2381
+ }
2382
+ function stripSlashes(path) {
2383
+ return path.replace(/^\/+/, "").replace(/\/+$/, "");
2384
+ }
2385
+ async function listComponentFiles(source) {
2386
+ const root = DEFAULT_DIRS.components;
2387
+ if (!await source.exists(root)) return [];
2388
+ const out = [];
2389
+ const walk = async (current, prefix) => {
2390
+ for (const entry of await source.list(current)) {
2391
+ const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
2392
+ if (entry.kind === "dir") await walk(`${current}/${entry.name}`, relative);
2393
+ else if (entry.name.endsWith(".html")) out.push(relative);
2394
+ }
2395
+ };
2396
+ await walk(root, "");
2397
+ return out.sort();
2398
+ }
2399
+
2400
+ // ../engine/src/publish/purge.ts
2401
+ var noopPurgeService = {
2402
+ async purgeAll() {
2403
+ }
2404
+ };
2405
+
2406
+ // ../engine/src/publish/publish.ts
2407
+ var DEFAULT_WRITE_CONCURRENCY = 8;
2408
+ async function publishSite(source, sink, options = {}) {
2409
+ const validation = await validateSite(source);
2410
+ if (!validation.ok) {
2411
+ return { ok: false, validation, written: [] };
2412
+ }
2413
+ const {
2414
+ purge = noopPurgeService,
2415
+ publicDir = DEFAULT_DIRS.public,
2416
+ prebuild,
2417
+ writeConcurrency = DEFAULT_WRITE_CONCURRENCY,
2418
+ ...compileOptions
2419
+ } = options;
2420
+ if (prebuild !== void 0) {
2421
+ await prebuild.run();
2422
+ }
2423
+ const artifacts = await compileSite(source, compileOptions);
2424
+ const passthrough = await collectPublic(source, publicDir);
2425
+ const byPath = /* @__PURE__ */ new Map();
2426
+ for (const artifact of [...artifacts, ...passthrough])
2427
+ byPath.set(artifact.path, artifact.contents);
2428
+ await mapWithConcurrency(
2429
+ [...byPath],
2430
+ writeConcurrency,
2431
+ ([path, contents]) => sink.write(path, contents)
2432
+ );
2433
+ await purge.purgeAll();
2434
+ return { ok: true, validation, written: [...byPath.keys()].sort() };
2435
+ }
2436
+ async function collectPublic(source, publicDir) {
2437
+ if (!await source.exists(publicDir)) return [];
2438
+ const out = [];
2439
+ const walk = async (dir, prefix) => {
2440
+ const entries = [...await source.list(dir)].sort((a, b) => a.name.localeCompare(b.name));
2441
+ for (const entry of entries) {
2442
+ const childPath = `${dir}/${entry.name}`;
2443
+ const outPath = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
2444
+ if (entry.kind === "dir") {
2445
+ await walk(childPath, outPath);
2446
+ } else {
2447
+ out.push({ path: outPath, contents: await source.readBytes(childPath) });
2448
+ }
2449
+ }
2450
+ };
2451
+ await walk(publicDir, "");
2452
+ return out;
2453
+ }
2454
+
2455
+ // ../engine/src/publish/sink.ts
2456
+ function normalizePath2(path) {
2457
+ return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
2458
+ }
2459
+ var InMemoryArtifactSink = class {
2460
+ store = /* @__PURE__ */ new Map();
2461
+ async write(path, contents) {
2462
+ this.store.set(normalizePath2(path), contents);
2463
+ }
2464
+ /** The artifacts written so far, path→contents, for assertions in tests. */
2465
+ get written() {
2466
+ return this.store;
2467
+ }
2468
+ };
2469
+
2470
+ // ../engine/src/identity.ts
2471
+ function hasRole(principal, role) {
2472
+ if (principal === null) return false;
2473
+ if (principal.roles.includes("admin")) return true;
2474
+ return principal.roles.includes(role);
2475
+ }
2476
+ function canEdit(principal) {
2477
+ return hasRole(principal, "author") || hasRole(principal, "developer");
2478
+ }
2479
+
2480
+ export {
2481
+ slugify,
2482
+ humanize,
2483
+ contentTypeFor,
2484
+ ContentParseError,
2485
+ parseContentItem,
2486
+ parseSortFile,
2487
+ InMemoryBlobStore,
2488
+ emptyIndex,
2489
+ loadIndex,
2490
+ saveIndex,
2491
+ IndexedStore,
2492
+ InMemoryContentSource,
2493
+ parseRedirects,
2494
+ buildRedirects,
2495
+ DEFAULT_DIRS,
2496
+ loadContentTree,
2497
+ loadRedirects,
2498
+ loadSiteConfig,
2499
+ loadComponents,
2500
+ loadComponentStyles,
2501
+ loadComponentScripts,
2502
+ loadTemplate,
2503
+ DOCUMENT_SHELL,
2504
+ loadDocumentShell,
2505
+ FIELD_TYPES,
2506
+ isFieldType,
2507
+ valueKindOf,
2508
+ inferControl,
2509
+ findTokens,
2510
+ wholeValueToken,
2511
+ escapeHtmlText,
2512
+ escapeHtmlAttribute,
2513
+ escapeJsonStringContent,
2514
+ sanitizeUrl,
2515
+ toAuthorRefs,
2516
+ joinAuthors,
2517
+ renderAuthors,
2518
+ contentHash,
2519
+ processImage,
2520
+ buildPictureMarkup,
2521
+ compileTemplate,
2522
+ textContent,
2523
+ buildSitemap,
2524
+ buildRss,
2525
+ buildContentIndex,
2526
+ deriveFields,
2527
+ analyzeTemplate,
2528
+ outputPathForItem,
2529
+ urlForItem,
2530
+ compileSite,
2531
+ compilePage,
2532
+ buildResolveContext,
2533
+ buildAuthoringReference,
2534
+ renderReferenceMarkdown,
2535
+ validateSite,
2536
+ noopPurgeService,
2537
+ publishSite,
2538
+ InMemoryArtifactSink,
2539
+ hasRole,
2540
+ canEdit
2541
+ };