@wizzlethorpe/vaults 0.13.5 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/asset-refs.js +282 -0
- package/dist/asset-refs.js.map +1 -0
- package/dist/auth.js.map +1 -1
- package/dist/build.js +393 -544
- package/dist/build.js.map +1 -1
- package/dist/commands/build.js +0 -4
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/preview.js +0 -4
- package/dist/commands/preview.js.map +1 -1
- package/dist/commands/push.js +4 -9
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/role.js +55 -14
- package/dist/commands/role.js.map +1 -1
- package/dist/config.js +25 -5
- package/dist/config.js.map +1 -1
- package/dist/escape.js +20 -0
- package/dist/escape.js.map +1 -1
- package/dist/foundry-adventure.js +86 -0
- package/dist/foundry-adventure.js.map +1 -0
- package/dist/foundry-defaults.js +65 -0
- package/dist/foundry-defaults.js.map +1 -0
- package/dist/foundry-grafts.js +610 -0
- package/dist/foundry-grafts.js.map +1 -0
- package/dist/foundry-html.js +196 -0
- package/dist/foundry-html.js.map +1 -0
- package/dist/foundry-importer.bundle.js +1192 -322
- package/dist/foundry-importer.js +2 -7
- package/dist/foundry-importer.js.map +1 -1
- package/dist/foundry-meta.js +56 -0
- package/dist/foundry-meta.js.map +1 -0
- package/dist/foundry-module-journal.js +133 -0
- package/dist/foundry-module-journal.js.map +1 -0
- package/dist/foundry-module-render.js +75 -0
- package/dist/foundry-module-render.js.map +1 -0
- package/dist/foundry-module.js +1082 -0
- package/dist/foundry-module.js.map +1 -0
- package/dist/foundry-types.js +28 -0
- package/dist/foundry-types.js.map +1 -0
- package/dist/foundry-version.js +30 -0
- package/dist/foundry-version.js.map +1 -0
- package/dist/frontmatter-defaults.js +68 -0
- package/dist/frontmatter-defaults.js.map +1 -0
- package/dist/index.js +5 -9
- package/dist/index.js.map +1 -1
- package/dist/manifest.js +115 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrate/0.15-foundry-patch-keys.js +51 -0
- package/dist/migrate/0.15-foundry-patch-keys.js.map +1 -0
- package/dist/migrate/0.15-foundry-pinned-id.js +68 -0
- package/dist/migrate/0.15-foundry-pinned-id.js.map +1 -0
- package/dist/migrate/files.js +66 -0
- package/dist/migrate/files.js.map +1 -0
- package/dist/migrate/registry.js +4 -0
- package/dist/migrate/registry.js.map +1 -1
- package/dist/migrate/run.js +36 -2
- package/dist/migrate/run.js.map +1 -1
- package/dist/render/auth-template.js +331 -42
- package/dist/render/auth-template.js.map +1 -1
- package/dist/render/bases.js +22 -38
- package/dist/render/bases.js.map +1 -1
- package/dist/render/cover.js +23 -1
- package/dist/render/cover.js.map +1 -1
- package/dist/render/handlers/assets.js +5 -34
- package/dist/render/handlers/assets.js.map +1 -1
- package/dist/render/handlers/builtin/battlemap.js +4 -1
- package/dist/render/handlers/builtin/battlemap.js.map +1 -1
- package/dist/render/handlers/builtin/download.js +90 -0
- package/dist/render/handlers/builtin/download.js.map +1 -0
- package/dist/render/handlers/builtin/fm-code.js +2 -2
- package/dist/render/handlers/builtin/fm-code.js.map +1 -1
- package/dist/render/handlers/builtin/foundry-manifest.js +158 -0
- package/dist/render/handlers/builtin/foundry-manifest.js.map +1 -0
- package/dist/render/handlers/builtin/fvtt-link.js +39 -0
- package/dist/render/handlers/builtin/fvtt-link.js.map +1 -0
- package/dist/render/handlers/builtin/index.js +4 -1
- package/dist/render/handlers/builtin/index.js.map +1 -1
- package/dist/render/handlers/builtin/statblock.js +0 -4
- package/dist/render/handlers/builtin/statblock.js.map +1 -1
- package/dist/render/handlers/types.js.map +1 -1
- package/dist/render/pipeline.js +4 -1
- package/dist/render/pipeline.js.map +1 -1
- package/dist/render/slug.js +0 -5
- package/dist/render/slug.js.map +1 -1
- package/dist/settings.js +156 -21
- package/dist/settings.js.map +1 -1
- package/dist/zip.js +78 -0
- package/dist/zip.js.map +1 -0
- package/package.json +4 -3
|
@@ -16,10 +16,15 @@ async function fetchManifest(vault) {
|
|
|
16
16
|
var BATCH_SIZE = 100;
|
|
17
17
|
var BATCH_CONCURRENCY = 4;
|
|
18
18
|
var DIRECT_CONCURRENCY = 8;
|
|
19
|
-
|
|
19
|
+
function batchEndpoint(vault, role) {
|
|
20
|
+
const endpoint = new URL(url(vault, "/_batch"));
|
|
21
|
+
if (role) endpoint.searchParams.set("role", role);
|
|
22
|
+
return endpoint;
|
|
23
|
+
}
|
|
24
|
+
async function fetchSourceBatch(vault, paths, role) {
|
|
20
25
|
if (paths.length === 0) return /* @__PURE__ */ new Map();
|
|
21
26
|
if (vault.public) return fetchSourceDirect(vault, paths);
|
|
22
|
-
const endpoint =
|
|
27
|
+
const endpoint = batchEndpoint(vault, role);
|
|
23
28
|
const chunks = [];
|
|
24
29
|
for (let i = 0; i < paths.length; i += BATCH_SIZE) chunks.push(paths.slice(i, i + BATCH_SIZE));
|
|
25
30
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -27,7 +32,7 @@ async function fetchSourceBatch(vault, paths) {
|
|
|
27
32
|
const workers = Array.from({ length: Math.min(BATCH_CONCURRENCY, chunks.length) }, async () => {
|
|
28
33
|
while (next < chunks.length) {
|
|
29
34
|
const idx = next++;
|
|
30
|
-
const res = await fetch(endpoint, {
|
|
35
|
+
const res = await fetch(endpoint.toString(), {
|
|
31
36
|
method: "POST",
|
|
32
37
|
headers: { "Content-Type": "text/plain" },
|
|
33
38
|
body: chunks[idx].join("\n")
|
|
@@ -69,6 +74,194 @@ async function fetchSourceDirect(vault, paths) {
|
|
|
69
74
|
// ../foundry/scripts/settings.mjs
|
|
70
75
|
var MODULE_ID = "vaults";
|
|
71
76
|
|
|
77
|
+
// ../foundry/scripts/foundry-base.mjs
|
|
78
|
+
var PACK_KEY = {
|
|
79
|
+
Actor: "actors",
|
|
80
|
+
Item: "items",
|
|
81
|
+
Scene: "scenes",
|
|
82
|
+
JournalEntry: "journal",
|
|
83
|
+
RollTable: "tables",
|
|
84
|
+
Macro: "macros",
|
|
85
|
+
Cards: "cards",
|
|
86
|
+
Playlist: "playlists"
|
|
87
|
+
};
|
|
88
|
+
var CORE_PAGE_TYPES = ["text", "image", "pdf", "video"];
|
|
89
|
+
function journalPageSpec(fm) {
|
|
90
|
+
const j = fm?.journal;
|
|
91
|
+
if (j === false) return null;
|
|
92
|
+
const overlay = j && typeof j === "object" && !Array.isArray(j) ? { ...j } : {};
|
|
93
|
+
const type = typeof overlay.type === "string" && overlay.type ? overlay.type : "text";
|
|
94
|
+
delete overlay.type;
|
|
95
|
+
return { type, overlay, dropsBody: type !== "text" };
|
|
96
|
+
}
|
|
97
|
+
var BLANK_DOC_TYPES = [
|
|
98
|
+
"Actor",
|
|
99
|
+
"Item",
|
|
100
|
+
"Scene",
|
|
101
|
+
"JournalEntry",
|
|
102
|
+
"RollTable",
|
|
103
|
+
"Macro",
|
|
104
|
+
"Cards",
|
|
105
|
+
"Playlist"
|
|
106
|
+
];
|
|
107
|
+
function canonicalType(raw) {
|
|
108
|
+
if (!raw) return null;
|
|
109
|
+
return BLANK_DOC_TYPES.find((t) => t.toLowerCase() === raw.toLowerCase()) ?? null;
|
|
110
|
+
}
|
|
111
|
+
function parseFoundryBase(spec) {
|
|
112
|
+
if (typeof spec !== "string" || !spec) return null;
|
|
113
|
+
if (spec.startsWith("@moulinette/")) {
|
|
114
|
+
const ref = spec.slice("@moulinette/".length);
|
|
115
|
+
return ref ? { kind: "moulinette", ref } : null;
|
|
116
|
+
}
|
|
117
|
+
if (spec.includes(".")) return { kind: "uuid", uuid: spec };
|
|
118
|
+
const [typeRaw, subtype] = spec.split(":");
|
|
119
|
+
const docName = canonicalType(typeRaw);
|
|
120
|
+
if (!docName) return null;
|
|
121
|
+
return { kind: "blank", docName, subtype: subtype || void 0 };
|
|
122
|
+
}
|
|
123
|
+
function docNameOf(parsed) {
|
|
124
|
+
if (!parsed) return null;
|
|
125
|
+
if (parsed.kind === "blank") return parsed.docName;
|
|
126
|
+
if (parsed.kind === "moulinette") return null;
|
|
127
|
+
const parts = parsed.uuid.split(".");
|
|
128
|
+
if (parts.length < 2) return null;
|
|
129
|
+
const raw = parts[parts.length - 2];
|
|
130
|
+
return canonicalType(raw) ?? raw ?? null;
|
|
131
|
+
}
|
|
132
|
+
function docNameFromBase(base) {
|
|
133
|
+
const specs = Array.isArray(base) ? base : [base];
|
|
134
|
+
for (const spec of specs) {
|
|
135
|
+
const docName = docNameOf(parseFoundryBase(spec));
|
|
136
|
+
if (docName) return docName;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ../foundry/scripts/packs.mjs
|
|
142
|
+
function packName(vault, docName) {
|
|
143
|
+
const key = docName === "Adventure" ? "adventure" : PACK_KEY[docName];
|
|
144
|
+
if (!key) return null;
|
|
145
|
+
return `${vault.id}-${key}`;
|
|
146
|
+
}
|
|
147
|
+
function packCollection(vault, docName) {
|
|
148
|
+
const name = packName(vault, docName);
|
|
149
|
+
return name ? `world.${name}` : null;
|
|
150
|
+
}
|
|
151
|
+
function isAdventure(vault) {
|
|
152
|
+
return vault.foundryPackage === "adventure";
|
|
153
|
+
}
|
|
154
|
+
function uuidPrefix(vault, docName) {
|
|
155
|
+
return isAdventure(vault) ? "" : `Compendium.${packCollection(vault, docName)}.`;
|
|
156
|
+
}
|
|
157
|
+
async function pruneStalePacks(vault) {
|
|
158
|
+
const wanted = isAdventure(vault) ? /* @__PURE__ */ new Set(["Adventure"]) : new Set(Object.keys(PACK_KEY));
|
|
159
|
+
for (const docName of [...Object.keys(PACK_KEY), "Adventure"]) {
|
|
160
|
+
if (wanted.has(docName)) continue;
|
|
161
|
+
const pack = getPack(vault, docName);
|
|
162
|
+
if (!pack) continue;
|
|
163
|
+
try {
|
|
164
|
+
await pack.deleteCompendium();
|
|
165
|
+
console.info(
|
|
166
|
+
`Vaults | ${vault.label}: removed ${pack.collection}, left over from the previous foundry_package setting.`
|
|
167
|
+
);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.warn(`Vaults | could not remove the stale pack ${pack.collection}:`, err);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function journalPageUuid(vault, entryId2, pageId2) {
|
|
174
|
+
return uuidPrefix(vault, "JournalEntry") + `JournalEntry.${entryId2}.JournalEntryPage.${pageId2}`;
|
|
175
|
+
}
|
|
176
|
+
function instanceUuid(vault, docName, id) {
|
|
177
|
+
return uuidPrefix(vault, docName) + `${docName}.${id}`;
|
|
178
|
+
}
|
|
179
|
+
function getPack(vault, docName) {
|
|
180
|
+
const collection = packCollection(vault, docName);
|
|
181
|
+
return collection ? game.packs.get(collection) ?? null : null;
|
|
182
|
+
}
|
|
183
|
+
function vaultPacks(vault) {
|
|
184
|
+
return [...Object.keys(PACK_KEY), "Adventure"].map((docName) => getPack(vault, docName)).filter((pack) => pack !== null);
|
|
185
|
+
}
|
|
186
|
+
async function deleteVaultPacks(vaultId) {
|
|
187
|
+
for (const pack of vaultPacks({ id: vaultId })) {
|
|
188
|
+
try {
|
|
189
|
+
await pack.deleteCompendium();
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.warn(`Vaults | failed to delete pack ${pack.collection}:`, err);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
196
|
+
async function ensurePack(vault, docName) {
|
|
197
|
+
const collection = packCollection(vault, docName);
|
|
198
|
+
if (!collection) throw new Error(`No pack is defined for ${docName} documents`);
|
|
199
|
+
const existing = game.packs.get(collection);
|
|
200
|
+
if (existing) {
|
|
201
|
+
assertWritable(existing, vault);
|
|
202
|
+
await enforceOwnership(existing, vault);
|
|
203
|
+
return existing;
|
|
204
|
+
}
|
|
205
|
+
const pending = inFlight.get(collection);
|
|
206
|
+
if (pending) return pending;
|
|
207
|
+
const promise = createPack(vault, docName, collection).finally(() => inFlight.delete(collection));
|
|
208
|
+
inFlight.set(collection, promise);
|
|
209
|
+
return promise;
|
|
210
|
+
}
|
|
211
|
+
function ownershipFor(vault) {
|
|
212
|
+
const base = { GAMEMASTER: "OWNER", ASSISTANT: "OWNER" };
|
|
213
|
+
return vault.public ? { ...base, TRUSTED: "OBSERVER", PLAYER: "OBSERVER" } : { ...base, TRUSTED: "NONE", PLAYER: "NONE" };
|
|
214
|
+
}
|
|
215
|
+
async function enforceOwnership(pack, vault) {
|
|
216
|
+
const want = ownershipFor(vault);
|
|
217
|
+
const have = pack.config.ownership;
|
|
218
|
+
if (have && Object.entries(want).every(([k, v]) => have[k] === v)) return;
|
|
219
|
+
await pack.configure({ ownership: want });
|
|
220
|
+
if (!vault.public) {
|
|
221
|
+
console.info(
|
|
222
|
+
`Vaults | ${pack.collection}: restricted to GM. It was readable by ${have?.PLAYER ?? "PLAYER: OBSERVER (Foundry's default)"}, which exposes every name and image in the pack index.`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function createPack(vault, docName, collection) {
|
|
227
|
+
const label = `${vault.label || "Vault"}: ${LABEL[docName] ?? docName}`;
|
|
228
|
+
await foundry.documents.collections.CompendiumCollection.createCompendium({
|
|
229
|
+
type: docName,
|
|
230
|
+
label,
|
|
231
|
+
name: packName(vault, docName),
|
|
232
|
+
packageType: "world"
|
|
233
|
+
});
|
|
234
|
+
const pack = game.packs.get(collection);
|
|
235
|
+
if (!pack) throw new Error(`Compendium pack ${collection} was not created`);
|
|
236
|
+
await pack.setFolder(await ensurePackFolder(vault));
|
|
237
|
+
assertWritable(pack, vault);
|
|
238
|
+
await enforceOwnership(pack, vault);
|
|
239
|
+
return pack;
|
|
240
|
+
}
|
|
241
|
+
function assertWritable(pack, vault) {
|
|
242
|
+
if (!pack.locked) return;
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Compendium pack ${pack.collection} is locked. Unlock it in the sidebar to let ${vault.label || "this vault"} sync into it.`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
async function ensurePackFolder(vault) {
|
|
248
|
+
const name = vault.rootFolder || vault.label || "Vault";
|
|
249
|
+
const existing = game.folders.find((f) => f.type === "Compendium" && f.name === name);
|
|
250
|
+
if (existing) return existing;
|
|
251
|
+
return Folder.create({ name, type: "Compendium" });
|
|
252
|
+
}
|
|
253
|
+
var LABEL = {
|
|
254
|
+
Adventure: "Adventure",
|
|
255
|
+
Actor: "Actors",
|
|
256
|
+
Item: "Items",
|
|
257
|
+
Scene: "Scenes",
|
|
258
|
+
JournalEntry: "Journals",
|
|
259
|
+
RollTable: "Roll Tables",
|
|
260
|
+
Macro: "Macros",
|
|
261
|
+
Cards: "Cards",
|
|
262
|
+
Playlist: "Playlists"
|
|
263
|
+
};
|
|
264
|
+
|
|
72
265
|
// ../foundry/scripts/util.mjs
|
|
73
266
|
function escapeAttr(s) {
|
|
74
267
|
return String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
@@ -83,6 +276,11 @@ async function hexDigest(algorithm, text) {
|
|
|
83
276
|
const buf = await crypto.subtle.digest(algorithm, new TextEncoder().encode(text));
|
|
84
277
|
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
85
278
|
}
|
|
279
|
+
function localizeOr(host, key, fallback, args) {
|
|
280
|
+
const text = host.localize(key, args);
|
|
281
|
+
if (text && text !== key) return text;
|
|
282
|
+
return String(fallback).replace(/\{(\w+)\}/g, (whole, name) => args && name in args ? String(args[name]) : whole);
|
|
283
|
+
}
|
|
86
284
|
|
|
87
285
|
// ../foundry/scripts/ids.mjs
|
|
88
286
|
async function det(kind, key) {
|
|
@@ -98,10 +296,65 @@ var pageId = (vaultId, path) => det("page", `${vaultId}:${path}`);
|
|
|
98
296
|
var folderId = (vaultId, path) => det("folder", `${vaultId}:${path}`);
|
|
99
297
|
var instanceId = (vaultId, path) => det("instance", `${vaultId}:${path}`);
|
|
100
298
|
var subdocId = (vaultId, path, pointer) => det("subdoc", `${vaultId}:${path}:${pointer}`);
|
|
299
|
+
var adventureId = (vaultId) => det("adventure", vaultId);
|
|
101
300
|
|
|
102
301
|
// ../foundry/scripts/parser.mjs
|
|
103
302
|
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
303
|
|
|
304
|
+
// ../foundry/scripts/progress.mjs
|
|
305
|
+
var bar = null;
|
|
306
|
+
var vaultLabel = "";
|
|
307
|
+
var phaseLabel = "";
|
|
308
|
+
var done = 0;
|
|
309
|
+
var total = 0;
|
|
310
|
+
function paint(message) {
|
|
311
|
+
if (!bar) return;
|
|
312
|
+
const counter = total > 0 ? ` ${Math.min(done, total)}/${total}` : "";
|
|
313
|
+
const head = phaseLabel ? `${vaultLabel}: ${phaseLabel}${counter}` : vaultLabel;
|
|
314
|
+
try {
|
|
315
|
+
bar.update({
|
|
316
|
+
pct: total > 0 ? Math.min(done / total, 1) : 0,
|
|
317
|
+
message: message ? `${head} \u2014 ${message}` : head
|
|
318
|
+
});
|
|
319
|
+
} catch {
|
|
320
|
+
bar = null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function begin(label) {
|
|
324
|
+
vaultLabel = label;
|
|
325
|
+
phaseLabel = "";
|
|
326
|
+
done = 0;
|
|
327
|
+
total = 0;
|
|
328
|
+
try {
|
|
329
|
+
const handle = ui?.notifications?.info(label, { progress: true, permanent: true });
|
|
330
|
+
bar = typeof handle?.update === "function" ? handle : null;
|
|
331
|
+
} catch {
|
|
332
|
+
bar = null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function phase(name, count) {
|
|
336
|
+
phaseLabel = name;
|
|
337
|
+
done = 0;
|
|
338
|
+
total = count;
|
|
339
|
+
paint("");
|
|
340
|
+
}
|
|
341
|
+
function step(message) {
|
|
342
|
+
done++;
|
|
343
|
+
paint(message);
|
|
344
|
+
}
|
|
345
|
+
function note(message) {
|
|
346
|
+
paint(message);
|
|
347
|
+
}
|
|
348
|
+
function end() {
|
|
349
|
+
if (!bar) return;
|
|
350
|
+
try {
|
|
351
|
+
ui?.notifications?.remove?.(bar);
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
bar = null;
|
|
355
|
+
phaseLabel = "";
|
|
356
|
+
}
|
|
357
|
+
|
|
105
358
|
// ../foundry/scripts/media.mjs
|
|
106
359
|
var CACHE_DIR = "vaults-cache";
|
|
107
360
|
var BATCH_SIZE2 = 25;
|
|
@@ -111,6 +364,7 @@ function isCacheable(path) {
|
|
|
111
364
|
if (!CACHED_EXT_RE.test(path)) return false;
|
|
112
365
|
if (path.startsWith("_") || path.includes("/_")) return false;
|
|
113
366
|
if (/\.preview\.json$/i.test(path)) return false;
|
|
367
|
+
if (/(^|\/)module\.json$/i.test(path)) return false;
|
|
114
368
|
return true;
|
|
115
369
|
}
|
|
116
370
|
function localFileUrl(vaultId, vaultPath) {
|
|
@@ -178,6 +432,7 @@ async function syncImages(host, vault, manifestFiles) {
|
|
|
178
432
|
chunkBytes += bytes;
|
|
179
433
|
}
|
|
180
434
|
if (chunk.length > 0) chunks.push(chunk);
|
|
435
|
+
phase("Images", toDownload.length);
|
|
181
436
|
let next = 0;
|
|
182
437
|
const downloaded = [];
|
|
183
438
|
const errors = [];
|
|
@@ -194,6 +449,7 @@ async function syncImages(host, vault, manifestFiles) {
|
|
|
194
449
|
continue;
|
|
195
450
|
}
|
|
196
451
|
try {
|
|
452
|
+
step(path.split("/").pop());
|
|
197
453
|
await uploadToWorld(baseDir, path, blob);
|
|
198
454
|
downloaded.push(path);
|
|
199
455
|
} catch (err) {
|
|
@@ -420,23 +676,15 @@ function buildPathIndex(manifestFiles) {
|
|
|
420
676
|
for (const f of manifestFiles) if (f.hash) hashes.set(f.path, f.hash);
|
|
421
677
|
return { paths, idOverrides, docTargets, hashes };
|
|
422
678
|
}
|
|
423
|
-
function
|
|
424
|
-
if (typeof base !== "string" || !base) return null;
|
|
425
|
-
if (base.startsWith("Compendium.")) {
|
|
426
|
-
const parts = base.split(".");
|
|
427
|
-
return parts.length >= 5 ? parts[3] : null;
|
|
428
|
-
}
|
|
429
|
-
return base.split(":")[0] || null;
|
|
430
|
-
}
|
|
431
|
-
async function targetUuid(vaultId, path, index) {
|
|
679
|
+
async function targetUuid(vault, path, index) {
|
|
432
680
|
const docName = index.docTargets?.get(path);
|
|
433
681
|
if (docName) {
|
|
434
|
-
const id = index.idOverrides?.get(path) ?? await instanceId(
|
|
435
|
-
return
|
|
682
|
+
const id = index.idOverrides?.get(path) ?? await instanceId(vault.id, path);
|
|
683
|
+
return instanceUuid(vault, docName, id);
|
|
436
684
|
}
|
|
437
|
-
const eId = await entryId(
|
|
438
|
-
const pId = index.idOverrides?.get(path) ?? await pageId(
|
|
439
|
-
return
|
|
685
|
+
const eId = await entryId(vault.id, path);
|
|
686
|
+
const pId = index.idOverrides?.get(path) ?? await pageId(vault.id, path);
|
|
687
|
+
return journalPageUuid(vault, eId, pId);
|
|
440
688
|
}
|
|
441
689
|
function logicalPathFromHref(href) {
|
|
442
690
|
const decoded = decodeHtmlEntities(href);
|
|
@@ -453,7 +701,7 @@ function decodeHtmlEntities(s) {
|
|
|
453
701
|
return ta.value;
|
|
454
702
|
}
|
|
455
703
|
async function transformHtmlForFoundry(vault, html, index, mediaRefs) {
|
|
456
|
-
html = await rewriteWikilinks(vault
|
|
704
|
+
html = await rewriteWikilinks(vault, html, index);
|
|
457
705
|
html = rewriteMediaSrcs(vault.id, html, index?.hashes, mediaRefs);
|
|
458
706
|
html = rewritePassthroughLinks(vault.id, html);
|
|
459
707
|
html = await applyDomTransforms(html, vault, index);
|
|
@@ -465,7 +713,7 @@ async function applyDomTransforms(html, vault, index) {
|
|
|
465
713
|
touched = stripWebOnlyWidgets(doc) || touched;
|
|
466
714
|
touched = flattenBasesTabs(doc) || touched;
|
|
467
715
|
touched = neutralizeEnrichersInCode(doc) || touched;
|
|
468
|
-
touched = await rewriteBasesCardLinks(doc, vault
|
|
716
|
+
touched = await rewriteBasesCardLinks(doc, vault, index) || touched;
|
|
469
717
|
touched = rewriteDiceButtons(doc) || touched;
|
|
470
718
|
touched = wrapRestrictedCalloutsAsSecret(doc, vault) || touched;
|
|
471
719
|
return touched ? doc.body.innerHTML : html;
|
|
@@ -519,7 +767,18 @@ function neutralizeEnrichersInCode(doc) {
|
|
|
519
767
|
}
|
|
520
768
|
return touched;
|
|
521
769
|
}
|
|
522
|
-
|
|
770
|
+
function contentLinkAttrs(uuid) {
|
|
771
|
+
return {
|
|
772
|
+
// Empty string, matching what TextEditor.createAnchor emits: the selector
|
|
773
|
+
// tests for presence, not value.
|
|
774
|
+
"data-link": "",
|
|
775
|
+
"data-uuid": uuid,
|
|
776
|
+
// Real content links are draggable, and the drag handler keys off the same
|
|
777
|
+
// attribute; without this a card cannot be dropped onto the canvas.
|
|
778
|
+
draggable: "true"
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
async function rewriteBasesCardLinks(doc, vault, index) {
|
|
523
782
|
const cards = doc.querySelectorAll("a.bases-card[href]");
|
|
524
783
|
if (cards.length === 0) return false;
|
|
525
784
|
let touched = false;
|
|
@@ -529,23 +788,25 @@ async function rewriteBasesCardLinks(doc, vaultId, index) {
|
|
|
529
788
|
const path = logicalPathFromHref(href);
|
|
530
789
|
if (!index.paths.has(path)) continue;
|
|
531
790
|
a.classList.add("content-link");
|
|
532
|
-
|
|
791
|
+
for (const [k, v] of Object.entries(contentLinkAttrs(await targetUuid(vault, path, index)))) {
|
|
792
|
+
a.setAttribute(k, v);
|
|
793
|
+
}
|
|
533
794
|
a.removeAttribute("href");
|
|
534
795
|
touched = true;
|
|
535
796
|
}
|
|
536
797
|
return touched;
|
|
537
798
|
}
|
|
538
799
|
function wrapRestrictedCalloutsAsSecret(doc, vault) {
|
|
539
|
-
if (!vault?.
|
|
800
|
+
if (!vault?.playerRole || !Array.isArray(vault.knownRoles) || vault.knownRoles.length === 0) {
|
|
540
801
|
return false;
|
|
541
802
|
}
|
|
542
|
-
const
|
|
543
|
-
if (
|
|
544
|
-
const restrictedRoles = vault.knownRoles.slice(
|
|
803
|
+
const playerIdx = vault.knownRoles.indexOf(vault.playerRole);
|
|
804
|
+
if (playerIdx < 0) return false;
|
|
805
|
+
const restrictedRoles = vault.knownRoles.slice(playerIdx + 1);
|
|
545
806
|
if (restrictedRoles.length === 0) return false;
|
|
546
807
|
let touched = false;
|
|
547
808
|
for (const role of restrictedRoles) {
|
|
548
|
-
for (const el of doc.querySelectorAll(
|
|
809
|
+
for (const el of doc.querySelectorAll(calloutSelectorFor(role))) {
|
|
549
810
|
const section = doc.createElement("section");
|
|
550
811
|
section.className = "secret";
|
|
551
812
|
el.parentNode.insertBefore(section, el);
|
|
@@ -555,10 +816,13 @@ function wrapRestrictedCalloutsAsSecret(doc, vault) {
|
|
|
555
816
|
}
|
|
556
817
|
return touched;
|
|
557
818
|
}
|
|
819
|
+
function calloutSelectorFor(role) {
|
|
820
|
+
return ".callout.callout-" + cssEscape(String(role).toLowerCase());
|
|
821
|
+
}
|
|
558
822
|
function cssEscape(s) {
|
|
559
823
|
return String(s).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
|
560
824
|
}
|
|
561
|
-
async function rewriteWikilinks(
|
|
825
|
+
async function rewriteWikilinks(vault, html, index) {
|
|
562
826
|
const matches = [];
|
|
563
827
|
let m;
|
|
564
828
|
ANCHOR_RE.lastIndex = 0;
|
|
@@ -579,7 +843,7 @@ async function rewriteWikilinks(vaultId, html, index) {
|
|
|
579
843
|
}
|
|
580
844
|
const uuidMatches = matches.filter((r) => r.kind === "uuid");
|
|
581
845
|
const resolved = await Promise.all(
|
|
582
|
-
uuidMatches.map((r) => targetUuid(
|
|
846
|
+
uuidMatches.map((r) => targetUuid(vault, r.path, index))
|
|
583
847
|
);
|
|
584
848
|
uuidMatches.forEach((r, i) => {
|
|
585
849
|
r.uuid = resolved[i];
|
|
@@ -624,46 +888,17 @@ function stripTags(s) {
|
|
|
624
888
|
// ../foundry/scripts/importer.mjs
|
|
625
889
|
var INDEX_BASENAME = "index";
|
|
626
890
|
var NON_INDEX_SORT_BASE = 1e5;
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
"
|
|
630
|
-
"Scene",
|
|
631
|
-
"JournalEntry",
|
|
632
|
-
"RollTable",
|
|
633
|
-
"Macro",
|
|
634
|
-
"Cards",
|
|
635
|
-
"Playlist"
|
|
636
|
-
];
|
|
637
|
-
function inferInstanceDocName(base) {
|
|
638
|
-
if (typeof base !== "string" || !base) return null;
|
|
639
|
-
const raw = base.includes(".") ? base.split(".").at(-2) : base.split(":")[0];
|
|
640
|
-
return KNOWN_INSTANCE_DOC_TYPES.find((t) => t.toLowerCase() === raw?.toLowerCase()) ?? null;
|
|
641
|
-
}
|
|
642
|
-
async function ensureFolderChain(vault, segments) {
|
|
643
|
-
const rootName = vault.rootFolder || vault.label || "Vault";
|
|
644
|
-
const rootKey = `${vault.id}/__root__/${rootName}`;
|
|
645
|
-
const rootFId = await folderId(vault.id, rootKey);
|
|
646
|
-
await upsertFolder(rootFId, rootName, null);
|
|
647
|
-
let parentId = rootFId;
|
|
648
|
-
let acc = rootKey;
|
|
891
|
+
async function ensureFolderChain(target, vault, segments) {
|
|
892
|
+
let parentId = null;
|
|
893
|
+
let acc = `${vault.id}/__root__/${vault.rootFolder || vault.label || "Vault"}`;
|
|
649
894
|
for (const seg of segments) {
|
|
650
895
|
acc += "/" + seg;
|
|
651
896
|
const fId = await folderId(vault.id, acc);
|
|
652
|
-
await
|
|
897
|
+
await target.putFolder("JournalEntry", { _id: fId, name: seg, folder: parentId });
|
|
653
898
|
parentId = fId;
|
|
654
899
|
}
|
|
655
900
|
return parentId;
|
|
656
901
|
}
|
|
657
|
-
async function upsertFolder(id, name, parentId) {
|
|
658
|
-
const existing = game.folders.get(id);
|
|
659
|
-
if (existing) {
|
|
660
|
-
if (existing.name !== name || existing.folder?.id !== parentId) {
|
|
661
|
-
await existing.update({ name, folder: parentId });
|
|
662
|
-
}
|
|
663
|
-
return existing;
|
|
664
|
-
}
|
|
665
|
-
return Folder.create({ _id: id, name, type: "JournalEntry", folder: parentId }, { keepId: true });
|
|
666
|
-
}
|
|
667
902
|
function buildFolderInfo(mdPaths) {
|
|
668
903
|
const map = /* @__PURE__ */ new Map();
|
|
669
904
|
const ensure = (folderPath) => {
|
|
@@ -689,7 +924,29 @@ function buildFolderInfo(mdPaths) {
|
|
|
689
924
|
function isIndexFile(filename) {
|
|
690
925
|
return filename.replace(/\.md$/i, "") === INDEX_BASENAME;
|
|
691
926
|
}
|
|
692
|
-
|
|
927
|
+
function resolvePageType(want, path) {
|
|
928
|
+
if (want === "text") return "text";
|
|
929
|
+
const known = /* @__PURE__ */ new Set([
|
|
930
|
+
...CORE_PAGE_TYPES,
|
|
931
|
+
...game.documentTypes?.JournalEntryPage ?? []
|
|
932
|
+
]);
|
|
933
|
+
if (known.has(want)) return want;
|
|
934
|
+
console.warn(
|
|
935
|
+
`Vaults | ${path}: this world has no "${want}" journal page type (${game.system?.id ?? "no system"} provides ${[...known].join(", ")}). Imported as a text page instead.`
|
|
936
|
+
);
|
|
937
|
+
return "text";
|
|
938
|
+
}
|
|
939
|
+
function deepMergePage(target, patch) {
|
|
940
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
941
|
+
if (v && typeof v === "object" && !Array.isArray(v) && target[k] && typeof target[k] === "object" && !Array.isArray(target[k])) {
|
|
942
|
+
deepMergePage(target[k], v);
|
|
943
|
+
} else {
|
|
944
|
+
target[k] = v;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return target;
|
|
948
|
+
}
|
|
949
|
+
async function upsertFile(target, vault, path, body, index, meta, folderInfo, mediaRefs) {
|
|
693
950
|
let html = await transformHtmlForFoundry(vault, body, index, mediaRefs);
|
|
694
951
|
html = await appendInstanceDocLink(html, vault, path, meta);
|
|
695
952
|
const segs = path.split("/");
|
|
@@ -698,7 +955,7 @@ async function upsertFile(vault, path, body, index, meta, folderInfo, mediaRefs)
|
|
|
698
955
|
const fInfo = folderInfo?.get(folderPath);
|
|
699
956
|
const leaf = !!fInfo && folderPath !== "" && !fInfo.hasSubfolders;
|
|
700
957
|
const hostSegs = leaf ? segs.slice(0, -1) : segs;
|
|
701
|
-
const folderFId = await ensureFolderChain(vault, hostSegs);
|
|
958
|
+
const folderFId = await ensureFolderChain(target, vault, hostSegs);
|
|
702
959
|
const entryName = folderPath === "" ? vault.rootFolder || vault.label || "Vault" : fInfo?.displayName || segs[segs.length - 1] || "";
|
|
703
960
|
const pageName = meta?.title || filename.replace(/\.md$/i, "");
|
|
704
961
|
const eId = await entryId(vault.id, path);
|
|
@@ -706,15 +963,19 @@ async function upsertFile(vault, path, body, index, meta, folderInfo, mediaRefs)
|
|
|
706
963
|
const pId = typeof idOverride === "string" && idOverride ? idOverride : await pageId(vault.id, path);
|
|
707
964
|
const pageOwnership = pageOwnershipLevelFor(vault, meta?.role);
|
|
708
965
|
const flags = { [MODULE_ID]: { vaultId: vault.id, path } };
|
|
966
|
+
const spec = journalPageSpec(meta?.foundry);
|
|
967
|
+
const type = resolvePageType(spec.type, path);
|
|
709
968
|
const pageData = {
|
|
710
969
|
_id: pId,
|
|
711
970
|
name: pageName,
|
|
712
|
-
type
|
|
713
|
-
|
|
971
|
+
type,
|
|
972
|
+
// A page whose content is its `src` has nowhere to put an article, so the
|
|
973
|
+
// body is dropped rather than written somewhere it will not be read.
|
|
974
|
+
...type === "text" ? { text: {
|
|
714
975
|
content: html,
|
|
715
976
|
format: 1
|
|
716
977
|
/* HTML */
|
|
717
|
-
},
|
|
978
|
+
} } : {},
|
|
718
979
|
sort: isIndexFile(filename) ? 0 : NON_INDEX_SORT_BASE,
|
|
719
980
|
flags,
|
|
720
981
|
// Ownership is set on the *page*, not the parent entry: in the
|
|
@@ -726,311 +987,556 @@ async function upsertFile(vault, path, body, index, meta, folderInfo, mediaRefs)
|
|
|
726
987
|
// page in it). See reconcileOwnership below.
|
|
727
988
|
...pageOwnership !== null ? { ownership: { default: pageOwnership } } : {}
|
|
728
989
|
};
|
|
729
|
-
|
|
990
|
+
if (Object.keys(spec.overlay).length > 0) deepMergePage(pageData, spec.overlay);
|
|
991
|
+
const existing = await target.get("JournalEntry", eId);
|
|
730
992
|
if (existing) {
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
993
|
+
const pages = (existing.pages ?? []).filter((pg) => pg._id !== pId);
|
|
994
|
+
pages.push(pageData);
|
|
995
|
+
await target.put("JournalEntry", {
|
|
996
|
+
...existing,
|
|
997
|
+
name: entryName,
|
|
998
|
+
folder: folderFId,
|
|
999
|
+
pages,
|
|
1000
|
+
flags
|
|
1001
|
+
});
|
|
738
1002
|
return "modified";
|
|
739
1003
|
}
|
|
740
|
-
|
|
1004
|
+
return target.put("JournalEntry", {
|
|
741
1005
|
_id: eId,
|
|
742
1006
|
name: entryName,
|
|
743
1007
|
folder: folderFId,
|
|
744
1008
|
pages: [pageData],
|
|
745
1009
|
flags,
|
|
746
|
-
// Bootstrap the entry visible at the page's tier so player-visible
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
// via reconcile.
|
|
1010
|
+
// Bootstrap the entry visible at the page's tier so player-visible pages
|
|
1011
|
+
// aren't hidden in the gap before reconcileEntries runs. Mixed-tier
|
|
1012
|
+
// folders converge to max(pages) there.
|
|
750
1013
|
...pageOwnership !== null ? { ownership: { default: pageOwnership } } : {}
|
|
751
|
-
}
|
|
752
|
-
return "added";
|
|
1014
|
+
});
|
|
753
1015
|
}
|
|
754
1016
|
function pageOwnershipLevelFor(vault, pageRole) {
|
|
755
|
-
if (!vault.
|
|
756
|
-
const
|
|
757
|
-
if (
|
|
1017
|
+
if (!vault.playerRole || !vault.knownRoles?.length) return null;
|
|
1018
|
+
const playerIdx = vault.knownRoles.indexOf(vault.playerRole);
|
|
1019
|
+
if (playerIdx < 0) return null;
|
|
758
1020
|
const pageIdx = pageRole ? vault.knownRoles.indexOf(pageRole) : -1;
|
|
759
1021
|
const effectiveIdx = pageIdx < 0 ? 0 : pageIdx;
|
|
760
|
-
return effectiveIdx
|
|
1022
|
+
return effectiveIdx <= playerIdx ? CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER : CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE;
|
|
761
1023
|
}
|
|
762
|
-
async function
|
|
763
|
-
|
|
764
|
-
const
|
|
765
|
-
(j) => j.getFlag(MODULE_ID, "vaultId") === vault.id
|
|
766
|
-
);
|
|
1024
|
+
async function reconcileEntries(target, vault, folderInfo, bodyMetaIndex) {
|
|
1025
|
+
const ours = (await target.contents("JournalEntry")).filter((j) => j.flags?.[MODULE_ID]?.vaultId === vault.id);
|
|
1026
|
+
const gated = !!vault.playerRole && !!vault.knownRoles?.length;
|
|
767
1027
|
for (const entry of ours) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const bodyPath = pPath.replace(/\.md$/i, ".body.html");
|
|
773
|
-
const meta = bodyMetaIndex.get(bodyPath);
|
|
774
|
-
if (!meta) continue;
|
|
775
|
-
const level = pageOwnershipLevelFor(vault, meta.role);
|
|
776
|
-
if (level === null) continue;
|
|
777
|
-
if (page.ownership?.default !== level) {
|
|
778
|
-
try {
|
|
779
|
-
await page.update({ ownership: { default: level } });
|
|
780
|
-
} catch (err) {
|
|
781
|
-
console.warn(`Vaults | reconcile ownership ${pPath} \u2192 ${level} failed:`, err);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
if (entryMax === null || level > entryMax) entryMax = level;
|
|
785
|
-
}
|
|
786
|
-
if (entryMax !== null && entry.ownership?.default !== entryMax) {
|
|
1028
|
+
const patched = reconcileOwnershipData(gated ? vault : null, entry, bodyMetaIndex);
|
|
1029
|
+
const folder = await expectedFolder(target, vault, entry, folderInfo);
|
|
1030
|
+
if (folder !== void 0 && folder !== (patched.folder ?? null)) patched.folder = folder;
|
|
1031
|
+
if (JSON.stringify(patched) !== JSON.stringify(entry)) {
|
|
787
1032
|
try {
|
|
788
|
-
await
|
|
1033
|
+
await target.put("JournalEntry", patched);
|
|
789
1034
|
} catch (err) {
|
|
790
|
-
console.warn(`Vaults | reconcile
|
|
1035
|
+
console.warn(`Vaults | reconcile failed for ${entry.name}:`, err);
|
|
791
1036
|
}
|
|
792
1037
|
}
|
|
793
1038
|
}
|
|
794
1039
|
}
|
|
795
|
-
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
for (const
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1040
|
+
function reconcileOwnershipData(vault, entry, bodyMetaIndex) {
|
|
1041
|
+
const patched = structuredClone(entry);
|
|
1042
|
+
if (!vault) return patched;
|
|
1043
|
+
let entryMax = null;
|
|
1044
|
+
for (const page of patched.pages ?? []) {
|
|
1045
|
+
const pPath = page.flags?.[MODULE_ID]?.path;
|
|
1046
|
+
if (!pPath) continue;
|
|
1047
|
+
const meta = bodyMetaIndex.get(pPath.replace(/\.md$/i, ".body.html"));
|
|
1048
|
+
if (!meta) continue;
|
|
1049
|
+
const level = pageOwnershipLevelFor(vault, meta.role);
|
|
1050
|
+
if (level === null) continue;
|
|
1051
|
+
page.ownership = { ...page.ownership, default: level };
|
|
1052
|
+
if (entryMax === null || level > entryMax) entryMax = level;
|
|
1053
|
+
}
|
|
1054
|
+
if (entryMax !== null) patched.ownership = { ...patched.ownership, default: entryMax };
|
|
1055
|
+
return patched;
|
|
1056
|
+
}
|
|
1057
|
+
async function expectedFolder(target, vault, entry, folderInfo) {
|
|
1058
|
+
const path = entry.pages?.[0]?.flags?.[MODULE_ID]?.path;
|
|
1059
|
+
if (!path) return void 0;
|
|
1060
|
+
const segs = path.split("/");
|
|
1061
|
+
segs.pop();
|
|
1062
|
+
const folderPath = segs.join("/");
|
|
1063
|
+
const fInfo = folderInfo?.get(folderPath);
|
|
1064
|
+
const leaf = !!fInfo && folderPath !== "" && !fInfo.hasSubfolders;
|
|
1065
|
+
return ensureFolderChain(target, vault, leaf ? segs.slice(0, -1) : segs);
|
|
818
1066
|
}
|
|
819
|
-
async function deleteFile(vault, path) {
|
|
1067
|
+
async function deleteFile(target, vault, path) {
|
|
820
1068
|
const eId = await entryId(vault.id, path);
|
|
821
1069
|
const pId = await pageId(vault.id, path);
|
|
822
|
-
const entry =
|
|
1070
|
+
const entry = await target.get("JournalEntry", eId);
|
|
823
1071
|
if (!entry) return;
|
|
824
|
-
const
|
|
825
|
-
if (
|
|
826
|
-
|
|
827
|
-
}
|
|
828
|
-
async function deleteVaultJournals(vaultId) {
|
|
829
|
-
const journals = game.journal.contents.filter((j) => j.getFlag(MODULE_ID, "vaultId") === vaultId);
|
|
830
|
-
for (const j of journals) await j.delete();
|
|
831
|
-
const folders = game.folders.contents.filter((f) => f.type === "JournalEntry");
|
|
832
|
-
for (const f of folders.reverse()) {
|
|
833
|
-
if (f.contents.length === 0 && f.children.length === 0) {
|
|
834
|
-
const root = await folderId(vaultId, `${vaultId}/__root__/${f.name}`);
|
|
835
|
-
if (f.id === root) await f.delete();
|
|
836
|
-
}
|
|
837
|
-
}
|
|
1072
|
+
const pages = (entry.pages ?? []).filter((pg) => pg._id !== pId);
|
|
1073
|
+
if (pages.length === 0) await target.remove("JournalEntry", eId);
|
|
1074
|
+
else await target.put("JournalEntry", { ...entry, pages });
|
|
838
1075
|
}
|
|
839
1076
|
async function appendInstanceDocLink(html, vault, path, meta) {
|
|
840
1077
|
const base = meta?.foundry?.base;
|
|
841
|
-
const docName =
|
|
1078
|
+
const docName = docNameFromBase(base);
|
|
842
1079
|
if (!docName) return html;
|
|
843
1080
|
const idOverride = meta?.foundry?.id;
|
|
844
1081
|
const docId = typeof idOverride === "string" && idOverride ? idOverride : await instanceId(vault.id, path);
|
|
845
1082
|
const label = meta?.title || path.split("/").pop().replace(/\.md$/i, "");
|
|
1083
|
+
const uuid = instanceUuid(vault, docName, docId);
|
|
846
1084
|
return html + `
|
|
847
|
-
<p class="vaults-instance-link"><em>Foundry document:</em> @UUID[${
|
|
1085
|
+
<p class="vaults-instance-link"><em>Foundry document:</em> @UUID[${uuid}]{${escapeBraces(label)}}</p>`;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// ../foundry/scripts/moulinette.mjs
|
|
1089
|
+
var MOULINETTE_PREFIX = "@moulinette/";
|
|
1090
|
+
var CACHED_COLLECTION = "mou-cloud-cached";
|
|
1091
|
+
function parseMoulinetteRef(s) {
|
|
1092
|
+
if (typeof s !== "string" || !s.startsWith(MOULINETTE_PREFIX)) return null;
|
|
1093
|
+
const [pack, ...fileParts] = s.slice(MOULINETTE_PREFIX.length).split("/");
|
|
1094
|
+
const file = fileParts.join("/");
|
|
1095
|
+
if (!pack || !file) return null;
|
|
1096
|
+
return { pack, file };
|
|
1097
|
+
}
|
|
1098
|
+
async function loadIndex(log) {
|
|
1099
|
+
const mod = game.modules?.get("moulinette");
|
|
1100
|
+
if (!mod) {
|
|
1101
|
+
log("references need the Moulinette module, which is not installed");
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
if (!mod.active) {
|
|
1105
|
+
log("references need the Moulinette module, which is installed but not enabled in this world");
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
const collection = mod.collections?.find((c) => c.getId?.() === CACHED_COLLECTION);
|
|
1109
|
+
if (!collection?.initialize || !collection.selectAsset || !collection.downloadAsset) {
|
|
1110
|
+
log("Moulinette is installed but its asset index is not where we expect; skipping");
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
try {
|
|
1114
|
+
await collection.initialize();
|
|
1115
|
+
} catch (err) {
|
|
1116
|
+
log(`could not load the Moulinette index: ${err?.message ?? err}`);
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
const assets = mod.cache?.allAssets;
|
|
1120
|
+
if (!Array.isArray(assets)) {
|
|
1121
|
+
log("Moulinette's asset index is not a list; skipping");
|
|
1122
|
+
return null;
|
|
1123
|
+
}
|
|
1124
|
+
return { mod, collection, assets };
|
|
1125
|
+
}
|
|
1126
|
+
function findAsset(ref, index, log) {
|
|
1127
|
+
const match = index.assets.find(
|
|
1128
|
+
(a) => String(a?.pack_id) === ref.pack && a?.url === ref.file
|
|
1129
|
+
);
|
|
1130
|
+
if (!match) {
|
|
1131
|
+
log(`no asset ${ref.file} in pack ${ref.pack} \u2014 not subscribed, or it moved`);
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
return match;
|
|
1135
|
+
}
|
|
1136
|
+
async function resolveMoulinetteDocument(spec, warn) {
|
|
1137
|
+
const log = (msg) => warn(msg);
|
|
1138
|
+
const ref = parseMoulinetteRef(MOULINETTE_PREFIX + spec);
|
|
1139
|
+
if (!ref) {
|
|
1140
|
+
log(`malformed reference '${spec}' \u2014 expected <pack_ref>/<filepath>`);
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
const index = await loadIndex(log);
|
|
1144
|
+
if (!index) return null;
|
|
1145
|
+
const asset = findAsset(ref, index, log);
|
|
1146
|
+
if (!asset) return null;
|
|
1147
|
+
try {
|
|
1148
|
+
const descriptor = await index.mod.cloudclient.apiGET(`/asset/${asset.id}`, {
|
|
1149
|
+
session: index.mod.getSessionId()
|
|
1150
|
+
});
|
|
1151
|
+
const dl = await index.collection.downloadAsset(descriptor);
|
|
1152
|
+
if (!dl?.message) {
|
|
1153
|
+
log(`${ref.pack}/${ref.file} is not a document; foundry.base needs one`);
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
return JSON.parse(dl.message);
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
log(`could not read ${ref.pack}/${ref.file}: ${err?.message ?? err}`);
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
async function resolveOne(ref, index, log) {
|
|
1163
|
+
const match = findAsset(ref, index, log);
|
|
1164
|
+
if (!match) return null;
|
|
1165
|
+
try {
|
|
1166
|
+
note(`Moulinette: ${ref.file.split("/").pop()}`);
|
|
1167
|
+
const path = await index.collection.selectAsset(match);
|
|
1168
|
+
if (!path) {
|
|
1169
|
+
log(`${ref.pack}/${ref.file} is not a media asset; only files can be referenced`);
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
return path;
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
log(`download failed for ${ref.pack}/${ref.file}: ${err?.message ?? err}`);
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
async function resolveMoulinetteRefs(value, warn) {
|
|
1179
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1180
|
+
const stats = { resolved: 0, unresolved: 0 };
|
|
1181
|
+
const seenWarnings = /* @__PURE__ */ new Set();
|
|
1182
|
+
const log = (msg) => {
|
|
1183
|
+
if (seenWarnings.has(msg)) return;
|
|
1184
|
+
seenWarnings.add(msg);
|
|
1185
|
+
warn(msg);
|
|
1186
|
+
};
|
|
1187
|
+
let index;
|
|
1188
|
+
const lookup = async (s) => {
|
|
1189
|
+
if (cache.has(s)) return cache.get(s);
|
|
1190
|
+
const ref = parseMoulinetteRef(s);
|
|
1191
|
+
let path = null;
|
|
1192
|
+
if (!ref) {
|
|
1193
|
+
log(`malformed reference '${s}' \u2014 expected @moulinette/<pack_ref>/<filepath>`);
|
|
1194
|
+
} else {
|
|
1195
|
+
if (index === void 0) index = await loadIndex(log);
|
|
1196
|
+
if (index) path = await resolveOne(ref, index, log);
|
|
1197
|
+
}
|
|
1198
|
+
cache.set(s, path);
|
|
1199
|
+
if (path) stats.resolved++;
|
|
1200
|
+
else stats.unresolved++;
|
|
1201
|
+
return path;
|
|
1202
|
+
};
|
|
1203
|
+
const walk = async (node) => {
|
|
1204
|
+
if (Array.isArray(node)) {
|
|
1205
|
+
const kept = [];
|
|
1206
|
+
for (const item of node) {
|
|
1207
|
+
if (typeof item === "string" && item.startsWith(MOULINETTE_PREFIX)) {
|
|
1208
|
+
const path = await lookup(item);
|
|
1209
|
+
if (path) kept.push(path);
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if (await walk(item)) kept.push(item);
|
|
1213
|
+
}
|
|
1214
|
+
node.length = 0;
|
|
1215
|
+
node.push(...kept);
|
|
1216
|
+
return true;
|
|
1217
|
+
}
|
|
1218
|
+
if (node && typeof node === "object") {
|
|
1219
|
+
let viable = true;
|
|
1220
|
+
for (const key of Object.keys(node)) {
|
|
1221
|
+
const v = node[key];
|
|
1222
|
+
if (typeof v === "string" && v.startsWith(MOULINETTE_PREFIX)) {
|
|
1223
|
+
const path = await lookup(v);
|
|
1224
|
+
if (path) node[key] = path;
|
|
1225
|
+
else {
|
|
1226
|
+
delete node[key];
|
|
1227
|
+
viable = false;
|
|
1228
|
+
}
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
if (!await walk(v)) delete node[key];
|
|
1232
|
+
}
|
|
1233
|
+
return viable;
|
|
1234
|
+
}
|
|
1235
|
+
return true;
|
|
1236
|
+
};
|
|
1237
|
+
await walk(value);
|
|
1238
|
+
return stats;
|
|
848
1239
|
}
|
|
849
1240
|
|
|
850
1241
|
// ../foundry/scripts/instance.mjs
|
|
1242
|
+
var CORE_DESCRIPTION_FIELDS = {
|
|
1243
|
+
RollTable: "description",
|
|
1244
|
+
Playlist: "description"
|
|
1245
|
+
};
|
|
1246
|
+
function descriptionPathFor(docName, systemId) {
|
|
1247
|
+
return CORE_DESCRIPTION_FIELDS[docName] ?? DESCRIPTION_FIELDS[systemId]?.[docName];
|
|
1248
|
+
}
|
|
851
1249
|
var DESCRIPTION_FIELDS = {
|
|
852
1250
|
dnd5e: {
|
|
853
1251
|
Actor: "system.details.biography.value",
|
|
854
1252
|
Item: "system.description.value"
|
|
855
1253
|
}
|
|
856
1254
|
};
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
"
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1255
|
+
function sceneBackgroundSrc(data) {
|
|
1256
|
+
const levels = Array.isArray(data?.levels) ? data.levels : [];
|
|
1257
|
+
const initial = levels.find((l) => l._id === data.initialLevel) ?? levels[0];
|
|
1258
|
+
return initial?.background?.src || data?.background?.src || null;
|
|
1259
|
+
}
|
|
1260
|
+
async function attachSceneThumb(data, docName, vaultPath) {
|
|
1261
|
+
if (docName !== "Scene" || data.thumb) return;
|
|
1262
|
+
const src = sceneBackgroundSrc(data);
|
|
1263
|
+
if (!src) return;
|
|
1264
|
+
try {
|
|
1265
|
+
const { thumb } = await foundry.helpers.media.ImageHelper.createThumbnail(
|
|
1266
|
+
src,
|
|
1267
|
+
{ width: 300, height: 100 }
|
|
1268
|
+
);
|
|
1269
|
+
if (thumb) data.thumb = thumb;
|
|
1270
|
+
} catch (err) {
|
|
1271
|
+
console.warn(`Vaults | could not make a scene thumbnail for ${vaultPath}:`, err);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
function wantsJournalNote(docName, fm) {
|
|
1275
|
+
return docName === "Scene" && fm?.journal !== false;
|
|
1276
|
+
}
|
|
1277
|
+
async function applyInstance(target, vault, vaultPath, meta, { forceFull = false } = {}) {
|
|
879
1278
|
const fm = meta?.foundry;
|
|
880
|
-
if (!fm || typeof fm !== "object") return;
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
} else {
|
|
906
|
-
docName = parsed.docName;
|
|
907
|
-
baseData = parsed.subtype ? { type: parsed.subtype } : {};
|
|
908
|
-
}
|
|
909
|
-
const collection = COLLECTION_FOR[docName]?.();
|
|
910
|
-
if (!collection) {
|
|
911
|
-
console.warn(`Vaults | foundry.base: no world collection for ${docName}; skipping ${vaultPath}.`);
|
|
912
|
-
return;
|
|
1279
|
+
if (!fm || typeof fm !== "object") return null;
|
|
1280
|
+
if (fm.base === void 0 || fm.base === null) return null;
|
|
1281
|
+
const specs = Array.isArray(fm.base) ? fm.base : [fm.base];
|
|
1282
|
+
const candidates = specs.map(parseFoundryBase);
|
|
1283
|
+
if (candidates.length === 0 || candidates.some((c) => !c)) {
|
|
1284
|
+
console.warn(
|
|
1285
|
+
`Vaults | foundry.base: ${vaultPath} \u2192 unrecognised base ${JSON.stringify(fm.base)}; expected a UUID ("Compendium.<pkg>.<pack>.Actor.<id>") or a type ("Actor:npc"), or a list of those. Skipping.`
|
|
1286
|
+
);
|
|
1287
|
+
return { ok: false, reason: "unparseable" };
|
|
1288
|
+
}
|
|
1289
|
+
const docNames = new Set(candidates.map(docNameOf).filter(Boolean));
|
|
1290
|
+
const docName = [...docNames][0] ?? null;
|
|
1291
|
+
if (!docName) {
|
|
1292
|
+
console.warn(`Vaults | foundry.base: ${vaultPath} \u2192 could not read a document type from ${JSON.stringify(fm.base)}. Skipping.`);
|
|
1293
|
+
return { ok: false, reason: "unparseable" };
|
|
1294
|
+
}
|
|
1295
|
+
if (docNames.size > 1) {
|
|
1296
|
+
console.warn(
|
|
1297
|
+
`Vaults | foundry.base: ${vaultPath} \u2192 every entry must name the same document type, got ${[...docNames].join(", ")}. Skipping.`
|
|
1298
|
+
);
|
|
1299
|
+
return { ok: false, reason: "mixed-types" };
|
|
1300
|
+
}
|
|
1301
|
+
if (!CONFIG[docName]) {
|
|
1302
|
+
console.warn(`Vaults | foundry.base: this world has no ${docName} documents; skipping ${vaultPath}.`);
|
|
1303
|
+
return { ok: false, reason: "no-collection" };
|
|
913
1304
|
}
|
|
914
|
-
const docClass = CONFIG[docName].documentClass;
|
|
915
1305
|
const id = typeof fm.id === "string" && fm.id ? fm.id : await instanceId(vault.id, vaultPath);
|
|
916
1306
|
const dataJson = fm.data_json && typeof fm.data_json === "object" && !Array.isArray(fm.data_json) ? rewriteVaultPaths(structuredClone(fm.data_json), vault.id) : null;
|
|
917
|
-
|
|
1307
|
+
const moulinetteWarn = (msg) => console.warn(`Vaults | moulinette: ${vaultPath}: ${msg}`);
|
|
1308
|
+
if (dataJson) await resolveMoulinetteRefs(dataJson, moulinetteWarn);
|
|
1309
|
+
if (dataJson) {
|
|
1310
|
+
await resolveItemUuids(dataJson, vaultPath);
|
|
1311
|
+
await ensureEmbeddedIds(dataJson, vault.id, vaultPath);
|
|
1312
|
+
}
|
|
918
1313
|
const derived = {};
|
|
919
|
-
const overlay = await buildOverlay(vault, vaultPath, meta, docName, derived);
|
|
1314
|
+
const overlay = await buildOverlay(target, vault, vaultPath, meta, docName, derived);
|
|
920
1315
|
const tokenFloor = derived.tokenTexture && !dataJson?.prototypeToken?.texture?.src && !fm?.data?.prototypeToken?.texture?.src ? { prototypeToken: { texture: { src: derived.tokenTexture } } } : null;
|
|
921
|
-
const existing =
|
|
1316
|
+
const existing = await target.get(docName, id);
|
|
922
1317
|
if (existing) {
|
|
923
1318
|
const base = tokenFloor ? deepMerge(structuredClone(tokenFloor), dataJson ?? {}) : dataJson;
|
|
924
1319
|
const updatePatch = base ? deepMerge(structuredClone(base), overlay) : overlay;
|
|
1320
|
+
if (wantsJournalNote(docName, fm)) {
|
|
1321
|
+
await attachJournalNote(updatePatch, {
|
|
1322
|
+
width: updatePatch.width ?? existing.width,
|
|
1323
|
+
height: updatePatch.height ?? existing.height,
|
|
1324
|
+
padding: updatePatch.padding ?? existing.padding,
|
|
1325
|
+
grid: updatePatch.grid ?? { size: existing.grid?.size }
|
|
1326
|
+
}, vault, vaultPath, meta);
|
|
1327
|
+
}
|
|
1328
|
+
if (!forceFull) delete updatePatch.folder;
|
|
925
1329
|
try {
|
|
926
|
-
|
|
1330
|
+
const merged = deepMerge(structuredClone(existing), updatePatch);
|
|
1331
|
+
if (sceneBackgroundSrc(merged) !== sceneBackgroundSrc(existing) || !existing.thumb) {
|
|
1332
|
+
await attachSceneThumb(merged, docName, vaultPath);
|
|
1333
|
+
}
|
|
1334
|
+
await target.put(docName, merged);
|
|
927
1335
|
} catch (err) {
|
|
928
1336
|
console.warn(`Vaults | foundry.base update failed for ${vaultPath}:`, err);
|
|
1337
|
+
return { ok: false, reason: "update-failed" };
|
|
929
1338
|
}
|
|
930
|
-
return;
|
|
1339
|
+
return { ok: true, action: "updated" };
|
|
931
1340
|
}
|
|
1341
|
+
const resolved = await resolveBase(candidates, vaultPath);
|
|
1342
|
+
if (!resolved) return { ok: false, reason: "unresolved" };
|
|
1343
|
+
const baseData = resolved.data;
|
|
1344
|
+
const baseItems = Array.isArray(baseData.items) ? baseData.items : null;
|
|
932
1345
|
if (tokenFloor) deepMerge(baseData, tokenFloor);
|
|
933
1346
|
if (dataJson) deepMerge(baseData, dataJson);
|
|
934
1347
|
baseData._id = id;
|
|
935
1348
|
deepMerge(baseData, overlay);
|
|
1349
|
+
if (wantsJournalNote(docName, fm)) {
|
|
1350
|
+
await attachJournalNote(baseData, baseData, vault, vaultPath, meta);
|
|
1351
|
+
}
|
|
1352
|
+
if (baseItems && baseData.items !== baseItems) {
|
|
1353
|
+
baseData.items = mergeItemsById(baseItems, baseData.items);
|
|
1354
|
+
}
|
|
936
1355
|
try {
|
|
937
|
-
await
|
|
1356
|
+
await attachSceneThumb(baseData, docName, vaultPath);
|
|
1357
|
+
await target.put(docName, baseData);
|
|
938
1358
|
} catch (err) {
|
|
939
1359
|
console.warn(`Vaults | foundry.base create failed for ${vaultPath}:`, err);
|
|
940
|
-
return;
|
|
1360
|
+
return { ok: false, reason: "create-failed" };
|
|
941
1361
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1362
|
+
const created = await target.get(docName, id);
|
|
1363
|
+
if (!created) {
|
|
1364
|
+
console.warn(
|
|
1365
|
+
`Vaults | foundry.base: ${vaultPath} \u2192 ${docName} ${id} was rejected on create; no document exists. Foundry logged the validation error separately (look for "DataModelValidationError" above).`
|
|
1366
|
+
);
|
|
1367
|
+
return { ok: false, reason: "create-rejected" };
|
|
1368
|
+
}
|
|
1369
|
+
if (docName === "Scene" && (!created.thumb || resolved.from)) {
|
|
1370
|
+
try {
|
|
1371
|
+
const { thumb } = await created.createThumbnail();
|
|
1372
|
+
if (thumb) await created.update({ thumb });
|
|
1373
|
+
else console.warn(`Vaults | scene thumbnail came back empty for ${vaultPath}`);
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
console.warn(`Vaults | scene thumbnail generation failed for ${vaultPath}:`, err);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return { ok: true, action: "created", skew: resolved.skew ?? null };
|
|
1379
|
+
}
|
|
1380
|
+
function generationSkew(data, ref) {
|
|
1381
|
+
const exported = Number.parseInt(String(data?._stats?.coreVersion ?? ""), 10);
|
|
1382
|
+
const world = Number(game.release?.generation);
|
|
1383
|
+
if (!Number.isFinite(exported) || !Number.isFinite(world) || exported === world) return null;
|
|
1384
|
+
return { ref, exported: data._stats.coreVersion, world: game.release.generation };
|
|
1385
|
+
}
|
|
1386
|
+
async function resolveBase(candidates, vaultPath) {
|
|
1387
|
+
const tried = [];
|
|
1388
|
+
for (const parsed of candidates) {
|
|
1389
|
+
if (parsed.kind === "blank") {
|
|
1390
|
+
if (tried.length > 0) {
|
|
1391
|
+
console.info(
|
|
1392
|
+
`Vaults | foundry.base: ${vaultPath} \u2192 fell back to blank ${parsed.docName}${parsed.subtype ? `:${parsed.subtype}` : ""} after ${tried.length} earlier candidate(s) did not resolve.`
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
return { data: parsed.subtype ? { type: parsed.subtype } : {}, from: null };
|
|
1396
|
+
}
|
|
1397
|
+
if (parsed.kind === "moulinette") {
|
|
1398
|
+
const data2 = await resolveMoulinetteDocument(
|
|
1399
|
+
parsed.ref,
|
|
1400
|
+
(msg) => console.warn(`Vaults | moulinette: ${vaultPath}: ${msg}`)
|
|
1401
|
+
);
|
|
1402
|
+
if (!data2) {
|
|
1403
|
+
tried.push(`@moulinette/${parsed.ref} \u2014 did not resolve`);
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
const skew = generationSkew(data2, parsed.ref);
|
|
1407
|
+
delete data2._id;
|
|
1408
|
+
if (tried.length > 0) {
|
|
1409
|
+
console.info(
|
|
1410
|
+
`Vaults | foundry.base: ${vaultPath} \u2192 using @moulinette/${parsed.ref}; earlier candidate(s) skipped:
|
|
1411
|
+
` + tried.join("\n ")
|
|
1412
|
+
);
|
|
950
1413
|
}
|
|
1414
|
+
return { data: data2, from: `@moulinette/${parsed.ref}`, skew };
|
|
951
1415
|
}
|
|
1416
|
+
const template = await safeFromUuid(parsed.uuid);
|
|
1417
|
+
if (!template) {
|
|
1418
|
+
tried.push(`${parsed.uuid} \u2014 did not resolve`);
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
if (!BLANK_DOC_TYPES.includes(template.documentName)) {
|
|
1422
|
+
tried.push(`${parsed.uuid} \u2014 is a ${template.documentName}; vaults can instantiate ${BLANK_DOC_TYPES.join(", ")}`);
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
let data;
|
|
1426
|
+
try {
|
|
1427
|
+
data = template.toObject();
|
|
1428
|
+
} catch (err) {
|
|
1429
|
+
tried.push(`${parsed.uuid} \u2014 unreadable: ${err.message}`);
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
delete data._id;
|
|
1433
|
+
const sourceUuid = template.uuid ?? parsed.uuid;
|
|
1434
|
+
if (sourceUuid.startsWith("Compendium.")) {
|
|
1435
|
+
data._stats = { ...data._stats, compendiumSource: sourceUuid };
|
|
1436
|
+
}
|
|
1437
|
+
const via = sourceUuid === parsed.uuid ? "" : ` (redirected to ${sourceUuid})`;
|
|
1438
|
+
if (tried.length > 0) {
|
|
1439
|
+
console.info(
|
|
1440
|
+
`Vaults | foundry.base: ${vaultPath} \u2192 using ${parsed.uuid}${via}; earlier candidate(s) skipped:
|
|
1441
|
+
` + tried.join("\n ")
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
return { data, from: sourceUuid };
|
|
952
1445
|
}
|
|
1446
|
+
console.warn(
|
|
1447
|
+
`Vaults | foundry.base: ${vaultPath} \u2192 no candidate resolved:
|
|
1448
|
+
` + tried.join("\n ") + `
|
|
1449
|
+
Add a blank-document entry (e.g. "Actor:npc") as the last item so this can't fail.`
|
|
1450
|
+
);
|
|
1451
|
+
return null;
|
|
1452
|
+
}
|
|
1453
|
+
async function missingBasePackages(metas) {
|
|
1454
|
+
const pageSpecs = [];
|
|
1455
|
+
for (const meta of metas) {
|
|
1456
|
+
const base = meta?.foundry?.base;
|
|
1457
|
+
if (base === void 0 || base === null) continue;
|
|
1458
|
+
const specs = (Array.isArray(base) ? base : [base]).filter((s) => typeof s === "string");
|
|
1459
|
+
if (specs.length === 0) continue;
|
|
1460
|
+
if (specs.some((s) => !s.includes("."))) continue;
|
|
1461
|
+
if (specs.some((s) => !s.startsWith("Compendium."))) continue;
|
|
1462
|
+
if (specs.some((s) => s.split(".")[1] === "world")) continue;
|
|
1463
|
+
pageSpecs.push(specs);
|
|
1464
|
+
}
|
|
1465
|
+
if (pageSpecs.length === 0) return /* @__PURE__ */ new Map();
|
|
1466
|
+
const PROBES_PER_PACK = 3;
|
|
1467
|
+
const packOf = (spec) => spec.split(".").slice(1, 3).join(".");
|
|
1468
|
+
const probes = /* @__PURE__ */ new Map();
|
|
1469
|
+
for (const specs of pageSpecs) {
|
|
1470
|
+
for (const spec of specs) {
|
|
1471
|
+
const pack = packOf(spec);
|
|
1472
|
+
const chosen = probes.get(pack) ?? [];
|
|
1473
|
+
if (chosen.length < PROBES_PER_PACK && !chosen.includes(spec)) chosen.push(spec);
|
|
1474
|
+
probes.set(pack, chosen);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
const reachable = /* @__PURE__ */ new Map();
|
|
1478
|
+
for (const [pack, specs] of probes) {
|
|
1479
|
+
let ok = false;
|
|
1480
|
+
for (const spec of specs) if (await safeFromUuid(spec)) {
|
|
1481
|
+
ok = true;
|
|
1482
|
+
break;
|
|
1483
|
+
}
|
|
1484
|
+
reachable.set(pack, ok);
|
|
1485
|
+
}
|
|
1486
|
+
const missing = /* @__PURE__ */ new Map();
|
|
1487
|
+
for (const specs of pageSpecs) {
|
|
1488
|
+
if (specs.some((s) => reachable.get(packOf(s)))) continue;
|
|
1489
|
+
for (const pkg of new Set(specs.map((s) => s.split(".")[1]).filter(Boolean))) {
|
|
1490
|
+
missing.set(pkg, (missing.get(pkg) ?? 0) + 1);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return missing;
|
|
953
1494
|
}
|
|
954
|
-
function
|
|
955
|
-
if (typeof spec !== "string" || !spec) return null;
|
|
956
|
-
if (spec.includes(".")) return { kind: "uuid", uuid: spec };
|
|
957
|
-
const [typeRaw, subtype] = spec.split(":");
|
|
958
|
-
const docName = [...BLANK_DOC_TYPES].find((t) => t.toLowerCase() === typeRaw.toLowerCase());
|
|
959
|
-
if (!docName) return null;
|
|
960
|
-
return { kind: "blank", docName, subtype: subtype || void 0 };
|
|
961
|
-
}
|
|
962
|
-
async function deleteInstance(vault, vaultPath) {
|
|
1495
|
+
async function deleteInstance(target, vault, vaultPath) {
|
|
963
1496
|
const id = await instanceId(vault.id, vaultPath);
|
|
964
|
-
for (const
|
|
965
|
-
const
|
|
966
|
-
const doc = collection?.get(id);
|
|
1497
|
+
for (const docName of BLANK_DOC_TYPES) {
|
|
1498
|
+
const doc = await target.get(docName, id);
|
|
967
1499
|
if (!doc) continue;
|
|
968
|
-
if (doc.
|
|
1500
|
+
if (doc.flags?.[MODULE_ID]?.vaultId !== vault.id) continue;
|
|
969
1501
|
try {
|
|
970
|
-
await
|
|
1502
|
+
await target.remove(docName, id);
|
|
971
1503
|
} catch (err) {
|
|
972
|
-
console.warn(`Vaults | failed to delete ${
|
|
1504
|
+
console.warn(`Vaults | failed to delete ${docName} for ${vaultPath}:`, err);
|
|
973
1505
|
}
|
|
974
1506
|
}
|
|
975
1507
|
}
|
|
976
|
-
async function
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
try {
|
|
983
|
-
await doc.delete();
|
|
984
|
-
} catch (err) {
|
|
985
|
-
console.warn(`Vaults | failed to delete ${docName} ${doc.id}:`, err);
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
for (const docName of BLANK_DOC_TYPES) {
|
|
990
|
-
const fId = await instanceFolderId(vaultId, docName);
|
|
991
|
-
const folder = game.folders.get(fId);
|
|
992
|
-
if (!folder || folder.type !== docName) continue;
|
|
993
|
-
if (folder.contents.length > 0 || folder.children.length > 0) continue;
|
|
1508
|
+
async function ensureInstanceFolder(target, vault, docName, subPath = "") {
|
|
1509
|
+
let parentId = null;
|
|
1510
|
+
let key = `${vault.id}/__instance__/${docName}`;
|
|
1511
|
+
for (const segment of splitFolderPath(subPath)) {
|
|
1512
|
+
key += `/${segment}`;
|
|
1513
|
+
const fId = await folderId(vault.id, key);
|
|
994
1514
|
try {
|
|
995
|
-
await
|
|
1515
|
+
await target.putFolder(docName, { _id: fId, name: segment, folder: parentId });
|
|
996
1516
|
} catch (err) {
|
|
997
|
-
console.warn(`Vaults |
|
|
1517
|
+
console.warn(`Vaults | could not create ${docName} folder for ${vault.label}:`, err);
|
|
1518
|
+
return parentId;
|
|
998
1519
|
}
|
|
1520
|
+
parentId = fId;
|
|
999
1521
|
}
|
|
1522
|
+
return parentId;
|
|
1000
1523
|
}
|
|
1001
|
-
|
|
1002
|
-
return
|
|
1524
|
+
function splitFolderPath(subPath) {
|
|
1525
|
+
return typeof subPath === "string" ? subPath.split("/").map((s) => s.trim()).filter(Boolean) : [];
|
|
1003
1526
|
}
|
|
1004
|
-
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
if (existing) {
|
|
1009
|
-
if (existing.name !== name) {
|
|
1010
|
-
try {
|
|
1011
|
-
await existing.update({ name });
|
|
1012
|
-
} catch (err) {
|
|
1013
|
-
console.warn(`Vaults | could not rename ${docName} folder for ${vault.label}:`, err);
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
return fId;
|
|
1017
|
-
}
|
|
1018
|
-
try {
|
|
1019
|
-
await Folder.create({ _id: fId, name, type: docName, folder: null }, { keepId: true });
|
|
1020
|
-
return fId;
|
|
1021
|
-
} catch (err) {
|
|
1022
|
-
console.warn(`Vaults | could not create ${docName} folder for ${vault.label}:`, err);
|
|
1023
|
-
return null;
|
|
1024
|
-
}
|
|
1527
|
+
function instanceSubPath(vaultPath, meta) {
|
|
1528
|
+
const override = meta?.foundry?.folder;
|
|
1529
|
+
if (typeof override === "string" && override.trim()) return override;
|
|
1530
|
+
return folderOfPath(vaultPath);
|
|
1025
1531
|
}
|
|
1026
|
-
async function buildOverlay(vault, vaultPath, meta, docName, derived = {}) {
|
|
1532
|
+
async function buildOverlay(target, vault, vaultPath, meta, docName, derived = {}) {
|
|
1027
1533
|
const overlay = {
|
|
1028
1534
|
// Prefer the page's frontmatter `title:` over the filename — the wiki
|
|
1029
1535
|
// already treats title as the page's display name, and a doc named
|
|
1030
1536
|
// "Potion of Healing (Mossfoot Brew)" reads better in the Foundry
|
|
1031
1537
|
// sidebar than "Healing Potion".
|
|
1032
1538
|
name: meta.title || baseName(vaultPath),
|
|
1033
|
-
folder: await ensureInstanceFolder(vault, docName),
|
|
1539
|
+
folder: await ensureInstanceFolder(target, vault, docName, instanceSubPath(vaultPath, meta)),
|
|
1034
1540
|
flags: { [MODULE_ID]: { vaultId: vault.id, path: vaultPath } }
|
|
1035
1541
|
};
|
|
1036
1542
|
if (meta.image) {
|
|
@@ -1041,42 +1547,45 @@ async function buildOverlay(vault, vaultPath, meta, docName, derived = {}) {
|
|
|
1041
1547
|
}
|
|
1042
1548
|
}
|
|
1043
1549
|
const fm = meta?.foundry;
|
|
1044
|
-
const descPath =
|
|
1550
|
+
const descPath = descriptionPathFor(docName, game.system.id);
|
|
1045
1551
|
const embedAuto = fm?.embed !== false && fm?.journal !== false;
|
|
1046
1552
|
if (descPath && embedAuto) {
|
|
1047
1553
|
const eId = await entryId(vault.id, vaultPath);
|
|
1048
1554
|
const pId = await pageId(vault.id, vaultPath);
|
|
1049
|
-
setPath(overlay, descPath, `<p>@Embed[
|
|
1555
|
+
setPath(overlay, descPath, `<p>@Embed[${journalPageUuid(vault, eId, pId)} inline]</p>`);
|
|
1050
1556
|
}
|
|
1051
1557
|
if (fm?.data && typeof fm.data === "object") {
|
|
1052
1558
|
const cloned = rewriteVaultPaths(structuredClone(fm.data), vault.id);
|
|
1559
|
+
await resolveMoulinetteRefs(cloned, (msg) => console.warn(`Vaults | moulinette: ${vaultPath}: ${msg}`));
|
|
1560
|
+
await resolveItemUuids(cloned, vaultPath);
|
|
1053
1561
|
await ensureEmbeddedIds(cloned, vault.id, vaultPath);
|
|
1054
1562
|
deepMerge(overlay, cloned);
|
|
1055
1563
|
}
|
|
1056
|
-
if (docName === "Scene") {
|
|
1057
|
-
const note = await buildJournalNote(vault, vaultPath, meta);
|
|
1058
|
-
if (note) overlay.notes = [...overlay.notes ?? [], note];
|
|
1059
|
-
}
|
|
1060
1564
|
return overlay;
|
|
1061
1565
|
}
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
const
|
|
1065
|
-
const
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1566
|
+
function notePosition(geom) {
|
|
1567
|
+
const width = Number(geom.width) || 4e3;
|
|
1568
|
+
const height = Number(geom.height) || 3e3;
|
|
1569
|
+
const padding = Number(geom.padding ?? 0.25);
|
|
1570
|
+
const gridSize = Number(geom.grid?.size) || 100;
|
|
1571
|
+
return {
|
|
1572
|
+
x: gridSize * (Math.ceil(width / gridSize * padding) - 0.5),
|
|
1573
|
+
y: gridSize * (Math.ceil(height / gridSize * padding) + 0.5)
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
async function attachJournalNote(sceneData, geom, vault, vaultPath, meta) {
|
|
1577
|
+
const gridSize = Number(geom.grid?.size) || 100;
|
|
1578
|
+
const { x, y } = notePosition(geom);
|
|
1070
1579
|
const eId = await entryId(vault.id, vaultPath);
|
|
1071
1580
|
const idOverride = meta?.foundry?.id;
|
|
1072
1581
|
const pId = typeof idOverride === "string" && idOverride ? idOverride : await pageId(vault.id, vaultPath);
|
|
1073
|
-
|
|
1582
|
+
const note2 = {
|
|
1074
1583
|
_id: await subdocId(vault.id, vaultPath, "/notes/__journalLink__"),
|
|
1075
1584
|
entryId: eId,
|
|
1076
1585
|
pageId: pId,
|
|
1077
|
-
x
|
|
1078
|
-
y
|
|
1079
|
-
iconSize,
|
|
1586
|
+
x,
|
|
1587
|
+
y,
|
|
1588
|
+
iconSize: gridSize,
|
|
1080
1589
|
texture: {
|
|
1081
1590
|
src: "icons/svg/book.svg",
|
|
1082
1591
|
anchorX: 0.5,
|
|
@@ -1086,8 +1595,48 @@ async function buildJournalNote(vault, vaultPath, meta) {
|
|
|
1086
1595
|
},
|
|
1087
1596
|
text: ""
|
|
1088
1597
|
};
|
|
1598
|
+
sceneData.notes = [...sceneData.notes ?? [], note2];
|
|
1089
1599
|
}
|
|
1090
1600
|
var VALID_SUBDOC_ID = /^[A-Za-z0-9]{16}$/;
|
|
1601
|
+
function mergeItemsById(baseItems, pageItems) {
|
|
1602
|
+
if (!Array.isArray(pageItems)) return baseItems;
|
|
1603
|
+
if (!baseItems.length) return pageItems;
|
|
1604
|
+
const merged = baseItems.map((item) => structuredClone(item));
|
|
1605
|
+
const positionOf = new Map(merged.map((item, i) => [item?._id, i]));
|
|
1606
|
+
for (const item of pageItems) {
|
|
1607
|
+
const at = item && item._id !== void 0 ? positionOf.get(item._id) : void 0;
|
|
1608
|
+
if (at === void 0) merged.push(item);
|
|
1609
|
+
else deepMerge(merged[at], item);
|
|
1610
|
+
}
|
|
1611
|
+
return merged;
|
|
1612
|
+
}
|
|
1613
|
+
async function resolveItemUuids(data, vaultPath) {
|
|
1614
|
+
if (!Array.isArray(data?.items)) return;
|
|
1615
|
+
const resolved = [];
|
|
1616
|
+
for (const entry of data.items) {
|
|
1617
|
+
if (!entry || typeof entry !== "object" || typeof entry.uuid !== "string") {
|
|
1618
|
+
resolved.push(entry);
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
const { uuid, ...overrides } = entry;
|
|
1622
|
+
const source = await safeFromUuid(uuid);
|
|
1623
|
+
if (!source) {
|
|
1624
|
+
console.warn(`Vaults | foundry item uuid: ${vaultPath} \u2192 ${uuid} did not resolve; skipping that item.`);
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
if (source.documentName !== "Item") {
|
|
1628
|
+
console.warn(`Vaults | foundry item uuid: ${vaultPath} \u2192 ${uuid} is a ${source.documentName}, not an Item; skipping that item.`);
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
const itemData = source.toObject();
|
|
1632
|
+
delete itemData._id;
|
|
1633
|
+
if (uuid.startsWith("Compendium.")) {
|
|
1634
|
+
itemData._stats = { ...itemData._stats, compendiumSource: uuid };
|
|
1635
|
+
}
|
|
1636
|
+
resolved.push(deepMerge(itemData, overrides));
|
|
1637
|
+
}
|
|
1638
|
+
data.items = resolved;
|
|
1639
|
+
}
|
|
1091
1640
|
async function ensureEmbeddedIds(value, vaultId, pagePath, ptr = "") {
|
|
1092
1641
|
if (Array.isArray(value)) {
|
|
1093
1642
|
for (let i = 0; i < value.length; i++) {
|
|
@@ -1161,6 +1710,37 @@ function deepMerge(target, source) {
|
|
|
1161
1710
|
}
|
|
1162
1711
|
return target;
|
|
1163
1712
|
}
|
|
1713
|
+
async function findMissingDocuments(target, vault, entries) {
|
|
1714
|
+
const out = [];
|
|
1715
|
+
const journalPages = /* @__PURE__ */ new Set();
|
|
1716
|
+
for (const entry of await target.contents("JournalEntry")) {
|
|
1717
|
+
for (const page of entry.pages ?? []) journalPages.add(`${entry._id}.${page._id}`);
|
|
1718
|
+
}
|
|
1719
|
+
const idsByType = /* @__PURE__ */ new Map();
|
|
1720
|
+
const packIds = async (docName) => {
|
|
1721
|
+
if (!idsByType.has(docName)) idsByType.set(docName, await target.ids(docName));
|
|
1722
|
+
return idsByType.get(docName);
|
|
1723
|
+
};
|
|
1724
|
+
for (const { logicalPath, meta } of entries) {
|
|
1725
|
+
const fm = meta?.foundry;
|
|
1726
|
+
const missing = [];
|
|
1727
|
+
if (fm?.journal !== false) {
|
|
1728
|
+
const eId = await entryId(vault.id, logicalPath);
|
|
1729
|
+
const pId = typeof fm?.id === "string" && fm.id ? fm.id : await pageId(vault.id, logicalPath);
|
|
1730
|
+
if (!journalPages.has(`${eId}.${pId}`)) missing.push("journal");
|
|
1731
|
+
}
|
|
1732
|
+
if (fm?.base !== void 0 && fm?.base !== null) {
|
|
1733
|
+
const specs = Array.isArray(fm.base) ? fm.base : [fm.base];
|
|
1734
|
+
const docName = docNameOf(parseFoundryBase(specs[0]));
|
|
1735
|
+
if (docName) {
|
|
1736
|
+
const id = typeof fm.id === "string" && fm.id ? fm.id : await instanceId(vault.id, logicalPath);
|
|
1737
|
+
if (!(await packIds(docName)).has(id)) missing.push("document");
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
if (missing.length > 0) out.push({ path: logicalPath, missing: missing.join(" + ") });
|
|
1741
|
+
}
|
|
1742
|
+
return out;
|
|
1743
|
+
}
|
|
1164
1744
|
|
|
1165
1745
|
// ../foundry/scripts/auth.mjs
|
|
1166
1746
|
function tokenInfo(token) {
|
|
@@ -1174,8 +1754,187 @@ function tokenInfo(token) {
|
|
|
1174
1754
|
};
|
|
1175
1755
|
}
|
|
1176
1756
|
|
|
1757
|
+
// ../foundry/scripts/target.mjs
|
|
1758
|
+
async function openTarget(vault) {
|
|
1759
|
+
return isAdventure(vault) ? openAdventure(vault) : openCompendium(vault);
|
|
1760
|
+
}
|
|
1761
|
+
async function openCompendium(vault) {
|
|
1762
|
+
return {
|
|
1763
|
+
vault,
|
|
1764
|
+
adventure: false,
|
|
1765
|
+
async get(docName, id) {
|
|
1766
|
+
const pack = await ensurePack(vault, docName);
|
|
1767
|
+
const doc = await pack.getDocument(id);
|
|
1768
|
+
return doc ? doc.toObject() : null;
|
|
1769
|
+
},
|
|
1770
|
+
async put(docName, data) {
|
|
1771
|
+
const pack = await ensurePack(vault, docName);
|
|
1772
|
+
const existing = await pack.getDocument(data._id);
|
|
1773
|
+
if (existing) {
|
|
1774
|
+
await existing.update(data);
|
|
1775
|
+
return "modified";
|
|
1776
|
+
}
|
|
1777
|
+
const cls = CONFIG[docName].documentClass;
|
|
1778
|
+
await cls.create(data, { pack: pack.collection, keepId: true, keepEmbeddedIds: true });
|
|
1779
|
+
if (!await pack.getDocument(data._id)) {
|
|
1780
|
+
throw new Error(`${docName} ${data._id} was rejected on create`);
|
|
1781
|
+
}
|
|
1782
|
+
return "added";
|
|
1783
|
+
},
|
|
1784
|
+
async remove(docName, id) {
|
|
1785
|
+
const pack = getPack(vault, docName);
|
|
1786
|
+
const doc = pack && await pack.getDocument(id);
|
|
1787
|
+
if (doc) await doc.delete();
|
|
1788
|
+
},
|
|
1789
|
+
async putFolder(docName, folder) {
|
|
1790
|
+
const pack = await ensurePack(vault, docName);
|
|
1791
|
+
const existing = pack.folders.get(folder._id);
|
|
1792
|
+
if (existing) {
|
|
1793
|
+
if (existing.name !== folder.name || (existing.folder?.id ?? null) !== (folder.folder ?? null)) {
|
|
1794
|
+
await existing.update({ name: folder.name, folder: folder.folder ?? null });
|
|
1795
|
+
}
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
await Folder.create(
|
|
1799
|
+
{ ...folder, type: docName },
|
|
1800
|
+
{ pack: pack.collection, keepId: true }
|
|
1801
|
+
);
|
|
1802
|
+
if (!pack.folders.get(folder._id)) {
|
|
1803
|
+
throw new Error(`Folder ${folder._id} ("${folder.name}") was rejected on create`);
|
|
1804
|
+
}
|
|
1805
|
+
},
|
|
1806
|
+
/** Every document of a type this vault owns. One request, not one per id. */
|
|
1807
|
+
async contents(docName) {
|
|
1808
|
+
const pack = getPack(vault, docName);
|
|
1809
|
+
return pack ? (await pack.getDocuments()).map((d) => d.toObject()) : [];
|
|
1810
|
+
},
|
|
1811
|
+
async ids(docName) {
|
|
1812
|
+
const pack = getPack(vault, docName);
|
|
1813
|
+
return new Set(pack ? (await pack.getIndex()).map((e) => e._id) : []);
|
|
1814
|
+
},
|
|
1815
|
+
// Each write already landed.
|
|
1816
|
+
async commit() {
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
var FIELD = {
|
|
1821
|
+
Actor: "actors",
|
|
1822
|
+
Item: "items",
|
|
1823
|
+
Scene: "scenes",
|
|
1824
|
+
JournalEntry: "journal",
|
|
1825
|
+
RollTable: "tables",
|
|
1826
|
+
Macro: "macros",
|
|
1827
|
+
Cards: "cards",
|
|
1828
|
+
Playlist: "playlists"
|
|
1829
|
+
};
|
|
1830
|
+
async function openAdventure(vault) {
|
|
1831
|
+
const pack = await ensurePack(vault, "Adventure");
|
|
1832
|
+
const id = await adventureId(vault.id);
|
|
1833
|
+
const existing = await pack.getDocument(id);
|
|
1834
|
+
const source = existing ? existing.toObject() : null;
|
|
1835
|
+
const docs = /* @__PURE__ */ new Map();
|
|
1836
|
+
for (const [docName, field] of Object.entries(FIELD)) {
|
|
1837
|
+
docs.set(docName, new Map((source?.[field] ?? []).map((d) => [d._id, d])));
|
|
1838
|
+
}
|
|
1839
|
+
const folders = new Map((source?.folders ?? []).map((f) => [f._id, f]));
|
|
1840
|
+
let dirty = false;
|
|
1841
|
+
return {
|
|
1842
|
+
vault,
|
|
1843
|
+
adventure: true,
|
|
1844
|
+
async get(docName, docId) {
|
|
1845
|
+
return docs.get(docName)?.get(docId) ?? null;
|
|
1846
|
+
},
|
|
1847
|
+
async put(docName, data) {
|
|
1848
|
+
const byId = docs.get(docName);
|
|
1849
|
+
if (!byId) throw new Error(`An Adventure cannot hold a ${docName}`);
|
|
1850
|
+
const had = byId.has(data._id);
|
|
1851
|
+
byId.set(data._id, data);
|
|
1852
|
+
dirty = true;
|
|
1853
|
+
return had ? "modified" : "added";
|
|
1854
|
+
},
|
|
1855
|
+
async remove(docName, docId) {
|
|
1856
|
+
if (docs.get(docName)?.delete(docId)) dirty = true;
|
|
1857
|
+
},
|
|
1858
|
+
async putFolder(docName, folder) {
|
|
1859
|
+
const want = { ...folder, type: docName, folder: folder.folder ?? null };
|
|
1860
|
+
const have = folders.get(folder._id);
|
|
1861
|
+
if (have && have.name === want.name && have.folder === want.folder) return;
|
|
1862
|
+
folders.set(folder._id, want);
|
|
1863
|
+
dirty = true;
|
|
1864
|
+
},
|
|
1865
|
+
async contents(docName) {
|
|
1866
|
+
return [...docs.get(docName)?.values() ?? []];
|
|
1867
|
+
},
|
|
1868
|
+
async ids(docName) {
|
|
1869
|
+
return new Set(docs.get(docName)?.keys() ?? []);
|
|
1870
|
+
},
|
|
1871
|
+
async commit() {
|
|
1872
|
+
if (!dirty && existing) return;
|
|
1873
|
+
const data = {
|
|
1874
|
+
_id: id,
|
|
1875
|
+
name: vault.label || "Vault",
|
|
1876
|
+
// The sheet shows these before import, and they are the only
|
|
1877
|
+
// description a GM gets of what they are about to bring in.
|
|
1878
|
+
caption: `Synced from ${vault.url}`,
|
|
1879
|
+
description: `<p>Every page, document and folder from the <strong>${vault.label || "vault"}</strong> vault. Importing creates them in this world; importing again updates what is already here rather than duplicating it.</p>`,
|
|
1880
|
+
folders: [...folders.values()],
|
|
1881
|
+
flags: { [MODULE_ID]: { vaultId: vault.id } }
|
|
1882
|
+
};
|
|
1883
|
+
for (const [docName, field] of Object.entries(FIELD)) {
|
|
1884
|
+
data[field] = [...docs.get(docName).values()];
|
|
1885
|
+
}
|
|
1886
|
+
if (existing) await existing.update(data);
|
|
1887
|
+
else {
|
|
1888
|
+
await Adventure.create(data, { pack: pack.collection, keepId: true, keepEmbeddedIds: true });
|
|
1889
|
+
if (!await pack.getDocument(id)) {
|
|
1890
|
+
throw new Error(`Adventure ${id} was rejected on create`);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1177
1897
|
// ../foundry/scripts/sync.mjs
|
|
1178
|
-
async function
|
|
1898
|
+
async function orderByBaseDeps(vault, paths, bodyMetaIndex) {
|
|
1899
|
+
const ownerOf = /* @__PURE__ */ new Map();
|
|
1900
|
+
for (const p of paths) {
|
|
1901
|
+
const fm = bodyMetaIndex.get(p)?.foundry;
|
|
1902
|
+
if (!fm?.base) continue;
|
|
1903
|
+
const id = typeof fm.id === "string" && fm.id ? fm.id : await instanceId(vault.id, p.replace(/\.body\.html$/i, ".md"));
|
|
1904
|
+
ownerOf.set(id, p);
|
|
1905
|
+
}
|
|
1906
|
+
if (!ownerOf.size) return paths;
|
|
1907
|
+
const dependsOn = /* @__PURE__ */ new Map();
|
|
1908
|
+
for (const p of paths) {
|
|
1909
|
+
const base = bodyMetaIndex.get(p)?.foundry?.base;
|
|
1910
|
+
const match = typeof base === "string" && base.match(/^(?:Actor|Item)\.([A-Za-z0-9]{16})$/);
|
|
1911
|
+
const from = match && ownerOf.get(match[1]);
|
|
1912
|
+
if (from && from !== p) dependsOn.set(p, from);
|
|
1913
|
+
}
|
|
1914
|
+
if (!dependsOn.size) return paths;
|
|
1915
|
+
const ordered = [];
|
|
1916
|
+
const placed = /* @__PURE__ */ new Set();
|
|
1917
|
+
const visit = (p, chain) => {
|
|
1918
|
+
if (placed.has(p) || chain.has(p)) return;
|
|
1919
|
+
chain.add(p);
|
|
1920
|
+
const from = dependsOn.get(p);
|
|
1921
|
+
if (from) visit(from, chain);
|
|
1922
|
+
if (!placed.has(p)) {
|
|
1923
|
+
placed.add(p);
|
|
1924
|
+
ordered.push(p);
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
for (const p of paths) visit(p, /* @__PURE__ */ new Set());
|
|
1928
|
+
return ordered;
|
|
1929
|
+
}
|
|
1930
|
+
async function sync(host, vault, opts = {}) {
|
|
1931
|
+
try {
|
|
1932
|
+
return await runSync(host, vault, opts);
|
|
1933
|
+
} finally {
|
|
1934
|
+
end();
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
async function runSync(host, vault, { forceFull = false } = {}) {
|
|
1179
1938
|
if (!vault?.url) {
|
|
1180
1939
|
host.notify("error", host.localize("VAULTS.Sync.NoUrl"));
|
|
1181
1940
|
return { ok: false, refreshHandlerAssets: false };
|
|
@@ -1191,6 +1950,7 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1191
1950
|
}
|
|
1192
1951
|
const start = Date.now();
|
|
1193
1952
|
host.notify("info", host.localize("VAULTS.Sync.StartingNamed", { name: vault.label }));
|
|
1953
|
+
begin(vault.label);
|
|
1194
1954
|
let manifest;
|
|
1195
1955
|
try {
|
|
1196
1956
|
manifest = await fetchManifest(vault);
|
|
@@ -1209,6 +1969,8 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1209
1969
|
const knownRoles = Array.isArray(manifest.auth?.roles) ? manifest.auth.roles : [];
|
|
1210
1970
|
const patch = {};
|
|
1211
1971
|
if (vault.public !== isPublic) patch.public = isPublic;
|
|
1972
|
+
const foundryPackage = manifest.foundry_package || "compendium";
|
|
1973
|
+
if (vault.foundryPackage !== foundryPackage) patch.foundryPackage = foundryPackage;
|
|
1212
1974
|
if (!arraysEqual(vault.knownRoles, knownRoles)) patch.knownRoles = knownRoles;
|
|
1213
1975
|
const remoteAssets = manifest.assets?.foundry || {};
|
|
1214
1976
|
const newAssetPaths = {
|
|
@@ -1218,7 +1980,8 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1218
1980
|
if (JSON.stringify(vault.handlerAssetPaths || {}) !== JSON.stringify(newAssetPaths)) {
|
|
1219
1981
|
patch.handlerAssetPaths = newAssetPaths;
|
|
1220
1982
|
}
|
|
1221
|
-
|
|
1983
|
+
const playerRole = typeof manifest.foundry_player_role === "string" ? manifest.foundry_player_role : "";
|
|
1984
|
+
if (vault.playerRole !== playerRole) patch.playerRole = playerRole;
|
|
1222
1985
|
if (Object.keys(patch).length > 0) {
|
|
1223
1986
|
await host.updateVaultEntry(vault.id, patch);
|
|
1224
1987
|
Object.assign(vault, patch);
|
|
@@ -1228,7 +1991,15 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1228
1991
|
);
|
|
1229
1992
|
const remote = new Map(files.map((f) => [f.path, f.hash]));
|
|
1230
1993
|
const lastSync = host.getVaultState(vault.id);
|
|
1231
|
-
const
|
|
1994
|
+
const remoteIdScheme = manifest.id_scheme || "v1";
|
|
1995
|
+
const schemeChanged = !!lastSync.lastIdScheme && lastSync.lastIdScheme !== remoteIdScheme;
|
|
1996
|
+
if (schemeChanged) {
|
|
1997
|
+
console.warn(
|
|
1998
|
+
`Vaults | ${vault.label}: document id scheme changed (${lastSync.lastIdScheme} \u2192 ${remoteIdScheme}); forcing a full re-sync so existing documents are re-derived rather than duplicated.`
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
const fullPass = forceFull || schemeChanged;
|
|
2002
|
+
const local = fullPass ? /* @__PURE__ */ new Map() : new Map(Object.entries(lastSync.lastManifest || {}));
|
|
1232
2003
|
const bodyPaths = files.filter((f) => f.path.endsWith(".body.html")).map((f) => f.path);
|
|
1233
2004
|
const pathIndex = buildPathIndex(files);
|
|
1234
2005
|
const allMdPaths = bodyPaths.map((p) => p.replace(/\.body\.html$/i, ".md"));
|
|
@@ -1237,10 +2008,19 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1237
2008
|
for (const f of files) {
|
|
1238
2009
|
if (f.meta && f.path.endsWith(".body.html")) bodyMetaIndex.set(f.path, f.meta);
|
|
1239
2010
|
}
|
|
2011
|
+
const missingPackages = await missingBasePackages(bodyMetaIndex.values());
|
|
2012
|
+
if (missingPackages.size > 0) {
|
|
2013
|
+
const summary = [...missingPackages].map(([pkg, n]) => `${pkg} (${n})`).join(", ");
|
|
2014
|
+
host.notify("warn", host.localize("VAULTS.Sync.MissingPackages", { packages: summary }));
|
|
2015
|
+
console.warn(
|
|
2016
|
+
`Vaults | ${vault.label}: foundry.base points into package(s) this world can't read: ${summary}. Those pages will sync as journals but instantiate no document.`
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
1240
2019
|
const prevImages = forceFull ? /* @__PURE__ */ new Map() : new Map(Object.entries(lastSync.lastImageManifest || {}));
|
|
1241
2020
|
const lastMediaRefs = lastSync.lastMediaRefs || {};
|
|
1242
2021
|
const mediaStale = (bodyPath) => (lastMediaRefs[bodyPath] || []).some((m) => remote.get(m) !== prevImages.get(m));
|
|
1243
|
-
const
|
|
2022
|
+
const changed = bodyPaths.filter((p) => remote.get(p) !== local.get(p) || mediaStale(p));
|
|
2023
|
+
const toUpsert = await orderByBaseDeps(vault, changed, bodyMetaIndex);
|
|
1244
2024
|
const toDelete = [...local.keys()].filter((p) => p.endsWith(".body.html") && !remote.has(p));
|
|
1245
2025
|
if (forceFull) await host.setVaultState(vault.id, { lastImageManifest: {} });
|
|
1246
2026
|
let imageStats = { downloaded: 0, removed: 0, errors: 0 };
|
|
@@ -1249,8 +2029,25 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1249
2029
|
} catch (err) {
|
|
1250
2030
|
console.warn(`Vaults | image sync failed for ${vault.label}:`, err);
|
|
1251
2031
|
}
|
|
2032
|
+
const untouched = new Set(bodyPaths);
|
|
2033
|
+
for (const p of toUpsert) untouched.delete(p);
|
|
2034
|
+
for (const p of toDelete) untouched.delete(p);
|
|
2035
|
+
const target = await openTarget(vault);
|
|
2036
|
+
const missingDocs = await findMissingDocuments(target, vault, [...untouched].map((bodyPath) => ({
|
|
2037
|
+
logicalPath: bodyPath.replace(/\.body\.html$/i, ".md"),
|
|
2038
|
+
meta: bodyMetaIndex.get(bodyPath)
|
|
2039
|
+
})));
|
|
2040
|
+
const reportMissingDocs = () => {
|
|
2041
|
+
if (missingDocs.length === 0) return;
|
|
2042
|
+
host.notify("warn", host.localize("VAULTS.Sync.MissingDocuments", { count: missingDocs.length }));
|
|
2043
|
+
console.warn(
|
|
2044
|
+
`Vaults | ${vault.label}: ${missingDocs.length} page(s) have no document in this world. An incremental sync will not notice them again \u2014 use Force Sync to restore:`,
|
|
2045
|
+
missingDocs
|
|
2046
|
+
);
|
|
2047
|
+
};
|
|
1252
2048
|
if (toUpsert.length === 0 && toDelete.length === 0 && imageStats.downloaded === 0 && imageStats.removed === 0) {
|
|
1253
2049
|
host.notify("info", host.localize("VAULTS.Sync.NothingToDo"));
|
|
2050
|
+
reportMissingDocs();
|
|
1254
2051
|
return {
|
|
1255
2052
|
ok: true,
|
|
1256
2053
|
refreshHandlerAssets: false,
|
|
@@ -1258,12 +2055,18 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1258
2055
|
modified: 0,
|
|
1259
2056
|
removed: 0,
|
|
1260
2057
|
imageStats,
|
|
1261
|
-
instances: 0
|
|
2058
|
+
instances: 0,
|
|
2059
|
+
skipped: [],
|
|
2060
|
+
failed: [],
|
|
2061
|
+
// The drift list is computed above this return, so report it here too —
|
|
2062
|
+
// an up-to-date sync with missing documents is exactly the case a caller
|
|
2063
|
+
// most wants to see.
|
|
2064
|
+
missingDocuments: missingDocs
|
|
1262
2065
|
};
|
|
1263
2066
|
}
|
|
1264
2067
|
host.notify(
|
|
1265
2068
|
"info",
|
|
1266
|
-
|
|
2069
|
+
fullPass ? host.localize("VAULTS.Sync.Initial", { count: toUpsert.length }) : host.localize("VAULTS.Sync.Incremental", {
|
|
1267
2070
|
add: toUpsert.length,
|
|
1268
2071
|
mod: 0,
|
|
1269
2072
|
del: toDelete.length
|
|
@@ -1271,69 +2074,134 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1271
2074
|
);
|
|
1272
2075
|
let bodies;
|
|
1273
2076
|
try {
|
|
1274
|
-
|
|
2077
|
+
const byRole = /* @__PURE__ */ new Map();
|
|
2078
|
+
for (const bodyPath of toUpsert) {
|
|
2079
|
+
const pageRole = bodyMetaIndex.get(bodyPath)?.role;
|
|
2080
|
+
const key = pageRole || "";
|
|
2081
|
+
if (!byRole.has(key)) byRole.set(key, []);
|
|
2082
|
+
byRole.get(key).push(bodyPath);
|
|
2083
|
+
}
|
|
2084
|
+
bodies = /* @__PURE__ */ new Map();
|
|
2085
|
+
for (const [pageRole, group] of byRole) {
|
|
2086
|
+
const fetched = await fetchSourceBatch(vault, group, pageRole || void 0);
|
|
2087
|
+
for (const [k, v] of fetched) bodies.set(k, v);
|
|
2088
|
+
}
|
|
1275
2089
|
} catch (err) {
|
|
1276
2090
|
console.error(`Vaults | batch fetch failed for ${vault.label}:`, err);
|
|
1277
2091
|
host.notify("error", host.localize("VAULTS.Sync.Error", { message: err.message }));
|
|
1278
2092
|
return { ok: false, refreshHandlerAssets: false };
|
|
1279
2093
|
}
|
|
1280
2094
|
let added = 0, modified = 0, instances = 0;
|
|
2095
|
+
const skipped = [];
|
|
2096
|
+
const versionSkew = [];
|
|
2097
|
+
const failedPages = /* @__PURE__ */ new Map();
|
|
1281
2098
|
const mediaRefs = {};
|
|
1282
2099
|
for (const p of bodyPaths) if (lastMediaRefs[p]) mediaRefs[p] = lastMediaRefs[p];
|
|
2100
|
+
phase("Pages", toUpsert.length);
|
|
1283
2101
|
for (const bodyPath of toUpsert) {
|
|
2102
|
+
step(bodyPath.replace(/\.body\.html$/i, "").split("/").pop());
|
|
1284
2103
|
const html = bodies.get(bodyPath);
|
|
1285
2104
|
if (html == null) {
|
|
1286
2105
|
console.warn(`Vaults | server returned no content for ${bodyPath}`);
|
|
2106
|
+
failedPages.set(bodyPath, "no content returned");
|
|
1287
2107
|
continue;
|
|
1288
2108
|
}
|
|
1289
2109
|
const logicalPath = bodyPath.replace(/\.body\.html$/i, ".md");
|
|
1290
2110
|
const pageMeta = bodyMetaIndex.get(bodyPath);
|
|
1291
2111
|
try {
|
|
1292
2112
|
if (pageMeta?.foundry?.journal === false) {
|
|
1293
|
-
await deleteFile(vault, logicalPath);
|
|
2113
|
+
await deleteFile(target, vault, logicalPath);
|
|
1294
2114
|
} else {
|
|
1295
2115
|
const refs = /* @__PURE__ */ new Set();
|
|
1296
|
-
const result = await upsertFile(vault, logicalPath, html, pathIndex, pageMeta, folderInfo, refs);
|
|
2116
|
+
const result = await upsertFile(target, vault, logicalPath, html, pathIndex, pageMeta, folderInfo, refs);
|
|
1297
2117
|
mediaRefs[bodyPath] = [...refs];
|
|
1298
2118
|
if (result === "added") added++;
|
|
1299
2119
|
else modified++;
|
|
1300
2120
|
}
|
|
1301
2121
|
if (pageMeta?.foundry?.base) {
|
|
1302
2122
|
try {
|
|
1303
|
-
await applyInstance(vault, logicalPath, pageMeta);
|
|
1304
|
-
|
|
2123
|
+
const outcome = await applyInstance(target, vault, logicalPath, pageMeta, { forceFull: fullPass });
|
|
2124
|
+
if (outcome?.ok) {
|
|
2125
|
+
instances++;
|
|
2126
|
+
if (outcome.skew) versionSkew.push({ path: logicalPath, ...outcome.skew });
|
|
2127
|
+
} else if (outcome) skipped.push({ path: logicalPath, reason: outcome.reason });
|
|
1305
2128
|
} catch (err) {
|
|
1306
2129
|
console.warn(`Vaults | foundry instantiation failed for ${logicalPath}:`, err);
|
|
2130
|
+
skipped.push({ path: logicalPath, reason: "threw" });
|
|
1307
2131
|
}
|
|
1308
2132
|
}
|
|
1309
2133
|
} catch (err) {
|
|
1310
2134
|
console.warn(`Vaults | upsert failed for ${logicalPath}:`, err);
|
|
2135
|
+
failedPages.set(bodyPath, err?.message || "upsert threw");
|
|
1311
2136
|
}
|
|
1312
2137
|
}
|
|
1313
2138
|
let removed = 0;
|
|
2139
|
+
const failedDeletes = /* @__PURE__ */ new Set();
|
|
2140
|
+
if (toDelete.length > 0) phase("Removing", toDelete.length);
|
|
1314
2141
|
for (const bodyPath of toDelete) {
|
|
1315
2142
|
const logicalPath = bodyPath.replace(/\.body\.html$/i, ".md");
|
|
2143
|
+
step(logicalPath.split("/").pop());
|
|
1316
2144
|
try {
|
|
1317
|
-
await deleteFile(vault, logicalPath);
|
|
2145
|
+
await deleteFile(target, vault, logicalPath);
|
|
1318
2146
|
removed++;
|
|
1319
2147
|
} catch (err) {
|
|
1320
2148
|
console.warn(`Vaults | delete failed for ${logicalPath}:`, err);
|
|
2149
|
+
failedDeletes.add(bodyPath);
|
|
1321
2150
|
}
|
|
1322
2151
|
try {
|
|
1323
|
-
await deleteInstance(vault, logicalPath);
|
|
2152
|
+
await deleteInstance(target, vault, logicalPath);
|
|
1324
2153
|
} catch (err) {
|
|
1325
2154
|
console.warn(`Vaults | delete instance failed for ${logicalPath}:`, err);
|
|
1326
2155
|
}
|
|
1327
2156
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
2157
|
+
const persisted = new Map(remote);
|
|
2158
|
+
for (const bodyPath of failedPages.keys()) {
|
|
2159
|
+
const previous = local.get(bodyPath);
|
|
2160
|
+
if (previous === void 0) persisted.delete(bodyPath);
|
|
2161
|
+
else persisted.set(bodyPath, previous);
|
|
2162
|
+
}
|
|
2163
|
+
for (const bodyPath of failedDeletes) {
|
|
2164
|
+
const previous = local.get(bodyPath);
|
|
2165
|
+
if (previous !== void 0) persisted.set(bodyPath, previous);
|
|
2166
|
+
}
|
|
2167
|
+
await host.setVaultState(vault.id, {
|
|
2168
|
+
lastManifest: Object.fromEntries(persisted),
|
|
2169
|
+
lastMediaRefs: mediaRefs,
|
|
2170
|
+
lastIdScheme: remoteIdScheme
|
|
2171
|
+
});
|
|
2172
|
+
await reconcileEntries(target, vault, folderInfo, bodyMetaIndex);
|
|
2173
|
+
await target.commit();
|
|
2174
|
+
await pruneStalePacks(vault);
|
|
1331
2175
|
const seconds = ((Date.now() - start) / 1e3).toFixed(1);
|
|
1332
2176
|
host.notify("info", host.localize("VAULTS.Sync.Done", { added, modified, removed, seconds }));
|
|
2177
|
+
reportMissingDocs();
|
|
1333
2178
|
if (imageStats.downloaded > 0 || imageStats.removed > 0) {
|
|
1334
2179
|
console.info(`Vaults | ${vault.label} images: ${imageStats.downloaded} downloaded, ${imageStats.removed} removed` + (imageStats.errors ? `, ${imageStats.errors} failed` : ""));
|
|
1335
2180
|
}
|
|
1336
2181
|
if (instances > 0) console.info(`Vaults | ${vault.label} instantiated ${instances} document(s) from page foundry.base.`);
|
|
2182
|
+
if (versionSkew.length > 0) {
|
|
2183
|
+
host.notify("warn", localizeOr(
|
|
2184
|
+
host,
|
|
2185
|
+
"VAULTS.Sync.VersionSkew",
|
|
2186
|
+
"{count} document(s) came from a different Foundry generation and may not render correctly. See the console for which pack, and which version it targets.",
|
|
2187
|
+
{ count: versionSkew.length }
|
|
2188
|
+
));
|
|
2189
|
+
console.warn(
|
|
2190
|
+
`Vaults | ${vault.label}: ${versionSkew.length} document(s) were exported for a different Foundry generation than this world (${game.release?.generation}). They import, but parts of a stale document may not place correctly \u2014 a Foundry 13 Scene keeps its walls and lights but draws its tiles at the canvas origin. Ask the creator for a re-export, or point the base at a pack built for this generation:`,
|
|
2191
|
+
versionSkew
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
if (failedPages.size > 0) {
|
|
2195
|
+
host.notify("warn", host.localize("VAULTS.Sync.PagesFailed", { count: failedPages.size }));
|
|
2196
|
+
console.warn(
|
|
2197
|
+
`Vaults | ${vault.label} failed to sync ${failedPages.size} page(s); they stay in the diff and retry next sync:`,
|
|
2198
|
+
[...failedPages].map(([path, reason]) => ({ path, reason }))
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
if (skipped.length > 0) {
|
|
2202
|
+
host.notify("warn", host.localize("VAULTS.Sync.InstancesSkipped", { count: skipped.length }));
|
|
2203
|
+
console.warn(`Vaults | ${vault.label} no document created for ${skipped.length} page(s):`, skipped);
|
|
2204
|
+
}
|
|
1337
2205
|
return {
|
|
1338
2206
|
ok: true,
|
|
1339
2207
|
refreshHandlerAssets: true,
|
|
@@ -1341,7 +2209,10 @@ async function sync(host, vault, { forceFull = false } = {}) {
|
|
|
1341
2209
|
modified,
|
|
1342
2210
|
removed,
|
|
1343
2211
|
imageStats,
|
|
1344
|
-
instances
|
|
2212
|
+
instances,
|
|
2213
|
+
skipped,
|
|
2214
|
+
missingDocuments: missingDocs,
|
|
2215
|
+
failed: [...failedPages].map(([path, reason]) => ({ path, reason }))
|
|
1345
2216
|
};
|
|
1346
2217
|
}
|
|
1347
2218
|
function arraysEqual(a, b) {
|
|
@@ -1353,16 +2224,15 @@ function arraysEqual(a, b) {
|
|
|
1353
2224
|
|
|
1354
2225
|
// ../foundry/scripts/importer-entry.mjs
|
|
1355
2226
|
var REQUIRED_HOST_VERSION = 1;
|
|
1356
|
-
async function
|
|
2227
|
+
async function runSync2(host, vault, options = {}) {
|
|
1357
2228
|
return sync(host, vault, options);
|
|
1358
2229
|
}
|
|
1359
2230
|
async function runRemove(_host, vault) {
|
|
1360
|
-
await
|
|
2231
|
+
await deleteVaultPacks(vault.id);
|
|
1361
2232
|
await deleteVaultCache(vault.id);
|
|
1362
|
-
await deleteVaultInstances(vault.id);
|
|
1363
2233
|
}
|
|
1364
2234
|
export {
|
|
1365
2235
|
REQUIRED_HOST_VERSION,
|
|
1366
2236
|
runRemove,
|
|
1367
|
-
runSync
|
|
2237
|
+
runSync2 as runSync
|
|
1368
2238
|
};
|