@wizzlethorpe/vaults 0.14.0 → 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 +18 -10
- package/dist/asset-refs.js.map +1 -1
- package/dist/build.js +159 -70
- package/dist/build.js.map +1 -1
- package/dist/commands/build.js +1 -54
- package/dist/commands/build.js.map +1 -1
- 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 +39 -3
- package/dist/foundry-meta.js +15 -243
- package/dist/foundry-meta.js.map +1 -1
- package/dist/foundry-module-journal.js +24 -3
- package/dist/foundry-module-journal.js.map +1 -1
- package/dist/foundry-module.js +19 -12
- package/dist/foundry-module.js.map +1 -1
- 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 +1 -1
- package/dist/frontmatter-defaults.js.map +1 -1
- package/dist/index.js +0 -4
- package/dist/index.js.map +1 -1
- package/dist/manifest.js +1 -1
- package/dist/manifest.js.map +1 -1
- 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 +9 -8
- package/dist/render/auth-template.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/fm-code.js +2 -2
- package/dist/render/handlers/builtin/fm-code.js.map +1 -1
- 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 +2 -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/scan.js +0 -8
- package/dist/scan.js.map +1 -1
- package/dist/settings.js +57 -28
- package/dist/settings.js.map +1 -1
- package/dist/zip.js +78 -0
- package/dist/zip.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Turn a page's rendered article HTML into what a Foundry journal wants:
|
|
2
|
+
// links become Foundry UUIDs, media becomes `@vaults/<variant>/<path>`
|
|
3
|
+
// references the provider resolves on the reader's machine.
|
|
4
|
+
//
|
|
5
|
+
// The variant segment is load-bearing: each role's deploy holds only the
|
|
6
|
+
// files its pages reference, so resolving without it means guessing, and
|
|
7
|
+
// guessing upward hands a player a file their role was built to withhold.
|
|
8
|
+
// A `.foundry.html` suffix marks a body to inline; anything else is a file.
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { htmlAttr, htmlUnescape } from "./escape.js";
|
|
11
|
+
const ANCHOR_RE = /<a\b([^>]*)>([\s\S]*?)<\/a>/gi;
|
|
12
|
+
const MEDIA_SRC_RE = /<(img|audio|video)\b([^>]*?)src="([^"]+)"([^>]*)>/gi;
|
|
13
|
+
const HREF_RE = /\bhref="([^"]+)"/i;
|
|
14
|
+
const CLASS_RE = /\bclass="([^"]+)"/i;
|
|
15
|
+
const TAG_RE = /<[^>]+>/g;
|
|
16
|
+
/** `"/Characters/Marlo"` back to `"Characters/Marlo.md"`. */
|
|
17
|
+
export function pathFromHref(href) {
|
|
18
|
+
// The entity pass comes first because it is the parser layer: the serializer
|
|
19
|
+
// wrote `'` as `'`, and only what it produces is percent-encoded.
|
|
20
|
+
const unescaped = htmlUnescape(href);
|
|
21
|
+
if (!unescaped.startsWith("/"))
|
|
22
|
+
return null;
|
|
23
|
+
const clean = unescaped.split("#")[0].split("?")[0];
|
|
24
|
+
let decoded;
|
|
25
|
+
try {
|
|
26
|
+
decoded = decodeURIComponent(clean);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
decoded = clean;
|
|
30
|
+
}
|
|
31
|
+
const trimmed = decoded.replace(/^\/+/, "").replace(/\.html$/i, "");
|
|
32
|
+
return trimmed ? `${trimmed}.md` : null;
|
|
33
|
+
}
|
|
34
|
+
/** The UUID a link to `path` should carry, or null if nothing points there. */
|
|
35
|
+
export function uuidFor(path, index, prefer = "journal") {
|
|
36
|
+
const target = index.targets.get(path);
|
|
37
|
+
if (!target)
|
|
38
|
+
return null;
|
|
39
|
+
const docUuid = target.doc
|
|
40
|
+
? (index.packaging === "adventure"
|
|
41
|
+
? `${target.doc.type}.${target.doc.id}`
|
|
42
|
+
: `Compendium.${index.moduleId}.${target.doc.pack}.${target.doc.type}.${target.doc.id}`)
|
|
43
|
+
: null;
|
|
44
|
+
const pageUuid = target.entry && target.page
|
|
45
|
+
? (index.packaging === "adventure"
|
|
46
|
+
? `JournalEntry.${target.entry}.JournalEntryPage.${target.page}`
|
|
47
|
+
: `Compendium.${index.moduleId}.${index.journalPack}.JournalEntry.${target.entry}.JournalEntryPage.${target.page}`)
|
|
48
|
+
: null;
|
|
49
|
+
return prefer === "doc" ? (docUuid ?? pageUuid) : (pageUuid ?? docUuid);
|
|
50
|
+
}
|
|
51
|
+
const stripTags = (s) => s.replace(TAG_RE, "").trim();
|
|
52
|
+
/** `}` inside a label would close the enricher early. */
|
|
53
|
+
const escapeBraces = (s) => s.replace(/\{/g, "{").replace(/\}/g, "}");
|
|
54
|
+
/**
|
|
55
|
+
* Rewrite internal links to Foundry UUID enrichers.
|
|
56
|
+
*
|
|
57
|
+
* A link the index cannot place is left exactly as it is: an unresolved
|
|
58
|
+
* wikilink already renders as broken-styled text on the wiki, and turning it
|
|
59
|
+
* into a UUID that resolves to nothing would look worse in Foundry, not
|
|
60
|
+
* better.
|
|
61
|
+
*/
|
|
62
|
+
export function rewriteLinks(html, index) {
|
|
63
|
+
return html.replace(ANCHOR_RE, (whole, attrs, inner) => {
|
|
64
|
+
const cls = CLASS_RE.exec(attrs)?.[1] ?? "";
|
|
65
|
+
const card = /\bbases-card\b/.test(cls);
|
|
66
|
+
if (!card && !/\binternal-link\b/.test(cls))
|
|
67
|
+
return whole;
|
|
68
|
+
if (/\bis-unresolved\b/.test(cls))
|
|
69
|
+
return whole;
|
|
70
|
+
const href = HREF_RE.exec(attrs)?.[1];
|
|
71
|
+
if (!href)
|
|
72
|
+
return whole;
|
|
73
|
+
const path = pathFromHref(href);
|
|
74
|
+
if (!path)
|
|
75
|
+
return whole;
|
|
76
|
+
const uuid = uuidFor(path, index, /\bfvtt-doc-link\b/.test(cls) ? "doc" : "journal");
|
|
77
|
+
if (!uuid)
|
|
78
|
+
return whole;
|
|
79
|
+
// A card's layout lives in its markup, which an @UUID enricher would
|
|
80
|
+
// flatten to a text link. A content-link anchor keeps the markup and
|
|
81
|
+
// Foundry's click handler opens the document all the same.
|
|
82
|
+
if (card) {
|
|
83
|
+
return `<a class="${htmlAttr(cls)} content-link" draggable="true" data-link="" data-uuid="${htmlAttr(uuid)}">${inner}</a>`;
|
|
84
|
+
}
|
|
85
|
+
const label = escapeBraces(stripTags(inner));
|
|
86
|
+
return label ? `@UUID[${uuid}]{${label}}` : `@UUID[${uuid}]`;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Point media at the variant-scoped file the provider should fetch.
|
|
91
|
+
*
|
|
92
|
+
* Where a file lands is a runtime fact — it depends on the world it is being
|
|
93
|
+
* built into — so the CLI names what it wants rather than guessing a path.
|
|
94
|
+
*/
|
|
95
|
+
export function rewriteAssets(html, variant) {
|
|
96
|
+
const mark = (p) => `@vaults/${variant}/${p.replace(/^\/+/, "")}`;
|
|
97
|
+
let out = html.replace(MEDIA_SRC_RE, (whole, tag, before, src, after) => src.startsWith("/") ? `<${tag}${before}src="${htmlAttr(mark(src))}"${after}>` : whole);
|
|
98
|
+
out = out.replace(ANCHOR_RE, (whole, attrs, inner) => {
|
|
99
|
+
const cls = CLASS_RE.exec(attrs)?.[1] ?? "";
|
|
100
|
+
if (!/\bpassthrough-link\b/.test(cls))
|
|
101
|
+
return whole;
|
|
102
|
+
const href = HREF_RE.exec(attrs)?.[1];
|
|
103
|
+
if (!href?.startsWith("/"))
|
|
104
|
+
return whole;
|
|
105
|
+
return `<a${attrs.replace(HREF_RE, `href="${htmlAttr(mark(href))}"`)}>${inner}</a>`;
|
|
106
|
+
});
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Rewrite `@vault/PATH` values inside a document patch to vault references.
|
|
111
|
+
*
|
|
112
|
+
* `@vault/` is the authoring form: written by hand in frontmatter, and left in
|
|
113
|
+
* the Scene sidecars exported from Foundry. It has no variant because the
|
|
114
|
+
* author is describing their own vault, where there is only one copy of the
|
|
115
|
+
* file. Resolving it means deciding which role's copy this document reads, and
|
|
116
|
+
* that is the same decision `visibility` already made for the body.
|
|
117
|
+
*/
|
|
118
|
+
export function rewriteVaultRefs(value, variant) {
|
|
119
|
+
if (typeof value === "string") {
|
|
120
|
+
return (value.startsWith("@vault/")
|
|
121
|
+
? `@vaults/${variant}/${value.slice("@vault/".length)}`
|
|
122
|
+
: value);
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(value))
|
|
125
|
+
return value.map((v) => rewriteVaultRefs(v, variant));
|
|
126
|
+
if (value && typeof value === "object") {
|
|
127
|
+
return Object.fromEntries(Object.entries(value)
|
|
128
|
+
.map(([k, v]) => [k, rewriteVaultRefs(v, variant)]));
|
|
129
|
+
}
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
/** Everything a body needs before it can be a journal page. */
|
|
133
|
+
/**
|
|
134
|
+
* Wrap role-gated callouts in Foundry's own secret sections.
|
|
135
|
+
*
|
|
136
|
+
* A player-visible document carries the GM's body, so DM callouts reach the
|
|
137
|
+
* table inside it — hidden by Foundry from anyone below owner, which is what
|
|
138
|
+
* `<section class="secret">` means to a journal. Hidden, not absent: the text
|
|
139
|
+
* is in the document's data, the same trade the old sync made.
|
|
140
|
+
*/
|
|
141
|
+
/** Where the element opening at `start` closes, balancing same-name tags. */
|
|
142
|
+
function elementEnd(html, start, tagName) {
|
|
143
|
+
const tag = new RegExp(`</?${tagName}\\b`, "g");
|
|
144
|
+
tag.lastIndex = start;
|
|
145
|
+
let depth = 0;
|
|
146
|
+
let t;
|
|
147
|
+
while ((t = tag.exec(html)) !== null) {
|
|
148
|
+
depth += t[0].startsWith("</") ? -1 : 1;
|
|
149
|
+
if (depth === 0)
|
|
150
|
+
return html.indexOf(">", t.index) + 1;
|
|
151
|
+
}
|
|
152
|
+
return -1;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* A player-visible document's body: the GM's full rendering inside one
|
|
156
|
+
* secret section (stripped by Foundry below owner), then the player
|
|
157
|
+
* variant's in the open. The player gets the actual player render, so no
|
|
158
|
+
* difference between the two — base rows, transclusions, links — needs to
|
|
159
|
+
* be found and marked. The module's CSS hides the player copy for the GM.
|
|
160
|
+
*/
|
|
161
|
+
export function dualVariantBody(gmHtml, playerHtml) {
|
|
162
|
+
const id = createHash("sha1").update(gmHtml).digest("hex").slice(0, 16);
|
|
163
|
+
return `<section class="secret vaults-gm" id="secret-${id}">${gmHtml}</section>`
|
|
164
|
+
+ `<div class="vaults-player-view">${playerHtml}</div>`;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Drop every element carrying the `vaults-web-only` class from the Foundry
|
|
168
|
+
* body; the wiki keeps it. Handlers set it on their wrapper, and a page can
|
|
169
|
+
* put it on raw HTML of its own.
|
|
170
|
+
*/
|
|
171
|
+
export function stripWebOnly(html) {
|
|
172
|
+
// The class matched as a whole token: \b treats a hyphen as a boundary, so
|
|
173
|
+
// it alone would also match "vaults-web-only-not".
|
|
174
|
+
const open = /<([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*\bclass="(?:[^"]* )?vaults-web-only(?: [^"]*)?"[^>]*>/g;
|
|
175
|
+
let out = "";
|
|
176
|
+
let at = 0;
|
|
177
|
+
let m;
|
|
178
|
+
while ((m = open.exec(html)) !== null) {
|
|
179
|
+
if (m.index < at)
|
|
180
|
+
continue; // nested inside something already dropped
|
|
181
|
+
const end = VOID_TAGS.has(m[1].toLowerCase()) || m[0].endsWith("/>")
|
|
182
|
+
? m.index + m[0].length
|
|
183
|
+
: elementEnd(html, m.index, m[1]);
|
|
184
|
+
if (end < 0)
|
|
185
|
+
continue; // unclosed: leave it, keep looking
|
|
186
|
+
out += html.slice(at, m.index);
|
|
187
|
+
at = end;
|
|
188
|
+
open.lastIndex = end;
|
|
189
|
+
}
|
|
190
|
+
return out + html.slice(at);
|
|
191
|
+
}
|
|
192
|
+
const VOID_TAGS = new Set(["img", "br", "hr", "input", "source", "track", "wbr", "embed", "area", "col", "link", "meta"]);
|
|
193
|
+
export function toFoundryHtml(html, index, variant) {
|
|
194
|
+
return rewriteAssets(rewriteLinks(stripWebOnly(html), index), variant);
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=foundry-html.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"foundry-html.js","sourceRoot":"","sources":["../src/foundry-html.ts"],"names":[],"mappings":"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"}
|
|
@@ -85,6 +85,15 @@ var PACK_KEY = {
|
|
|
85
85
|
Cards: "cards",
|
|
86
86
|
Playlist: "playlists"
|
|
87
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
|
+
}
|
|
88
97
|
var BLANK_DOC_TYPES = [
|
|
89
98
|
"Actor",
|
|
90
99
|
"Item",
|
|
@@ -915,6 +924,28 @@ function buildFolderInfo(mdPaths) {
|
|
|
915
924
|
function isIndexFile(filename) {
|
|
916
925
|
return filename.replace(/\.md$/i, "") === INDEX_BASENAME;
|
|
917
926
|
}
|
|
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
|
+
}
|
|
918
949
|
async function upsertFile(target, vault, path, body, index, meta, folderInfo, mediaRefs) {
|
|
919
950
|
let html = await transformHtmlForFoundry(vault, body, index, mediaRefs);
|
|
920
951
|
html = await appendInstanceDocLink(html, vault, path, meta);
|
|
@@ -932,15 +963,19 @@ async function upsertFile(target, vault, path, body, index, meta, folderInfo, me
|
|
|
932
963
|
const pId = typeof idOverride === "string" && idOverride ? idOverride : await pageId(vault.id, path);
|
|
933
964
|
const pageOwnership = pageOwnershipLevelFor(vault, meta?.role);
|
|
934
965
|
const flags = { [MODULE_ID]: { vaultId: vault.id, path } };
|
|
966
|
+
const spec = journalPageSpec(meta?.foundry);
|
|
967
|
+
const type = resolvePageType(spec.type, path);
|
|
935
968
|
const pageData = {
|
|
936
969
|
_id: pId,
|
|
937
970
|
name: pageName,
|
|
938
|
-
type
|
|
939
|
-
|
|
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: {
|
|
940
975
|
content: html,
|
|
941
976
|
format: 1
|
|
942
977
|
/* HTML */
|
|
943
|
-
},
|
|
978
|
+
} } : {},
|
|
944
979
|
sort: isIndexFile(filename) ? 0 : NON_INDEX_SORT_BASE,
|
|
945
980
|
flags,
|
|
946
981
|
// Ownership is set on the *page*, not the parent entry: in the
|
|
@@ -952,6 +987,7 @@ async function upsertFile(target, vault, path, body, index, meta, folderInfo, me
|
|
|
952
987
|
// page in it). See reconcileOwnership below.
|
|
953
988
|
...pageOwnership !== null ? { ownership: { default: pageOwnership } } : {}
|
|
954
989
|
};
|
|
990
|
+
if (Object.keys(spec.overlay).length > 0) deepMergePage(pageData, spec.overlay);
|
|
955
991
|
const existing = await target.get("JournalEntry", eId);
|
|
956
992
|
if (existing) {
|
|
957
993
|
const pages = (existing.pages ?? []).filter((pg) => pg._id !== pId);
|
package/dist/foundry-meta.js
CHANGED
|
@@ -1,50 +1,26 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// Kept in one file because this is a shared contract, not a private detail:
|
|
4
|
-
// foundry/scripts/ reads exactly these keys back out of the manifest, and the
|
|
5
|
-
// two sides disagreeing is how `base: actor:npc` once created an Actor while
|
|
6
|
-
// every inbound wikilink addressed a nonexistent "actor". The doc-type rule
|
|
7
|
-
// here is held to foundry/scripts/foundry-base.mjs by
|
|
8
|
-
// cli/test/foundry-base-conformance.test.ts.
|
|
1
|
+
// What the CLI knows about the `foundry:` frontmatter block: which document a
|
|
2
|
+
// `source` names, and whether two pages are fighting over the same one.
|
|
9
3
|
import { readFile } from "node:fs/promises";
|
|
10
4
|
import { join } from "node:path";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*
|
|
14
|
-
* Both forms do, for every type vaults can instantiate. This used to be
|
|
15
|
-
* narrower — the module cloned from a UUID only for Actor and Item, so a lone
|
|
16
|
-
* `Compendium.<pkg>.<pack>.Scene.<id>` created nothing and warning about a
|
|
17
|
-
* collision would have described documents that never exist. That restriction
|
|
18
|
-
* is gone (map packs ship their content as compendium Scenes, and those were
|
|
19
|
-
* all being skipped), so a well-formed base always produces a document and
|
|
20
|
-
* the only question left is whether the type is one we recognise.
|
|
21
|
-
*
|
|
22
|
-
* All entries name the same type by the time this runs, so `docType` answers
|
|
23
|
-
* for the whole list.
|
|
24
|
-
*/
|
|
25
|
-
function willInstantiate(_specs, docType) {
|
|
26
|
-
return canonicalFoundryType(docType) !== null;
|
|
27
|
-
}
|
|
5
|
+
import { DOC_TYPES } from "./foundry-types.js";
|
|
6
|
+
import { documentFolder, documentTypeOf, firstBase } from "./foundry-grafts.js";
|
|
28
7
|
export function warnFoundryDocCollisions(pages) {
|
|
29
8
|
const seen = new Map(); // key → first page path
|
|
30
9
|
for (const p of pages) {
|
|
31
10
|
const fo = p.frontmatter?.["foundry"];
|
|
32
11
|
if (!fo || typeof fo !== "object" || Array.isArray(fo))
|
|
33
12
|
continue;
|
|
34
|
-
|
|
35
|
-
const specs = (Array.isArray(base) ? base : [base])
|
|
36
|
-
.filter((x) => typeof x === "string" && x.length > 0);
|
|
37
|
-
if (specs.length === 0)
|
|
13
|
+
if (fo["sync"] === false)
|
|
38
14
|
continue;
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
15
|
+
const spec = firstBase(fo["source"]);
|
|
16
|
+
if (!spec)
|
|
41
17
|
continue;
|
|
42
|
-
|
|
18
|
+
// The same reading the emitter uses, so a warning never describes a
|
|
19
|
+
// document that will not exist: a type with no pack produces nothing.
|
|
20
|
+
const docType = documentTypeOf(spec);
|
|
21
|
+
if (!docType || !DOC_TYPES[docType])
|
|
43
22
|
continue;
|
|
44
|
-
const
|
|
45
|
-
const folder = typeof override === "string" && override.trim()
|
|
46
|
-
? override.trim().replace(/^\/+|\/+$/g, "")
|
|
47
|
-
: p.path.split("/").slice(0, -1).join("/");
|
|
23
|
+
const folder = documentFolder({ path: p.path, foundry: fo });
|
|
48
24
|
const name = p.title || p.path.split("/").pop().replace(/\.md$/i, "");
|
|
49
25
|
const key = `${docType}\u0000${folder}\u0000${name}`;
|
|
50
26
|
const previous = seen.get(key);
|
|
@@ -57,208 +33,7 @@ export function warnFoundryDocCollisions(pages) {
|
|
|
57
33
|
+ `separate them with foundry.folder.`);
|
|
58
34
|
}
|
|
59
35
|
}
|
|
60
|
-
/**
|
|
61
|
-
* The document type a `foundry.base` spec names, read off the string without
|
|
62
|
-
* resolving anything. Every UUID form puts the type second-to-last
|
|
63
|
-
* (`Actor.<id>`, `Compendium.<pkg>.<pack>.Actor.<id>`), and a blank-doc spec
|
|
64
|
-
* (`Actor:npc`) carries it outright. `links.mjs` derives it the same way, so
|
|
65
|
-
* both sides agree on where a wikilink to the page points.
|
|
66
|
-
*/
|
|
67
|
-
export function foundryBaseDocName(spec) {
|
|
68
|
-
// A Moulinette reference names no type. Checked before the UUID rule
|
|
69
|
-
// because its file segment ends in ".json", which the dot test would
|
|
70
|
-
// otherwise read as a UUID. The type comes from another entry in the list;
|
|
71
|
-
// normalizeFoundryBase makes sure one is there.
|
|
72
|
-
if (spec.startsWith("@moulinette/"))
|
|
73
|
-
return null;
|
|
74
|
-
if (spec.includes(".")) {
|
|
75
|
-
const parts = spec.split(".");
|
|
76
|
-
if (parts.length < 2)
|
|
77
|
-
return null;
|
|
78
|
-
const raw = parts[parts.length - 2];
|
|
79
|
-
// Unknown types pass through: vaults can't instantiate a Combat, but
|
|
80
|
-
// Foundry may still resolve the UUID, and reporting the type beats
|
|
81
|
-
// claiming the spec names none.
|
|
82
|
-
return canonicalFoundryType(raw) ?? raw ?? null;
|
|
83
|
-
}
|
|
84
|
-
// Blank-document form. An unrecognised type is not a base at all, so it is
|
|
85
|
-
// rejected rather than passed through — matching the Foundry module, which
|
|
86
|
-
// would create nothing for it.
|
|
87
|
-
return canonicalFoundryType(spec.split(":")[0]);
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Fold a type segment to its canonical spelling, or null if vaults doesn't
|
|
91
|
-
* instantiate it.
|
|
92
|
-
*
|
|
93
|
-
* Case matters downstream and is hand-typed here: `base: actor:npc` is
|
|
94
|
-
* supported, but Foundry's `@UUID[...]` enricher does a case-sensitive
|
|
95
|
-
* lookup. Returning "actor" made the CLI treat it as a different type from
|
|
96
|
-
* "Actor" — so a list mixing the two failed the same-type check and had its
|
|
97
|
-
* whole foundry.base dropped. Kept in step with
|
|
98
|
-
* foundry/scripts/foundry-base.mjs by cli/test/foundry-base-conformance.test.ts.
|
|
99
|
-
*/
|
|
100
|
-
export function canonicalFoundryType(raw) {
|
|
101
|
-
if (!raw)
|
|
102
|
-
return null;
|
|
103
|
-
return FOUNDRY_BLANK_DOC_TYPES.find((t) => t.toLowerCase() === raw.toLowerCase()) ?? null;
|
|
104
|
-
}
|
|
105
|
-
export const FOUNDRY_BLANK_DOC_TYPES = [
|
|
106
|
-
"Actor", "Item", "Scene", "JournalEntry",
|
|
107
|
-
"RollTable", "Macro", "Cards", "Playlist",
|
|
108
|
-
];
|
|
109
|
-
/**
|
|
110
|
-
* Validate `foundry.base` and normalize it for the manifest: a single string
|
|
111
|
-
* stays a string (so an older Foundry module keeps working), a list of two or
|
|
112
|
-
* more stays a list. Returns null when the value can't be used, having said
|
|
113
|
-
* why — the build continues, and the page syncs as a journal with no document.
|
|
114
|
-
*
|
|
115
|
-
* Every entry must name the same document type. The module reads that type
|
|
116
|
-
* off the spec rather than off a resolved template, and `links.mjs` has to
|
|
117
|
-
* reach the same answer with no lookup at all, so a list that disagrees with
|
|
118
|
-
* itself has no single answer to give.
|
|
119
|
-
*/
|
|
120
|
-
function normalizeFoundryBase(base, pagePath) {
|
|
121
|
-
const raw = Array.isArray(base) ? base : [base];
|
|
122
|
-
const specs = [];
|
|
123
|
-
for (const entry of raw) {
|
|
124
|
-
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
125
|
-
console.warn(` ${pagePath}: foundry.base entries must be non-empty strings (a UUID like `
|
|
126
|
-
+ `"Compendium.<pkg>.<pack>.Actor.<id>", or a type like "Actor:npc"); `
|
|
127
|
-
+ `got ${entry === null ? "null" : typeof entry}. Ignoring foundry.base — this page `
|
|
128
|
-
+ `will sync as a journal but create no document.`);
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
specs.push(entry.trim());
|
|
132
|
-
}
|
|
133
|
-
if (specs.length === 0) {
|
|
134
|
-
console.warn(` ${pagePath}: foundry.base is an empty list; ignoring.`);
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
const types = new Map(); // docName → first spec that named it
|
|
138
|
-
for (const spec of specs) {
|
|
139
|
-
// A Moulinette entry is deliberately typeless: only the reader's own
|
|
140
|
-
// library knows whether that asset is a Scene or an Actor, and this runs
|
|
141
|
-
// at build time with no library to ask.
|
|
142
|
-
if (spec.startsWith("@moulinette/"))
|
|
143
|
-
continue;
|
|
144
|
-
const docName = foundryBaseDocName(spec);
|
|
145
|
-
if (!docName) {
|
|
146
|
-
console.warn(` ${pagePath}: foundry.base entry "${spec}" names no document type; ignoring foundry.base.`);
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
if (!types.has(docName))
|
|
150
|
-
types.set(docName, spec);
|
|
151
|
-
}
|
|
152
|
-
if (types.size === 0) {
|
|
153
|
-
console.warn(` ${pagePath}: foundry.base names only Moulinette references, which carry no document `
|
|
154
|
-
+ `type. Add an entry naming the type (e.g. "Scene") — it is also what the page falls `
|
|
155
|
-
+ `back to for a reader without that pack. Ignoring foundry.base.`);
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
if (types.size > 1) {
|
|
159
|
-
const detail = [...types].map(([t, spec]) => `${t} (from "${spec}")`).join(", ");
|
|
160
|
-
console.warn(` ${pagePath}: every foundry.base entry must name the same document type, got ${detail}. `
|
|
161
|
-
+ `Ignoring foundry.base — this page will sync as a journal but create no document.`);
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
// A list whose last entry is a UUID can still fail on a world that lacks
|
|
165
|
-
// every package named. A blank-doc tail is what makes the chain total.
|
|
166
|
-
const last = specs[specs.length - 1];
|
|
167
|
-
if (specs.length > 1 && (last.includes(".") || last.startsWith("@moulinette/"))) {
|
|
168
|
-
console.warn(` ${pagePath}: foundry.base list ends with "${last}", so it can still resolve to nothing. `
|
|
169
|
-
+ `End with a blank-document entry (e.g. "${[...types.keys()][0]}:npc" or "${[...types.keys()][0]}") `
|
|
170
|
-
+ `to guarantee a document.`);
|
|
171
|
-
}
|
|
172
|
-
return specs.length === 1 ? specs[0] : specs;
|
|
173
|
-
}
|
|
174
|
-
export async function collectBodyMeta(p, vaultPath) {
|
|
175
|
-
const fm = p.frontmatter ?? {};
|
|
176
|
-
const out = { role: p.role };
|
|
177
|
-
const basename = p.path.split("/").pop().replace(/\.md$/i, "");
|
|
178
|
-
if (p.title && p.title !== basename)
|
|
179
|
-
out.title = p.title;
|
|
180
|
-
const fo = fm["foundry"];
|
|
181
|
-
if (fo && typeof fo === "object" && !Array.isArray(fo)) {
|
|
182
|
-
const block = {};
|
|
183
|
-
// `base` is one spec, or a priority list the module tries in order so a
|
|
184
|
-
// vault degrades across worlds with different content installed. A
|
|
185
|
-
// malformed base is dropped with a warning rather than failing the build,
|
|
186
|
-
// same as foundry.id below. Silence here is worse than it looks: the
|
|
187
|
-
// module never receives the key, so it can't report the page either, and
|
|
188
|
-
// the page syncs as a journal with no Actor/Item and no explanation.
|
|
189
|
-
const base = fo["base"];
|
|
190
|
-
if (base !== undefined && base !== null) {
|
|
191
|
-
const normalized = normalizeFoundryBase(base, p.path);
|
|
192
|
-
if (normalized !== null)
|
|
193
|
-
block.base = normalized;
|
|
194
|
-
}
|
|
195
|
-
const embed = fo["embed"];
|
|
196
|
-
if (typeof embed === "boolean")
|
|
197
|
-
block.embed = embed;
|
|
198
|
-
// foundry.sync: false keeps the page out of Foundry altogether — no
|
|
199
|
-
// JournalEntryPage, no derived doc. The page still renders on the wiki.
|
|
200
|
-
// Unlike `embed`, which only suppresses the article inside a derived
|
|
201
|
-
// doc's description, this drops the page from the sync set entirely.
|
|
202
|
-
const sync = fo["sync"];
|
|
203
|
-
if (typeof sync === "boolean")
|
|
204
|
-
block.sync = sync;
|
|
205
|
-
// foundry.journal: false makes the derived doc without the JournalEntryPage
|
|
206
|
-
// that normally accompanies it. For a page that exists to carry a Scene or
|
|
207
|
-
// an Actor and has no article worth reading in the sidebar.
|
|
208
|
-
const journal = fo["journal"];
|
|
209
|
-
if (typeof journal === "boolean")
|
|
210
|
-
block.journal = journal;
|
|
211
|
-
// foundry.link: "doc" makes wikilinks to this page resolve to the document
|
|
212
|
-
// it instantiates rather than to its journal page. Implied by
|
|
213
|
-
// `journal: false`, where there is no journal page to link to.
|
|
214
|
-
const link = fo["link"];
|
|
215
|
-
if (link === "doc" || link === "journal")
|
|
216
|
-
block.link = link;
|
|
217
|
-
const data = fo["data"];
|
|
218
|
-
if (data && typeof data === "object" && !Array.isArray(data))
|
|
219
|
-
block.data = data;
|
|
220
|
-
// foundry.folder: a "/"-separated folder path the instantiated doc is
|
|
221
|
-
// filed under, nested inside the vault's own sidebar folder. Absent
|
|
222
|
-
// means the vault folder itself, which is where everything used to land.
|
|
223
|
-
const folder = fo["folder"];
|
|
224
|
-
if (typeof folder === "string" && folder.trim().length > 0)
|
|
225
|
-
block.folder = folder.trim();
|
|
226
|
-
// foundry.id: an explicit Foundry document id for this page. When set,
|
|
227
|
-
// overrides the SHA1-derived id used for both the JournalEntryPage and
|
|
228
|
-
// (if foundry.base is present) the instantiated derived doc. Lets users
|
|
229
|
-
// hardcode UUIDs that other Foundry-side code (macros, scene flags,
|
|
230
|
-
// module integrations) needs to reference. Foundry ids are 16 chars from
|
|
231
|
-
// [A-Za-z0-9]; a malformed value is dropped with a warning rather than
|
|
232
|
-
// failing the build.
|
|
233
|
-
const idVal = fo["id"];
|
|
234
|
-
if (typeof idVal === "string") {
|
|
235
|
-
const trimmed = idVal.trim();
|
|
236
|
-
if (FOUNDRY_ID_RE.test(trimmed))
|
|
237
|
-
block.id = trimmed;
|
|
238
|
-
else if (trimmed.length > 0) {
|
|
239
|
-
console.warn(` ${p.path}: foundry.id "${trimmed}" is not a valid Foundry id (16 chars [A-Za-z0-9]); ignoring`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
// foundry.data_json: vault-relative path to a JSON file. Read + parse
|
|
243
|
-
// at build time and inline into the meta as `data_json`. The Foundry
|
|
244
|
-
// module deep-merges it onto the base doc BEFORE foundry.data, so a
|
|
245
|
-
// user can layer hand-tuned overrides on top of an exported sheet.
|
|
246
|
-
// Folding the parsed object into meta means the body-row hash already
|
|
247
|
-
// changes when the JSON content does — no separate change-detection.
|
|
248
|
-
const dataJsonPath = fo["data_json"];
|
|
249
|
-
if (typeof dataJsonPath === "string" && dataJsonPath.trim().length > 0) {
|
|
250
|
-
const parsed = await loadDataJson(vaultPath, dataJsonPath.trim(), p.path);
|
|
251
|
-
if (parsed !== null)
|
|
252
|
-
block.data_json = parsed;
|
|
253
|
-
}
|
|
254
|
-
if (Object.keys(block).length > 0)
|
|
255
|
-
out.foundry = block;
|
|
256
|
-
}
|
|
257
|
-
if (p.coverImage)
|
|
258
|
-
out.image = p.coverImage;
|
|
259
|
-
return out;
|
|
260
|
-
}
|
|
261
|
-
/** Read + parse a vault-relative JSON file referenced by `foundry.data_json`.
|
|
36
|
+
/** Read + parse a vault-relative JSON file referenced by `foundry.patch_json`.
|
|
262
37
|
* Warns on missing / unparseable file and returns null so the page renders
|
|
263
38
|
* without the overlay rather than failing the build. */
|
|
264
39
|
export async function loadDataJson(vaultPath, relPath, pagePath) {
|
|
@@ -270,15 +45,12 @@ export async function loadDataJson(vaultPath, relPath, pagePath) {
|
|
|
270
45
|
catch (err) {
|
|
271
46
|
const code = err.code;
|
|
272
47
|
if (code === "ENOENT") {
|
|
273
|
-
console.warn(` ${pagePath}: foundry.
|
|
48
|
+
console.warn(` ${pagePath}: foundry.patch_json "${relPath}" not found, skipping`);
|
|
274
49
|
}
|
|
275
50
|
else {
|
|
276
|
-
console.warn(` ${pagePath}: foundry.
|
|
51
|
+
console.warn(` ${pagePath}: foundry.patch_json "${relPath}" failed to parse: ${err.message}`);
|
|
277
52
|
}
|
|
278
53
|
return null;
|
|
279
54
|
}
|
|
280
55
|
}
|
|
281
|
-
/** Foundry document ids: exactly 16 chars from [A-Za-z0-9]. Validated when
|
|
282
|
-
* authors set `foundry.id` to override the SHA1-derived default. */
|
|
283
|
-
const FOUNDRY_ID_RE = /^[A-Za-z0-9]{16}$/;
|
|
284
56
|
//# sourceMappingURL=foundry-meta.js.map
|
package/dist/foundry-meta.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"foundry-meta.js","sourceRoot":"","sources":["../src/foundry-meta.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"foundry-meta.js","sourceRoot":"","sources":["../src/foundry-meta.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,wEAAwE;AAExE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhF,MAAM,UAAU,wBAAwB,CAAC,KAAiB;IACxD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC,CAAC,wBAAwB;IAChE,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,SAAS,CAAC,CAAC;QACtC,IAAI,CAAC,EAAE,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;YAAE,SAAS;QACjE,IAAK,EAA8B,CAAC,MAAM,CAAC,KAAK,KAAK;YAAE,SAAS;QAChE,MAAM,IAAI,GAAG,SAAS,CAAE,EAA8B,CAAC,QAAQ,CAAC,CAAC,CAAC;QAClE,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,oEAAoE;QACpE,sEAAsE;QACtE,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;YAAE,SAAS;QAE9C,MAAM,MAAM,GAAG,cAAc,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAyB,EAAE,CAAC,CAAC;QACpF,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAEvE,MAAM,GAAG,GAAG,GAAG,OAAO,SAAS,MAAM,SAAS,IAAI,EAAE,CAAC;QACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;YACtB,SAAS;QACX,CAAC;QACD,OAAO,CAAC,IAAI,CACV,KAAK,CAAC,CAAC,IAAI,oBAAoB,OAAO,WAAW,IAAI,wBAAwB;cAC3E,YAAY,MAAM,IAAI,cAAc,UAAU,QAAQ,oBAAoB;cAC1E,oCAAoC,CACvC,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;yDAEyD;AACzD,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,SAAiB,EACjB,OAAe,EACf,QAAgB;IAEhB,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACxC,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;IACpC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,GAAI,GAA6B,CAAC,IAAI,CAAC;QACjD,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;YACtB,OAAO,CAAC,IAAI,CAAC,KAAK,QAAQ,yBAAyB,OAAO,uBAAuB,CAAC,CAAC;QACrF,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,KAAK,QAAQ,yBAAyB,OAAO,sBAAuB,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QAC5G,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -70,6 +70,20 @@ export function transformForModule(html, moduleId, targets, assets) {
|
|
|
70
70
|
* an entry named for the module — the same shape the sync path produces, where
|
|
71
71
|
* a directory becomes an entry and its files become that entry's pages.
|
|
72
72
|
*/
|
|
73
|
+
/** Deep-merge an overlay onto page data, arrays and scalars replacing. */
|
|
74
|
+
function deepMergePage(target, patch) {
|
|
75
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
76
|
+
const cur = target[k];
|
|
77
|
+
if (v && typeof v === "object" && !Array.isArray(v)
|
|
78
|
+
&& cur && typeof cur === "object" && !Array.isArray(cur)) {
|
|
79
|
+
deepMergePage(cur, v);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
target[k] = v;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return target;
|
|
86
|
+
}
|
|
73
87
|
export function buildJournalEntries(sources, ns, rootName, stats) {
|
|
74
88
|
const byFolder = new Map();
|
|
75
89
|
for (const s of sources) {
|
|
@@ -86,18 +100,25 @@ export function buildJournalEntries(sources, ns, rootName, stats) {
|
|
|
86
100
|
name: folder ? folder.split("/").pop() : rootName,
|
|
87
101
|
pages: pages.map((p, i) => {
|
|
88
102
|
const pid = journalPageId(ns, p.path);
|
|
89
|
-
|
|
103
|
+
const type = p.spec?.type ?? "text";
|
|
104
|
+
const page = {
|
|
90
105
|
_id: pid,
|
|
91
106
|
name: p.title,
|
|
92
|
-
type
|
|
107
|
+
type,
|
|
93
108
|
title: { show: true, level: 1 },
|
|
94
|
-
|
|
109
|
+
// A page whose content is its `src` has nowhere to put an article.
|
|
110
|
+
// Unlike sync, the compiler cannot degrade an unknown type: it does
|
|
111
|
+
// not know the system of the world this module will be installed in.
|
|
112
|
+
...(type === "text" ? { text: { format: 1, content: p.html } } : {}),
|
|
95
113
|
sort: (i + 1) * 100000,
|
|
96
114
|
ownership: { default: -1 },
|
|
97
115
|
flags: {},
|
|
98
116
|
_stats: stats,
|
|
99
117
|
_key: `!journal.pages!${id}.${pid}`,
|
|
100
118
|
};
|
|
119
|
+
if (p.spec && Object.keys(p.spec.overlay).length > 0)
|
|
120
|
+
deepMergePage(page, p.spec.overlay);
|
|
121
|
+
return page;
|
|
101
122
|
}),
|
|
102
123
|
folder: null,
|
|
103
124
|
sort: 0,
|