@heroiclands/package-build 22.9.2 → 22.11.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/CHANGELOG.md +56 -0
- package/CONTENT.md +54 -0
- package/bin/content-build.mjs +3 -1
- package/docs/api.md +11 -6
- package/docs/commands.md +1 -1
- package/docs/content-format.md +411 -201
- package/engine/actor-compiler.mjs +20 -7
- package/engine/address-fields.mjs +426 -0
- package/engine/address-values.mjs +80 -0
- package/engine/address.mjs +769 -0
- package/engine/art-fields.mjs +112 -97
- package/engine/art-slots.mjs +81 -0
- package/engine/base-compiler.mjs +61 -7
- package/engine/bundle-notes.mjs +5 -2
- package/engine/bundles.mjs +17 -10
- package/engine/content-address.mjs +23 -186
- package/engine/content-embeds.mjs +18 -4
- package/engine/content-format-check.mjs +50 -22
- package/engine/content-index.mjs +85 -43
- package/engine/content-links.mjs +105 -104
- package/engine/content-lint.mjs +11 -1
- package/engine/content-tables.mjs +9 -1
- package/engine/field-spec.mjs +21 -4
- package/engine/folder-notes.mjs +44 -15
- package/engine/foundry-entries.mjs +3 -0
- package/engine/frontmatter-lint.mjs +150 -47
- package/engine/frontmatter.mjs +55 -10
- package/engine/generate.mjs +7 -1
- package/engine/helpers.mjs +37 -9
- package/engine/holdings.mjs +41 -12
- package/engine/index-records.mjs +2 -0
- package/engine/infobox-render.mjs +2 -1
- package/engine/infobox.mjs +26 -6
- package/engine/item-compiler.mjs +5 -0
- package/engine/item-docs.mjs +7 -44
- package/engine/map-dot.mjs +2 -0
- package/engine/map-notes.mjs +7 -2
- package/engine/map-places.mjs +42 -10
- package/engine/metadata-index.mjs +60 -5
- package/engine/note-addresses.mjs +325 -0
- package/engine/note-vocabulary.mjs +140 -28
- package/engine/pdf-build.mjs +1 -1
- package/engine/pdf-toc.mjs +17 -2
- package/engine/place-relations.mjs +160 -50
- package/engine/populations.mjs +17 -9
- package/engine/reference-targets.mjs +54 -0
- package/engine/scenes.mjs +5 -2
- package/engine/shortcode-references.mjs +68 -0
- package/engine/site-build.mjs +39 -15
- package/engine/site-index.mjs +57 -75
- package/engine/sql-tables.mjs +43 -9
- package/engine/system-block.mjs +21 -0
- package/engine/system-vocabulary.mjs +46 -0
- package/engine/web-wikilinks.mjs +11 -9
- package/engine/wikilink-syntax.mjs +21 -0
- package/engine/wikilinks.mjs +92 -306
- package/hm3/actors.mjs +8 -24
- package/hm3/item-fields.mjs +15 -5
- package/package.json +1 -1
- package/sohl/infobox.mjs +111 -17
- package/sohl/item-fields.mjs +131 -41
- package/types/engine/address-fields.d.mts +95 -0
- package/types/engine/address-values.d.mts +34 -0
- package/types/engine/address.d.mts +452 -0
- package/types/engine/art-fields.d.mts +59 -53
- package/types/engine/art-slots.d.mts +40 -0
- package/types/engine/base-compiler.d.mts +24 -2
- package/types/engine/bundle-notes.d.mts +2 -2
- package/types/engine/content-address.d.mts +3 -139
- package/types/engine/content-embeds.d.mts +1 -1
- package/types/engine/content-format-check.d.mts +25 -0
- package/types/engine/content-index.d.mts +2 -2
- package/types/engine/content-links.d.mts +6 -5
- package/types/engine/field-spec.d.mts +46 -9
- package/types/engine/folder-notes.d.mts +11 -10
- package/types/engine/frontmatter-lint.d.mts +7 -97
- package/types/engine/frontmatter.d.mts +16 -1
- package/types/engine/helpers.d.mts +2 -2
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/item-docs.d.mts +1 -35
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/note-addresses.d.mts +41 -0
- package/types/engine/note-vocabulary.d.mts +20 -3
- package/types/engine/place-relations.d.mts +12 -1
- package/types/engine/populations.d.mts +17 -2
- package/types/engine/reference-targets.d.mts +9 -0
- package/types/engine/shortcode-references.d.mts +14 -0
- package/types/engine/site-build.d.mts +3 -3
- package/types/engine/site-index.d.mts +9 -30
- package/types/engine/sql-tables.d.mts +2 -2
- package/types/engine/system-block.d.mts +17 -0
- package/types/engine/system-vocabulary.d.mts +16 -0
- package/types/engine/wikilink-syntax.d.mts +7 -0
- package/types/engine/wikilinks.d.mts +6 -109
- package/types/hm3/item-fields.d.mts +5 -0
- package/types/sohl/infobox.d.mts +6 -5
- package/types/sohl/item-fields.d.mts +8 -0
package/engine/art-fields.mjs
CHANGED
|
@@ -15,16 +15,22 @@
|
|
|
15
15
|
* The art slots a note declares, and turning one of them into a path.
|
|
16
16
|
*
|
|
17
17
|
* A note names the art a *document field* needs, and every other image in it is
|
|
18
|
-
* inline. Each slot is an ordinary `
|
|
18
|
+
* inline. Each slot is an ordinary `Address` field declaring a default type,
|
|
19
19
|
* exactly as `seat` declares `place`: a bare shortcode takes its type from the
|
|
20
20
|
* declaration, and a value that qualifies itself climbs the same short-form
|
|
21
|
-
* ladder every other link uses.
|
|
21
|
+
* ladder every other link uses. {@link module:engine/address.parseAddress}
|
|
22
|
+
* reads it, taking the slot's `type` as the default an omitted segment fills
|
|
23
|
+
* in — this module states no separator rule of its own.
|
|
22
24
|
*
|
|
23
|
-
* **
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* **The accepted set is a slot's second declaration, and it is not the
|
|
26
|
+
* default.** A slot's `type` says what a bare shortcode names; its `accepts`
|
|
27
|
+
* says what a qualified one may name, and {@link module:engine/address.acceptsType}
|
|
28
|
+
* checks a parsed value against it. `icon` defaults to `icon` and accepts
|
|
29
|
+
* `image` too, because a faith tradition's profile art is a full illustration
|
|
30
|
+
* rather than a game icon — twenty notes in `sohl-kethira-basic` author exactly
|
|
31
|
+
* that. A type outside the set is an error naming the set, never a silent fall
|
|
32
|
+
* back to the default art: the parser never refuses on type grounds, only the
|
|
33
|
+
* slot does.
|
|
28
34
|
*
|
|
29
35
|
* **Resolution is one step, because the record carries the path.** The address
|
|
30
36
|
* names a record, the record names the file's path inside its own package, and
|
|
@@ -38,60 +44,17 @@
|
|
|
38
44
|
* @module
|
|
39
45
|
*/
|
|
40
46
|
|
|
41
|
-
import {
|
|
47
|
+
import { encodeAddresses } from "./address-values.mjs";
|
|
42
48
|
import { hasTag } from "./note-vocabulary.mjs";
|
|
43
49
|
import { ASSET_SYSTEM, isAssetType } from "./asset-types.mjs";
|
|
44
50
|
import { ASSETS_SEGMENT } from "./pathnames.mjs";
|
|
45
51
|
import { isAssetRecord } from "./index-records.mjs";
|
|
46
52
|
import { ASSET_TYPE_NAMES } from "./asset-types.mjs";
|
|
47
|
-
import {
|
|
53
|
+
import { acceptsType, parseAddress, renderAddress } from "./address.mjs";
|
|
48
54
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
* @typedef {object} ArtSlot
|
|
53
|
-
* @property {string} key - The key under `data:`.
|
|
54
|
-
* @property {string} type - The asset type a bare shortcode defaults to.
|
|
55
|
-
* @property {boolean} document - Whether the slot reaches a compiled document.
|
|
56
|
-
* @property {string} describe - One line, for the author-facing reference.
|
|
57
|
-
*/
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* The four art slots, in the order the specification tabulates them.
|
|
61
|
-
*
|
|
62
|
-
* `banner` is the one that reaches no compiled document: it is the page's hero
|
|
63
|
-
* image, read by the site and by the book's section plates and by nothing else.
|
|
64
|
-
* That is what `document: false` states, and it is why the inert-art check
|
|
65
|
-
* skips it — a key that is *meant* to reach no document is not an inert key.
|
|
66
|
-
*
|
|
67
|
-
* @type {readonly ArtSlot[]}
|
|
68
|
-
*/
|
|
69
|
-
export const ART_SLOTS = Object.freeze([
|
|
70
|
-
Object.freeze({
|
|
71
|
-
key: "icon",
|
|
72
|
-
type: "icon",
|
|
73
|
-
document: true,
|
|
74
|
-
describe: "The document's profile art, resolved into `img`.",
|
|
75
|
-
}),
|
|
76
|
-
Object.freeze({
|
|
77
|
-
key: "tokenIcon",
|
|
78
|
-
type: "icon",
|
|
79
|
-
document: true,
|
|
80
|
-
describe: "What a token on the canvas wears; unset, it follows `icon`.",
|
|
81
|
-
}),
|
|
82
|
-
Object.freeze({
|
|
83
|
-
key: "bgImage",
|
|
84
|
-
type: "image",
|
|
85
|
-
document: true,
|
|
86
|
-
describe: "A map's background art, resolved into `background.src`.",
|
|
87
|
-
}),
|
|
88
|
-
Object.freeze({
|
|
89
|
-
key: "banner",
|
|
90
|
-
type: "image",
|
|
91
|
-
document: false,
|
|
92
|
-
describe: "The page's hero image. Reaches no compiled document.",
|
|
93
|
-
}),
|
|
94
|
-
]);
|
|
55
|
+
import { ART_SLOTS } from "./art-slots.mjs";
|
|
56
|
+
export { ART_SLOTS } from "./art-slots.mjs";
|
|
57
|
+
/** @typedef {import("./art-slots.mjs").ArtSlot} ArtSlot */
|
|
95
58
|
|
|
96
59
|
/**
|
|
97
60
|
* The art slot one key names, or `undefined`.
|
|
@@ -104,15 +67,33 @@ export function artSlot(key) {
|
|
|
104
67
|
}
|
|
105
68
|
|
|
106
69
|
/**
|
|
107
|
-
* The
|
|
70
|
+
* The Address an authored art value names, checked against what this position
|
|
71
|
+
* accepts.
|
|
108
72
|
*
|
|
109
|
-
* @
|
|
110
|
-
*
|
|
111
|
-
*
|
|
73
|
+
* The two questions {@link module:engine/address} keeps separate: does the
|
|
74
|
+
* value parse, and is what it names acceptable *here*. {@link parseAddress}
|
|
75
|
+
* answers the first, taking the slot's own `type` as the default an omitted
|
|
76
|
+
* segment fills in; {@link acceptsType} answers the second, against the set
|
|
77
|
+
* the position declares rather than against that same default.
|
|
78
|
+
*
|
|
79
|
+
* @param {unknown} value - The value as authored.
|
|
80
|
+
* @param {string} defaultType - The type a bare value takes.
|
|
81
|
+
* @param {Iterable<string>|undefined} accepts - The types this position
|
|
82
|
+
* accepts, or `undefined` to accept whatever parses.
|
|
83
|
+
* @param {import("./address.mjs").AddressDefaults} [vocabulary] - The
|
|
84
|
+
* position's remaining defaults and the tree's vocabularies.
|
|
85
|
+
* @returns {import("./address.mjs").AddressTuple
|
|
86
|
+
* |import("./address.mjs").AddressProblem
|
|
87
|
+
* |{reason: "not-accepted", type: string}} The Address; a reason it does
|
|
88
|
+
* not parse; or `not-accepted` when it parses to a type outside `accepts`.
|
|
112
89
|
*/
|
|
113
|
-
export function artTarget(value, defaultType) {
|
|
114
|
-
const
|
|
115
|
-
|
|
90
|
+
export function artTarget(value, defaultType, accepts, vocabulary = {}) {
|
|
91
|
+
const tuple = parseAddress(value, { ...vocabulary, type: defaultType });
|
|
92
|
+
if (tuple.reason) return tuple;
|
|
93
|
+
if (accepts && !acceptsType(tuple, accepts)) {
|
|
94
|
+
return { reason: "not-accepted", type: tuple.type };
|
|
95
|
+
}
|
|
96
|
+
return tuple;
|
|
116
97
|
}
|
|
117
98
|
|
|
118
99
|
/**
|
|
@@ -145,7 +126,7 @@ export function assetAddressIndex(records = [], { config, foreign, types = [] }
|
|
|
145
126
|
assets: new Map(
|
|
146
127
|
records
|
|
147
128
|
.filter(isAssetRecord)
|
|
148
|
-
.map((record) => [record.address?.canonical, record])
|
|
129
|
+
.map((record) => [encodeAddresses(record.address?.canonical), record])
|
|
149
130
|
.filter(([key]) => key),
|
|
150
131
|
),
|
|
151
132
|
foreign: foreign?.index ?? new Map(),
|
|
@@ -169,33 +150,39 @@ export function assetAddressIndex(records = [], { config, foreign, types = [] }
|
|
|
169
150
|
* or the equivalent the site and the book build.
|
|
170
151
|
* @param {unknown} value - The value as authored.
|
|
171
152
|
* @param {string} defaultType - The type a bare value takes.
|
|
153
|
+
* @param {Iterable<string>} [accepts] - The types this position accepts, for a
|
|
154
|
+
* position that declares a set narrower than "every asset type" — an art
|
|
155
|
+
* slot's. Omitted, any asset type is taken, which is an embed's rule: it
|
|
156
|
+
* draws a file, and every asset type is a file.
|
|
172
157
|
* @returns {{record: {package: string, asset: {path: string}}, pathname: string}
|
|
173
158
|
* |{record: null, reason: string, type?: string}} The asset and the pathname
|
|
174
159
|
* it is at, or a reason from
|
|
175
|
-
* {@link module:engine/wikilink-syntax.LINK_FINDING_REASONS}
|
|
160
|
+
* {@link module:engine/wikilink-syntax.LINK_FINDING_REASONS}, or `not-accepted`
|
|
161
|
+
* when `accepts` is given and the parsed type falls outside it.
|
|
176
162
|
*/
|
|
177
|
-
export function readAssetAddress(index, value, defaultType) {
|
|
178
|
-
if (
|
|
163
|
+
export function readAssetAddress(index, value, defaultType, accepts) {
|
|
164
|
+
if (!value) {
|
|
179
165
|
return { record: null, reason: "not-an-address" };
|
|
180
166
|
}
|
|
181
|
-
const
|
|
182
|
-
const read = readQualifier(target, index?.types, index?.packages);
|
|
183
|
-
if (!read) return { record: null, reason: "not-an-address" };
|
|
184
|
-
if (read.reason) return { record: null, reason: read.reason };
|
|
185
|
-
// **Asset types only.** An address may name any type the vocabulary holds,
|
|
186
|
-
// and most of them name a note — which has no file to draw. Refused by its
|
|
187
|
-
// own reason rather than left to resolve to nothing, because the fix is a
|
|
188
|
-
// different one: an ordinary link, not a corrected shortcode.
|
|
189
|
-
if (!isAssetType(read.type)) {
|
|
190
|
-
return { record: null, reason: "not-an-asset", type: String(read.type) };
|
|
191
|
-
}
|
|
192
|
-
// The system segment is fixed at `none` for an asset type, so where the
|
|
193
|
-
// value was written does not enter into it — an embedded item's art is
|
|
194
|
-
// authored inside a system block and still names the same file.
|
|
195
|
-
const canonical = expandAddress(read, {
|
|
196
|
-
package: index?.contentPackage ?? index?.packageId,
|
|
167
|
+
const read = artTarget(value, defaultType, accepts, {
|
|
197
168
|
system: ASSET_SYSTEM,
|
|
169
|
+
package: index?.contentPackage ?? index?.packageId,
|
|
170
|
+
types: index?.types,
|
|
171
|
+
packages: index?.packages,
|
|
198
172
|
});
|
|
173
|
+
if (read.reason === "not-accepted") {
|
|
174
|
+
return { record: null, reason: "not-accepted", type: read.type };
|
|
175
|
+
}
|
|
176
|
+
if (read.reason) return { record: null, reason: read.reason, type: read.type };
|
|
177
|
+
// **Asset types only**, where the position declares no narrower set. An
|
|
178
|
+
// address may name any type the vocabulary holds, and most of them name a
|
|
179
|
+
// note — which has no file to draw. Refused by its own reason rather than
|
|
180
|
+
// left to resolve to nothing, because the fix is a different one: an
|
|
181
|
+
// ordinary link, not a corrected shortcode.
|
|
182
|
+
if (!accepts && !isAssetType(read.type)) {
|
|
183
|
+
return { record: null, reason: "not-an-asset", type: String(read.type) };
|
|
184
|
+
}
|
|
185
|
+
const canonical = renderAddress(read);
|
|
199
186
|
const hit = index?.assets?.get(canonical) ?? index?.foreign?.get(canonical) ?? null;
|
|
200
187
|
if (!hit?.asset?.path || !hit.package) return { record: null, reason: "unresolved" };
|
|
201
188
|
return { record: hit, pathname: `${hit.package}/${ASSETS_SEGMENT}/${hit.asset.path}` };
|
|
@@ -207,10 +194,11 @@ export function readAssetAddress(index, value, defaultType) {
|
|
|
207
194
|
* @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex}.
|
|
208
195
|
* @param {unknown} value - The value as authored.
|
|
209
196
|
* @param {string} defaultType - The type the field declares.
|
|
197
|
+
* @param {Iterable<string>} [accepts] - The types this position accepts.
|
|
210
198
|
* @returns {{package: string, asset: {path: string}}|null} The record.
|
|
211
199
|
*/
|
|
212
|
-
export function resolveArtRecord(index, value, defaultType) {
|
|
213
|
-
return readAssetAddress(index, value, defaultType).record;
|
|
200
|
+
export function resolveArtRecord(index, value, defaultType, accepts) {
|
|
201
|
+
return readAssetAddress(index, value, defaultType, accepts).record;
|
|
214
202
|
}
|
|
215
203
|
|
|
216
204
|
/**
|
|
@@ -224,15 +212,21 @@ export function resolveArtRecord(index, value, defaultType) {
|
|
|
224
212
|
* @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex}.
|
|
225
213
|
* @param {unknown} value - The value as authored.
|
|
226
214
|
* @param {string} defaultType - The type the field declares.
|
|
227
|
-
* @
|
|
228
|
-
*
|
|
229
|
-
*
|
|
215
|
+
* @param {Iterable<string>} [accepts] - The types this position accepts.
|
|
216
|
+
* @returns {{pathname: string|null, resolved: boolean, reason?: string,
|
|
217
|
+
* type?: string}} The pathname, and whether an address was actually
|
|
218
|
+
* answered — which tells a caller applying a default apart from one whose
|
|
219
|
+
* address named nothing. `reason` and `type` carry why, unresolved — in
|
|
220
|
+
* particular `not-accepted`, which the caller reports as an error rather
|
|
221
|
+
* than falling back silently.
|
|
230
222
|
*/
|
|
231
|
-
export function artPathname(index, value, defaultType) {
|
|
223
|
+
export function artPathname(index, value, defaultType, accepts) {
|
|
232
224
|
if (value == null) return { pathname: null, resolved: true };
|
|
233
225
|
if (value === "") return { pathname: "", resolved: true };
|
|
234
|
-
const read = readAssetAddress(index, value, defaultType);
|
|
235
|
-
if (!read.record)
|
|
226
|
+
const read = readAssetAddress(index, value, defaultType, accepts);
|
|
227
|
+
if (!read.record) {
|
|
228
|
+
return { pathname: null, resolved: false, reason: read.reason, type: read.type };
|
|
229
|
+
}
|
|
236
230
|
return { pathname: read.pathname, resolved: true };
|
|
237
231
|
}
|
|
238
232
|
|
|
@@ -276,17 +270,38 @@ export function beingDefaultArt(fm) {
|
|
|
276
270
|
* What an unresolved art address is reported as.
|
|
277
271
|
*
|
|
278
272
|
* One wording, so the four compilers that can meet the case do not each invent
|
|
279
|
-
* their own.
|
|
273
|
+
* their own. A **warning**: nothing in this package or a fetched index answers
|
|
274
|
+
* the address, so the document falls back to its default art rather than
|
|
275
|
+
* shipping a path that installs nowhere.
|
|
280
276
|
*
|
|
281
277
|
* @param {string} key - The key that was authored.
|
|
282
|
-
* @param {unknown} value - The value it carried.
|
|
283
|
-
* @
|
|
278
|
+
* @param {unknown} value - The value it carried, named exactly as written.
|
|
279
|
+
* @returns {string} The message.
|
|
280
|
+
*/
|
|
281
|
+
export function unresolvedArtMessage(key, value) {
|
|
282
|
+
return (
|
|
283
|
+
`\`data.${key}\` names \`${encodeAddresses(value)}\`, and no asset in this package or in ` +
|
|
284
|
+
`a fetched index carries that address — the document takes its default art ` +
|
|
285
|
+
`instead`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* What an art address naming a type this slot does not accept is reported as.
|
|
291
|
+
*
|
|
292
|
+
* An **error**, not a warning: unlike an address nothing answers, this one
|
|
293
|
+
* names a real type, and no default art repairs an author asking for a sound
|
|
294
|
+
* where the slot takes an icon or an image.
|
|
295
|
+
*
|
|
296
|
+
* @param {string} key - The key that was authored.
|
|
297
|
+
* @param {unknown} value - The value it carried, named exactly as written.
|
|
298
|
+
* @param {string} type - The type the address named.
|
|
299
|
+
* @param {Iterable<string>} accepts - The types this slot accepts.
|
|
284
300
|
* @returns {string} The message.
|
|
285
301
|
*/
|
|
286
|
-
export function
|
|
302
|
+
export function unacceptedArtMessage(key, value, type, accepts) {
|
|
287
303
|
return (
|
|
288
|
-
`\`data.${key}\` names \`${
|
|
289
|
-
`
|
|
290
|
-
`document takes its default art instead`
|
|
304
|
+
`\`data.${key}\` names \`${encodeAddresses(value)}\`, whose type \`${type}\` this slot does ` +
|
|
305
|
+
`not accept — it accepts ${[...accepts].join(" or ")}`
|
|
291
306
|
);
|
|
292
307
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One art slot: the key a note authors, and the type a bare value takes.
|
|
16
|
+
*
|
|
17
|
+
* @typedef {object} ArtSlot
|
|
18
|
+
* @property {string} key - The key under `data:`.
|
|
19
|
+
* @property {string} type - The asset type a bare shortcode defaults to.
|
|
20
|
+
* @property {readonly string[]} accepts - The types an authored value may
|
|
21
|
+
* name. **Not the default** — a faith tradition's profile art is a full
|
|
22
|
+
* illustration rather than a game icon, so an `icon` slot's default stays
|
|
23
|
+
* `icon` while its accepted set also takes `image`. A value naming a type
|
|
24
|
+
* outside it is an error, against this set rather than against `type`.
|
|
25
|
+
* @property {boolean} document - Whether the slot reaches a compiled document.
|
|
26
|
+
* @property {string} describe - One line, for the author-facing reference.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* What every art slot accepts, whichever type it defaults to.
|
|
31
|
+
*
|
|
32
|
+
* A sound is not art: `audio` is refused at all four slots, and every slot
|
|
33
|
+
* that defaults to `icon` or to `image` accepts either — the corpus is why:
|
|
34
|
+
* `sohl-kethira-basic` writes a deity's profile art as `icon: image-…` twenty
|
|
35
|
+
* times, one full illustration per faith tradition, and a slot refusing the
|
|
36
|
+
* type it did not default to would refuse that content.
|
|
37
|
+
*
|
|
38
|
+
* @type {readonly string[]}
|
|
39
|
+
*/
|
|
40
|
+
const ART_TYPES = Object.freeze(["icon", "image"]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The four art slots, in the order the specification tabulates them.
|
|
44
|
+
*
|
|
45
|
+
* `banner` is the one that reaches no compiled document: it is the page's hero
|
|
46
|
+
* image, read by the site and by the book's section plates and by nothing else.
|
|
47
|
+
* That is what `document: false` states, and it is why the inert-art check
|
|
48
|
+
* skips it — a key that is *meant* to reach no document is not an inert key.
|
|
49
|
+
*
|
|
50
|
+
* @type {readonly ArtSlot[]}
|
|
51
|
+
*/
|
|
52
|
+
export const ART_SLOTS = Object.freeze([
|
|
53
|
+
Object.freeze({
|
|
54
|
+
key: "icon",
|
|
55
|
+
type: "icon",
|
|
56
|
+
accepts: ART_TYPES,
|
|
57
|
+
document: true,
|
|
58
|
+
describe: "The document's profile art, resolved into `img`.",
|
|
59
|
+
}),
|
|
60
|
+
Object.freeze({
|
|
61
|
+
key: "tokenIcon",
|
|
62
|
+
type: "icon",
|
|
63
|
+
accepts: ART_TYPES,
|
|
64
|
+
document: true,
|
|
65
|
+
describe: "What a token on the canvas wears; unset, it follows `icon`.",
|
|
66
|
+
}),
|
|
67
|
+
Object.freeze({
|
|
68
|
+
key: "bgImage",
|
|
69
|
+
type: "image",
|
|
70
|
+
accepts: ART_TYPES,
|
|
71
|
+
document: true,
|
|
72
|
+
describe: "A map's background art, resolved into `background.src`.",
|
|
73
|
+
}),
|
|
74
|
+
Object.freeze({
|
|
75
|
+
key: "banner",
|
|
76
|
+
type: "image",
|
|
77
|
+
accepts: ART_TYPES,
|
|
78
|
+
document: false,
|
|
79
|
+
describe: "The page's hero image. Reaches no compiled document.",
|
|
80
|
+
}),
|
|
81
|
+
]);
|
package/engine/base-compiler.mjs
CHANGED
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
* @module
|
|
62
62
|
*/
|
|
63
63
|
|
|
64
|
+
import { authoredFrontmatter } from "./index-records.mjs";
|
|
65
|
+
import { cloneAddressState } from "./address-values.mjs";
|
|
64
66
|
import fs from "fs";
|
|
65
67
|
import path from "path";
|
|
66
68
|
import log from "loglevel";
|
|
@@ -77,7 +79,8 @@ import {
|
|
|
77
79
|
// The record accessors only — see `engine/index-records.mjs` for why they live
|
|
78
80
|
// apart from the index that builds them.
|
|
79
81
|
import { isNoteRecord, noteFile } from "./index-records.mjs";
|
|
80
|
-
import {
|
|
82
|
+
import { reduceAddressFields } from "./address-fields.mjs";
|
|
83
|
+
import { artPathname, artSlot, unacceptedArtMessage, unresolvedArtMessage } from "./art-fields.mjs";
|
|
81
84
|
import { emitDiagnostic } from "./diagnostics.mjs";
|
|
82
85
|
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
83
86
|
import { assertNoDeclaredFolder } from "./folder-notes.mjs";
|
|
@@ -660,23 +663,73 @@ export class BasePackCompiler {
|
|
|
660
663
|
*
|
|
661
664
|
* An address nothing answers is reported against the note and treated as
|
|
662
665
|
* unnamed, so the document takes its default rather than shipping a path
|
|
663
|
-
* that installs nowhere.
|
|
666
|
+
* that installs nowhere. **A type this slot does not accept is different**:
|
|
667
|
+
* the address is real, so no default repairs it, and it is an error rather
|
|
668
|
+
* than a fallback.
|
|
664
669
|
*
|
|
665
670
|
* @param {unknown} value - The value as authored.
|
|
666
671
|
* @param {string} key - The key it was authored at, for the message.
|
|
667
672
|
* @param {string} type - The asset type a bare value takes.
|
|
673
|
+
* @param {Iterable<string>} [accepts] - The types this slot accepts.
|
|
668
674
|
* @returns {string|null} The Foundry-relative path, `""` for a deliberate
|
|
669
675
|
* blank, or `null` where the note names none.
|
|
670
676
|
*/
|
|
671
|
-
artPathOf(value, key, type) {
|
|
672
|
-
const {
|
|
677
|
+
artPathOf(value, key, type, accepts) {
|
|
678
|
+
const {
|
|
679
|
+
pathname,
|
|
680
|
+
resolved,
|
|
681
|
+
reason,
|
|
682
|
+
type: named,
|
|
683
|
+
} = artPathname(this.linkIndex, value, type, accepts);
|
|
673
684
|
if (!resolved) {
|
|
674
|
-
|
|
685
|
+
if (reason === "not-accepted") {
|
|
686
|
+
this.errorCount++;
|
|
687
|
+
this.noteError(unacceptedArtMessage(key, value, named, accepts));
|
|
688
|
+
} else {
|
|
689
|
+
this.noteWarn(unresolvedArtMessage(key, value));
|
|
690
|
+
}
|
|
675
691
|
return null;
|
|
676
692
|
}
|
|
677
693
|
return resolveImg(pathname);
|
|
678
694
|
}
|
|
679
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Reduce every Address an assembled `system` block holds to the Shortcode
|
|
698
|
+
* the document reads.
|
|
699
|
+
*
|
|
700
|
+
* The positions come from the type's own field declarations, so a system
|
|
701
|
+
* adding a field that names a note gets the reduction by declaring it; see
|
|
702
|
+
* {@link module:engine/address-fields}.
|
|
703
|
+
*
|
|
704
|
+
* Called **after** the builder and the authored merge have both written, so
|
|
705
|
+
* one step reduces a value the field read and a value authored straight at
|
|
706
|
+
* the destination alike. Each finding is located at the value inside the
|
|
707
|
+
* note, which for a list or a map is the entry that is wrong rather than the
|
|
708
|
+
* key that introduces it.
|
|
709
|
+
*
|
|
710
|
+
* @param {object} emitted - The `system` block, assembled.
|
|
711
|
+
* @param {readonly object[]} [fields] - The type's field declaration.
|
|
712
|
+
* @returns {number} How many findings were reported.
|
|
713
|
+
*/
|
|
714
|
+
reduceEmittedAddresses(emitted, fields) {
|
|
715
|
+
const findings = reduceAddressFields(emitted, fields ?? [], {
|
|
716
|
+
package: this.linkIndex?.contentPackage,
|
|
717
|
+
types: this.linkIndex?.types,
|
|
718
|
+
packages: this.linkIndex?.packages,
|
|
719
|
+
noIndexPackages: this.linkIndex?.noIndexPackages,
|
|
720
|
+
referenceTargets: this.linkIndex?.referenceTargets,
|
|
721
|
+
foreignReferences: this.linkIndex?.foreignReferences,
|
|
722
|
+
});
|
|
723
|
+
for (const finding of findings) {
|
|
724
|
+
this.errorCount++;
|
|
725
|
+
this.noteError(
|
|
726
|
+
finding.message,
|
|
727
|
+
locateFrontmatterKey(this.currentNote?.absPath, finding.key, finding.written),
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
return findings.length;
|
|
731
|
+
}
|
|
732
|
+
|
|
680
733
|
/**
|
|
681
734
|
* The Foundry path one art slot of a note names, or `null`.
|
|
682
735
|
*
|
|
@@ -690,7 +743,7 @@ export class BasePackCompiler {
|
|
|
690
743
|
const data = fm?.data;
|
|
691
744
|
const value =
|
|
692
745
|
data && typeof data === "object" && !Array.isArray(data) ? data[key] : undefined;
|
|
693
|
-
return this.artPathOf(value, key, slot.type);
|
|
746
|
+
return this.artPathOf(value, key, slot.type, slot.accepts);
|
|
694
747
|
}
|
|
695
748
|
|
|
696
749
|
/**
|
|
@@ -1015,7 +1068,8 @@ export class BasePackCompiler {
|
|
|
1015
1068
|
for (const record of this.corpus.records) {
|
|
1016
1069
|
if (!isNoteRecord(record)) continue;
|
|
1017
1070
|
const absPath = noteFile(this.contentBase, record);
|
|
1018
|
-
const {
|
|
1071
|
+
const { body, bodyLine, bodyColumn } = parseMarkdownFile(absPath);
|
|
1072
|
+
const fm = cloneAddressState(authoredFrontmatter(record));
|
|
1019
1073
|
// Which note this pass is on, so anything it calls can report a
|
|
1020
1074
|
// position without every method having to be handed one.
|
|
1021
1075
|
this.currentNote = { absPath, bodyLine, bodyColumn };
|
package/engine/bundle-notes.mjs
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { isAddressTuple } from "./address.mjs";
|
|
15
|
+
|
|
14
16
|
/**
|
|
15
17
|
* Bundles, as notes — the Foundry `Adventure` a `type: bundle` note compiles
|
|
16
18
|
* into.
|
|
@@ -114,8 +116,8 @@ export function stripAdventureKeys(value) {
|
|
|
114
116
|
/**
|
|
115
117
|
* An authored address with any wikilink brackets and label stripped.
|
|
116
118
|
*
|
|
117
|
-
* The specification types `contents` as
|
|
118
|
-
*
|
|
119
|
+
* The specification types `contents` as an `Address[]`, written bare — but
|
|
120
|
+
* `[[address]]` is what an author reaches
|
|
119
121
|
* for, and it is what a folder note's `parent` already accepts. Accepting both
|
|
120
122
|
* costs one regex and removes a failure whose message would have to explain the
|
|
121
123
|
* difference.
|
|
@@ -124,6 +126,7 @@ export function stripAdventureKeys(value) {
|
|
|
124
126
|
* @returns {string|null} The bare address, or `null` for a blank entry.
|
|
125
127
|
*/
|
|
126
128
|
export function bareAddress(value) {
|
|
129
|
+
if (isAddressTuple(value)) return value;
|
|
127
130
|
if (value == null) return null;
|
|
128
131
|
const text = String(value).trim();
|
|
129
132
|
if (!text) return null;
|
package/engine/bundles.mjs
CHANGED
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
* @module
|
|
56
56
|
*/
|
|
57
57
|
|
|
58
|
+
import { isAddressTuple, resolveItemDocType } from "./address.mjs";
|
|
59
|
+
import { encodeAddresses } from "./address-values.mjs";
|
|
60
|
+
|
|
58
61
|
import fs from "node:fs";
|
|
59
62
|
import path from "node:path";
|
|
60
63
|
|
|
@@ -214,7 +217,7 @@ export class Bundles extends BasePackCompiler {
|
|
|
214
217
|
const read = readQualifier(address, index.types, index.packages);
|
|
215
218
|
if (!read || read.reason) {
|
|
216
219
|
throw new Error(
|
|
217
|
-
`bundle "${bundleName}" lists "${address}", which is not an ` +
|
|
220
|
+
`bundle "${bundleName}" lists "${encodeAddresses(address)}", which is not an ` +
|
|
218
221
|
`address — a member is named "<type>-<shortcode>", the ` +
|
|
219
222
|
`same form a wikilink uses`,
|
|
220
223
|
);
|
|
@@ -224,7 +227,7 @@ export class Bundles extends BasePackCompiler {
|
|
|
224
227
|
// is a different fact from a typo and is worth its own sentence.
|
|
225
228
|
if (read.package && read.package !== contentPackage()) {
|
|
226
229
|
throw new Error(
|
|
227
|
-
`bundle "${bundleName}" lists "${address}", which belongs to ` +
|
|
230
|
+
`bundle "${bundleName}" lists "${encodeAddresses(address)}", which belongs to ` +
|
|
228
231
|
`package "${read.package}" — an Adventure carries copies ` +
|
|
229
232
|
`of compiled documents, so a bundle can only hold this ` +
|
|
230
233
|
`package's own`,
|
|
@@ -232,7 +235,7 @@ export class Bundles extends BasePackCompiler {
|
|
|
232
235
|
}
|
|
233
236
|
if (read.type === FOLDER_TYPE) {
|
|
234
237
|
throw new Error(
|
|
235
|
-
`bundle "${bundleName}" lists the folder "${address}". A ` +
|
|
238
|
+
`bundle "${bundleName}" lists the folder "${encodeAddresses(address)}". A ` +
|
|
236
239
|
`folder is not a member: it materialises in every pack ` +
|
|
237
240
|
`holding something filed in it, so it belongs to no ` +
|
|
238
241
|
`one pack and there is no single copy to bundle. List the ` +
|
|
@@ -241,15 +244,19 @@ export class Bundles extends BasePackCompiler {
|
|
|
241
244
|
}
|
|
242
245
|
if (read.type === HOMEPAGE_TYPE || read.type === BUNDLE_TYPE) {
|
|
243
246
|
throw new Error(
|
|
244
|
-
`bundle "${bundleName}" lists "${address}", a ${read.type} — ` +
|
|
247
|
+
`bundle "${bundleName}" lists "${encodeAddresses(address)}", a ${read.type} — ` +
|
|
245
248
|
`which compiles into no document an Adventure can hold`,
|
|
246
249
|
);
|
|
247
250
|
}
|
|
248
251
|
|
|
249
|
-
const
|
|
252
|
+
const baseType =
|
|
253
|
+
isAddressTuple(read) ? resolveItemDocType(read.type, index.types) : undefined;
|
|
254
|
+
const itemDoc = read.itemDoc || Boolean(baseType);
|
|
255
|
+
const type = baseType ?? read.type;
|
|
256
|
+
const note = index.byShortcode.get(`${type}/${read.shortcode}`);
|
|
250
257
|
if (!note) {
|
|
251
258
|
throw new Error(
|
|
252
|
-
`bundle "${bundleName}" lists "${address}", which no note in ` +
|
|
259
|
+
`bundle "${bundleName}" lists "${encodeAddresses(address)}", which no note in ` +
|
|
253
260
|
`this tree publishes`,
|
|
254
261
|
);
|
|
255
262
|
}
|
|
@@ -257,8 +264,8 @@ export class Bundles extends BasePackCompiler {
|
|
|
257
264
|
// The note's own document, which is the router's rule for `pack:` too.
|
|
258
265
|
// Its documentation journal is a second document with a second address,
|
|
259
266
|
// and is bundled only when the note names that one.
|
|
260
|
-
const docType =
|
|
261
|
-
const id =
|
|
267
|
+
const docType = itemDoc ? "JournalEntry" : packForType(type).docType;
|
|
268
|
+
const id = itemDoc ? itemDocEntryId(note.id) : note.id;
|
|
262
269
|
|
|
263
270
|
return { docType, id, name: note.name ?? address };
|
|
264
271
|
}
|
|
@@ -286,7 +293,7 @@ export class Bundles extends BasePackCompiler {
|
|
|
286
293
|
// away. See `missingMemberVerdict`.
|
|
287
294
|
if (missingMemberVerdict(this.packSystem) === "omit") {
|
|
288
295
|
this.noteWarn(
|
|
289
|
-
`bundle "${name}" leaves out "${address}": it publishes ` +
|
|
296
|
+
`bundle "${name}" leaves out "${encodeAddresses(address)}": it publishes ` +
|
|
290
297
|
`no ${this.packSystem} ${member.docType}, and pack ` +
|
|
291
298
|
`"${this.packName}" declares ` +
|
|
292
299
|
`\`system: ${this.packSystem}\``,
|
|
@@ -294,7 +301,7 @@ export class Bundles extends BasePackCompiler {
|
|
|
294
301
|
continue;
|
|
295
302
|
}
|
|
296
303
|
throw new Error(
|
|
297
|
-
`bundle "${name}" lists "${address}", which resolves to ` +
|
|
304
|
+
`bundle "${name}" lists "${encodeAddresses(address)}", which resolves to ` +
|
|
298
305
|
`${member.docType} ${member.id} — and no compiled ` +
|
|
299
306
|
`${member.docType} pack holds it. The note publishes ` +
|
|
300
307
|
`nothing into a pack this bundle can read`,
|