@xtrable-ltd/nanoesis 0.1.9 → 0.1.11
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-XO3CT6GL.js} +1699 -1167
- package/dist/editor-api.d.ts +39 -2
- package/dist/editor-api.js +396 -6
- package/dist/index.d.ts +491 -222
- package/dist/index.js +25 -1
- package/editor/assets/MigrationsPane-BAHPPSXP.css +1 -0
- package/editor/assets/MigrationsPane-BYGqWBAA.js +4 -0
- package/editor/assets/{TemplatesPane-CHzfB00-.js → TemplatesPane-B5hn_v0Z.js} +208 -202
- package/editor/assets/{TemplatesPane-B4_sg2u5.css → TemplatesPane-D0gGehUt.css} +1 -1
- package/editor/assets/{cssMode-BahdJh1A.js → cssMode-BbIf5k6I.js} +1 -1
- package/editor/assets/{freemarker2-2FC3twUE.js → freemarker2-DoW0pSYV.js} +1 -1
- package/editor/assets/{handlebars-pMjPHNx1.js → handlebars-DLlET-qc.js} +1 -1
- package/editor/assets/{html-KTToTG0n.js → html-4khbqrhe.js} +1 -1
- package/editor/assets/{htmlMode-ufik94dZ.js → htmlMode-DblHkZ-k.js} +1 -1
- package/editor/assets/index-CkESQLMV.css +7 -0
- package/editor/assets/index-Do1drqEQ.js +138 -0
- package/editor/assets/{javascript-CD4kAZXr.js → javascript-CgPO2Hmj.js} +1 -1
- package/editor/assets/{jsonMode-ClHucayn.js → jsonMode-BrWh2436.js} +1 -1
- package/editor/assets/{liquid-B-uYib60.js → liquid-BsQJXwPT.js} +1 -1
- package/editor/assets/{mdx-BOc9oMkZ.js → mdx-AO8t67gx.js} +1 -1
- package/editor/assets/{python-BipLFHGs.js → python-3w4sZj5c.js} +1 -1
- package/editor/assets/{razor-C0di_gwM.js → razor-BFsvo06w.js} +1 -1
- package/editor/assets/{tsMode-B7fenrcD.js → tsMode-QrC4ERjp.js} +1 -1
- package/editor/assets/{typescript-CDg7c2A-.js → typescript-BXJ3QLad.js} +1 -1
- package/editor/assets/{xml-DTAdn5Pw.js → xml-CxKYn1FP.js} +1 -1
- package/editor/assets/{yaml-B9-OjY0Z.js → yaml-BmWLvF7Q.js} +1 -1
- package/editor/index.html +2 -2
- package/package.json +1 -1
- package/editor/assets/index-BsRGVHEP.css +0 -7
- package/editor/assets/index-CPKtfzWD.js +0 -134
|
@@ -154,785 +154,1029 @@ 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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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]));
|
|
335
|
+
}
|
|
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);
|
|
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;
|
|
449
|
+
}
|
|
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
|
+
}
|
|
593
455
|
}
|
|
456
|
+
return void 0;
|
|
594
457
|
}
|
|
595
|
-
function
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ../engine/src/template/stamp.ts
|
|
474
|
+
function isDestructiveTypeChange(from, to) {
|
|
475
|
+
if (from === to) return false;
|
|
476
|
+
const fromKind = valueKindOf(from);
|
|
477
|
+
const toKind = valueKindOf(to);
|
|
478
|
+
if (toKind === "html") return false;
|
|
479
|
+
if (fromKind === "html") return true;
|
|
480
|
+
if (from === "date" && to === "time" || from === "time" && to === "date") return true;
|
|
481
|
+
const isFreeText = (t) => t === "shorttext" || t === "text" || t === "code" || t === "email" || t === "phone";
|
|
482
|
+
if (isFreeText(from) && (to === "date" || to === "time")) return true;
|
|
483
|
+
if (fromKind === toKind) return false;
|
|
484
|
+
if (toKind === "text") return false;
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
function computeSchemaDelta(oldFields, newFields) {
|
|
488
|
+
const oldByName = new Map(oldFields.map((f) => [f.name, f]));
|
|
489
|
+
const newByName = new Map(newFields.map((f) => [f.name, f]));
|
|
490
|
+
const added = [];
|
|
491
|
+
const removed = [];
|
|
492
|
+
const typeChanged = [];
|
|
493
|
+
for (const [name, oldField] of oldByName) {
|
|
494
|
+
const newField = newByName.get(name);
|
|
495
|
+
if (newField === void 0) {
|
|
496
|
+
removed.push({ name, type: oldField.type });
|
|
497
|
+
} else if (newField.type !== oldField.type) {
|
|
498
|
+
typeChanged.push({
|
|
499
|
+
name,
|
|
500
|
+
from: oldField.type,
|
|
501
|
+
to: newField.type,
|
|
502
|
+
destructive: isDestructiveTypeChange(oldField.type, newField.type)
|
|
503
|
+
});
|
|
603
504
|
}
|
|
604
505
|
}
|
|
605
|
-
for (const [
|
|
606
|
-
if (!
|
|
506
|
+
for (const [name, newField] of newByName) {
|
|
507
|
+
if (!oldByName.has(name)) added.push({ name, type: newField.type });
|
|
607
508
|
}
|
|
608
|
-
return
|
|
509
|
+
return {
|
|
510
|
+
added: added.sort((a, b) => a.name.localeCompare(b.name)),
|
|
511
|
+
removed: removed.sort((a, b) => a.name.localeCompare(b.name)),
|
|
512
|
+
typeChanged: typeChanged.sort((a, b) => a.name.localeCompare(b.name))
|
|
513
|
+
};
|
|
609
514
|
}
|
|
610
|
-
function
|
|
611
|
-
|
|
515
|
+
function detectStamp(oldSource, newSource, components = /* @__PURE__ */ new Map()) {
|
|
516
|
+
const oldFields = deriveFields(oldSource, components);
|
|
517
|
+
const newFields = deriveFields(newSource, components);
|
|
518
|
+
const delta = computeSchemaDelta(oldFields, newFields);
|
|
519
|
+
const destructive = delta.removed.length > 0 || delta.typeChanged.some((change) => change.destructive);
|
|
520
|
+
return {
|
|
521
|
+
destructive,
|
|
522
|
+
removedTokens: delta.removed.map((r) => r.name),
|
|
523
|
+
addedTokens: delta.added.map((a) => a.name),
|
|
524
|
+
typeChanged: delta.typeChanged
|
|
525
|
+
};
|
|
612
526
|
}
|
|
613
|
-
|
|
614
|
-
|
|
527
|
+
|
|
528
|
+
// ../engine/src/template/versions.ts
|
|
529
|
+
var VERSION_PATTERN = /^(.+)@v([1-9]\d*)$/;
|
|
530
|
+
function parseVersionedName(name) {
|
|
531
|
+
const match = VERSION_PATTERN.exec(name);
|
|
532
|
+
if (!match) return null;
|
|
533
|
+
const base = match[1];
|
|
534
|
+
if (VERSION_PATTERN.test(base)) return null;
|
|
535
|
+
return { base, version: Number.parseInt(match[2], 10) };
|
|
536
|
+
}
|
|
537
|
+
function isVersionedTemplateName(name) {
|
|
538
|
+
return parseVersionedName(name) !== null;
|
|
539
|
+
}
|
|
540
|
+
function baseTemplateName(name) {
|
|
541
|
+
const parsed = parseVersionedName(name);
|
|
542
|
+
return parsed ? parsed.base : name;
|
|
543
|
+
}
|
|
544
|
+
function versionNumber(name) {
|
|
545
|
+
const parsed = parseVersionedName(name);
|
|
546
|
+
return parsed ? parsed.version : null;
|
|
547
|
+
}
|
|
548
|
+
function snapshotName(base, version) {
|
|
549
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
550
|
+
throw new Error(`Snapshot version must be a positive integer, got ${version}`);
|
|
551
|
+
}
|
|
552
|
+
if (isVersionedTemplateName(base)) {
|
|
553
|
+
throw new Error(`snapshotName base "${base}" is already versioned`);
|
|
554
|
+
}
|
|
555
|
+
return `${base}@v${version}`;
|
|
556
|
+
}
|
|
557
|
+
function nextVersionNumber(existingNames) {
|
|
558
|
+
let max = 0;
|
|
559
|
+
for (const name of existingNames) {
|
|
560
|
+
const n = versionNumber(name);
|
|
561
|
+
if (n !== null && n > max) max = n;
|
|
562
|
+
}
|
|
563
|
+
return max + 1;
|
|
564
|
+
}
|
|
565
|
+
function isReservedVersionedPath(path) {
|
|
566
|
+
if (!path.endsWith(".html")) return false;
|
|
567
|
+
if (!(path.startsWith("templates/") || path.startsWith("components/"))) return false;
|
|
568
|
+
const stem = path.slice(0, -".html".length);
|
|
569
|
+
const lastSlash = stem.lastIndexOf("/");
|
|
570
|
+
const baseName = stem.slice(lastSlash + 1);
|
|
571
|
+
return isVersionedTemplateName(baseName);
|
|
615
572
|
}
|
|
616
|
-
|
|
617
|
-
|
|
573
|
+
|
|
574
|
+
// ../engine/src/store/content-index.ts
|
|
575
|
+
var RESERVED_PREFIX = ".nanoesis/";
|
|
576
|
+
var INDEX_KEY = `${RESERVED_PREFIX}index.json`;
|
|
577
|
+
var BACKUP_RING_SIZE = 3;
|
|
578
|
+
function backupKey(slot) {
|
|
579
|
+
return `${RESERVED_PREFIX}index.bak.${slot}`;
|
|
618
580
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
map.set(key, await source.readText(full));
|
|
631
|
-
}
|
|
581
|
+
function emptyIndex() {
|
|
582
|
+
return freezeIndex(0, []);
|
|
583
|
+
}
|
|
584
|
+
async function loadIndex(store) {
|
|
585
|
+
const live = parseIndex(await store.get(INDEX_KEY));
|
|
586
|
+
if (live !== void 0) return live;
|
|
587
|
+
let best;
|
|
588
|
+
for (let slot = 0; slot < BACKUP_RING_SIZE; slot += 1) {
|
|
589
|
+
const candidate = parseIndex(await store.get(backupKey(slot)));
|
|
590
|
+
if (candidate !== void 0 && (best === void 0 || candidate.version > best.version)) {
|
|
591
|
+
best = candidate;
|
|
632
592
|
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
return map;
|
|
593
|
+
}
|
|
594
|
+
return best ?? emptyIndex();
|
|
636
595
|
}
|
|
637
|
-
async function
|
|
638
|
-
|
|
596
|
+
async function saveIndex(store, prev, nextKeys) {
|
|
597
|
+
await store.put(backupKey(prev.version % BACKUP_RING_SIZE), serialize2(prev));
|
|
598
|
+
const next = freezeIndex(prev.version + 1, nextKeys);
|
|
599
|
+
await store.put(INDEX_KEY, serialize2(next));
|
|
600
|
+
return next;
|
|
639
601
|
}
|
|
640
|
-
function
|
|
641
|
-
|
|
602
|
+
async function reconcileIndex(store, actualKeys) {
|
|
603
|
+
const prev = await loadIndex(store);
|
|
604
|
+
const keys = [...new Set(actualKeys.filter((key) => !key.startsWith(RESERVED_PREFIX)))].sort();
|
|
605
|
+
const prevKeys = new Set(prev.keys);
|
|
606
|
+
const nextKeys = new Set(keys);
|
|
607
|
+
const added = keys.filter((key) => !prevKeys.has(key));
|
|
608
|
+
const removed = prev.keys.filter((key) => !nextKeys.has(key));
|
|
609
|
+
if (added.length === 0 && removed.length === 0) return { index: prev, added, removed };
|
|
610
|
+
return { index: await saveIndex(store, prev, keys), added, removed };
|
|
642
611
|
}
|
|
643
|
-
function
|
|
644
|
-
|
|
612
|
+
function freezeIndex(version, keys) {
|
|
613
|
+
const sorted = [...new Set(keys)].sort();
|
|
614
|
+
return { version, keys: sorted, checksum: checksumOf(version, sorted) };
|
|
645
615
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
616
|
+
function serialize2(index) {
|
|
617
|
+
return new TextEncoder().encode(`${JSON.stringify(index, null, 2)}
|
|
618
|
+
`);
|
|
649
619
|
}
|
|
650
|
-
|
|
620
|
+
function parseIndex(bytes) {
|
|
621
|
+
if (bytes === void 0) return void 0;
|
|
622
|
+
let raw;
|
|
651
623
|
try {
|
|
652
|
-
|
|
624
|
+
raw = JSON.parse(new TextDecoder().decode(bytes));
|
|
653
625
|
} catch {
|
|
654
626
|
return void 0;
|
|
655
627
|
}
|
|
628
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
629
|
+
const { version, keys, checksum } = raw;
|
|
630
|
+
if (typeof version !== "number" || typeof checksum !== "string" || !Array.isArray(keys)) {
|
|
631
|
+
return void 0;
|
|
632
|
+
}
|
|
633
|
+
const stringKeys = keys.filter((key) => typeof key === "string");
|
|
634
|
+
if (stringKeys.length !== keys.length) return void 0;
|
|
635
|
+
const rebuilt = freezeIndex(version, stringKeys);
|
|
636
|
+
return rebuilt.checksum === checksum ? rebuilt : void 0;
|
|
656
637
|
}
|
|
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;
|
|
638
|
+
function checksumOf(version, sortedKeys) {
|
|
639
|
+
const text = `${version}
|
|
640
|
+
${sortedKeys.join("\n")}`;
|
|
641
|
+
let hash = 2166136261;
|
|
642
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
643
|
+
hash ^= text.charCodeAt(i);
|
|
644
|
+
hash = Math.imul(hash, 16777619);
|
|
645
|
+
}
|
|
646
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
678
647
|
}
|
|
679
648
|
|
|
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;
|
|
649
|
+
// ../engine/src/store/indexed-store.ts
|
|
650
|
+
var IndexedStore = class {
|
|
651
|
+
constructor(store) {
|
|
652
|
+
this.store = store;
|
|
724
653
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
654
|
+
store;
|
|
655
|
+
index;
|
|
656
|
+
loading;
|
|
657
|
+
/**
|
|
658
|
+
* Per-instance mutex (DESIGN §11d): every mutation (`write`/`delete`/`rename`/`reconcile`)
|
|
659
|
+
* runs through {@link serializeMutation}, which chains onto the previous mutation's
|
|
660
|
+
* completion. Without it, two concurrent mutations both read the cached `this.index`,
|
|
661
|
+
* each compute their "next" key set from that stale snapshot, and the second
|
|
662
|
+
* `saveIndex` clobbers the first — the file writes land in blob, but the index keeps
|
|
663
|
+
* stale references or loses fresh ones (the "I deleted it and it's still there" or
|
|
664
|
+
* "I added it and it didn't appear" bugs surfaced dogfooding the marketing site,
|
|
665
|
+
* 2026-05-28). Reads (`list`/`exists`/`readBytes`) are *not* serialised; an
|
|
666
|
+
* in-flight mutation simply means a reader sees the pre-mutation index, eventually
|
|
667
|
+
* consistent and safe. Crashes inside `work()` release the lock so a single failure
|
|
668
|
+
* does not deadlock subsequent operations.
|
|
669
|
+
*/
|
|
670
|
+
mutationLock = Promise.resolve();
|
|
671
|
+
/** The index, loaded once on first need and cached (mutations replace the cached copy). */
|
|
672
|
+
async loaded() {
|
|
673
|
+
if (this.index !== void 0) return this.index;
|
|
674
|
+
this.loading ??= loadIndex(this.store);
|
|
675
|
+
this.index = await this.loading;
|
|
676
|
+
return this.index;
|
|
740
677
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
678
|
+
serializeMutation(work) {
|
|
679
|
+
const previous = this.mutationLock;
|
|
680
|
+
let release;
|
|
681
|
+
this.mutationLock = new Promise((resolve) => {
|
|
682
|
+
release = resolve;
|
|
683
|
+
});
|
|
684
|
+
return (async () => {
|
|
685
|
+
try {
|
|
686
|
+
await previous;
|
|
687
|
+
return await work();
|
|
688
|
+
} finally {
|
|
689
|
+
release();
|
|
690
|
+
}
|
|
691
|
+
})();
|
|
755
692
|
}
|
|
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;
|
|
693
|
+
async list(dir) {
|
|
694
|
+
return childrenOf((await this.loaded()).keys, dir);
|
|
791
695
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
return "#";
|
|
696
|
+
async readText(path) {
|
|
697
|
+
return new TextDecoder().decode(await this.readBytes(path));
|
|
795
698
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
699
|
+
async readBytes(path) {
|
|
700
|
+
const bytes = await this.store.get(path);
|
|
701
|
+
if (bytes === void 0) throw new Error(`No such file in content source: ${path}`);
|
|
702
|
+
return bytes;
|
|
703
|
+
}
|
|
704
|
+
async exists(path) {
|
|
705
|
+
return pathExists((await this.loaded()).keys, path);
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Create or overwrite `key`. The index is rewritten only when `key` is new (an
|
|
709
|
+
* overwrite leaves the key set unchanged, so editing an item is a single `put`).
|
|
710
|
+
*
|
|
711
|
+
* **Auto-stamp** (PROPOSAL §4.1): a destructive write to a template or component
|
|
712
|
+
* path (one that removes a token from the existing source) snapshots the prior
|
|
713
|
+
* bytes to `<base>@v<N+1>.html` before the new bytes land. Order is
|
|
714
|
+
* **current first, snapshot second** (§16.3): if the snapshot copy fails after
|
|
715
|
+
* the current write succeeded, the site stays consistent — only the migration
|
|
716
|
+
* UX's left-pane comparison is unavailable for items affected by this stamp,
|
|
717
|
+
* surfaced as `templates.stamp-incomplete`.
|
|
718
|
+
*
|
|
719
|
+
* Direct writes to a reserved-version path are refused (snapshots are
|
|
720
|
+
* immutable; the user retires them via `delete`, not by overwriting them).
|
|
721
|
+
*/
|
|
722
|
+
write(key, bytes) {
|
|
723
|
+
return this.serializeMutation(async () => {
|
|
724
|
+
const target = guarded(normalize(key));
|
|
725
|
+
if (isReservedVersionedPath(target)) {
|
|
726
|
+
throw new Error(
|
|
727
|
+
`Refusing to write to a reserved-version path: ${target} (snapshots are immutable; retire via delete)`
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
const stampCandidate = stampTargetOf(target);
|
|
731
|
+
const previousBytes = stampCandidate !== void 0 ? await this.store.get(target) : void 0;
|
|
732
|
+
let stampDecision;
|
|
733
|
+
let schemaDelta;
|
|
734
|
+
if (stampCandidate !== void 0 && previousBytes !== void 0) {
|
|
735
|
+
const oldSource = new TextDecoder().decode(previousBytes);
|
|
736
|
+
const newSource = new TextDecoder().decode(bytes);
|
|
737
|
+
const oldFields = deriveFields(oldSource);
|
|
738
|
+
const newFields = deriveFields(newSource);
|
|
739
|
+
schemaDelta = computeSchemaDelta(oldFields, newFields);
|
|
740
|
+
const destructive = schemaDelta.removed.length > 0 || schemaDelta.typeChanged.some((change) => change.destructive);
|
|
741
|
+
if (destructive) {
|
|
742
|
+
const index2 = await this.loaded();
|
|
743
|
+
const siblings = index2.keys.filter((k) => k.startsWith(`${stampCandidate.dir}/`) && k.endsWith(".html")).map((k) => k.slice(stampCandidate.dir.length + 1, -".html".length));
|
|
744
|
+
const version = nextVersionNumber(siblings);
|
|
745
|
+
stampDecision = {
|
|
746
|
+
snapshotPath: `${stampCandidate.dir}/${stampCandidate.name}@v${version}.html`,
|
|
747
|
+
bytes: previousBytes,
|
|
748
|
+
version
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
await this.store.put(target, bytes);
|
|
753
|
+
const index = await this.loaded();
|
|
754
|
+
let nextIndex = index;
|
|
755
|
+
if (!nextIndex.keys.includes(target)) {
|
|
756
|
+
nextIndex = await saveIndex(this.store, nextIndex, [...nextIndex.keys, target]);
|
|
757
|
+
}
|
|
758
|
+
let stamped;
|
|
759
|
+
let stampIncomplete;
|
|
760
|
+
if (stampDecision !== void 0) {
|
|
761
|
+
try {
|
|
762
|
+
await this.store.put(stampDecision.snapshotPath, stampDecision.bytes);
|
|
763
|
+
if (!nextIndex.keys.includes(stampDecision.snapshotPath)) {
|
|
764
|
+
nextIndex = await saveIndex(this.store, nextIndex, [
|
|
765
|
+
...nextIndex.keys,
|
|
766
|
+
stampDecision.snapshotPath
|
|
767
|
+
]);
|
|
768
|
+
}
|
|
769
|
+
stamped = {
|
|
770
|
+
name: stampCandidate.name,
|
|
771
|
+
version: stampDecision.version,
|
|
772
|
+
snapshotPath: stampDecision.snapshotPath
|
|
773
|
+
};
|
|
774
|
+
} catch {
|
|
775
|
+
stampIncomplete = true;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
this.index = nextIndex;
|
|
779
|
+
return {
|
|
780
|
+
...stamped !== void 0 && { stamped },
|
|
781
|
+
...stampIncomplete === true && { stampIncomplete: true },
|
|
782
|
+
...schemaDelta !== void 0 && { schemaDelta }
|
|
783
|
+
};
|
|
812
784
|
});
|
|
813
785
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
|
|
786
|
+
/**
|
|
787
|
+
* Manually stamp a snapshot of the current `<kind>/<name>.html` to the next available
|
|
788
|
+
* `<name>@v<N+1>.html` slot. Uses the same internal `this.store.put` pathway the
|
|
789
|
+
* auto-stamp uses (so the public `write`'s reserved-version refusal doesn't apply),
|
|
790
|
+
* and runs under the mutation mutex so a concurrent destructive auto-stamp can't
|
|
791
|
+
* race with a manual stamp.
|
|
792
|
+
*/
|
|
793
|
+
stamp(name, kind) {
|
|
794
|
+
return this.serializeMutation(async () => {
|
|
795
|
+
if (isVersionedTemplateName(name)) {
|
|
796
|
+
throw new Error(`stamp: cannot stamp an already-versioned name (${name})`);
|
|
797
|
+
}
|
|
798
|
+
const dir = kind === "template" ? "templates" : "components";
|
|
799
|
+
const currentPath = `${dir}/${name}.html`;
|
|
800
|
+
const bytes = await this.store.get(currentPath);
|
|
801
|
+
if (bytes === void 0) {
|
|
802
|
+
throw new Error(`stamp: ${currentPath} does not exist (nothing to snapshot)`);
|
|
803
|
+
}
|
|
804
|
+
const index = await this.loaded();
|
|
805
|
+
const siblings = index.keys.filter((k) => k.startsWith(`${dir}/`) && k.endsWith(".html")).map((k) => k.slice(dir.length + 1, -".html".length));
|
|
806
|
+
const version = nextVersionNumber(siblings);
|
|
807
|
+
const snapshotPath = `${dir}/${name}@v${version}.html`;
|
|
808
|
+
await this.store.put(snapshotPath, bytes);
|
|
809
|
+
if (!index.keys.includes(snapshotPath)) {
|
|
810
|
+
this.index = await saveIndex(this.store, index, [...index.keys, snapshotPath]);
|
|
811
|
+
} else {
|
|
812
|
+
this.index = index;
|
|
813
|
+
}
|
|
814
|
+
return { name, version, snapshotPath };
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Delete a file, or a whole directory subtree (every key under `key/`). Idempotent: a
|
|
819
|
+
* path the index does not know is still deleted from the store (clearing an orphan),
|
|
820
|
+
* and deleting nothing is a no-op.
|
|
821
|
+
*/
|
|
822
|
+
delete(key) {
|
|
823
|
+
return this.serializeMutation(async () => {
|
|
824
|
+
const target = guarded(normalize(key));
|
|
825
|
+
const index = await this.loaded();
|
|
826
|
+
const prefix = `${target}/`;
|
|
827
|
+
const removed = index.keys.filter((k) => k === target || k.startsWith(prefix));
|
|
828
|
+
if (removed.length === 0) {
|
|
829
|
+
await this.store.delete(target);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
await Promise.all(removed.map((k) => this.store.delete(k)));
|
|
833
|
+
const remaining = index.keys.filter((k) => k !== target && !k.startsWith(prefix));
|
|
834
|
+
this.index = await saveIndex(this.store, index, remaining);
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Move/rename a file, or a whole directory subtree (every key under `from/` is remapped
|
|
839
|
+
* under `to/`). Clobbering an existing destination, or renaming a missing path, are
|
|
840
|
+
* returned as data (`ok: false`), not thrown (CLAUDE §2), the same contract the host's
|
|
841
|
+
* `/api/rename` enforces. (Mutating the reserved namespace is a programmer error and
|
|
842
|
+
* still throws.)
|
|
843
|
+
*/
|
|
844
|
+
rename(from, to) {
|
|
845
|
+
return this.serializeMutation(async () => {
|
|
846
|
+
const source = guarded(normalize(from));
|
|
847
|
+
const dest = guarded(normalize(to));
|
|
848
|
+
if (source === dest) return { ok: true };
|
|
849
|
+
if (isReservedVersionedPath(source) || isReservedVersionedPath(dest)) {
|
|
850
|
+
return { ok: false, reason: "reserved-version" };
|
|
851
|
+
}
|
|
852
|
+
const index = await this.loaded();
|
|
853
|
+
const sourcePrefix = `${source}/`;
|
|
854
|
+
const affected = index.keys.filter((k) => k === source || k.startsWith(sourcePrefix));
|
|
855
|
+
if (affected.length === 0) return { ok: false, reason: "missing" };
|
|
856
|
+
const destPrefix = `${dest}/`;
|
|
857
|
+
if (index.keys.some((k) => k === dest || k.startsWith(destPrefix))) {
|
|
858
|
+
return { ok: false, reason: "exists" };
|
|
859
|
+
}
|
|
860
|
+
const moves = affected.map((k) => ({
|
|
861
|
+
from: k,
|
|
862
|
+
to: k === source ? dest : dest + k.slice(source.length)
|
|
863
|
+
}));
|
|
864
|
+
for (const move of moves) {
|
|
865
|
+
const bytes = await this.store.get(move.from);
|
|
866
|
+
if (bytes === void 0) continue;
|
|
867
|
+
await this.store.put(move.to, bytes);
|
|
868
|
+
await this.store.delete(move.from);
|
|
869
|
+
}
|
|
870
|
+
const movedFrom = new Set(moves.map((move) => move.from));
|
|
871
|
+
const next = index.keys.filter((k) => !movedFrom.has(k)).concat(moves.map((m) => m.to));
|
|
872
|
+
this.index = await saveIndex(this.store, index, next);
|
|
873
|
+
return { ok: true };
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Rebuild this store's index from the *actual* keys the underlying store holds (DESIGN
|
|
878
|
+
* §11d), recovering files that arrived by a path that bypassed the index. A
|
|
879
|
+
* {@link BlobStore} cannot enumerate itself, so the caller supplies the real key set
|
|
880
|
+
* from an adapter that can (e.g. `BlobContainer.list`). Unlike calling
|
|
881
|
+
* {@link reconcileIndex} on a fresh store, this also refreshes the cached in-memory
|
|
882
|
+
* index, so this live instance sees the recovered files immediately.
|
|
883
|
+
*/
|
|
884
|
+
reconcile(actualKeys) {
|
|
885
|
+
return this.serializeMutation(async () => {
|
|
886
|
+
const result = await reconcileIndex(this.store, actualKeys);
|
|
887
|
+
this.index = result.index;
|
|
888
|
+
return result;
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
function stampTargetOf(target) {
|
|
893
|
+
if (!target.endsWith(".html")) return void 0;
|
|
894
|
+
const dir = target.startsWith("templates/") ? "templates" : target.startsWith("components/") ? "components" : void 0;
|
|
895
|
+
if (dir === void 0) return void 0;
|
|
896
|
+
const stem = target.slice(dir.length + 1, -".html".length);
|
|
897
|
+
if (stem === "" || stem.includes("@v")) return void 0;
|
|
898
|
+
return { dir, name: stem };
|
|
838
899
|
}
|
|
839
|
-
function
|
|
840
|
-
|
|
900
|
+
function guarded(key) {
|
|
901
|
+
if (key === "" || key.startsWith(RESERVED_PREFIX)) {
|
|
902
|
+
throw new Error(`Refusing to mutate a reserved key: ${key === "" ? "(root)" : key}`);
|
|
903
|
+
}
|
|
904
|
+
return key;
|
|
841
905
|
}
|
|
842
|
-
function
|
|
843
|
-
return
|
|
906
|
+
function normalize(path) {
|
|
907
|
+
return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
844
908
|
}
|
|
845
|
-
function
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
909
|
+
function childrenOf(keys, dir) {
|
|
910
|
+
const base = normalize(dir);
|
|
911
|
+
const prefix = base === "" ? "" : `${base}/`;
|
|
912
|
+
const entries = /* @__PURE__ */ new Map();
|
|
913
|
+
for (const key of keys) {
|
|
914
|
+
if (prefix !== "" && !key.startsWith(prefix)) continue;
|
|
915
|
+
const rest = key.slice(prefix.length);
|
|
916
|
+
if (rest === "") continue;
|
|
917
|
+
const slash = rest.indexOf("/");
|
|
918
|
+
if (slash === -1) entries.set(rest, "file");
|
|
919
|
+
else entries.set(rest.slice(0, slash), "dir");
|
|
920
|
+
}
|
|
921
|
+
return [...entries].map(([name, kind]) => ({ name, kind })).sort((a, b) => a.name.localeCompare(b.name));
|
|
849
922
|
}
|
|
850
|
-
function
|
|
851
|
-
|
|
923
|
+
function pathExists(keys, path) {
|
|
924
|
+
const target = normalize(path);
|
|
925
|
+
if (target === "") return true;
|
|
926
|
+
if (keys.includes(target)) return true;
|
|
927
|
+
const prefix = `${target}/`;
|
|
928
|
+
return keys.some((key) => key.startsWith(prefix));
|
|
852
929
|
}
|
|
853
|
-
|
|
854
|
-
|
|
930
|
+
|
|
931
|
+
// ../engine/src/content/source.ts
|
|
932
|
+
function normalizePath(path) {
|
|
933
|
+
return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
855
934
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
935
|
+
var InMemoryContentSource = class {
|
|
936
|
+
files;
|
|
937
|
+
constructor(files) {
|
|
938
|
+
this.files = new Map(Object.entries(files).map(([key, value]) => [normalizePath(key), value]));
|
|
939
|
+
}
|
|
940
|
+
async readText(path) {
|
|
941
|
+
const value = this.files.get(normalizePath(path));
|
|
942
|
+
if (value === void 0) throw new Error(`No such file in content source: ${path}`);
|
|
943
|
+
return typeof value === "string" ? value : new TextDecoder().decode(value);
|
|
944
|
+
}
|
|
945
|
+
async readBytes(path) {
|
|
946
|
+
const value = this.files.get(normalizePath(path));
|
|
947
|
+
if (value === void 0) throw new Error(`No such file in content source: ${path}`);
|
|
948
|
+
return typeof value === "string" ? new TextEncoder().encode(value) : value;
|
|
949
|
+
}
|
|
950
|
+
async exists(path) {
|
|
951
|
+
const target = normalizePath(path);
|
|
952
|
+
if (target === "") return true;
|
|
953
|
+
if (this.files.has(target)) return true;
|
|
954
|
+
const prefix = `${target}/`;
|
|
955
|
+
for (const key of this.files.keys()) {
|
|
956
|
+
if (key.startsWith(prefix)) return true;
|
|
957
|
+
}
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
async list(dir) {
|
|
961
|
+
const base = normalizePath(dir);
|
|
962
|
+
const prefix = base === "" ? "" : `${base}/`;
|
|
963
|
+
const entries = /* @__PURE__ */ new Map();
|
|
964
|
+
for (const key of this.files.keys()) {
|
|
965
|
+
if (prefix !== "" && !key.startsWith(prefix)) continue;
|
|
966
|
+
const rest = key.slice(prefix.length);
|
|
967
|
+
if (rest === "") continue;
|
|
968
|
+
const slash = rest.indexOf("/");
|
|
969
|
+
if (slash === -1) entries.set(rest, "file");
|
|
970
|
+
else entries.set(rest.slice(0, slash), "dir");
|
|
971
|
+
}
|
|
972
|
+
return [...entries].map(([name, kind]) => ({ name, kind })).sort((a, b) => a.name.localeCompare(b.name));
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
// ../engine/src/url/redirects.ts
|
|
977
|
+
var DEFAULT_STATUS = 301;
|
|
978
|
+
var OUTPUT_PATH = "_redirects";
|
|
979
|
+
function isRule(value) {
|
|
980
|
+
if (typeof value !== "object" || value === null) return false;
|
|
981
|
+
const record = value;
|
|
982
|
+
return typeof record.from === "string" && typeof record.to === "string";
|
|
859
983
|
}
|
|
860
|
-
function
|
|
861
|
-
|
|
984
|
+
function parseRedirects(raw) {
|
|
985
|
+
if (!Array.isArray(raw)) return [];
|
|
986
|
+
const rules = [];
|
|
987
|
+
for (const entry of raw) {
|
|
988
|
+
if (!isRule(entry)) continue;
|
|
989
|
+
const status = entry.status;
|
|
990
|
+
rules.push(
|
|
991
|
+
typeof status === "number" && Number.isInteger(status) ? { from: entry.from, to: entry.to, status } : { from: entry.from, to: entry.to }
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
return rules;
|
|
862
995
|
}
|
|
863
|
-
function
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
996
|
+
function resolveTarget(from, exact) {
|
|
997
|
+
const seen = /* @__PURE__ */ new Set([from]);
|
|
998
|
+
let target = exact.get(from).to;
|
|
999
|
+
while (exact.has(target) && !seen.has(target)) {
|
|
1000
|
+
seen.add(target);
|
|
1001
|
+
target = exact.get(target).to;
|
|
1002
|
+
}
|
|
1003
|
+
return target;
|
|
1004
|
+
}
|
|
1005
|
+
function buildRedirects(rules, liveUrls) {
|
|
1006
|
+
const exact = /* @__PURE__ */ new Map();
|
|
1007
|
+
const wildcard = /* @__PURE__ */ new Map();
|
|
1008
|
+
for (const rule of rules) {
|
|
1009
|
+
(rule.from.includes("*") ? wildcard : exact).set(rule.from, rule);
|
|
1010
|
+
}
|
|
1011
|
+
const resolved = [];
|
|
1012
|
+
for (const [from, rule] of exact) {
|
|
1013
|
+
if (liveUrls.has(from)) continue;
|
|
1014
|
+
const to = resolveTarget(from, exact);
|
|
1015
|
+
if (to === from) continue;
|
|
1016
|
+
resolved.push(rule.status !== void 0 ? { from, to, status: rule.status } : { from, to });
|
|
1017
|
+
}
|
|
1018
|
+
resolved.push(...wildcard.values());
|
|
1019
|
+
if (resolved.length === 0) return void 0;
|
|
1020
|
+
resolved.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
|
|
1021
|
+
const contents = resolved.map((rule) => `${rule.from} ${rule.to} ${rule.status ?? DEFAULT_STATUS}
|
|
1022
|
+
`).join("");
|
|
1023
|
+
return { path: OUTPUT_PATH, contents };
|
|
867
1024
|
}
|
|
868
1025
|
|
|
869
|
-
// ../engine/src/
|
|
870
|
-
var
|
|
871
|
-
var
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1026
|
+
// ../engine/src/content/loader.ts
|
|
1027
|
+
var SORT_FILE = "_sort.json";
|
|
1028
|
+
var REDIRECTS_FILE = "_redirects.json";
|
|
1029
|
+
var SITE_CONFIG_FILE = "_site.json";
|
|
1030
|
+
var ASSETS_DIR = "assets";
|
|
1031
|
+
var ITEM_EXT = ".json";
|
|
1032
|
+
var DEFAULT_DIRS = {
|
|
1033
|
+
content: "content",
|
|
1034
|
+
templates: "templates",
|
|
1035
|
+
components: "components",
|
|
1036
|
+
/** Static passthrough, copied verbatim to the published root (DESIGN §8). The
|
|
1037
|
+
* engine never reads it; named here so hosts/editor share one source of truth. */
|
|
1038
|
+
public: "public"
|
|
876
1039
|
};
|
|
877
|
-
function
|
|
878
|
-
return
|
|
1040
|
+
function join(...parts) {
|
|
1041
|
+
return parts.filter((part) => part !== "").join("/");
|
|
879
1042
|
}
|
|
880
|
-
function
|
|
881
|
-
|
|
882
|
-
if (ext === "png") return "png";
|
|
883
|
-
if (ext === "webp") return "webp";
|
|
884
|
-
return "jpeg";
|
|
1043
|
+
function stripBom(text) {
|
|
1044
|
+
return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
885
1045
|
}
|
|
886
|
-
function
|
|
887
|
-
|
|
888
|
-
for (let i = 0; i < bytes.length; i += 1) {
|
|
889
|
-
hash ^= bytes[i] ?? 0;
|
|
890
|
-
hash = Math.imul(hash, 16777619);
|
|
891
|
-
}
|
|
892
|
-
return (hash >>> 0).toString(36);
|
|
1046
|
+
async function loadContentTree(source, contentDir = DEFAULT_DIRS.content) {
|
|
1047
|
+
return loadDir(source, contentDir, "", "");
|
|
893
1048
|
}
|
|
894
|
-
async function
|
|
895
|
-
const
|
|
896
|
-
const
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1049
|
+
async function loadDir(source, dirPath, slug, treePath) {
|
|
1050
|
+
const [entries, sort] = await Promise.all([source.list(dirPath), readSort(source, dirPath)]);
|
|
1051
|
+
const childMap = /* @__PURE__ */ new Map();
|
|
1052
|
+
for (const entry of entries) {
|
|
1053
|
+
if (entry.kind === "file") {
|
|
1054
|
+
if (entry.name === SORT_FILE || entry.name === REDIRECTS_FILE || entry.name === SITE_CONFIG_FILE || !entry.name.endsWith(ITEM_EXT))
|
|
1055
|
+
continue;
|
|
1056
|
+
const childSlug = entry.name.slice(0, -ITEM_EXT.length);
|
|
1057
|
+
childMap.set(
|
|
1058
|
+
childSlug,
|
|
1059
|
+
await loadItem(source, join(dirPath, entry.name), childSlug, treePath)
|
|
1060
|
+
);
|
|
1061
|
+
} else {
|
|
1062
|
+
if (entry.name === ASSETS_DIR) continue;
|
|
1063
|
+
const childPath = join(treePath, entry.name);
|
|
1064
|
+
childMap.set(
|
|
1065
|
+
entry.name,
|
|
1066
|
+
await loadDir(source, join(dirPath, entry.name), entry.name, childPath)
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const name = sort.name ?? (slug === "" ? "" : humanize(slug));
|
|
1071
|
+
return {
|
|
1072
|
+
kind: "dir",
|
|
1073
|
+
slug,
|
|
1074
|
+
name,
|
|
1075
|
+
path: treePath,
|
|
1076
|
+
children: orderChildren(childMap, sort.order),
|
|
1077
|
+
...sort.collection !== void 0 && { collection: sort.collection },
|
|
1078
|
+
...sort.defaultTemplate !== void 0 && { defaultTemplate: sort.defaultTemplate }
|
|
908
1079
|
};
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1080
|
+
}
|
|
1081
|
+
async function loadItem(source, filePath, slug, parentPath2) {
|
|
1082
|
+
const raw = await source.readText(filePath);
|
|
1083
|
+
try {
|
|
1084
|
+
const item = parseContentItem(JSON.parse(raw));
|
|
1085
|
+
return { kind: "item", slug, path: join(parentPath2, slug), item };
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
throw new Error(`Failed to load ${filePath}: ${error.message}`);
|
|
915
1088
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1089
|
+
}
|
|
1090
|
+
async function readSort(source, dirPath) {
|
|
1091
|
+
const path = join(dirPath, SORT_FILE);
|
|
1092
|
+
if (!await source.exists(path)) return { order: [] };
|
|
1093
|
+
try {
|
|
1094
|
+
return parseSortFile(JSON.parse(await source.readText(path)));
|
|
1095
|
+
} catch {
|
|
1096
|
+
return { order: [] };
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
async function loadRedirects(source, contentDir = DEFAULT_DIRS.content) {
|
|
1100
|
+
const path = join(contentDir, REDIRECTS_FILE);
|
|
1101
|
+
if (!await source.exists(path)) return [];
|
|
1102
|
+
try {
|
|
1103
|
+
return parseRedirects(JSON.parse(stripBom(await source.readText(path))));
|
|
1104
|
+
} catch {
|
|
1105
|
+
return [];
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
async function loadSiteConfig(source, contentDir = DEFAULT_DIRS.content) {
|
|
1109
|
+
const path = join(contentDir, SITE_CONFIG_FILE);
|
|
1110
|
+
if (!await source.exists(path)) return {};
|
|
1111
|
+
try {
|
|
1112
|
+
const raw = JSON.parse(stripBom(await source.readText(path)));
|
|
1113
|
+
const baseUrl = typeof raw.baseUrl === "string" ? raw.baseUrl.trim() : "";
|
|
1114
|
+
return baseUrl !== "" ? { baseUrl } : {};
|
|
1115
|
+
} catch {
|
|
1116
|
+
return {};
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function orderChildren(map, order) {
|
|
1120
|
+
const out = [];
|
|
1121
|
+
const used = /* @__PURE__ */ new Set();
|
|
1122
|
+
for (const slug of order) {
|
|
1123
|
+
const node = map.get(slug);
|
|
1124
|
+
if (node !== void 0) {
|
|
1125
|
+
out.push(node);
|
|
1126
|
+
used.add(slug);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
for (const [slug, node] of map) {
|
|
1130
|
+
if (!used.has(slug)) out.push(node);
|
|
1131
|
+
}
|
|
1132
|
+
return out;
|
|
1133
|
+
}
|
|
1134
|
+
function loadComponents(source, componentsDir = DEFAULT_DIRS.components) {
|
|
1135
|
+
return loadComponentFiles(source, componentsDir, "html");
|
|
1136
|
+
}
|
|
1137
|
+
function loadComponentStyles(source, componentsDir = DEFAULT_DIRS.components) {
|
|
1138
|
+
return loadComponentFiles(source, componentsDir, "css");
|
|
1139
|
+
}
|
|
1140
|
+
function loadComponentScripts(source, componentsDir = DEFAULT_DIRS.components) {
|
|
1141
|
+
return loadComponentFiles(source, componentsDir, "js");
|
|
1142
|
+
}
|
|
1143
|
+
async function loadComponentFiles(source, componentsDir, extension) {
|
|
1144
|
+
const suffix = `.${extension}`;
|
|
1145
|
+
const map = /* @__PURE__ */ new Map();
|
|
1146
|
+
const walk = async (dir) => {
|
|
1147
|
+
if (!await source.exists(dir)) return;
|
|
1148
|
+
for (const entry of await source.list(dir)) {
|
|
1149
|
+
const full = join(dir, entry.name);
|
|
1150
|
+
if (entry.kind === "dir") {
|
|
1151
|
+
await walk(full);
|
|
1152
|
+
} else if (entry.name.endsWith(suffix)) {
|
|
1153
|
+
const key = entry.name.slice(0, -suffix.length).toLowerCase();
|
|
1154
|
+
map.set(key, await source.readText(full));
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
926
1157
|
};
|
|
927
|
-
|
|
1158
|
+
await walk(componentsDir);
|
|
1159
|
+
return map;
|
|
928
1160
|
}
|
|
929
|
-
function
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1161
|
+
async function loadTemplate(source, name, templatesDir = DEFAULT_DIRS.templates) {
|
|
1162
|
+
return source.readText(join(templatesDir, `${name}.html`));
|
|
1163
|
+
}
|
|
1164
|
+
function loadTemplateStyle(source, name, templatesDir = DEFAULT_DIRS.templates) {
|
|
1165
|
+
return tryReadText(source, join(templatesDir, `${name}.css`));
|
|
1166
|
+
}
|
|
1167
|
+
function loadTemplateScript(source, name, templatesDir = DEFAULT_DIRS.templates) {
|
|
1168
|
+
return tryReadText(source, join(templatesDir, `${name}.js`));
|
|
1169
|
+
}
|
|
1170
|
+
var DOCUMENT_SHELL = "document";
|
|
1171
|
+
function loadDocumentShell(source, templatesDir = DEFAULT_DIRS.templates) {
|
|
1172
|
+
return tryReadText(source, join(templatesDir, `${DOCUMENT_SHELL}.html`));
|
|
1173
|
+
}
|
|
1174
|
+
async function tryReadText(source, path) {
|
|
1175
|
+
try {
|
|
1176
|
+
return await source.readText(path);
|
|
1177
|
+
} catch {
|
|
1178
|
+
return void 0;
|
|
1179
|
+
}
|
|
936
1180
|
}
|
|
937
1181
|
|
|
938
1182
|
// ../engine/src/url/references.ts
|
|
@@ -956,35 +1200,418 @@ function collectHtmlReferences(html) {
|
|
|
956
1200
|
return [...found].sort();
|
|
957
1201
|
}
|
|
958
1202
|
|
|
959
|
-
// ../engine/src/
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1203
|
+
// ../engine/src/template/analyze.ts
|
|
1204
|
+
function analyzeTemplate(source, components = /* @__PURE__ */ new Map()) {
|
|
1205
|
+
const requiredFields = /* @__PURE__ */ new Set();
|
|
1206
|
+
const queryLoopPaths = [];
|
|
1207
|
+
const curatedLoopFields = [];
|
|
1208
|
+
const imageFields = /* @__PURE__ */ new Set();
|
|
1209
|
+
const fileFields = /* @__PURE__ */ new Set();
|
|
1210
|
+
const richTextFields = /* @__PURE__ */ new Set();
|
|
1211
|
+
const constraints = /* @__PURE__ */ new Map();
|
|
1212
|
+
const references = /* @__PURE__ */ new Set();
|
|
1213
|
+
const visit = (nodes) => {
|
|
1214
|
+
for (const node of nodes) {
|
|
1215
|
+
if (!isElement(node)) continue;
|
|
1216
|
+
const richText = richTextFieldName(node);
|
|
1217
|
+
if (richText !== void 0) richTextFields.add(richText);
|
|
1218
|
+
const eachIn = getAttribute(node, "data-each-in");
|
|
1219
|
+
if (eachIn !== void 0) {
|
|
1220
|
+
queryLoopPaths.push(eachIn);
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
const each = getAttribute(node, "data-each");
|
|
1224
|
+
if (each !== void 0) curatedLoopFields.push(each);
|
|
1225
|
+
if (hasAttribute(node, "data-required")) {
|
|
1226
|
+
const field = annotatedFieldName(node);
|
|
1227
|
+
if (field !== void 0) requiredFields.add(field);
|
|
1228
|
+
}
|
|
1229
|
+
const bounds = lengthConstraints(node);
|
|
1230
|
+
if (bounds.minLength !== void 0 || bounds.maxLength !== void 0) {
|
|
1231
|
+
const field = annotatedFieldName(node);
|
|
1232
|
+
if (field !== void 0) constraints.set(field, bounds);
|
|
1233
|
+
}
|
|
1234
|
+
const annotation = getAttribute(node, "data-type");
|
|
1235
|
+
const propTypes = components.has(node.tagName) ? componentPropTypes(node.tagName, components, /* @__PURE__ */ new Set()) : void 0;
|
|
1236
|
+
for (const attr of node.attrs) {
|
|
1237
|
+
const reference = referenceTarget(attr.value);
|
|
1238
|
+
if (reference !== void 0) references.add(reference);
|
|
1239
|
+
const token = wholeValueToken(attr.value);
|
|
1240
|
+
if (token === null || token.dotted) continue;
|
|
1241
|
+
const type = propTypes?.get(attr.name) ?? inferControl({
|
|
1242
|
+
tag: node.tagName,
|
|
1243
|
+
attribute: attr.name,
|
|
1244
|
+
wholeValue: true,
|
|
1245
|
+
valuePrefix: literalPrefix(attr.value),
|
|
1246
|
+
download: hasAttribute(node, "download"),
|
|
1247
|
+
...annotation !== void 0 && { annotation }
|
|
1248
|
+
});
|
|
1249
|
+
if (type === "image") imageFields.add(token.name);
|
|
1250
|
+
else if (type === "file") fileFields.add(token.name);
|
|
1251
|
+
else if (type === "richtext") richTextFields.add(token.name);
|
|
1252
|
+
}
|
|
1253
|
+
visit(node.childNodes);
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
visit(parseNodes(source));
|
|
1257
|
+
return {
|
|
1258
|
+
requiredFields: [...requiredFields],
|
|
1259
|
+
queryLoopPaths,
|
|
1260
|
+
curatedLoopFields,
|
|
1261
|
+
imageFields: [...imageFields],
|
|
1262
|
+
fileFields: [...fileFields],
|
|
1263
|
+
richTextFields: [...richTextFields],
|
|
1264
|
+
constraints,
|
|
1265
|
+
references: [...references].sort()
|
|
970
1266
|
};
|
|
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);
|
|
975
1267
|
}
|
|
976
|
-
function
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1268
|
+
function richTextFieldName(el) {
|
|
1269
|
+
const meaningful = el.childNodes.filter(
|
|
1270
|
+
(child) => !(isTextNode(child) && child.value.trim() === "")
|
|
1271
|
+
);
|
|
1272
|
+
const only = meaningful[0];
|
|
1273
|
+
if (meaningful.length !== 1 || only === void 0 || !isTextNode(only)) return void 0;
|
|
1274
|
+
const token = wholeValueToken(only.value);
|
|
1275
|
+
if (token === null || token.dotted) return void 0;
|
|
1276
|
+
const annotation = getAttribute(el, "data-type");
|
|
1277
|
+
const type = inferControl({
|
|
1278
|
+
tag: el.tagName,
|
|
1279
|
+
wholeValue: true,
|
|
1280
|
+
...annotation !== void 0 && { annotation }
|
|
1281
|
+
});
|
|
1282
|
+
return valueKindOf(type) === "html" ? token.name : void 0;
|
|
1283
|
+
}
|
|
1284
|
+
function annotatedFieldName(el) {
|
|
1285
|
+
const explicit = getAttribute(el, "data-field");
|
|
1286
|
+
if (explicit !== void 0) return explicit;
|
|
1287
|
+
for (const attr of el.attrs) {
|
|
1288
|
+
const token = findTokens(attr.value).find((candidate) => !candidate.dotted);
|
|
1289
|
+
if (token !== void 0) return token.name;
|
|
1290
|
+
}
|
|
1291
|
+
for (const child of el.childNodes) {
|
|
1292
|
+
if (isTextNode(child)) {
|
|
1293
|
+
const token = findTokens(child.value).find((candidate) => !candidate.dotted);
|
|
1294
|
+
if (token !== void 0) return token.name;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return void 0;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// ../engine/src/migrations/migrations.ts
|
|
1301
|
+
async function pendingMigrations(store) {
|
|
1302
|
+
const components = await loadComponents(store).catch(() => /* @__PURE__ */ new Map());
|
|
1303
|
+
const items = await collectItems(store);
|
|
1304
|
+
const templateFieldsCache = /* @__PURE__ */ new Map();
|
|
1305
|
+
const requiredFieldsCache = /* @__PURE__ */ new Map();
|
|
1306
|
+
const snapshotFieldsCache = /* @__PURE__ */ new Map();
|
|
1307
|
+
const fieldsFor = async (templateName) => {
|
|
1308
|
+
const cached = templateFieldsCache.get(templateName);
|
|
1309
|
+
if (cached !== void 0) return cached;
|
|
1310
|
+
const html = await readTemplateOrNull(store, templateName);
|
|
1311
|
+
const fields = new Set(html === null ? [] : deriveFields(html, components).map((f) => f.name));
|
|
1312
|
+
templateFieldsCache.set(templateName, fields);
|
|
1313
|
+
return fields;
|
|
1314
|
+
};
|
|
1315
|
+
const requiredFor = async (templateName) => {
|
|
1316
|
+
const cached = requiredFieldsCache.get(templateName);
|
|
1317
|
+
if (cached !== void 0) return cached;
|
|
1318
|
+
const html = await readTemplateOrNull(store, templateName);
|
|
1319
|
+
let required = [];
|
|
1320
|
+
if (html !== null) {
|
|
1321
|
+
try {
|
|
1322
|
+
required = analyzeTemplate(html, components).requiredFields;
|
|
1323
|
+
} catch {
|
|
1324
|
+
required = [];
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
requiredFieldsCache.set(templateName, required);
|
|
1328
|
+
return required;
|
|
1329
|
+
};
|
|
1330
|
+
const out = [];
|
|
1331
|
+
const byTemplate = /* @__PURE__ */ new Map();
|
|
1332
|
+
for (const item of items) {
|
|
1333
|
+
const expected = await fieldsFor(item.template);
|
|
1334
|
+
const itemFieldNames = Object.keys(item.fields);
|
|
1335
|
+
const orphans = itemFieldNames.filter((name) => !expected.has(name));
|
|
1336
|
+
const required = await requiredFor(item.template);
|
|
1337
|
+
const missing = required.filter((name) => {
|
|
1338
|
+
const value = item.fields[name];
|
|
1339
|
+
return value === void 0 || value === null || value === "";
|
|
1340
|
+
});
|
|
1341
|
+
if (orphans.length === 0 && missing.length === 0) continue;
|
|
1342
|
+
const base = baseTemplateName(item.template);
|
|
1343
|
+
const bestFit = isVersionedTemplateName(item.template) ? null : await pickBestFitSnapshot(store, base, itemFieldNames, snapshotFieldsCache, components);
|
|
1344
|
+
const finding2 = {
|
|
1345
|
+
path: item.path,
|
|
1346
|
+
boundTemplate: item.template,
|
|
1347
|
+
currentTemplate: base,
|
|
1348
|
+
orphanFields: orphans.sort(),
|
|
1349
|
+
missingRequiredFields: [...missing].sort(),
|
|
1350
|
+
bestFitSnapshot: bestFit
|
|
1351
|
+
};
|
|
1352
|
+
out.push(finding2);
|
|
1353
|
+
const bucket = byTemplate.get(base);
|
|
1354
|
+
if (bucket === void 0) byTemplate.set(base, [finding2]);
|
|
1355
|
+
else bucket.push(finding2);
|
|
1356
|
+
}
|
|
1357
|
+
return { items: out, byTemplate };
|
|
1358
|
+
}
|
|
1359
|
+
async function bestFitSnapshot(store, base, itemFieldNames) {
|
|
1360
|
+
const components = await loadComponents(store).catch(() => /* @__PURE__ */ new Map());
|
|
1361
|
+
return pickBestFitSnapshot(store, base, itemFieldNames, /* @__PURE__ */ new Map(), components);
|
|
1362
|
+
}
|
|
1363
|
+
async function applyMigration(store, itemPath, resolution) {
|
|
1364
|
+
const text = await store.readText(itemPath);
|
|
1365
|
+
const raw = JSON.parse(text);
|
|
1366
|
+
if (typeof raw !== "object" || raw === null) {
|
|
1367
|
+
throw new Error(`applyMigration: ${itemPath} is not a JSON object`);
|
|
1368
|
+
}
|
|
1369
|
+
const obj = raw;
|
|
1370
|
+
const fields = typeof obj["fields"] === "object" && obj["fields"] !== null ? { ...obj["fields"] } : {};
|
|
1371
|
+
for (const [from, to] of Object.entries(resolution.rename)) {
|
|
1372
|
+
if (Object.prototype.hasOwnProperty.call(fields, from)) {
|
|
1373
|
+
fields[to] = fields[from];
|
|
1374
|
+
delete fields[from];
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
for (const name of resolution.drop) delete fields[name];
|
|
1378
|
+
for (const [name, value] of Object.entries(resolution.fill)) {
|
|
1379
|
+
if (value !== "") fields[name] = value;
|
|
1380
|
+
}
|
|
1381
|
+
obj["fields"] = Object.fromEntries(Object.entries(fields).sort(([a], [b]) => a.localeCompare(b)));
|
|
1382
|
+
await store.write(itemPath, new TextEncoder().encode(`${JSON.stringify(obj, null, 2)}
|
|
1383
|
+
`));
|
|
1384
|
+
}
|
|
1385
|
+
async function collectItems(store, dir = "content") {
|
|
1386
|
+
const out = [];
|
|
1387
|
+
let entries;
|
|
1388
|
+
try {
|
|
1389
|
+
entries = await store.list(dir);
|
|
1390
|
+
} catch {
|
|
1391
|
+
return out;
|
|
1392
|
+
}
|
|
1393
|
+
for (const entry of entries) {
|
|
1394
|
+
const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
1395
|
+
if (entry.kind === "dir") {
|
|
1396
|
+
out.push(...await collectItems(store, path));
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (!entry.name.endsWith(".json") || entry.name.startsWith("_")) continue;
|
|
1400
|
+
let data;
|
|
1401
|
+
try {
|
|
1402
|
+
data = JSON.parse(await store.readText(path));
|
|
1403
|
+
} catch {
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
if (typeof data !== "object" || data === null) continue;
|
|
1407
|
+
const obj = data;
|
|
1408
|
+
const template = obj["template"];
|
|
1409
|
+
if (typeof template !== "string") continue;
|
|
1410
|
+
const fields = obj["fields"];
|
|
1411
|
+
out.push({
|
|
1412
|
+
path,
|
|
1413
|
+
template,
|
|
1414
|
+
fields: typeof fields === "object" && fields !== null ? fields : {}
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
return out;
|
|
1418
|
+
}
|
|
1419
|
+
async function readTemplateOrNull(store, templateName) {
|
|
1420
|
+
try {
|
|
1421
|
+
return await store.readText(`templates/${templateName}.html`);
|
|
1422
|
+
} catch {
|
|
1423
|
+
return null;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
async function pickBestFitSnapshot(store, base, itemFieldNames, cache, components) {
|
|
1427
|
+
let entries;
|
|
1428
|
+
try {
|
|
1429
|
+
entries = await store.list("templates");
|
|
1430
|
+
} catch {
|
|
1431
|
+
return null;
|
|
1432
|
+
}
|
|
1433
|
+
const itemSet = new Set(itemFieldNames);
|
|
1434
|
+
let bestVersion = 0;
|
|
1435
|
+
let bestName = null;
|
|
1436
|
+
for (const entry of entries) {
|
|
1437
|
+
if (entry.kind !== "file" || !entry.name.endsWith(".html")) continue;
|
|
1438
|
+
const stem = entry.name.slice(0, -".html".length);
|
|
1439
|
+
if (baseTemplateName(stem) !== base || !isVersionedTemplateName(stem)) continue;
|
|
1440
|
+
const version = versionNumber(stem);
|
|
1441
|
+
if (version === null) continue;
|
|
1442
|
+
let tokens = cache.get(stem);
|
|
1443
|
+
if (tokens === void 0) {
|
|
1444
|
+
try {
|
|
1445
|
+
const html = await store.readText(`templates/${entry.name}`);
|
|
1446
|
+
tokens = new Set(deriveFields(html, components).map((f) => f.name));
|
|
1447
|
+
} catch {
|
|
1448
|
+
tokens = /* @__PURE__ */ new Set();
|
|
1449
|
+
}
|
|
1450
|
+
cache.set(stem, tokens);
|
|
1451
|
+
}
|
|
1452
|
+
const covers = [...itemSet].every((name) => tokens.has(name));
|
|
1453
|
+
if (covers && version > bestVersion) {
|
|
1454
|
+
bestVersion = version;
|
|
1455
|
+
bestName = stem;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
return bestName;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// ../engine/src/html/escape.ts
|
|
1462
|
+
function escapeHtmlText(value) {
|
|
1463
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1464
|
+
}
|
|
1465
|
+
function escapeHtmlAttribute(value) {
|
|
1466
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1467
|
+
}
|
|
1468
|
+
function escapeJsonStringContent(value) {
|
|
1469
|
+
const literal = JSON.stringify(value);
|
|
1470
|
+
return literal.slice(1, -1);
|
|
1471
|
+
}
|
|
1472
|
+
function sanitizeUrl(url) {
|
|
1473
|
+
let cleaned = "";
|
|
1474
|
+
for (const ch of url) {
|
|
1475
|
+
const code = ch.codePointAt(0);
|
|
1476
|
+
if (code !== void 0 && code > 32) cleaned += ch;
|
|
1477
|
+
}
|
|
1478
|
+
const scheme = cleaned.toLowerCase();
|
|
1479
|
+
if (scheme.startsWith("javascript:") || scheme.startsWith("vbscript:")) {
|
|
1480
|
+
return "#";
|
|
1481
|
+
}
|
|
1482
|
+
return url;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// ../engine/src/authors/authors.ts
|
|
1486
|
+
function toAuthorRefs(value) {
|
|
1487
|
+
if (!Array.isArray(value)) return [];
|
|
1488
|
+
const refs = [];
|
|
1489
|
+
for (const entry of value) {
|
|
1490
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
1491
|
+
const record = entry;
|
|
1492
|
+
const name = typeof record.name === "string" ? record.name : "";
|
|
1493
|
+
if (name.trim() === "") continue;
|
|
1494
|
+
refs.push({
|
|
1495
|
+
name,
|
|
1496
|
+
...typeof record.user === "string" && record.user !== "" && { user: record.user },
|
|
1497
|
+
...typeof record.href === "string" && record.href !== "" && { href: record.href }
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
return refs;
|
|
1501
|
+
}
|
|
1502
|
+
function joinAuthors(names) {
|
|
1503
|
+
if (names.length === 0) return "";
|
|
1504
|
+
if (names.length === 1) return names[0] ?? "";
|
|
1505
|
+
const last = names[names.length - 1] ?? "";
|
|
1506
|
+
const head = names.slice(0, -1).join(", ");
|
|
1507
|
+
return `${head} and ${last}`;
|
|
1508
|
+
}
|
|
1509
|
+
function renderAuthors(value, directory) {
|
|
1510
|
+
const names = toAuthorRefs(value).map((ref) => {
|
|
1511
|
+
const resolved = ref.user !== void 0 ? directory?.(ref.user) : void 0;
|
|
1512
|
+
return escapeHtmlText(resolved?.displayName ?? ref.name);
|
|
1513
|
+
});
|
|
1514
|
+
return joinAuthors(names);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// ../engine/src/media/media.ts
|
|
1518
|
+
var DEFAULT_WIDTHS = [400, 800, 1200, 1600];
|
|
1519
|
+
var MIME = {
|
|
1520
|
+
avif: "image/avif",
|
|
1521
|
+
webp: "image/webp",
|
|
1522
|
+
jpeg: "image/jpeg",
|
|
1523
|
+
png: "image/png"
|
|
1524
|
+
};
|
|
1525
|
+
function extOf(format) {
|
|
1526
|
+
return format === "jpeg" ? "jpg" : format;
|
|
1527
|
+
}
|
|
1528
|
+
function fallbackFormatFor(assetPath) {
|
|
1529
|
+
const ext = assetPath.slice(assetPath.lastIndexOf(".") + 1).toLowerCase();
|
|
1530
|
+
if (ext === "png") return "png";
|
|
1531
|
+
if (ext === "webp") return "webp";
|
|
1532
|
+
return "jpeg";
|
|
1533
|
+
}
|
|
1534
|
+
function contentHash(bytes) {
|
|
1535
|
+
let hash = 2166136261;
|
|
1536
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
1537
|
+
hash ^= bytes[i] ?? 0;
|
|
1538
|
+
hash = Math.imul(hash, 16777619);
|
|
1539
|
+
}
|
|
1540
|
+
return (hash >>> 0).toString(36);
|
|
1541
|
+
}
|
|
1542
|
+
async function processImage(input, assetPath, encoder) {
|
|
1543
|
+
const fallbackFormat = fallbackFormatFor(assetPath);
|
|
1544
|
+
const formats = [.../* @__PURE__ */ new Set(["avif", "webp", fallbackFormat])];
|
|
1545
|
+
const encoded = await encoder.encode(input, { widths: DEFAULT_WIDTHS, formats });
|
|
1546
|
+
const hash = contentHash(input);
|
|
1547
|
+
const base = assetPath.slice(
|
|
1548
|
+
0,
|
|
1549
|
+
assetPath.lastIndexOf(".") >= 0 ? assetPath.lastIndexOf(".") : assetPath.length
|
|
1550
|
+
);
|
|
1551
|
+
const artifacts = [];
|
|
1552
|
+
const urlOf = (variant) => {
|
|
1553
|
+
const path = `${base}.${hash}-${variant.width}.${extOf(variant.format)}`;
|
|
1554
|
+
artifacts.push({ path, contents: variant.bytes });
|
|
1555
|
+
return `/${path}`;
|
|
1556
|
+
};
|
|
1557
|
+
const byFormat = /* @__PURE__ */ new Map();
|
|
1558
|
+
for (const variant of [...encoded.variants].sort((a, b) => a.width - b.width)) {
|
|
1559
|
+
const url = urlOf(variant);
|
|
1560
|
+
const list = byFormat.get(variant.format) ?? [];
|
|
1561
|
+
list.push({ url, width: variant.width });
|
|
1562
|
+
byFormat.set(variant.format, list);
|
|
1563
|
+
}
|
|
1564
|
+
const srcsetOf = (format) => (byFormat.get(format) ?? []).map((entry) => `${entry.url} ${entry.width}w`).join(", ");
|
|
1565
|
+
const sources = ["avif", "webp"].filter((format) => byFormat.has(format)).map((format) => ({ format, mime: MIME[format], srcset: srcsetOf(format) }));
|
|
1566
|
+
const fallbackList = byFormat.get(fallbackFormat) ?? [];
|
|
1567
|
+
const fallbackSrc = fallbackList[fallbackList.length - 1]?.url ?? `/${assetPath}`;
|
|
1568
|
+
const info = {
|
|
1569
|
+
width: encoded.sourceWidth,
|
|
1570
|
+
height: encoded.sourceHeight,
|
|
1571
|
+
sources,
|
|
1572
|
+
fallbackSrc,
|
|
1573
|
+
...encoded.blurDataUri !== void 0 && { blurDataUri: encoded.blurDataUri }
|
|
1574
|
+
};
|
|
1575
|
+
return { artifacts, info };
|
|
1576
|
+
}
|
|
1577
|
+
function buildPictureMarkup(info, extraAttrs) {
|
|
1578
|
+
const sources = info.sources.map(
|
|
1579
|
+
(source) => `<source type="${source.mime}" srcset="${escapeHtmlAttribute(source.srcset)}">`
|
|
1580
|
+
).join("");
|
|
1581
|
+
const carried = extraAttrs.filter(([name]) => name !== "src" && name !== "width" && name !== "height").map(([name, value]) => ` ${name}="${escapeHtmlAttribute(value)}"`).join("");
|
|
1582
|
+
const img = `<img src="${escapeHtmlAttribute(info.fallbackSrc)}" width="${info.width}" height="${info.height}" loading="lazy" decoding="async"${carried}>`;
|
|
1583
|
+
return `<picture>${sources}${img}</picture>`;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// ../engine/src/compile/compiler.ts
|
|
1587
|
+
var LOOP_ATTRS = ["data-each-in", "data-each", "data-sort", "data-limit", "data-pick-from"];
|
|
1588
|
+
var AUTHORING_ATTRS = ["data-type", "data-field", "data-required", "data-help", "data-label"];
|
|
1589
|
+
function compileTemplate(input) {
|
|
1590
|
+
const ctx = {
|
|
1591
|
+
components: input.components,
|
|
1592
|
+
usedComponents: /* @__PURE__ */ new Set(),
|
|
1593
|
+
...input.context !== void 0 && { context: input.context },
|
|
1594
|
+
...input.media !== void 0 && { media: input.media },
|
|
1595
|
+
...input.authorDirectory !== void 0 && { authorDirectory: input.authorDirectory },
|
|
1596
|
+
...input.componentStyles !== void 0 && { componentStyles: input.componentStyles }
|
|
1597
|
+
};
|
|
1598
|
+
const body = transformNodes(parseNodes(input.template), input.scope, ctx);
|
|
1599
|
+
const out = input.document === void 0 ? body : fillSlots(transformNodes(parseNodes(input.document), input.scope, ctx), body);
|
|
1600
|
+
injectPageAssets(out, input, ctx);
|
|
1601
|
+
return serializeNodes(out);
|
|
1602
|
+
}
|
|
1603
|
+
function injectPageAssets(nodes, input, ctx) {
|
|
1604
|
+
const style = input.templateStyle?.trim();
|
|
1605
|
+
if (style) {
|
|
1606
|
+
const styleNode = parseFragmentNodes(`<style>
|
|
1607
|
+
${style}
|
|
1608
|
+
</style>`);
|
|
1609
|
+
const head = findElement(nodes, "head");
|
|
1610
|
+
if (head) head.childNodes.push(...styleNode);
|
|
1611
|
+
else nodes.unshift(...styleNode);
|
|
1612
|
+
}
|
|
1613
|
+
const scripts = [];
|
|
1614
|
+
const templateScript = input.templateScript?.trim();
|
|
988
1615
|
if (templateScript) scripts.push(templateScript);
|
|
989
1616
|
for (const tag of [...ctx.usedComponents].sort()) {
|
|
990
1617
|
const js = input.componentScripts?.get(tag)?.trim();
|
|
@@ -1159,446 +1786,204 @@ function cloneNode(node) {
|
|
|
1159
1786
|
function substituteAttributes(el, scope, ctx) {
|
|
1160
1787
|
for (const attr of el.attrs) {
|
|
1161
1788
|
if (findTokens(attr.value).length === 0) {
|
|
1162
|
-
const referenced2 = resolveReference(attr.value, ctx);
|
|
1163
|
-
if (referenced2 !== void 0) attr.value = sanitizeUrl(referenced2);
|
|
1164
|
-
continue;
|
|
1165
|
-
}
|
|
1166
|
-
const annotation = getAttribute(el, "data-type");
|
|
1167
|
-
const whole = wholeValueToken(attr.value) !== null;
|
|
1168
|
-
const type = inferControl({
|
|
1169
|
-
tag: el.tagName,
|
|
1170
|
-
attribute: attr.name,
|
|
1171
|
-
wholeValue: whole,
|
|
1172
|
-
valuePrefix: literalPrefix(attr.value),
|
|
1173
|
-
download: hasAttribute(el, "download"),
|
|
1174
|
-
...annotation !== void 0 && { annotation }
|
|
1175
|
-
});
|
|
1176
|
-
let value = substituteTokens(attr.value, scopeResolver(scope));
|
|
1177
|
-
const referenced = resolveReference(value, ctx);
|
|
1178
|
-
if (referenced !== void 0) value = referenced;
|
|
1179
|
-
const fileUrl = ctx.media?.file(value);
|
|
1180
|
-
if (fileUrl !== void 0) value = fileUrl;
|
|
1181
|
-
if (whole && valueKindOf(type) === "url") value = sanitizeUrl(value);
|
|
1182
|
-
attr.value = value;
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
function substituteRawText(el, scope, mode) {
|
|
1186
|
-
const resolve = scopeResolver(scope);
|
|
1187
|
-
for (const child of el.childNodes) {
|
|
1188
|
-
if (!isTextNode(child)) continue;
|
|
1189
|
-
child.value = substituteTokens(child.value, (token) => {
|
|
1190
|
-
const text = resolve(token);
|
|
1191
|
-
return mode === "json" ? escapeJsonStringContent(text) : text;
|
|
1192
|
-
});
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
function wholeValueFieldToken(el, annotation) {
|
|
1196
|
-
const meaningful = el.childNodes.filter(
|
|
1197
|
-
(child) => !(isTextNode(child) && child.value.trim() === "")
|
|
1198
|
-
);
|
|
1199
|
-
const only = meaningful[0];
|
|
1200
|
-
if (meaningful.length !== 1 || only === void 0 || !isTextNode(only)) return null;
|
|
1201
|
-
const token = wholeValueToken(only.value);
|
|
1202
|
-
if (token === null || token.dotted) return null;
|
|
1203
|
-
const type = inferControl({
|
|
1204
|
-
tag: el.tagName,
|
|
1205
|
-
wholeValue: true,
|
|
1206
|
-
...annotation !== void 0 && { annotation }
|
|
1207
|
-
});
|
|
1208
|
-
return { token, kind: valueKindOf(type) };
|
|
1209
|
-
}
|
|
1210
|
-
function isJsonLd(el) {
|
|
1211
|
-
return el.tagName === "script" && getAttribute(el, "type")?.toLowerCase() === "application/ld+json";
|
|
1212
|
-
}
|
|
1213
|
-
function expandComponent(el, scope, ctx) {
|
|
1214
|
-
const source = ctx.components.get(el.tagName);
|
|
1215
|
-
if (source === void 0) return [el];
|
|
1216
|
-
const resolve = scopeResolver(scope);
|
|
1217
|
-
const propScope = {};
|
|
1218
|
-
for (const attr of el.attrs) {
|
|
1219
|
-
propScope[attr.name] = substituteTokens(attr.value, resolve);
|
|
1220
|
-
}
|
|
1221
|
-
const slotChildren = transformNodes(el.childNodes, scope, ctx);
|
|
1222
|
-
const body = transformNodes(parseNodes(source), propScope, ctx);
|
|
1223
|
-
const result = fillSlots(body, slotChildren);
|
|
1224
|
-
ctx.usedComponents.add(el.tagName);
|
|
1225
|
-
const css = ctx.componentStyles?.get(el.tagName)?.trim();
|
|
1226
|
-
if (css) {
|
|
1227
|
-
const root = result.find(isElement);
|
|
1228
|
-
if (root !== void 0) {
|
|
1229
|
-
const styleNode = parseFragmentNodes(`<style>@scope {
|
|
1230
|
-
${css}
|
|
1231
|
-
}</style>`);
|
|
1232
|
-
root.childNodes = [...styleNode, ...root.childNodes];
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
return result;
|
|
1236
|
-
}
|
|
1237
|
-
function fillSlots(body, slotChildren) {
|
|
1238
|
-
const named = /* @__PURE__ */ new Map();
|
|
1239
|
-
const defaultSlot = [];
|
|
1240
|
-
for (const child of slotChildren) {
|
|
1241
|
-
const slotName = isElement(child) ? getAttribute(child, "slot") : void 0;
|
|
1242
|
-
if (slotName !== void 0) {
|
|
1243
|
-
if (isElement(child)) removeAttribute(child, "slot");
|
|
1244
|
-
const group = named.get(slotName) ?? [];
|
|
1245
|
-
group.push(child);
|
|
1246
|
-
named.set(slotName, group);
|
|
1247
|
-
} else {
|
|
1248
|
-
defaultSlot.push(child);
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
return replaceSlots(body, named, defaultSlot);
|
|
1252
|
-
}
|
|
1253
|
-
function replaceSlots(nodes, named, defaultSlot) {
|
|
1254
|
-
const out = [];
|
|
1255
|
-
for (const node of nodes) {
|
|
1256
|
-
if (isElement(node) && node.tagName === "slot") {
|
|
1257
|
-
const name = getAttribute(node, "name");
|
|
1258
|
-
const provided = name !== void 0 ? named.get(name) : defaultSlot;
|
|
1259
|
-
if (provided && provided.length > 0) {
|
|
1260
|
-
out.push(...provided);
|
|
1261
|
-
} else {
|
|
1262
|
-
out.push(...replaceSlots(node.childNodes, named, defaultSlot));
|
|
1263
|
-
}
|
|
1264
|
-
} else if (isElement(node)) {
|
|
1265
|
-
node.childNodes = replaceSlots(node.childNodes, named, defaultSlot);
|
|
1266
|
-
out.push(node);
|
|
1267
|
-
} else {
|
|
1268
|
-
out.push(node);
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
return out;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
// ../engine/src/aggregate/aggregate.ts
|
|
1275
|
-
function joinUrl(baseUrl, sitePath) {
|
|
1276
|
-
return baseUrl.replace(/\/+$/, "") + sitePath;
|
|
1277
|
-
}
|
|
1278
|
-
function textContent(html) {
|
|
1279
|
-
const parts = [];
|
|
1280
|
-
const visit = (nodes) => {
|
|
1281
|
-
for (const node of nodes) {
|
|
1282
|
-
if (isTextNode(node)) parts.push(node.value);
|
|
1283
|
-
else if (isElement(node)) visit(node.childNodes);
|
|
1284
|
-
}
|
|
1285
|
-
};
|
|
1286
|
-
visit(parseFragmentNodes(html));
|
|
1287
|
-
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
1288
|
-
}
|
|
1289
|
-
function buildSitemap(entries, baseUrl) {
|
|
1290
|
-
const urls = entries.map((entry) => {
|
|
1291
|
-
const lastmod = entry.updated ?? entry.published;
|
|
1292
|
-
const mod = lastmod !== void 0 ? `
|
|
1293
|
-
<lastmod>${escapeHtmlText(lastmod)}</lastmod>` : "";
|
|
1294
|
-
return ` <url>
|
|
1295
|
-
<loc>${escapeHtmlText(joinUrl(baseUrl, entry.url))}</loc>${mod}
|
|
1296
|
-
</url>`;
|
|
1297
|
-
}).join("\n");
|
|
1298
|
-
const contents = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1299
|
-
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
1300
|
-
${urls}
|
|
1301
|
-
</urlset>
|
|
1302
|
-
`;
|
|
1303
|
-
return { path: "sitemap.xml", contents };
|
|
1304
|
-
}
|
|
1305
|
-
function buildRss(entries, options) {
|
|
1306
|
-
const items = [...entries].sort(
|
|
1307
|
-
(a, b) => String(b.published ?? "").localeCompare(String(a.published ?? ""))
|
|
1308
|
-
);
|
|
1309
|
-
const lastBuild = items[0]?.published;
|
|
1310
|
-
const rfc822 = (iso) => new Date(iso).toUTCString();
|
|
1311
|
-
const itemXml = items.map((entry) => {
|
|
1312
|
-
const link = joinUrl(options.baseUrl, entry.url);
|
|
1313
|
-
const pub = entry.published !== void 0 ? `
|
|
1314
|
-
<pubDate>${escapeHtmlText(rfc822(entry.published))}</pubDate>` : "";
|
|
1315
|
-
const desc = entry.summary !== void 0 ? `
|
|
1316
|
-
<description>${escapeHtmlText(entry.summary)}</description>` : "";
|
|
1317
|
-
return ` <item>
|
|
1318
|
-
<title>${escapeHtmlText(entry.title)}</title>
|
|
1319
|
-
<link>${escapeHtmlText(link)}</link>
|
|
1320
|
-
<guid>${escapeHtmlText(link)}</guid>${pub}${desc}
|
|
1321
|
-
</item>`;
|
|
1322
|
-
}).join("\n");
|
|
1323
|
-
const built = lastBuild !== void 0 ? `
|
|
1324
|
-
<lastBuildDate>${escapeHtmlText(rfc822(lastBuild))}</lastBuildDate>` : "";
|
|
1325
|
-
const contents = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1326
|
-
<rss version="2.0">
|
|
1327
|
-
<channel>
|
|
1328
|
-
<title>${escapeHtmlText(options.title)}</title>
|
|
1329
|
-
<link>${escapeHtmlText(options.baseUrl)}</link>
|
|
1330
|
-
<description>${escapeHtmlText(options.description)}</description>${built}
|
|
1331
|
-
${itemXml}
|
|
1332
|
-
</channel>
|
|
1333
|
-
</rss>
|
|
1334
|
-
`;
|
|
1335
|
-
return { path: "feed.xml", contents };
|
|
1336
|
-
}
|
|
1337
|
-
function buildContentIndex(entries) {
|
|
1338
|
-
const docs = entries.map((entry) => ({
|
|
1339
|
-
url: entry.url,
|
|
1340
|
-
title: entry.title,
|
|
1341
|
-
...entry.published !== void 0 && { published: entry.published },
|
|
1342
|
-
...entry.summary !== void 0 && { summary: entry.summary }
|
|
1343
|
-
}));
|
|
1344
|
-
return { path: "content.json", contents: JSON.stringify(docs) };
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
// ../engine/src/concurrency.ts
|
|
1348
|
-
async function mapWithConcurrency(items, limit, fn) {
|
|
1349
|
-
const results = new Array(items.length);
|
|
1350
|
-
const workers = Math.max(1, Math.min(Math.floor(limit), items.length));
|
|
1351
|
-
let next = 0;
|
|
1352
|
-
const run = async () => {
|
|
1353
|
-
while (next < items.length) {
|
|
1354
|
-
const index = next++;
|
|
1355
|
-
results[index] = await fn(items[index], index);
|
|
1356
|
-
}
|
|
1357
|
-
};
|
|
1358
|
-
await Promise.all(Array.from({ length: workers }, () => run()));
|
|
1359
|
-
return results;
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
// ../engine/src/template/fields.ts
|
|
1363
|
-
function lengthConstraints(el) {
|
|
1364
|
-
const min = parseLength(getAttribute(el, "data-minlength"));
|
|
1365
|
-
const max = parseLength(getAttribute(el, "data-maxlength"));
|
|
1366
|
-
return {
|
|
1367
|
-
...min !== void 0 && { minLength: min },
|
|
1368
|
-
...max !== void 0 && { maxLength: max }
|
|
1369
|
-
};
|
|
1370
|
-
}
|
|
1371
|
-
function parseLength(raw) {
|
|
1372
|
-
if (raw === void 0) return void 0;
|
|
1373
|
-
const value = Number(raw);
|
|
1374
|
-
return Number.isInteger(value) && value >= 0 ? value : void 0;
|
|
1375
|
-
}
|
|
1376
|
-
function deriveFields(template, components = /* @__PURE__ */ new Map()) {
|
|
1377
|
-
return deriveFromSource(template, components, /* @__PURE__ */ new Set());
|
|
1378
|
-
}
|
|
1379
|
-
function componentPropTypes(tag, components, seen) {
|
|
1380
|
-
if (seen.has(tag)) return /* @__PURE__ */ new Map();
|
|
1381
|
-
const source = components.get(tag);
|
|
1382
|
-
if (source === void 0) return /* @__PURE__ */ new Map();
|
|
1383
|
-
const fields = deriveFromSource(source, components, new Set(seen).add(tag));
|
|
1384
|
-
return new Map(fields.map((field) => [field.name, field.type]));
|
|
1385
|
-
}
|
|
1386
|
-
function deriveFromSource(source, components, seen) {
|
|
1387
|
-
const order = [];
|
|
1388
|
-
const fields = /* @__PURE__ */ new Map();
|
|
1389
|
-
const ensure = (name) => {
|
|
1390
|
-
const existing = fields.get(name);
|
|
1391
|
-
if (existing !== void 0) return existing;
|
|
1392
|
-
const created = {
|
|
1393
|
-
name,
|
|
1394
|
-
type: "shorttext",
|
|
1395
|
-
label: humanize(name),
|
|
1396
|
-
required: false,
|
|
1397
|
-
multiline: false
|
|
1398
|
-
};
|
|
1399
|
-
fields.set(name, created);
|
|
1400
|
-
order.push(name);
|
|
1401
|
-
return created;
|
|
1402
|
-
};
|
|
1403
|
-
const record = (name, type) => {
|
|
1404
|
-
if (fields.has(name)) return;
|
|
1405
|
-
const field = ensure(name);
|
|
1406
|
-
field.type = type;
|
|
1407
|
-
field.multiline = FIELD_TYPES[type].multiline;
|
|
1408
|
-
};
|
|
1409
|
-
const applyAnnotations = (el) => {
|
|
1410
|
-
const named = getAttribute(el, "data-field") ?? firstTokenName(el);
|
|
1411
|
-
if (named === void 0) return;
|
|
1412
|
-
const field = ensure(named);
|
|
1413
|
-
if (hasAttribute(el, "data-required")) field.required = true;
|
|
1414
|
-
const help = getAttribute(el, "data-help");
|
|
1415
|
-
if (help !== void 0) field.help = help;
|
|
1416
|
-
const label = getAttribute(el, "data-label");
|
|
1417
|
-
if (label !== void 0) field.label = label;
|
|
1418
|
-
const { minLength, maxLength } = lengthConstraints(el);
|
|
1419
|
-
if (minLength !== void 0) field.minLength = minLength;
|
|
1420
|
-
if (maxLength !== void 0) field.maxLength = maxLength;
|
|
1421
|
-
};
|
|
1422
|
-
const visit = (nodes) => {
|
|
1423
|
-
for (const node of nodes) {
|
|
1424
|
-
if (!isElement(node)) {
|
|
1425
|
-
if (isTextNode(node)) continue;
|
|
1426
|
-
continue;
|
|
1427
|
-
}
|
|
1428
|
-
const eachField = getAttribute(node, "data-each");
|
|
1429
|
-
if (eachField !== void 0) {
|
|
1430
|
-
const field = ensure(eachField);
|
|
1431
|
-
field.type = "url";
|
|
1432
|
-
field.multiple = true;
|
|
1433
|
-
const pickFrom = getAttribute(node, "data-pick-from");
|
|
1434
|
-
if (pickFrom !== void 0) field.pickFrom = pickFrom;
|
|
1435
|
-
applyAnnotations(node);
|
|
1436
|
-
continue;
|
|
1437
|
-
}
|
|
1438
|
-
if (hasAttribute(node, "data-each-in")) continue;
|
|
1439
|
-
const annotation = getAttribute(node, "data-type");
|
|
1440
|
-
const propTypes = components.has(node.tagName) ? componentPropTypes(node.tagName, components, seen) : void 0;
|
|
1441
|
-
for (const attr of node.attrs) {
|
|
1442
|
-
const whole = wholeValueToken(attr.value);
|
|
1443
|
-
for (const token of findTokens(attr.value)) {
|
|
1444
|
-
if (token.dotted) continue;
|
|
1445
|
-
const isWhole = whole?.name === token.name;
|
|
1446
|
-
const propType = isWhole ? propTypes?.get(attr.name) : void 0;
|
|
1447
|
-
record(
|
|
1448
|
-
token.name,
|
|
1449
|
-
propType ?? inferControl({
|
|
1450
|
-
tag: node.tagName,
|
|
1451
|
-
attribute: attr.name,
|
|
1452
|
-
wholeValue: isWhole,
|
|
1453
|
-
valuePrefix: literalPrefix(attr.value),
|
|
1454
|
-
download: hasAttribute(node, "download"),
|
|
1455
|
-
...annotation !== void 0 && { annotation }
|
|
1456
|
-
})
|
|
1457
|
-
);
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
for (const child of node.childNodes) {
|
|
1461
|
-
if (!isTextNode(child)) continue;
|
|
1462
|
-
const whole = wholeValueToken(child.value);
|
|
1463
|
-
for (const token of findTokens(child.value)) {
|
|
1464
|
-
if (token.dotted) continue;
|
|
1465
|
-
record(
|
|
1466
|
-
token.name,
|
|
1467
|
-
inferControl({
|
|
1468
|
-
tag: node.tagName,
|
|
1469
|
-
wholeValue: whole?.name === token.name,
|
|
1470
|
-
...annotation !== void 0 && { annotation }
|
|
1471
|
-
})
|
|
1472
|
-
);
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
applyAnnotations(node);
|
|
1476
|
-
visit(node.childNodes);
|
|
1477
|
-
}
|
|
1478
|
-
};
|
|
1479
|
-
visit(parseNodes(source));
|
|
1480
|
-
return order.map((name) => freeze(fields.get(name)));
|
|
1481
|
-
}
|
|
1482
|
-
function firstTokenName(el) {
|
|
1483
|
-
for (const attr of el.attrs) {
|
|
1484
|
-
const token = findTokens(attr.value).find((candidate) => !candidate.dotted);
|
|
1485
|
-
if (token !== void 0) return token.name;
|
|
1486
|
-
}
|
|
1487
|
-
for (const child of el.childNodes) {
|
|
1488
|
-
if (isTextNode(child)) {
|
|
1489
|
-
const token = findTokens(child.value).find((candidate) => !candidate.dotted);
|
|
1490
|
-
if (token !== void 0) return token.name;
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
return void 0;
|
|
1494
|
-
}
|
|
1495
|
-
function freeze(field) {
|
|
1496
|
-
return {
|
|
1497
|
-
name: field.name,
|
|
1498
|
-
type: field.type,
|
|
1499
|
-
label: field.label,
|
|
1500
|
-
required: field.required,
|
|
1501
|
-
multiline: field.multiline,
|
|
1502
|
-
...field.help !== void 0 && { help: field.help },
|
|
1503
|
-
...field.minLength !== void 0 && { minLength: field.minLength },
|
|
1504
|
-
...field.maxLength !== void 0 && { maxLength: field.maxLength },
|
|
1505
|
-
...field.multiple !== void 0 && { multiple: field.multiple },
|
|
1506
|
-
...field.pickFrom !== void 0 && { pickFrom: field.pickFrom }
|
|
1507
|
-
};
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
// ../engine/src/template/analyze.ts
|
|
1511
|
-
function analyzeTemplate(source, components = /* @__PURE__ */ new Map()) {
|
|
1512
|
-
const requiredFields = /* @__PURE__ */ new Set();
|
|
1513
|
-
const queryLoopPaths = [];
|
|
1514
|
-
const curatedLoopFields = [];
|
|
1515
|
-
const imageFields = /* @__PURE__ */ new Set();
|
|
1516
|
-
const fileFields = /* @__PURE__ */ new Set();
|
|
1517
|
-
const richTextFields = /* @__PURE__ */ new Set();
|
|
1518
|
-
const constraints = /* @__PURE__ */ new Map();
|
|
1519
|
-
const references = /* @__PURE__ */ new Set();
|
|
1520
|
-
const visit = (nodes) => {
|
|
1521
|
-
for (const node of nodes) {
|
|
1522
|
-
if (!isElement(node)) continue;
|
|
1523
|
-
const richText = richTextFieldName(node);
|
|
1524
|
-
if (richText !== void 0) richTextFields.add(richText);
|
|
1525
|
-
const eachIn = getAttribute(node, "data-each-in");
|
|
1526
|
-
if (eachIn !== void 0) queryLoopPaths.push(eachIn);
|
|
1527
|
-
const each = getAttribute(node, "data-each");
|
|
1528
|
-
if (each !== void 0) curatedLoopFields.push(each);
|
|
1529
|
-
if (hasAttribute(node, "data-required")) {
|
|
1530
|
-
const field = annotatedFieldName(node);
|
|
1531
|
-
if (field !== void 0) requiredFields.add(field);
|
|
1532
|
-
}
|
|
1533
|
-
const bounds = lengthConstraints(node);
|
|
1534
|
-
if (bounds.minLength !== void 0 || bounds.maxLength !== void 0) {
|
|
1535
|
-
const field = annotatedFieldName(node);
|
|
1536
|
-
if (field !== void 0) constraints.set(field, bounds);
|
|
1537
|
-
}
|
|
1538
|
-
const annotation = getAttribute(node, "data-type");
|
|
1539
|
-
const propTypes = components.has(node.tagName) ? componentPropTypes(node.tagName, components, /* @__PURE__ */ new Set()) : void 0;
|
|
1540
|
-
for (const attr of node.attrs) {
|
|
1541
|
-
const reference = referenceTarget(attr.value);
|
|
1542
|
-
if (reference !== void 0) references.add(reference);
|
|
1543
|
-
const token = wholeValueToken(attr.value);
|
|
1544
|
-
if (token === null || token.dotted) continue;
|
|
1545
|
-
const type = propTypes?.get(attr.name) ?? inferControl({
|
|
1546
|
-
tag: node.tagName,
|
|
1547
|
-
attribute: attr.name,
|
|
1548
|
-
wholeValue: true,
|
|
1549
|
-
valuePrefix: literalPrefix(attr.value),
|
|
1550
|
-
download: hasAttribute(node, "download"),
|
|
1551
|
-
...annotation !== void 0 && { annotation }
|
|
1552
|
-
});
|
|
1553
|
-
if (type === "image") imageFields.add(token.name);
|
|
1554
|
-
else if (type === "file") fileFields.add(token.name);
|
|
1555
|
-
else if (type === "richtext") richTextFields.add(token.name);
|
|
1556
|
-
}
|
|
1557
|
-
visit(node.childNodes);
|
|
1558
|
-
}
|
|
1559
|
-
};
|
|
1560
|
-
visit(parseNodes(source));
|
|
1561
|
-
return {
|
|
1562
|
-
requiredFields: [...requiredFields],
|
|
1563
|
-
queryLoopPaths,
|
|
1564
|
-
curatedLoopFields,
|
|
1565
|
-
imageFields: [...imageFields],
|
|
1566
|
-
fileFields: [...fileFields],
|
|
1567
|
-
richTextFields: [...richTextFields],
|
|
1568
|
-
constraints,
|
|
1569
|
-
references: [...references].sort()
|
|
1570
|
-
};
|
|
1789
|
+
const referenced2 = resolveReference(attr.value, ctx);
|
|
1790
|
+
if (referenced2 !== void 0) attr.value = sanitizeUrl(referenced2);
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
const annotation = getAttribute(el, "data-type");
|
|
1794
|
+
const whole = wholeValueToken(attr.value) !== null;
|
|
1795
|
+
const type = inferControl({
|
|
1796
|
+
tag: el.tagName,
|
|
1797
|
+
attribute: attr.name,
|
|
1798
|
+
wholeValue: whole,
|
|
1799
|
+
valuePrefix: literalPrefix(attr.value),
|
|
1800
|
+
download: hasAttribute(el, "download"),
|
|
1801
|
+
...annotation !== void 0 && { annotation }
|
|
1802
|
+
});
|
|
1803
|
+
let value = substituteTokens(attr.value, scopeResolver(scope));
|
|
1804
|
+
const referenced = resolveReference(value, ctx);
|
|
1805
|
+
if (referenced !== void 0) value = referenced;
|
|
1806
|
+
const fileUrl = ctx.media?.file(value);
|
|
1807
|
+
if (fileUrl !== void 0) value = fileUrl;
|
|
1808
|
+
if (whole && valueKindOf(type) === "url") value = sanitizeUrl(value);
|
|
1809
|
+
attr.value = value;
|
|
1810
|
+
}
|
|
1571
1811
|
}
|
|
1572
|
-
function
|
|
1812
|
+
function substituteRawText(el, scope, mode) {
|
|
1813
|
+
const resolve = scopeResolver(scope);
|
|
1814
|
+
for (const child of el.childNodes) {
|
|
1815
|
+
if (!isTextNode(child)) continue;
|
|
1816
|
+
child.value = substituteTokens(child.value, (token) => {
|
|
1817
|
+
const text = resolve(token);
|
|
1818
|
+
return mode === "json" ? escapeJsonStringContent(text) : text;
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
function wholeValueFieldToken(el, annotation) {
|
|
1573
1823
|
const meaningful = el.childNodes.filter(
|
|
1574
1824
|
(child) => !(isTextNode(child) && child.value.trim() === "")
|
|
1575
1825
|
);
|
|
1576
1826
|
const only = meaningful[0];
|
|
1577
|
-
if (meaningful.length !== 1 || only === void 0 || !isTextNode(only)) return
|
|
1827
|
+
if (meaningful.length !== 1 || only === void 0 || !isTextNode(only)) return null;
|
|
1578
1828
|
const token = wholeValueToken(only.value);
|
|
1579
|
-
if (token === null || token.dotted) return
|
|
1580
|
-
const annotation = getAttribute(el, "data-type");
|
|
1829
|
+
if (token === null || token.dotted) return null;
|
|
1581
1830
|
const type = inferControl({
|
|
1582
1831
|
tag: el.tagName,
|
|
1583
1832
|
wholeValue: true,
|
|
1584
1833
|
...annotation !== void 0 && { annotation }
|
|
1585
1834
|
});
|
|
1586
|
-
return
|
|
1835
|
+
return { token, kind: valueKindOf(type) };
|
|
1587
1836
|
}
|
|
1588
|
-
function
|
|
1589
|
-
|
|
1590
|
-
|
|
1837
|
+
function isJsonLd(el) {
|
|
1838
|
+
return el.tagName === "script" && getAttribute(el, "type")?.toLowerCase() === "application/ld+json";
|
|
1839
|
+
}
|
|
1840
|
+
function expandComponent(el, scope, ctx) {
|
|
1841
|
+
const source = ctx.components.get(el.tagName);
|
|
1842
|
+
if (source === void 0) return [el];
|
|
1843
|
+
const resolve = scopeResolver(scope);
|
|
1844
|
+
const propScope = {};
|
|
1591
1845
|
for (const attr of el.attrs) {
|
|
1592
|
-
|
|
1593
|
-
if (token !== void 0) return token.name;
|
|
1846
|
+
propScope[attr.name] = substituteTokens(attr.value, resolve);
|
|
1594
1847
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1848
|
+
const slotChildren = transformNodes(el.childNodes, scope, ctx);
|
|
1849
|
+
const body = transformNodes(parseNodes(source), propScope, ctx);
|
|
1850
|
+
const result = fillSlots(body, slotChildren);
|
|
1851
|
+
ctx.usedComponents.add(el.tagName);
|
|
1852
|
+
const css = ctx.componentStyles?.get(el.tagName)?.trim();
|
|
1853
|
+
if (css) {
|
|
1854
|
+
const root = result.find(isElement);
|
|
1855
|
+
if (root !== void 0) {
|
|
1856
|
+
const styleNode = parseFragmentNodes(`<style>@scope {
|
|
1857
|
+
${css}
|
|
1858
|
+
}</style>`);
|
|
1859
|
+
root.childNodes = [...styleNode, ...root.childNodes];
|
|
1599
1860
|
}
|
|
1600
1861
|
}
|
|
1601
|
-
return
|
|
1862
|
+
return result;
|
|
1863
|
+
}
|
|
1864
|
+
function fillSlots(body, slotChildren) {
|
|
1865
|
+
const named = /* @__PURE__ */ new Map();
|
|
1866
|
+
const defaultSlot = [];
|
|
1867
|
+
for (const child of slotChildren) {
|
|
1868
|
+
const slotName = isElement(child) ? getAttribute(child, "slot") : void 0;
|
|
1869
|
+
if (slotName !== void 0) {
|
|
1870
|
+
if (isElement(child)) removeAttribute(child, "slot");
|
|
1871
|
+
const group = named.get(slotName) ?? [];
|
|
1872
|
+
group.push(child);
|
|
1873
|
+
named.set(slotName, group);
|
|
1874
|
+
} else {
|
|
1875
|
+
defaultSlot.push(child);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
return replaceSlots(body, named, defaultSlot);
|
|
1879
|
+
}
|
|
1880
|
+
function replaceSlots(nodes, named, defaultSlot) {
|
|
1881
|
+
const out = [];
|
|
1882
|
+
for (const node of nodes) {
|
|
1883
|
+
if (isElement(node) && node.tagName === "slot") {
|
|
1884
|
+
const name = getAttribute(node, "name");
|
|
1885
|
+
const provided = name !== void 0 ? named.get(name) : defaultSlot;
|
|
1886
|
+
if (provided && provided.length > 0) {
|
|
1887
|
+
out.push(...provided);
|
|
1888
|
+
} else {
|
|
1889
|
+
out.push(...replaceSlots(node.childNodes, named, defaultSlot));
|
|
1890
|
+
}
|
|
1891
|
+
} else if (isElement(node)) {
|
|
1892
|
+
node.childNodes = replaceSlots(node.childNodes, named, defaultSlot);
|
|
1893
|
+
out.push(node);
|
|
1894
|
+
} else {
|
|
1895
|
+
out.push(node);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
return out;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
// ../engine/src/aggregate/aggregate.ts
|
|
1902
|
+
function joinUrl(baseUrl, sitePath) {
|
|
1903
|
+
return baseUrl.replace(/\/+$/, "") + sitePath;
|
|
1904
|
+
}
|
|
1905
|
+
function textContent(html) {
|
|
1906
|
+
const parts = [];
|
|
1907
|
+
const visit = (nodes) => {
|
|
1908
|
+
for (const node of nodes) {
|
|
1909
|
+
if (isTextNode(node)) parts.push(node.value);
|
|
1910
|
+
else if (isElement(node)) visit(node.childNodes);
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
visit(parseFragmentNodes(html));
|
|
1914
|
+
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
1915
|
+
}
|
|
1916
|
+
function buildSitemap(entries, baseUrl) {
|
|
1917
|
+
const urls = entries.map((entry) => {
|
|
1918
|
+
const lastmod = entry.updated ?? entry.published;
|
|
1919
|
+
const mod = lastmod !== void 0 ? `
|
|
1920
|
+
<lastmod>${escapeHtmlText(lastmod)}</lastmod>` : "";
|
|
1921
|
+
return ` <url>
|
|
1922
|
+
<loc>${escapeHtmlText(joinUrl(baseUrl, entry.url))}</loc>${mod}
|
|
1923
|
+
</url>`;
|
|
1924
|
+
}).join("\n");
|
|
1925
|
+
const contents = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1926
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
1927
|
+
${urls}
|
|
1928
|
+
</urlset>
|
|
1929
|
+
`;
|
|
1930
|
+
return { path: "sitemap.xml", contents };
|
|
1931
|
+
}
|
|
1932
|
+
function buildRss(entries, options) {
|
|
1933
|
+
const items = [...entries].sort(
|
|
1934
|
+
(a, b) => String(b.published ?? "").localeCompare(String(a.published ?? ""))
|
|
1935
|
+
);
|
|
1936
|
+
const lastBuild = items[0]?.published;
|
|
1937
|
+
const rfc822 = (iso) => new Date(iso).toUTCString();
|
|
1938
|
+
const itemXml = items.map((entry) => {
|
|
1939
|
+
const link = joinUrl(options.baseUrl, entry.url);
|
|
1940
|
+
const pub = entry.published !== void 0 ? `
|
|
1941
|
+
<pubDate>${escapeHtmlText(rfc822(entry.published))}</pubDate>` : "";
|
|
1942
|
+
const desc = entry.summary !== void 0 ? `
|
|
1943
|
+
<description>${escapeHtmlText(entry.summary)}</description>` : "";
|
|
1944
|
+
return ` <item>
|
|
1945
|
+
<title>${escapeHtmlText(entry.title)}</title>
|
|
1946
|
+
<link>${escapeHtmlText(link)}</link>
|
|
1947
|
+
<guid>${escapeHtmlText(link)}</guid>${pub}${desc}
|
|
1948
|
+
</item>`;
|
|
1949
|
+
}).join("\n");
|
|
1950
|
+
const built = lastBuild !== void 0 ? `
|
|
1951
|
+
<lastBuildDate>${escapeHtmlText(rfc822(lastBuild))}</lastBuildDate>` : "";
|
|
1952
|
+
const contents = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1953
|
+
<rss version="2.0">
|
|
1954
|
+
<channel>
|
|
1955
|
+
<title>${escapeHtmlText(options.title)}</title>
|
|
1956
|
+
<link>${escapeHtmlText(options.baseUrl)}</link>
|
|
1957
|
+
<description>${escapeHtmlText(options.description)}</description>${built}
|
|
1958
|
+
${itemXml}
|
|
1959
|
+
</channel>
|
|
1960
|
+
</rss>
|
|
1961
|
+
`;
|
|
1962
|
+
return { path: "feed.xml", contents };
|
|
1963
|
+
}
|
|
1964
|
+
function buildContentIndex(entries) {
|
|
1965
|
+
const docs = entries.map((entry) => ({
|
|
1966
|
+
url: entry.url,
|
|
1967
|
+
title: entry.title,
|
|
1968
|
+
...entry.published !== void 0 && { published: entry.published },
|
|
1969
|
+
...entry.summary !== void 0 && { summary: entry.summary }
|
|
1970
|
+
}));
|
|
1971
|
+
return { path: "content.json", contents: JSON.stringify(docs) };
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// ../engine/src/concurrency.ts
|
|
1975
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
1976
|
+
const results = new Array(items.length);
|
|
1977
|
+
const workers = Math.max(1, Math.min(Math.floor(limit), items.length));
|
|
1978
|
+
let next = 0;
|
|
1979
|
+
const run = async () => {
|
|
1980
|
+
while (next < items.length) {
|
|
1981
|
+
const index = next++;
|
|
1982
|
+
results[index] = await fn(items[index], index);
|
|
1983
|
+
}
|
|
1984
|
+
};
|
|
1985
|
+
await Promise.all(Array.from({ length: workers }, () => run()));
|
|
1986
|
+
return results;
|
|
1602
1987
|
}
|
|
1603
1988
|
|
|
1604
1989
|
// ../engine/src/url/urls.ts
|
|
@@ -2036,7 +2421,8 @@ function buildAuthoringReference(context = {}) {
|
|
|
2036
2421
|
annotationsSection(),
|
|
2037
2422
|
loopsSection(context),
|
|
2038
2423
|
componentsSection(context),
|
|
2039
|
-
shellSection()
|
|
2424
|
+
shellSection(),
|
|
2425
|
+
guardrailsSection()
|
|
2040
2426
|
]
|
|
2041
2427
|
};
|
|
2042
2428
|
}
|
|
@@ -2181,6 +2567,41 @@ function componentsSection(context) {
|
|
|
2181
2567
|
entries
|
|
2182
2568
|
};
|
|
2183
2569
|
}
|
|
2570
|
+
function guardrailsSection() {
|
|
2571
|
+
return {
|
|
2572
|
+
title: "Authoring guardrails for LLMs",
|
|
2573
|
+
intro: "Common pitfalls when authoring through MCP, where you can't see the editor form's shape change. Most stem from the same root: data-* annotations bind to the element they sit on, not to the token inside it.",
|
|
2574
|
+
entries: [
|
|
2575
|
+
{
|
|
2576
|
+
name: "data-* annotations are element-bound",
|
|
2577
|
+
summary: "data-type, data-required, data-field, data-minlength, and data-maxlength apply to the *element* that carries them \u2014 moving a token outside its annotated container silently strips those properties from the schema.",
|
|
2578
|
+
detail: 'Example: `<div data-type="richtext">{body}</div>` makes body a rich-text field. If you change it to `<div data-type="richtext"></div>{body}`, body drops back to plain shorttext. The compiler still substitutes the token at fragment root, but existing HTML content for body now renders escaped.',
|
|
2579
|
+
example: '<!-- WRONG (drops the annotation) -->\n<div data-type="richtext"></div>{body}\n\n<!-- RIGHT -->\n<div data-type="richtext">{body}</div>'
|
|
2580
|
+
},
|
|
2581
|
+
{
|
|
2582
|
+
name: "After write_file on a template, check `schemaDelta`",
|
|
2583
|
+
summary: "Every write to templates/*.html or components/*.html returns a `schemaDelta` describing added/removed/typeChanged fields. If `typeChanged` contains `destructive: true` entries or `removed` is non-empty, an auto-stamp has been written and authored content may need migration \u2014 surface to the user before further edits.",
|
|
2584
|
+
detail: "`schemaDelta` shape: `{added: [{name, type}], removed: [{name, type}], typeChanged: [{name, from, to, destructive}]}`. Empty arrays everywhere means the edit changed only whitespace, comments, or non-token markup \u2014 safe."
|
|
2585
|
+
},
|
|
2586
|
+
{
|
|
2587
|
+
name: "After rename_path on a template, search content for the old name",
|
|
2588
|
+
summary: 'Renaming `templates/article.html` to `templates/post.html` leaves every content item with `template: "article"` bound to a now-missing template. The validation gate will report these on publish, but the cheaper check is `list_pending_migrations` followed by editing the items\' `template` fields.'
|
|
2589
|
+
},
|
|
2590
|
+
{
|
|
2591
|
+
name: "After delete_path on a template, expect publish-blocking errors",
|
|
2592
|
+
summary: "Deleting a template that any item binds to will fail validation with `template-missing` per item. Use `list_pending_migrations` first to see who would be affected, and consider whether to rename items to a different template before deleting."
|
|
2593
|
+
},
|
|
2594
|
+
{
|
|
2595
|
+
name: "Read describe_template before editing a content item",
|
|
2596
|
+
summary: "A content item's `fields` object MUST match the bound template's schema (field names and value shapes). `describe_template` returns the field list with types so you build the right shape on the first try; mismatched keys become orphan fields the gate warns about."
|
|
2597
|
+
},
|
|
2598
|
+
{
|
|
2599
|
+
name: "richtext values are HTML, shorttext/text are plain text",
|
|
2600
|
+
summary: "For a `richtext` field, write the value as HTML (`<p>...</p>` etc). For `shorttext` or `text`, write plain text \u2014 any HTML you include is escaped at render time. If the gate emits a `content.type-drift` warning, a field's type changed destructively (commonly richtext \u2192 shorttext) and the old HTML content now renders as escaped text."
|
|
2601
|
+
}
|
|
2602
|
+
]
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2184
2605
|
function shellSection() {
|
|
2185
2606
|
return {
|
|
2186
2607
|
title: "The document shell",
|
|
@@ -2269,18 +2690,91 @@ async function validateSite(source) {
|
|
|
2269
2690
|
}
|
|
2270
2691
|
};
|
|
2271
2692
|
const analysisCache = /* @__PURE__ */ new Map();
|
|
2693
|
+
const sourceCache = /* @__PURE__ */ new Map();
|
|
2272
2694
|
const analyze = async (name) => {
|
|
2273
2695
|
const cached = analysisCache.get(name);
|
|
2274
2696
|
if (cached !== void 0 || analysisCache.has(name)) return cached ?? null;
|
|
2275
2697
|
let result;
|
|
2276
2698
|
try {
|
|
2277
|
-
|
|
2699
|
+
const html = await loadTemplate(source, name);
|
|
2700
|
+
sourceCache.set(name, html);
|
|
2701
|
+
result = analyzeTemplate(html, components);
|
|
2278
2702
|
} catch {
|
|
2279
2703
|
result = null;
|
|
2280
2704
|
}
|
|
2281
2705
|
analysisCache.set(name, result);
|
|
2282
2706
|
return result;
|
|
2283
2707
|
};
|
|
2708
|
+
const expectedFieldsCache = /* @__PURE__ */ new Map();
|
|
2709
|
+
const expectedFieldsFor = (templateName) => {
|
|
2710
|
+
const cached = expectedFieldsCache.get(templateName);
|
|
2711
|
+
if (cached !== void 0) return cached;
|
|
2712
|
+
const html = sourceCache.get(templateName);
|
|
2713
|
+
const fields = new Set(
|
|
2714
|
+
html !== void 0 ? deriveFields(html, components).map((f) => f.name) : []
|
|
2715
|
+
);
|
|
2716
|
+
expectedFieldsCache.set(templateName, fields);
|
|
2717
|
+
return fields;
|
|
2718
|
+
};
|
|
2719
|
+
const driftFieldsCache = /* @__PURE__ */ new Map();
|
|
2720
|
+
const driftFieldsFor = async (templateName) => {
|
|
2721
|
+
if (driftFieldsCache.has(templateName)) return driftFieldsCache.get(templateName) ?? null;
|
|
2722
|
+
const highest = await highestSnapshotName(templateName);
|
|
2723
|
+
if (highest === null) {
|
|
2724
|
+
driftFieldsCache.set(templateName, null);
|
|
2725
|
+
return null;
|
|
2726
|
+
}
|
|
2727
|
+
let snapshotHtml;
|
|
2728
|
+
try {
|
|
2729
|
+
snapshotHtml = await loadTemplate(source, highest);
|
|
2730
|
+
} catch {
|
|
2731
|
+
driftFieldsCache.set(templateName, null);
|
|
2732
|
+
return null;
|
|
2733
|
+
}
|
|
2734
|
+
const currentHtml = sourceCache.get(templateName);
|
|
2735
|
+
if (currentHtml === void 0) {
|
|
2736
|
+
driftFieldsCache.set(templateName, null);
|
|
2737
|
+
return null;
|
|
2738
|
+
}
|
|
2739
|
+
const delta = computeSchemaDelta(
|
|
2740
|
+
deriveFields(snapshotHtml, components),
|
|
2741
|
+
deriveFields(currentHtml, components)
|
|
2742
|
+
);
|
|
2743
|
+
const destructive = new Set(
|
|
2744
|
+
delta.typeChanged.filter((change) => change.destructive).map((change) => change.name)
|
|
2745
|
+
);
|
|
2746
|
+
driftFieldsCache.set(templateName, destructive);
|
|
2747
|
+
return destructive;
|
|
2748
|
+
};
|
|
2749
|
+
const snapshotNameCache = /* @__PURE__ */ new Map();
|
|
2750
|
+
const highestSnapshotName = async (baseName) => {
|
|
2751
|
+
if (snapshotNameCache.has(baseName)) return snapshotNameCache.get(baseName) ?? null;
|
|
2752
|
+
const slash = baseName.lastIndexOf("/");
|
|
2753
|
+
const dir = slash === -1 ? "templates" : `templates/${baseName.slice(0, slash)}`;
|
|
2754
|
+
const stem = slash === -1 ? baseName : baseName.slice(slash + 1);
|
|
2755
|
+
let entries;
|
|
2756
|
+
try {
|
|
2757
|
+
entries = await source.list(dir);
|
|
2758
|
+
} catch {
|
|
2759
|
+
snapshotNameCache.set(baseName, null);
|
|
2760
|
+
return null;
|
|
2761
|
+
}
|
|
2762
|
+
let highest = 0;
|
|
2763
|
+
let highestStem = null;
|
|
2764
|
+
const prefix = `${stem}@v`;
|
|
2765
|
+
for (const entry of entries) {
|
|
2766
|
+
if (entry.kind !== "file" || !entry.name.endsWith(".html")) continue;
|
|
2767
|
+
const entryStem = entry.name.slice(0, -".html".length);
|
|
2768
|
+
if (!entryStem.startsWith(prefix)) continue;
|
|
2769
|
+
const version = versionNumber(entryStem);
|
|
2770
|
+
if (version !== null && version > highest) {
|
|
2771
|
+
highest = version;
|
|
2772
|
+
highestStem = slash === -1 ? entryStem : `${baseName.slice(0, slash)}/${entryStem}`;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
snapshotNameCache.set(baseName, highestStem);
|
|
2776
|
+
return highestStem;
|
|
2777
|
+
};
|
|
2284
2778
|
const urlOwners = /* @__PURE__ */ new Map();
|
|
2285
2779
|
for (const { node, templateName } of published) {
|
|
2286
2780
|
const owners = urlOwners.get(urlForItem(node.path)) ?? [];
|
|
@@ -2297,14 +2791,40 @@ async function validateSite(source) {
|
|
|
2297
2791
|
}
|
|
2298
2792
|
const analysis = await analyze(templateName);
|
|
2299
2793
|
if (analysis === null) {
|
|
2794
|
+
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
2795
|
add(
|
|
2301
2796
|
"error",
|
|
2302
2797
|
"template-missing",
|
|
2303
|
-
`"${node.path}" uses template "${templateName}", which does not exist
|
|
2798
|
+
`"${node.path}" uses template "${templateName}", which does not exist.${snapshotHint}`,
|
|
2304
2799
|
node.path
|
|
2305
2800
|
);
|
|
2306
2801
|
continue;
|
|
2307
2802
|
}
|
|
2803
|
+
if (!isVersionedTemplateName(templateName)) {
|
|
2804
|
+
const expectedFields = expectedFieldsFor(templateName);
|
|
2805
|
+
const itemFieldNames = Object.keys(node.item.fields);
|
|
2806
|
+
const orphans = itemFieldNames.filter((name) => !expectedFields.has(name));
|
|
2807
|
+
if (orphans.length > 0) {
|
|
2808
|
+
add(
|
|
2809
|
+
"warning",
|
|
2810
|
+
"content.schema-drift",
|
|
2811
|
+
`"${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.`,
|
|
2812
|
+
node.path
|
|
2813
|
+
);
|
|
2814
|
+
}
|
|
2815
|
+
const driftFields = await driftFieldsFor(templateName);
|
|
2816
|
+
if (driftFields !== null && driftFields.size > 0) {
|
|
2817
|
+
const affected = itemFieldNames.filter((name) => driftFields.has(name));
|
|
2818
|
+
if (affected.length > 0) {
|
|
2819
|
+
add(
|
|
2820
|
+
"warning",
|
|
2821
|
+
"content.type-drift",
|
|
2822
|
+
`"${node.path}" has field${affected.length === 1 ? "" : "s"} whose type changed destructively in template "${templateName}": ${affected.map((f) => `"${f}"`).join(", ")}. The value will render under the new type (commonly HTML escaped as text). Open the Migrations workspace to migrate.`,
|
|
2823
|
+
node.path
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2308
2828
|
const scope = buildScope2(node.item);
|
|
2309
2829
|
for (const field of analysis.requiredFields) {
|
|
2310
2830
|
if (isEmpty(scope[field])) {
|
|
@@ -2590,10 +3110,10 @@ function createDiagnosticRegistry() {
|
|
|
2590
3110
|
repairs.set(repair.id, repair);
|
|
2591
3111
|
},
|
|
2592
3112
|
runAll,
|
|
2593
|
-
async runRepair(id, deps) {
|
|
3113
|
+
async runRepair(id, deps, args = {}) {
|
|
2594
3114
|
const repair = repairs.get(id);
|
|
2595
3115
|
if (repair === void 0) throw new Error(`Unknown repair id: ${id}`);
|
|
2596
|
-
await repair.run(deps);
|
|
3116
|
+
await repair.run(deps, args);
|
|
2597
3117
|
return runAll(deps);
|
|
2598
3118
|
}
|
|
2599
3119
|
};
|
|
@@ -2733,6 +3253,22 @@ export {
|
|
|
2733
3253
|
parseContentItem,
|
|
2734
3254
|
parseSortFile,
|
|
2735
3255
|
InMemoryBlobStore,
|
|
3256
|
+
FIELD_TYPES,
|
|
3257
|
+
isFieldType,
|
|
3258
|
+
valueKindOf,
|
|
3259
|
+
inferControl,
|
|
3260
|
+
findTokens,
|
|
3261
|
+
wholeValueToken,
|
|
3262
|
+
deriveFields,
|
|
3263
|
+
isDestructiveTypeChange,
|
|
3264
|
+
computeSchemaDelta,
|
|
3265
|
+
detectStamp,
|
|
3266
|
+
isVersionedTemplateName,
|
|
3267
|
+
baseTemplateName,
|
|
3268
|
+
versionNumber,
|
|
3269
|
+
snapshotName,
|
|
3270
|
+
nextVersionNumber,
|
|
3271
|
+
isReservedVersionedPath,
|
|
2736
3272
|
RESERVED_PREFIX,
|
|
2737
3273
|
emptyIndex,
|
|
2738
3274
|
loadIndex,
|
|
@@ -2752,12 +3288,10 @@ export {
|
|
|
2752
3288
|
loadTemplate,
|
|
2753
3289
|
DOCUMENT_SHELL,
|
|
2754
3290
|
loadDocumentShell,
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
findTokens,
|
|
2760
|
-
wholeValueToken,
|
|
3291
|
+
analyzeTemplate,
|
|
3292
|
+
pendingMigrations,
|
|
3293
|
+
bestFitSnapshot,
|
|
3294
|
+
applyMigration,
|
|
2761
3295
|
escapeHtmlText,
|
|
2762
3296
|
escapeHtmlAttribute,
|
|
2763
3297
|
escapeJsonStringContent,
|
|
@@ -2773,8 +3307,6 @@ export {
|
|
|
2773
3307
|
buildSitemap,
|
|
2774
3308
|
buildRss,
|
|
2775
3309
|
buildContentIndex,
|
|
2776
|
-
deriveFields,
|
|
2777
|
-
analyzeTemplate,
|
|
2778
3310
|
outputPathForItem,
|
|
2779
3311
|
urlForItem,
|
|
2780
3312
|
compileSite,
|