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