@wizzlethorpe/vaults 0.9.4 → 0.9.6

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.
@@ -0,0 +1,1270 @@
1
+ // ../foundry/scripts/api.mjs
2
+ function url(vault, path) {
3
+ if (!vault?.url) throw new Error("Vault URL is not configured.");
4
+ const u = new URL(path, vault.url.endsWith("/") ? vault.url : vault.url + "/");
5
+ if (vault.token) u.searchParams.set("_token", vault.token);
6
+ return u.toString();
7
+ }
8
+ async function fetchJson(u) {
9
+ const res = await fetch(u);
10
+ if (!res.ok) throw new Error(`GET ${u} \u2192 ${res.status}`);
11
+ return res.json();
12
+ }
13
+ async function fetchManifest(vault) {
14
+ return fetchJson(url(vault, "/_manifest.json"));
15
+ }
16
+ var BATCH_SIZE = 100;
17
+ var BATCH_CONCURRENCY = 4;
18
+ var DIRECT_CONCURRENCY = 8;
19
+ async function fetchSourceBatch(vault, paths) {
20
+ if (paths.length === 0) return /* @__PURE__ */ new Map();
21
+ if (vault.public) return fetchSourceDirect(vault, paths);
22
+ const endpoint = url(vault, "/_batch");
23
+ const chunks = [];
24
+ for (let i = 0; i < paths.length; i += BATCH_SIZE) chunks.push(paths.slice(i, i + BATCH_SIZE));
25
+ const out = /* @__PURE__ */ new Map();
26
+ let next = 0;
27
+ const workers = Array.from({ length: Math.min(BATCH_CONCURRENCY, chunks.length) }, async () => {
28
+ while (next < chunks.length) {
29
+ const idx = next++;
30
+ const res = await fetch(endpoint, {
31
+ method: "POST",
32
+ headers: { "Content-Type": "text/plain" },
33
+ body: chunks[idx].join("\n")
34
+ });
35
+ if (!res.ok) throw new Error(`POST /_batch \u2192 ${res.status}`);
36
+ const data = await res.json();
37
+ if (data.files) {
38
+ for (const [p, content] of Object.entries(data.files)) out.set(p, content);
39
+ }
40
+ }
41
+ });
42
+ await Promise.all(workers);
43
+ return out;
44
+ }
45
+ async function fetchSourceDirect(vault, paths) {
46
+ const out = /* @__PURE__ */ new Map();
47
+ let next = 0;
48
+ const workers = Array.from({ length: Math.min(DIRECT_CONCURRENCY, paths.length) }, async () => {
49
+ while (next < paths.length) {
50
+ const idx = next++;
51
+ const path = paths[idx];
52
+ const u = url(vault, "/" + path);
53
+ try {
54
+ const res = await fetch(u);
55
+ if (!res.ok) {
56
+ if (res.status !== 404) console.warn(`Vaults | GET ${path} \u2192 ${res.status}`);
57
+ continue;
58
+ }
59
+ out.set(path, await res.text());
60
+ } catch (err) {
61
+ console.warn(`Vaults | GET ${path} failed:`, err);
62
+ }
63
+ }
64
+ });
65
+ await Promise.all(workers);
66
+ return out;
67
+ }
68
+
69
+ // ../foundry/scripts/settings.mjs
70
+ var MODULE_ID = "vaults";
71
+
72
+ // ../foundry/scripts/util.mjs
73
+ function escapeAttr(s) {
74
+ return String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
75
+ }
76
+ function escapeHtml(s) {
77
+ return String(s ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
78
+ }
79
+ function escapeBraces(s) {
80
+ return String(s).replace(/[{}]/g, "");
81
+ }
82
+ async function hexDigest(algorithm, text) {
83
+ const buf = await crypto.subtle.digest(algorithm, new TextEncoder().encode(text));
84
+ return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
85
+ }
86
+
87
+ // ../foundry/scripts/ids.mjs
88
+ async function det(kind, key) {
89
+ const hex = await hexDigest("SHA-1", `vaults:${kind}:${key}`);
90
+ return hex.slice(0, 16);
91
+ }
92
+ function folderOfPath(path) {
93
+ const i = path.lastIndexOf("/");
94
+ return i < 0 ? "" : path.slice(0, i);
95
+ }
96
+ var entryId = (vaultId, path) => det("entry", `${vaultId}:${folderOfPath(path)}`);
97
+ var pageId = (vaultId, path) => det("page", `${vaultId}:${path}`);
98
+ var folderId = (vaultId, path) => det("folder", `${vaultId}:${path}`);
99
+ var instanceId = (vaultId, path) => det("instance", `${vaultId}:${path}`);
100
+ var subdocId = (vaultId, path, pointer) => det("subdoc", `${vaultId}:${path}:${pointer}`);
101
+
102
+ // ../foundry/scripts/parser.mjs
103
+ var CACHED_EXT_RE = /\.(png|jpe?g|webp|gif|svg|avif|tiff?|bmp|heic|apng|ogg|mp3|m4a|wav|flac|opus|aac|mp4|webm|mov|ogv|pdf|epub|json)$/i;
104
+
105
+ // ../foundry/scripts/media.mjs
106
+ var CACHE_DIR = "vaults-cache";
107
+ var BATCH_SIZE2 = 25;
108
+ var BATCH_BYTE_BUDGET = 8 * 1024 * 1024;
109
+ var BATCH_CONCURRENCY2 = 4;
110
+ function isCacheable(path) {
111
+ if (!CACHED_EXT_RE.test(path)) return false;
112
+ if (path.startsWith("_") || path.includes("/_")) return false;
113
+ if (/\.preview\.json$/i.test(path)) return false;
114
+ return true;
115
+ }
116
+ function localFileUrl(vaultId, vaultPath) {
117
+ const worldId = game.world?.id;
118
+ if (!worldId) throw new Error("No active world; cache path unavailable.");
119
+ const segs = vaultPath.split("/").map(encodeURIComponent).join("/");
120
+ const relative = `worlds/${worldId}/${CACHE_DIR}/${vaultId}/${segs}`;
121
+ return foundry.utils?.getRoute?.(relative) ?? `/${relative}`;
122
+ }
123
+ function vaultCacheDir(vaultId) {
124
+ const worldId = game.world.id;
125
+ return `worlds/${worldId}/${CACHE_DIR}/${vaultId}`;
126
+ }
127
+ async function syncImages(host, vault, manifestFiles) {
128
+ const remoteImages = /* @__PURE__ */ new Map();
129
+ for (const f of manifestFiles) {
130
+ if (isCacheable(f.path)) remoteImages.set(f.path, f.hash);
131
+ }
132
+ const lastImageManifest = host.getVaultState(vault.id).lastImageManifest;
133
+ const last = new Map(Object.entries(lastImageManifest || {}));
134
+ const toDownload = [];
135
+ for (const [path, hash] of remoteImages) {
136
+ if (last.get(path) !== hash) toDownload.push(path);
137
+ }
138
+ const toDelete = [...last.keys()].filter((p) => !remoteImages.has(p));
139
+ if (toDownload.length === 0 && toDelete.length === 0) return { downloaded: 0, removed: 0, errors: 0 };
140
+ const baseDir = vaultCacheDir(vault.id);
141
+ const worldRoot = `worlds/${game.world.id}`;
142
+ const dirsNeeded = /* @__PURE__ */ new Set();
143
+ const addChain = (fullPath) => {
144
+ const sub = fullPath.startsWith(worldRoot + "/") ? fullPath.slice(worldRoot.length + 1) : "";
145
+ if (!sub) return;
146
+ let acc = worldRoot;
147
+ for (const seg of sub.split("/")) {
148
+ acc += "/" + seg;
149
+ dirsNeeded.add(acc);
150
+ }
151
+ };
152
+ addChain(baseDir);
153
+ for (const p of toDownload) {
154
+ const segs = p.split("/").slice(0, -1);
155
+ if (segs.length > 0) addChain(`${baseDir}/${segs.join("/")}`);
156
+ }
157
+ await ensureDirs([...dirsNeeded]);
158
+ const sizeOf = /* @__PURE__ */ new Map();
159
+ for (const f of manifestFiles) sizeOf.set(f.path, f.size ?? 0);
160
+ const chunks = [];
161
+ let chunk = [];
162
+ let chunkBytes = 0;
163
+ for (const path of toDownload) {
164
+ const bytes = sizeOf.get(path) ?? 0;
165
+ if (chunk.length > 0 && (chunk.length >= BATCH_SIZE2 || chunkBytes + bytes > BATCH_BYTE_BUDGET)) {
166
+ chunks.push(chunk);
167
+ chunk = [];
168
+ chunkBytes = 0;
169
+ }
170
+ chunk.push(path);
171
+ chunkBytes += bytes;
172
+ }
173
+ if (chunk.length > 0) chunks.push(chunk);
174
+ let next = 0;
175
+ const downloaded = [];
176
+ const errors = [];
177
+ const workers = Array.from({ length: Math.min(BATCH_CONCURRENCY2, chunks.length) }, async () => {
178
+ while (next < chunks.length) {
179
+ const idx = next++;
180
+ const chunk2 = chunks[idx];
181
+ try {
182
+ const blobs = await fetchImagesBatch(vault, chunk2);
183
+ for (const path of chunk2) {
184
+ const blob = blobs.get(path);
185
+ if (!blob) {
186
+ errors.push({ path, err: new Error("missing in batch response") });
187
+ continue;
188
+ }
189
+ try {
190
+ await uploadToWorld(baseDir, path, blob);
191
+ downloaded.push(path);
192
+ } catch (err) {
193
+ errors.push({ path, err });
194
+ }
195
+ }
196
+ } catch (err) {
197
+ for (const path of chunk2) errors.push({ path, err });
198
+ }
199
+ }
200
+ });
201
+ await Promise.all(workers);
202
+ if (errors.length > 0) {
203
+ console.warn(`Vaults | ${errors.length} image(s) failed to download:`, errors);
204
+ }
205
+ let removed = 0;
206
+ for (const path of toDelete) {
207
+ try {
208
+ await deleteFromWorld(baseDir, path);
209
+ removed++;
210
+ } catch (err) {
211
+ console.warn(`Vaults | could not remove orphan ${path}:`, err?.message || err);
212
+ }
213
+ }
214
+ const persisted = {};
215
+ for (const [path, hash] of remoteImages) {
216
+ if (last.get(path) === hash) {
217
+ persisted[path] = hash;
218
+ continue;
219
+ }
220
+ if (downloaded.includes(path)) persisted[path] = hash;
221
+ }
222
+ await host.setVaultState(vault.id, { lastImageManifest: persisted });
223
+ return { downloaded: downloaded.length, removed, errors: errors.length };
224
+ }
225
+ async function deleteVaultCache(vaultId) {
226
+ const baseDir = vaultCacheDir(vaultId);
227
+ const impl = fp();
228
+ if (typeof impl.deleteFile !== "function") return false;
229
+ try {
230
+ await impl.deleteFile("data", baseDir);
231
+ return true;
232
+ } catch (err) {
233
+ console.warn(`Vaults | could not remove cache dir ${baseDir}:`, err?.message || err);
234
+ return false;
235
+ }
236
+ }
237
+ async function fetchImagesBatch(vault, paths) {
238
+ if (vault.public) return fetchImagesDirect(vault, paths);
239
+ const res = await fetch(url(vault, "/_batch-images"), {
240
+ method: "POST",
241
+ headers: { "Content-Type": "text/plain" },
242
+ // CORS-simple, no preflight
243
+ body: paths.join("\n")
244
+ });
245
+ if (!res.ok) throw new Error(`POST /_batch-images \u2192 ${res.status}`);
246
+ const data = await res.json();
247
+ const out = /* @__PURE__ */ new Map();
248
+ for (const [path, b64] of Object.entries(data.files || {})) {
249
+ out.set(path, base64ToBlob(b64, guessMime(path)));
250
+ }
251
+ return out;
252
+ }
253
+ async function fetchImagesDirect(vault, paths) {
254
+ const out = /* @__PURE__ */ new Map();
255
+ const PARALLEL = 6;
256
+ let next = 0;
257
+ const workers = Array.from({ length: Math.min(PARALLEL, paths.length) }, async () => {
258
+ while (next < paths.length) {
259
+ const idx = next++;
260
+ const path = paths[idx];
261
+ try {
262
+ const res = await fetch(url(vault, "/" + path));
263
+ if (!res.ok) continue;
264
+ const blob = await res.blob();
265
+ const typed = blob.type ? blob : new Blob([await blob.arrayBuffer()], { type: guessMime(path) });
266
+ out.set(path, typed);
267
+ } catch (err) {
268
+ console.warn(`Vaults | GET ${path} failed:`, err);
269
+ }
270
+ }
271
+ });
272
+ await Promise.all(workers);
273
+ return out;
274
+ }
275
+ function base64ToBlob(b64, type) {
276
+ const binary = atob(b64);
277
+ const bytes = new Uint8Array(binary.length);
278
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
279
+ return new Blob([bytes], { type });
280
+ }
281
+ function fp() {
282
+ return foundry.applications?.apps?.FilePicker?.implementation ?? FilePicker.implementation ?? FilePicker;
283
+ }
284
+ async function uploadToWorld(baseDir, path, blob) {
285
+ const segs = path.split("/");
286
+ const filename = segs.pop();
287
+ const dir = segs.length > 0 ? `${baseDir}/${segs.join("/")}` : baseDir;
288
+ const file = new File([blob], filename, { type: blob.type || guessMime(filename) });
289
+ const result = await fp().upload("data", dir, file, {}, { notify: false });
290
+ if (result === false || result?.status === "error") {
291
+ throw new Error(`upload failed: ${result?.message || "unknown"} (path=${dir}/${filename})`);
292
+ }
293
+ }
294
+ async function ensureDirs(paths) {
295
+ paths.sort((a, b) => a.length - b.length);
296
+ for (const p of paths) {
297
+ try {
298
+ await fp().createDirectory("data", p, {});
299
+ } catch (err) {
300
+ const msg = String(err?.message || err);
301
+ if (!/exists|already/i.test(msg)) throw err;
302
+ }
303
+ }
304
+ }
305
+ async function deleteFromWorld(baseDir, path) {
306
+ const full = `${baseDir}/${path}`;
307
+ const impl = fp();
308
+ if (typeof impl.deleteFile === "function") {
309
+ await impl.deleteFile("data", full);
310
+ return;
311
+ }
312
+ throw new Error("FilePicker.deleteFile is not available in this Foundry version.");
313
+ }
314
+ function guessMime(filename) {
315
+ const ext = filename.split(".").pop()?.toLowerCase();
316
+ return {
317
+ webp: "image/webp",
318
+ png: "image/png",
319
+ jpg: "image/jpeg",
320
+ jpeg: "image/jpeg",
321
+ gif: "image/gif",
322
+ svg: "image/svg+xml",
323
+ avif: "image/avif",
324
+ tiff: "image/tiff",
325
+ bmp: "image/bmp",
326
+ heic: "image/heic",
327
+ apng: "image/apng",
328
+ ogg: "audio/ogg",
329
+ mp3: "audio/mpeg",
330
+ m4a: "audio/mp4",
331
+ wav: "audio/wav",
332
+ flac: "audio/flac",
333
+ opus: "audio/ogg",
334
+ aac: "audio/aac",
335
+ mp4: "video/mp4",
336
+ webm: "video/webm",
337
+ mov: "video/quicktime",
338
+ ogv: "video/ogg",
339
+ pdf: "application/pdf",
340
+ epub: "application/epub+zip",
341
+ json: "application/json"
342
+ }[ext] || "application/octet-stream";
343
+ }
344
+
345
+ // ../foundry/scripts/links.mjs
346
+ var ANCHOR_RE = /<a\b([^>]*)>([\s\S]*?)<\/a>/gi;
347
+ var MEDIA_SRC_RE = /<(img|audio|video)\b([^>]*?)src="([^"]+)"([^>]*)>/gi;
348
+ var ATTR_HREF_RE = /\bhref="([^"]+)"/i;
349
+ var ATTR_CLASS_RE = /\bclass="([^"]+)"/i;
350
+ var TAG_RE = /<[^>]+>/g;
351
+ function buildPathIndex(manifestFiles) {
352
+ const paths = /* @__PURE__ */ new Set();
353
+ const idOverrides = /* @__PURE__ */ new Map();
354
+ for (const f of manifestFiles) {
355
+ if (!f.path.endsWith(".body.html")) continue;
356
+ const mdPath = f.path.replace(/\.body\.html$/i, "") + ".md";
357
+ paths.add(mdPath);
358
+ const override = f.meta?.foundry?.id;
359
+ if (typeof override === "string" && override) idOverrides.set(mdPath, override);
360
+ }
361
+ return { paths, idOverrides };
362
+ }
363
+ function logicalPathFromHref(href) {
364
+ const decoded = decodeHtmlEntities(href);
365
+ const cleaned = decoded.replace(/^\//, "").split("#")[0];
366
+ try {
367
+ return decodeURIComponent(cleaned) + ".md";
368
+ } catch {
369
+ return cleaned + ".md";
370
+ }
371
+ }
372
+ function decodeHtmlEntities(s) {
373
+ const ta = document.createElement("textarea");
374
+ ta.innerHTML = s;
375
+ return ta.value;
376
+ }
377
+ async function transformHtmlForFoundry(vault, html, index) {
378
+ html = await rewriteWikilinks(vault.id, html, index);
379
+ html = rewriteMediaSrcs(vault.id, html);
380
+ html = rewritePassthroughLinks(vault.id, html);
381
+ html = await applyDomTransforms(html, vault, index);
382
+ return html;
383
+ }
384
+ async function applyDomTransforms(html, vault, index) {
385
+ const doc = new DOMParser().parseFromString(html, "text/html");
386
+ let touched = false;
387
+ touched = stripWebOnlyWidgets(doc) || touched;
388
+ touched = flattenBasesTabs(doc) || touched;
389
+ touched = neutralizeEnrichersInCode(doc) || touched;
390
+ touched = await rewriteBasesCardLinks(doc, vault.id, index) || touched;
391
+ touched = rewriteDiceButtons(doc) || touched;
392
+ touched = wrapRestrictedCalloutsAsSecret(doc, vault) || touched;
393
+ return touched ? doc.body.innerHTML : html;
394
+ }
395
+ var WEB_ONLY_WIDGET_SELECTOR = ".vaults-battlemap";
396
+ function stripWebOnlyWidgets(doc) {
397
+ const widgets = doc.querySelectorAll(WEB_ONLY_WIDGET_SELECTOR);
398
+ for (const el of widgets) el.remove();
399
+ return widgets.length > 0;
400
+ }
401
+ function rewriteDiceButtons(doc) {
402
+ const buttons = doc.querySelectorAll("button.dice-roll[data-formula]");
403
+ if (buttons.length === 0) return false;
404
+ for (const btn of buttons) {
405
+ const formula = btn.getAttribute("data-formula");
406
+ if (!formula) continue;
407
+ btn.replaceWith(doc.createTextNode(`[[/r ${formula}]]`));
408
+ }
409
+ return true;
410
+ }
411
+ function flattenBasesTabs(doc) {
412
+ const tabbeds = doc.querySelectorAll(".bases-tabbed");
413
+ if (tabbeds.length === 0) return false;
414
+ for (const tabbed of tabbeds) {
415
+ const fragment = doc.createDocumentFragment();
416
+ for (const panel of tabbed.querySelectorAll(".bases-tab-panel")) {
417
+ panel.removeAttribute("hidden");
418
+ while (panel.firstChild) fragment.appendChild(panel.firstChild);
419
+ }
420
+ tabbed.replaceWith(fragment);
421
+ }
422
+ return true;
423
+ }
424
+ function neutralizeEnrichersInCode(doc) {
425
+ const codes = doc.querySelectorAll("code, pre");
426
+ if (codes.length === 0) return false;
427
+ const ZWS = "\u200B";
428
+ let touched = false;
429
+ for (const el of codes) {
430
+ const walker = doc.createTreeWalker(el, NodeFilter.SHOW_TEXT);
431
+ let node;
432
+ while (node = walker.nextNode()) {
433
+ const orig = node.nodeValue;
434
+ if (orig.indexOf("@") < 0) continue;
435
+ const next = orig.replace(/@/g, "@" + ZWS);
436
+ if (next !== orig) {
437
+ node.nodeValue = next;
438
+ touched = true;
439
+ }
440
+ }
441
+ }
442
+ return touched;
443
+ }
444
+ async function rewriteBasesCardLinks(doc, vaultId, index) {
445
+ const cards = doc.querySelectorAll("a.bases-card[href]");
446
+ if (cards.length === 0) return false;
447
+ let touched = false;
448
+ for (const a of cards) {
449
+ const href = a.getAttribute("href") || "";
450
+ if (!href.startsWith("/")) continue;
451
+ const path = logicalPathFromHref(href);
452
+ if (!index.paths.has(path)) continue;
453
+ const eId = await entryId(vaultId, path);
454
+ const pId = index.idOverrides?.get(path) ?? await pageId(vaultId, path);
455
+ a.classList.add("content-link");
456
+ a.setAttribute("data-uuid", `JournalEntry.${eId}.JournalEntryPage.${pId}`);
457
+ a.removeAttribute("href");
458
+ touched = true;
459
+ }
460
+ return touched;
461
+ }
462
+ function wrapRestrictedCalloutsAsSecret(doc, vault) {
463
+ if (!vault?.dmRole || !Array.isArray(vault.knownRoles) || vault.knownRoles.length === 0) {
464
+ return false;
465
+ }
466
+ const dmIdx = vault.knownRoles.indexOf(vault.dmRole);
467
+ if (dmIdx < 0) return false;
468
+ const restrictedRoles = vault.knownRoles.slice(dmIdx);
469
+ if (restrictedRoles.length === 0) return false;
470
+ let touched = false;
471
+ for (const role of restrictedRoles) {
472
+ for (const el of doc.querySelectorAll(".callout.callout-" + cssEscape(role))) {
473
+ const section = doc.createElement("section");
474
+ section.className = "secret";
475
+ el.parentNode.insertBefore(section, el);
476
+ section.appendChild(el);
477
+ touched = true;
478
+ }
479
+ }
480
+ return touched;
481
+ }
482
+ function cssEscape(s) {
483
+ return String(s).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
484
+ }
485
+ async function rewriteWikilinks(vaultId, html, index) {
486
+ const matches = [];
487
+ let m;
488
+ ANCHOR_RE.lastIndex = 0;
489
+ while ((m = ANCHOR_RE.exec(html)) !== null) {
490
+ const [full, attrs, inner] = m;
491
+ const cls = ATTR_CLASS_RE.exec(attrs)?.[1] || "";
492
+ if (!/\binternal-link\b/.test(cls)) continue;
493
+ const label = stripTags(inner);
494
+ const isUnresolved = /\bis-unresolved\b/.test(cls);
495
+ const href = ATTR_HREF_RE.exec(attrs)?.[1] || "";
496
+ const path = href.startsWith("/") ? logicalPathFromHref(href) : null;
497
+ const inIndex = path != null && index.paths.has(path);
498
+ if (isUnresolved || !inIndex) {
499
+ matches.push({ idx: m.index, length: full.length, kind: "broken", label });
500
+ continue;
501
+ }
502
+ matches.push({ idx: m.index, length: full.length, kind: "uuid", label, path });
503
+ }
504
+ const uuidMatches = matches.filter((r) => r.kind === "uuid");
505
+ const resolved = await Promise.all(uuidMatches.map(async (r) => ({
506
+ eId: await entryId(vaultId, r.path),
507
+ pId: index.idOverrides?.get(r.path) ?? await pageId(vaultId, r.path)
508
+ })));
509
+ uuidMatches.forEach((r, i) => {
510
+ r.eId = resolved[i].eId;
511
+ r.pId = resolved[i].pId;
512
+ });
513
+ matches.sort((a, b) => b.idx - a.idx);
514
+ for (const r of matches) {
515
+ const replacement = r.kind === "uuid" ? `@UUID[JournalEntry.${r.eId}.JournalEntryPage.${r.pId}]{${escapeBraces(r.label)}}` : `<span class="vaults-broken">${escapeHtml(r.label)}</span>`;
516
+ html = html.slice(0, r.idx) + replacement + html.slice(r.idx + r.length);
517
+ }
518
+ return html;
519
+ }
520
+ function rewriteMediaSrcs(vaultId, html) {
521
+ return html.replace(MEDIA_SRC_RE, (full, tag, before, src, after) => {
522
+ if (!src.startsWith("/")) return full;
523
+ const path = decodeURIComponent(src.replace(/^\//, ""));
524
+ if (!CACHED_EXT_RE.test(path)) return full;
525
+ return `<${tag}${before}src="${escapeAttr(localFileUrl(vaultId, path))}"${after}>`;
526
+ });
527
+ }
528
+ function rewritePassthroughLinks(vaultId, html) {
529
+ return html.replace(ANCHOR_RE, (full, attrs, inner) => {
530
+ const cls = ATTR_CLASS_RE.exec(attrs)?.[1] || "";
531
+ if (!/\bpassthrough-link\b/.test(cls)) return full;
532
+ const href = ATTR_HREF_RE.exec(attrs)?.[1] || "";
533
+ if (!href.startsWith("/")) return full;
534
+ const path = decodeURIComponent(href.replace(/^\//, ""));
535
+ if (!CACHED_EXT_RE.test(path)) return full;
536
+ const newAttrs = attrs.replace(ATTR_HREF_RE, `href="${escapeAttr(localFileUrl(vaultId, path))}"`);
537
+ return `<a${newAttrs}>${inner}</a>`;
538
+ });
539
+ }
540
+ function stripTags(s) {
541
+ return s.replace(TAG_RE, "").trim();
542
+ }
543
+
544
+ // ../foundry/scripts/importer.mjs
545
+ var INDEX_BASENAME = "index";
546
+ var NON_INDEX_SORT_BASE = 1e5;
547
+ var KNOWN_INSTANCE_DOC_TYPES = [
548
+ "Actor",
549
+ "Item",
550
+ "Scene",
551
+ "JournalEntry",
552
+ "RollTable",
553
+ "Macro",
554
+ "Cards",
555
+ "Playlist"
556
+ ];
557
+ function inferInstanceDocName(base) {
558
+ if (typeof base !== "string" || !base) return null;
559
+ const raw = base.includes(".") ? base.split(".").at(-2) : base.split(":")[0];
560
+ return KNOWN_INSTANCE_DOC_TYPES.find((t) => t.toLowerCase() === raw?.toLowerCase()) ?? null;
561
+ }
562
+ async function ensureFolderChain(vault, segments) {
563
+ const rootName = vault.rootFolder || vault.label || "Vault";
564
+ const rootKey = `${vault.id}/__root__/${rootName}`;
565
+ const rootFId = await folderId(vault.id, rootKey);
566
+ await upsertFolder(rootFId, rootName, null);
567
+ let parentId = rootFId;
568
+ let acc = rootKey;
569
+ for (const seg of segments) {
570
+ acc += "/" + seg;
571
+ const fId = await folderId(vault.id, acc);
572
+ await upsertFolder(fId, seg, parentId);
573
+ parentId = fId;
574
+ }
575
+ return parentId;
576
+ }
577
+ async function upsertFolder(id, name, parentId) {
578
+ const existing = game.folders.get(id);
579
+ if (existing) {
580
+ if (existing.name !== name || existing.folder?.id !== parentId) {
581
+ await existing.update({ name, folder: parentId });
582
+ }
583
+ return existing;
584
+ }
585
+ return Folder.create({ _id: id, name, type: "JournalEntry", folder: parentId }, { keepId: true });
586
+ }
587
+ function buildFolderInfo(mdPaths) {
588
+ const map = /* @__PURE__ */ new Map();
589
+ const ensure = (folderPath) => {
590
+ let info = map.get(folderPath);
591
+ if (!info) {
592
+ const segs = folderPath ? folderPath.split("/") : [];
593
+ info = { hasSubfolders: false, displayName: segs[segs.length - 1] || "" };
594
+ map.set(folderPath, info);
595
+ }
596
+ return info;
597
+ };
598
+ ensure("");
599
+ for (const p of mdPaths) {
600
+ const folderPath = folderOfPath(p);
601
+ ensure(folderPath);
602
+ const segs = folderPath ? folderPath.split("/") : [];
603
+ for (let i = 0; i < segs.length; i++) {
604
+ ensure(segs.slice(0, i).join("/")).hasSubfolders = true;
605
+ }
606
+ }
607
+ return map;
608
+ }
609
+ function isIndexFile(filename) {
610
+ return filename.replace(/\.md$/i, "") === INDEX_BASENAME;
611
+ }
612
+ async function upsertFile(vault, path, body, index, meta, folderInfo) {
613
+ let html = await transformHtmlForFoundry(vault, body, index);
614
+ html = await appendInstanceDocLink(html, vault, path, meta);
615
+ const segs = path.split("/");
616
+ const filename = segs.pop();
617
+ const folderPath = segs.join("/");
618
+ const fInfo = folderInfo?.get(folderPath);
619
+ const leaf = !!fInfo && folderPath !== "" && !fInfo.hasSubfolders;
620
+ const hostSegs = leaf ? segs.slice(0, -1) : segs;
621
+ const folderFId = await ensureFolderChain(vault, hostSegs);
622
+ const entryName = folderPath === "" ? vault.rootFolder || vault.label || "Vault" : fInfo?.displayName || segs[segs.length - 1] || "";
623
+ const pageName = meta?.title || filename.replace(/\.md$/i, "");
624
+ const eId = await entryId(vault.id, path);
625
+ const idOverride = meta?.foundry?.id;
626
+ const pId = typeof idOverride === "string" && idOverride ? idOverride : await pageId(vault.id, path);
627
+ const pageOwnership = pageOwnershipLevelFor(vault, meta?.role);
628
+ const flags = { [MODULE_ID]: { vaultId: vault.id, path } };
629
+ const pageData = {
630
+ _id: pId,
631
+ name: pageName,
632
+ type: "text",
633
+ text: {
634
+ content: html,
635
+ format: 1
636
+ /* HTML */
637
+ },
638
+ sort: isIndexFile(filename) ? 0 : NON_INDEX_SORT_BASE,
639
+ flags,
640
+ // Ownership is set on the *page*, not the parent entry: in the
641
+ // folder-as-entry model a single entry can host pages of different
642
+ // tiers, and entry-level ownership would leak DM pages to players via
643
+ // the default INHERIT on sibling pages. The entry's own ownership is
644
+ // reconciled separately as the max of its pages' levels (so the entry
645
+ // remains visible in the sidebar to anyone who can see at least one
646
+ // page in it). See reconcileOwnership below.
647
+ ...pageOwnership !== null ? { ownership: { default: pageOwnership } } : {}
648
+ };
649
+ const existing = game.journal.get(eId);
650
+ if (existing) {
651
+ const entryPatch = {};
652
+ if (existing.name !== entryName) entryPatch.name = entryName;
653
+ if (existing.folder?.id !== folderFId) entryPatch.folder = folderFId;
654
+ if (Object.keys(entryPatch).length > 0) await existing.update(entryPatch);
655
+ const existingPage = existing.pages.get(pId);
656
+ if (existingPage) await existingPage.update(pageData);
657
+ else await existing.createEmbeddedDocuments("JournalEntryPage", [pageData], { keepId: true });
658
+ return "modified";
659
+ }
660
+ await JournalEntry.create({
661
+ _id: eId,
662
+ name: entryName,
663
+ folder: folderFId,
664
+ pages: [pageData],
665
+ flags,
666
+ // Bootstrap the entry visible at the page's tier so player-visible
667
+ // pages aren't hidden in the gap between create and the post-sync
668
+ // reconcileOwnership pass. Mixed-tier folders converge to max(pages)
669
+ // via reconcile.
670
+ ...pageOwnership !== null ? { ownership: { default: pageOwnership } } : {}
671
+ }, { keepId: true });
672
+ return "added";
673
+ }
674
+ function pageOwnershipLevelFor(vault, pageRole) {
675
+ if (!vault.dmRole || !vault.knownRoles?.length) return null;
676
+ const dmIdx = vault.knownRoles.indexOf(vault.dmRole);
677
+ if (dmIdx < 0) return null;
678
+ const pageIdx = pageRole ? vault.knownRoles.indexOf(pageRole) : -1;
679
+ const effectiveIdx = pageIdx < 0 ? 0 : pageIdx;
680
+ return effectiveIdx < dmIdx ? CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER : CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE;
681
+ }
682
+ async function reconcileOwnership(vault, bodyMetaIndex) {
683
+ if (!vault.dmRole || !vault.knownRoles?.length) return;
684
+ const ours = game.journal.contents.filter(
685
+ (j) => j.getFlag(MODULE_ID, "vaultId") === vault.id
686
+ );
687
+ for (const entry of ours) {
688
+ let entryMax = null;
689
+ for (const page of entry.pages.contents) {
690
+ const pPath = page.getFlag(MODULE_ID, "path");
691
+ if (!pPath) continue;
692
+ const bodyPath = pPath.replace(/\.md$/i, ".body.html");
693
+ const meta = bodyMetaIndex.get(bodyPath);
694
+ if (!meta) continue;
695
+ const level = pageOwnershipLevelFor(vault, meta.role);
696
+ if (level === null) continue;
697
+ if (page.ownership?.default !== level) {
698
+ try {
699
+ await page.update({ ownership: { default: level } });
700
+ } catch (err) {
701
+ console.warn(`Vaults | reconcile ownership ${pPath} \u2192 ${level} failed:`, err);
702
+ }
703
+ }
704
+ if (entryMax === null || level > entryMax) entryMax = level;
705
+ }
706
+ if (entryMax !== null && entry.ownership?.default !== entryMax) {
707
+ try {
708
+ await entry.update({ ownership: { default: entryMax } });
709
+ } catch (err) {
710
+ console.warn(`Vaults | reconcile entry ownership for ${entry.name} \u2192 ${entryMax} failed:`, err);
711
+ }
712
+ }
713
+ }
714
+ }
715
+ async function reconcileEntryPlacement(vault, folderInfo) {
716
+ const ours = game.journal.contents.filter(
717
+ (j) => j.getFlag(MODULE_ID, "vaultId") === vault.id
718
+ );
719
+ for (const entry of ours) {
720
+ const firstPage = entry.pages.contents[0];
721
+ const path = firstPage?.getFlag(MODULE_ID, "path");
722
+ if (!path) continue;
723
+ const segs = path.split("/");
724
+ segs.pop();
725
+ const folderPath = segs.join("/");
726
+ const fInfo = folderInfo?.get(folderPath);
727
+ const leaf = !!fInfo && folderPath !== "" && !fInfo.hasSubfolders;
728
+ const hostSegs = leaf ? segs.slice(0, -1) : segs;
729
+ const expectedFolderId = await ensureFolderChain(vault, hostSegs);
730
+ if (entry.folder?.id !== expectedFolderId) {
731
+ try {
732
+ await entry.update({ folder: expectedFolderId });
733
+ } catch (err) {
734
+ console.warn(`Vaults | re-place ${path} \u2192 ${expectedFolderId} failed:`, err);
735
+ }
736
+ }
737
+ }
738
+ }
739
+ async function deleteFile(vault, path) {
740
+ const eId = await entryId(vault.id, path);
741
+ const pId = await pageId(vault.id, path);
742
+ const entry = game.journal.get(eId);
743
+ if (!entry) return;
744
+ const page = entry.pages.get(pId);
745
+ if (page) await page.delete();
746
+ if (entry.pages.size === 0) await entry.delete();
747
+ }
748
+ async function deleteVaultJournals(vaultId) {
749
+ const journals = game.journal.contents.filter((j) => j.getFlag(MODULE_ID, "vaultId") === vaultId);
750
+ for (const j of journals) await j.delete();
751
+ const folders = game.folders.contents.filter((f) => f.type === "JournalEntry");
752
+ for (const f of folders.reverse()) {
753
+ if (f.contents.length === 0 && f.children.length === 0) {
754
+ const root = await folderId(vaultId, `${vaultId}/__root__/${f.name}`);
755
+ if (f.id === root) await f.delete();
756
+ }
757
+ }
758
+ }
759
+ async function appendInstanceDocLink(html, vault, path, meta) {
760
+ const base = meta?.foundry?.base;
761
+ const docName = inferInstanceDocName(base);
762
+ if (!docName) return html;
763
+ const idOverride = meta?.foundry?.id;
764
+ const docId = typeof idOverride === "string" && idOverride ? idOverride : await instanceId(vault.id, path);
765
+ const label = meta?.title || path.split("/").pop().replace(/\.md$/i, "");
766
+ return html + `
767
+ <p class="vaults-instance-link"><em>Foundry document:</em> @UUID[${docName}.${docId}]{${escapeBraces(label)}}</p>`;
768
+ }
769
+
770
+ // ../foundry/scripts/instance.mjs
771
+ var DESCRIPTION_FIELDS = {
772
+ dnd5e: {
773
+ Actor: "system.details.biography.value",
774
+ Item: "system.description.value"
775
+ }
776
+ };
777
+ var CLONE_SUPPORTED_DOCS = /* @__PURE__ */ new Set(["Actor", "Item"]);
778
+ var BLANK_DOC_TYPES = /* @__PURE__ */ new Set([
779
+ "Actor",
780
+ "Item",
781
+ "Scene",
782
+ "JournalEntry",
783
+ "RollTable",
784
+ "Macro",
785
+ "Cards",
786
+ "Playlist"
787
+ ]);
788
+ var COLLECTION_FOR = {
789
+ Actor: () => game.actors,
790
+ Item: () => game.items,
791
+ Scene: () => game.scenes,
792
+ JournalEntry: () => game.journal,
793
+ RollTable: () => game.tables,
794
+ Macro: () => game.macros,
795
+ Cards: () => game.cards,
796
+ Playlist: () => game.playlists
797
+ };
798
+ async function applyInstance(vault, vaultPath, meta) {
799
+ const fm = meta?.foundry;
800
+ if (!fm || typeof fm !== "object") return;
801
+ const parsed = parseFoundryBase(fm.base);
802
+ if (!parsed) return;
803
+ let docName;
804
+ let baseData;
805
+ if (parsed.kind === "uuid") {
806
+ const template = await safeFromUuid(parsed.uuid);
807
+ if (!template) {
808
+ console.warn(`Vaults | foundry.base: ${vaultPath} \u2192 ${parsed.uuid} did not resolve; skipping.`);
809
+ return;
810
+ }
811
+ docName = template.documentName;
812
+ if (!CLONE_SUPPORTED_DOCS.has(docName)) {
813
+ console.warn(
814
+ `Vaults | foundry.base: ${vaultPath} \u2192 ${parsed.uuid} is a ${docName}; clone-from-UUID only supports ${[...CLONE_SUPPORTED_DOCS].join(", ")}.`
815
+ );
816
+ return;
817
+ }
818
+ try {
819
+ baseData = template.toObject();
820
+ } catch (err) {
821
+ console.warn(`Vaults | foundry.base: could not read template ${parsed.uuid}:`, err);
822
+ return;
823
+ }
824
+ delete baseData._id;
825
+ } else {
826
+ docName = parsed.docName;
827
+ baseData = parsed.subtype ? { type: parsed.subtype } : {};
828
+ }
829
+ const collection = COLLECTION_FOR[docName]?.();
830
+ if (!collection) {
831
+ console.warn(`Vaults | foundry.base: no world collection for ${docName}; skipping ${vaultPath}.`);
832
+ return;
833
+ }
834
+ const docClass = CONFIG[docName].documentClass;
835
+ const id = typeof fm.id === "string" && fm.id ? fm.id : await instanceId(vault.id, vaultPath);
836
+ const dataJson = fm.data_json && typeof fm.data_json === "object" && !Array.isArray(fm.data_json) ? rewriteVaultPaths(structuredClone(fm.data_json), vault.id) : null;
837
+ if (dataJson) await ensureEmbeddedIds(dataJson, vault.id, vaultPath);
838
+ const overlay = await buildOverlay(vault, vaultPath, meta, docName);
839
+ const existing = collection.get(id);
840
+ if (existing) {
841
+ const updatePatch = dataJson ? deepMerge(structuredClone(dataJson), overlay) : overlay;
842
+ try {
843
+ await existing.update(updatePatch);
844
+ } catch (err) {
845
+ console.warn(`Vaults | foundry.base update failed for ${vaultPath}:`, err);
846
+ }
847
+ return;
848
+ }
849
+ if (dataJson) deepMerge(baseData, dataJson);
850
+ baseData._id = id;
851
+ deepMerge(baseData, overlay);
852
+ try {
853
+ await docClass.create(baseData, { keepId: true, keepEmbeddedIds: true });
854
+ } catch (err) {
855
+ console.warn(`Vaults | foundry.base create failed for ${vaultPath}:`, err);
856
+ return;
857
+ }
858
+ if (docName === "Scene") {
859
+ const created = collection.get(id);
860
+ if (created && !created.thumb) {
861
+ try {
862
+ const { thumb } = await created.createThumbnail();
863
+ if (thumb) await created.update({ thumb });
864
+ } catch (err) {
865
+ console.warn(`Vaults | scene thumbnail generation failed for ${vaultPath}:`, err);
866
+ }
867
+ }
868
+ }
869
+ }
870
+ function parseFoundryBase(spec) {
871
+ if (typeof spec !== "string" || !spec) return null;
872
+ if (spec.includes(".")) return { kind: "uuid", uuid: spec };
873
+ const [typeRaw, subtype] = spec.split(":");
874
+ const docName = [...BLANK_DOC_TYPES].find((t) => t.toLowerCase() === typeRaw.toLowerCase());
875
+ if (!docName) return null;
876
+ return { kind: "blank", docName, subtype: subtype || void 0 };
877
+ }
878
+ async function deleteInstance(vault, vaultPath) {
879
+ const id = await instanceId(vault.id, vaultPath);
880
+ for (const getCollection of Object.values(COLLECTION_FOR)) {
881
+ const collection = getCollection();
882
+ const doc = collection?.get(id);
883
+ if (!doc) continue;
884
+ if (doc.getFlag(MODULE_ID, "vaultId") !== vault.id) continue;
885
+ try {
886
+ await doc.delete();
887
+ } catch (err) {
888
+ console.warn(`Vaults | failed to delete ${doc.documentName} for ${vaultPath}:`, err);
889
+ }
890
+ }
891
+ }
892
+ async function deleteVaultInstances(vaultId) {
893
+ for (const [docName, getCollection] of Object.entries(COLLECTION_FOR)) {
894
+ const collection = getCollection();
895
+ if (!collection) continue;
896
+ const ours = collection.contents.filter((d) => d.getFlag(MODULE_ID, "vaultId") === vaultId);
897
+ for (const doc of ours) {
898
+ try {
899
+ await doc.delete();
900
+ } catch (err) {
901
+ console.warn(`Vaults | failed to delete ${docName} ${doc.id}:`, err);
902
+ }
903
+ }
904
+ }
905
+ for (const docName of BLANK_DOC_TYPES) {
906
+ const fId = await instanceFolderId(vaultId, docName);
907
+ const folder = game.folders.get(fId);
908
+ if (!folder || folder.type !== docName) continue;
909
+ if (folder.contents.length > 0 || folder.children.length > 0) continue;
910
+ try {
911
+ await folder.delete();
912
+ } catch (err) {
913
+ console.warn(`Vaults | failed to delete ${docName} folder:`, err);
914
+ }
915
+ }
916
+ }
917
+ async function instanceFolderId(vaultId, docName) {
918
+ return folderId(vaultId, `${vaultId}/__instance__/${docName}`);
919
+ }
920
+ async function ensureInstanceFolder(vault, docName) {
921
+ const fId = await instanceFolderId(vault.id, docName);
922
+ const existing = game.folders.get(fId);
923
+ const name = vault.rootFolder || vault.label || "Vault";
924
+ if (existing) {
925
+ if (existing.name !== name) {
926
+ try {
927
+ await existing.update({ name });
928
+ } catch (err) {
929
+ console.warn(`Vaults | could not rename ${docName} folder for ${vault.label}:`, err);
930
+ }
931
+ }
932
+ return fId;
933
+ }
934
+ try {
935
+ await Folder.create({ _id: fId, name, type: docName, folder: null }, { keepId: true });
936
+ return fId;
937
+ } catch (err) {
938
+ console.warn(`Vaults | could not create ${docName} folder for ${vault.label}:`, err);
939
+ return null;
940
+ }
941
+ }
942
+ async function buildOverlay(vault, vaultPath, meta, docName) {
943
+ const overlay = {
944
+ // Prefer the page's frontmatter `title:` over the filename — the wiki
945
+ // already treats title as the page's display name, and a doc named
946
+ // "Potion of Healing (Mossfoot Brew)" reads better in the Foundry
947
+ // sidebar than "Healing Potion".
948
+ name: meta.title || baseName(vaultPath),
949
+ folder: await ensureInstanceFolder(vault, docName),
950
+ flags: { [MODULE_ID]: { vaultId: vault.id, path: vaultPath } }
951
+ };
952
+ if (meta.image) {
953
+ const localImg = imageUrlFromMeta(vault.id, meta.image);
954
+ if (localImg) {
955
+ overlay.img = localImg;
956
+ if (docName === "Actor") setPath(overlay, "prototypeToken.texture.src", localImg);
957
+ }
958
+ }
959
+ const fm = meta?.foundry;
960
+ const descPath = DESCRIPTION_FIELDS[game.system.id]?.[docName];
961
+ const embedAuto = fm?.embed !== false;
962
+ if (descPath && embedAuto) {
963
+ const eId = await entryId(vault.id, vaultPath);
964
+ const pId = await pageId(vault.id, vaultPath);
965
+ setPath(overlay, descPath, `<p>@Embed[JournalEntry.${eId}.JournalEntryPage.${pId} inline]</p>`);
966
+ }
967
+ if (fm?.data && typeof fm.data === "object") {
968
+ const cloned = rewriteVaultPaths(structuredClone(fm.data), vault.id);
969
+ await ensureEmbeddedIds(cloned, vault.id, vaultPath);
970
+ deepMerge(overlay, cloned);
971
+ }
972
+ if (docName === "Scene") {
973
+ const note = await buildJournalNote(vault, vaultPath, meta);
974
+ if (note) overlay.notes = [...overlay.notes ?? [], note];
975
+ }
976
+ return overlay;
977
+ }
978
+ async function buildJournalNote(vault, vaultPath, meta) {
979
+ const fm = meta?.foundry ?? {};
980
+ const cfg = { ...fm.data_json ?? {}, ...fm.data ?? {} };
981
+ const width = Number(cfg.width) || 4e3;
982
+ const height = Number(cfg.height) || 3e3;
983
+ const padding = Number(cfg.padding ?? 0.25);
984
+ const gridSize = Number(cfg.grid?.size) || 100;
985
+ const iconSize = gridSize;
986
+ const eId = await entryId(vault.id, vaultPath);
987
+ const idOverride = meta?.foundry?.id;
988
+ const pId = typeof idOverride === "string" && idOverride ? idOverride : await pageId(vault.id, vaultPath);
989
+ return {
990
+ _id: await subdocId(vault.id, vaultPath, "/notes/__journalLink__"),
991
+ entryId: eId,
992
+ pageId: pId,
993
+ x: gridSize * (Math.ceil(width / gridSize * padding) - 0.5),
994
+ y: gridSize * (Math.ceil(height / gridSize * padding) + 0.5),
995
+ iconSize,
996
+ texture: {
997
+ src: "icons/svg/book.svg",
998
+ anchorX: 0.5,
999
+ anchorY: 0.5,
1000
+ fit: "contain",
1001
+ tint: "#ffffff"
1002
+ },
1003
+ text: ""
1004
+ };
1005
+ }
1006
+ var VALID_SUBDOC_ID = /^[A-Za-z0-9]{16}$/;
1007
+ async function ensureEmbeddedIds(value, vaultId, pagePath, ptr = "") {
1008
+ if (Array.isArray(value)) {
1009
+ for (let i = 0; i < value.length; i++) {
1010
+ const childPtr = `${ptr}/${i}`;
1011
+ const item = value[i];
1012
+ if (item && typeof item === "object" && !Array.isArray(item)) {
1013
+ if (typeof item._id !== "string" || !VALID_SUBDOC_ID.test(item._id)) {
1014
+ item._id = await subdocId(vaultId, pagePath, childPtr);
1015
+ }
1016
+ }
1017
+ await ensureEmbeddedIds(item, vaultId, pagePath, childPtr);
1018
+ }
1019
+ } else if (value && typeof value === "object") {
1020
+ for (const k of Object.keys(value)) {
1021
+ await ensureEmbeddedIds(value[k], vaultId, pagePath, `${ptr}/${k}`);
1022
+ }
1023
+ }
1024
+ }
1025
+ function rewriteVaultPaths(value, vaultId) {
1026
+ if (typeof value === "string") return rewriteVaultString(value, vaultId);
1027
+ if (Array.isArray(value)) {
1028
+ for (let i = 0; i < value.length; i++) value[i] = rewriteVaultPaths(value[i], vaultId);
1029
+ return value;
1030
+ }
1031
+ if (value && typeof value === "object") {
1032
+ for (const k of Object.keys(value)) value[k] = rewriteVaultPaths(value[k], vaultId);
1033
+ return value;
1034
+ }
1035
+ return value;
1036
+ }
1037
+ function rewriteVaultString(s, vaultId) {
1038
+ if (!s.startsWith("@vault/")) return s;
1039
+ const vaultPath = s.slice("@vault/".length);
1040
+ if (!vaultPath) return s;
1041
+ return localFileUrl(vaultId, vaultPath);
1042
+ }
1043
+ async function safeFromUuid(uuid) {
1044
+ try {
1045
+ return await fromUuid(uuid);
1046
+ } catch {
1047
+ return null;
1048
+ }
1049
+ }
1050
+ function baseName(path) {
1051
+ return path.split("/").pop().replace(/\.md$/i, "");
1052
+ }
1053
+ function imageUrlFromMeta(vaultId, image) {
1054
+ if (/^https?:\/\//i.test(image)) return image;
1055
+ const vaultPath = decodeURIComponent(image.replace(/^\//, ""));
1056
+ if (!vaultPath) return null;
1057
+ return localFileUrl(vaultId, vaultPath);
1058
+ }
1059
+ function setPath(obj, path, value) {
1060
+ const segs = path.split(".");
1061
+ let cursor = obj;
1062
+ for (let i = 0; i < segs.length - 1; i++) {
1063
+ const seg = segs[i];
1064
+ if (cursor[seg] == null || typeof cursor[seg] !== "object") cursor[seg] = {};
1065
+ cursor = cursor[seg];
1066
+ }
1067
+ cursor[segs[segs.length - 1]] = value;
1068
+ return obj;
1069
+ }
1070
+ function deepMerge(target, source) {
1071
+ for (const [k, v] of Object.entries(source)) {
1072
+ if (v && typeof v === "object" && !Array.isArray(v) && target[k] && typeof target[k] === "object" && !Array.isArray(target[k])) {
1073
+ deepMerge(target[k], v);
1074
+ } else {
1075
+ target[k] = v;
1076
+ }
1077
+ }
1078
+ return target;
1079
+ }
1080
+
1081
+ // ../foundry/scripts/auth.mjs
1082
+ function tokenInfo(token) {
1083
+ if (!token) return null;
1084
+ const parts = token.split(".");
1085
+ if (parts.length !== 3) return null;
1086
+ const exp = Number(parts[1]);
1087
+ return {
1088
+ role: parts[0],
1089
+ expiresAt: Number.isFinite(exp) ? new Date(exp * 1e3) : null
1090
+ };
1091
+ }
1092
+
1093
+ // ../foundry/scripts/sync.mjs
1094
+ async function sync(host, vault, { forceFull = false } = {}) {
1095
+ if (!vault?.url) {
1096
+ host.notify("error", host.localize("VAULTS.Sync.NoUrl"));
1097
+ return { ok: false, refreshHandlerAssets: false };
1098
+ }
1099
+ if (vault.token) {
1100
+ const info = tokenInfo(vault.token);
1101
+ const stillValid = info?.expiresAt && info.expiresAt > /* @__PURE__ */ new Date();
1102
+ if (!stillValid) {
1103
+ await host.updateVaultEntry(vault.id, { token: "", role: "" });
1104
+ host.notify("warn", host.localize("VAULTS.Sync.TokenExpired", { name: vault.label }));
1105
+ return { ok: false, refreshHandlerAssets: false };
1106
+ }
1107
+ }
1108
+ const start = Date.now();
1109
+ host.notify("info", host.localize("VAULTS.Sync.StartingNamed", { name: vault.label }));
1110
+ let manifest;
1111
+ try {
1112
+ manifest = await fetchManifest(vault);
1113
+ } catch (err) {
1114
+ host.notify("error", host.localize("VAULTS.Sync.Error", { message: err.message }));
1115
+ return { ok: false, refreshHandlerAssets: false };
1116
+ }
1117
+ const OUR_MANIFEST_VERSION = 1;
1118
+ const remoteManifestVersion = Number(manifest.manifest_version) || 0;
1119
+ if (remoteManifestVersion > OUR_MANIFEST_VERSION) {
1120
+ console.warn(
1121
+ `Vaults | ${vault.label}: deploy manifest_version=${remoteManifestVersion}, our module supports up to ${OUR_MANIFEST_VERSION}. Some new fields may be ignored. cli_version: ${manifest.cli_version || "(unknown)"}`
1122
+ );
1123
+ }
1124
+ const isPublic = manifest.auth?.required === false;
1125
+ const knownRoles = Array.isArray(manifest.auth?.roles) ? manifest.auth.roles : [];
1126
+ const patch = {};
1127
+ if (vault.public !== isPublic) patch.public = isPublic;
1128
+ if (!arraysEqual(vault.knownRoles, knownRoles)) patch.knownRoles = knownRoles;
1129
+ const remoteAssets = manifest.assets?.foundry || {};
1130
+ const newAssetPaths = {
1131
+ foundryJs: remoteAssets.js || null,
1132
+ foundryCss: remoteAssets.css || null
1133
+ };
1134
+ if (JSON.stringify(vault.handlerAssetPaths || {}) !== JSON.stringify(newAssetPaths)) {
1135
+ patch.handlerAssetPaths = newAssetPaths;
1136
+ }
1137
+ if (vault.dmRole && !knownRoles.includes(vault.dmRole)) patch.dmRole = "";
1138
+ if (Object.keys(patch).length > 0) {
1139
+ await host.updateVaultEntry(vault.id, patch);
1140
+ Object.assign(vault, patch);
1141
+ }
1142
+ const remote = new Map(manifest.files.map((f) => [f.path, f.hash]));
1143
+ const lastSync = host.getVaultState(vault.id);
1144
+ const local = forceFull ? /* @__PURE__ */ new Map() : new Map(Object.entries(lastSync.lastManifest || {}));
1145
+ const bodyPaths = manifest.files.filter((f) => f.path.endsWith(".body.html")).map((f) => f.path);
1146
+ const pathIndex = buildPathIndex(manifest.files);
1147
+ const allMdPaths = bodyPaths.map((p) => p.replace(/\.body\.html$/i, ".md"));
1148
+ const folderInfo = buildFolderInfo(allMdPaths);
1149
+ const bodyMetaIndex = /* @__PURE__ */ new Map();
1150
+ for (const f of manifest.files) {
1151
+ if (f.meta && f.path.endsWith(".body.html")) bodyMetaIndex.set(f.path, f.meta);
1152
+ }
1153
+ const toUpsert = bodyPaths.filter((p) => remote.get(p) !== local.get(p));
1154
+ const toDelete = [...local.keys()].filter((p) => p.endsWith(".body.html") && !remote.has(p));
1155
+ if (forceFull) await host.setVaultState(vault.id, { lastImageManifest: {} });
1156
+ let imageStats = { downloaded: 0, removed: 0, errors: 0 };
1157
+ try {
1158
+ imageStats = await syncImages(host, vault, manifest.files);
1159
+ } catch (err) {
1160
+ console.warn(`Vaults | image sync failed for ${vault.label}:`, err);
1161
+ }
1162
+ if (toUpsert.length === 0 && toDelete.length === 0 && imageStats.downloaded === 0 && imageStats.removed === 0) {
1163
+ host.notify("info", host.localize("VAULTS.Sync.NothingToDo"));
1164
+ return {
1165
+ ok: true,
1166
+ refreshHandlerAssets: false,
1167
+ added: 0,
1168
+ modified: 0,
1169
+ removed: 0,
1170
+ imageStats,
1171
+ instances: 0
1172
+ };
1173
+ }
1174
+ host.notify(
1175
+ "info",
1176
+ forceFull ? host.localize("VAULTS.Sync.Initial", { count: toUpsert.length }) : host.localize("VAULTS.Sync.Incremental", {
1177
+ add: toUpsert.length,
1178
+ mod: 0,
1179
+ del: toDelete.length
1180
+ })
1181
+ );
1182
+ let bodies;
1183
+ try {
1184
+ bodies = await fetchSourceBatch(vault, toUpsert);
1185
+ } catch (err) {
1186
+ console.error(`Vaults | batch fetch failed for ${vault.label}:`, err);
1187
+ host.notify("error", host.localize("VAULTS.Sync.Error", { message: err.message }));
1188
+ return { ok: false, refreshHandlerAssets: false };
1189
+ }
1190
+ let added = 0, modified = 0, instances = 0;
1191
+ for (const bodyPath of toUpsert) {
1192
+ const html = bodies.get(bodyPath);
1193
+ if (html == null) {
1194
+ console.warn(`Vaults | server returned no content for ${bodyPath}`);
1195
+ continue;
1196
+ }
1197
+ const logicalPath = bodyPath.replace(/\.body\.html$/i, ".md");
1198
+ const pageMeta = bodyMetaIndex.get(bodyPath);
1199
+ try {
1200
+ const result = await upsertFile(vault, logicalPath, html, pathIndex, pageMeta, folderInfo);
1201
+ if (result === "added") added++;
1202
+ else modified++;
1203
+ if (pageMeta?.foundry?.base) {
1204
+ try {
1205
+ await applyInstance(vault, logicalPath, pageMeta);
1206
+ instances++;
1207
+ } catch (err) {
1208
+ console.warn(`Vaults | foundry instantiation failed for ${logicalPath}:`, err);
1209
+ }
1210
+ }
1211
+ } catch (err) {
1212
+ console.warn(`Vaults | upsert failed for ${logicalPath}:`, err);
1213
+ }
1214
+ }
1215
+ let removed = 0;
1216
+ for (const bodyPath of toDelete) {
1217
+ const logicalPath = bodyPath.replace(/\.body\.html$/i, ".md");
1218
+ try {
1219
+ await deleteFile(vault, logicalPath);
1220
+ removed++;
1221
+ } catch (err) {
1222
+ console.warn(`Vaults | delete failed for ${logicalPath}:`, err);
1223
+ }
1224
+ try {
1225
+ await deleteInstance(vault, logicalPath);
1226
+ } catch (err) {
1227
+ console.warn(`Vaults | delete instance failed for ${logicalPath}:`, err);
1228
+ }
1229
+ }
1230
+ await host.setVaultState(vault.id, { lastManifest: Object.fromEntries(remote) });
1231
+ await reconcileEntryPlacement(vault, folderInfo);
1232
+ await reconcileOwnership(vault, bodyMetaIndex);
1233
+ const seconds = ((Date.now() - start) / 1e3).toFixed(1);
1234
+ host.notify("info", host.localize("VAULTS.Sync.Done", { added, modified, removed, seconds }));
1235
+ if (imageStats.downloaded > 0 || imageStats.removed > 0) {
1236
+ console.info(`Vaults | ${vault.label} images: ${imageStats.downloaded} downloaded, ${imageStats.removed} removed` + (imageStats.errors ? `, ${imageStats.errors} failed` : ""));
1237
+ }
1238
+ if (instances > 0) console.info(`Vaults | ${vault.label} instantiated ${instances} document(s) from page foundry.base.`);
1239
+ return {
1240
+ ok: true,
1241
+ refreshHandlerAssets: true,
1242
+ added,
1243
+ modified,
1244
+ removed,
1245
+ imageStats,
1246
+ instances
1247
+ };
1248
+ }
1249
+ function arraysEqual(a, b) {
1250
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
1251
+ if (a.length !== b.length) return false;
1252
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
1253
+ return true;
1254
+ }
1255
+
1256
+ // ../foundry/scripts/importer-entry.mjs
1257
+ var REQUIRED_HOST_VERSION = 1;
1258
+ async function runSync(host, vault, options = {}) {
1259
+ return sync(host, vault, options);
1260
+ }
1261
+ async function runRemove(_host, vault) {
1262
+ await deleteVaultJournals(vault.id);
1263
+ await deleteVaultCache(vault.id);
1264
+ await deleteVaultInstances(vault.id);
1265
+ }
1266
+ export {
1267
+ REQUIRED_HOST_VERSION,
1268
+ runRemove,
1269
+ runSync
1270
+ };