@heroiclands/package-build 20.3.1 → 20.5.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 +351 -0
- package/CONTENT.md +322 -110
- package/README.md +21 -10
- package/bin/content-build.mjs +204 -92
- package/bin/package-build.mjs +48 -15
- package/config.mjs +6 -8
- package/content-config.mjs +518 -151
- package/docs/api.md +1352 -0
- package/docs/commands.md +1609 -0
- package/docs/configuration.md +1432 -0
- package/docs/content-format.md +73 -74
- package/docs/diagnostics.md +356 -0
- package/docs/getting-started.md +813 -0
- package/docs/project-setup.md +469 -0
- package/e2e.mjs +4 -4
- package/engine/actor-compiler.mjs +64 -61
- package/engine/address-charset.mjs +6 -6
- package/engine/address-diff.mjs +57 -52
- package/engine/anchored-sections.mjs +1 -1
- package/engine/anchors.mjs +1 -1
- package/engine/base-compiler.mjs +42 -38
- package/engine/bundle-notes.mjs +4 -4
- package/engine/bundles.mjs +17 -8
- package/engine/code-fences.mjs +3 -3
- package/engine/compendiums.mjs +8 -8
- package/engine/compile-corpus.mjs +4 -5
- package/engine/content-address.mjs +24 -24
- package/engine/content-charset.mjs +1 -1
- package/engine/content-format-check.mjs +24 -25
- package/engine/content-format.mjs +8 -8
- package/engine/content-html.mjs +202 -0
- package/engine/content-icons.mjs +196 -165
- package/engine/content-index.mjs +61 -40
- package/engine/content-links.mjs +74 -41
- package/engine/content-lint.mjs +64 -34
- package/engine/content-package.mjs +7 -9
- package/engine/content-slug.mjs +2 -2
- package/engine/content-tables.mjs +38 -34
- package/engine/content-tree.mjs +1 -1
- package/engine/derived-fields.mjs +174 -0
- package/engine/diagnostics.mjs +3 -4
- package/engine/document-subtypes.mjs +6 -7
- package/engine/field-reference.mjs +8 -8
- package/engine/field-spec.mjs +29 -30
- package/engine/folder-notes.mjs +18 -16
- package/engine/foreign-catalog.mjs +8 -9
- package/engine/foundry-entries.mjs +19 -20
- package/engine/frontmatter-lint.mjs +95 -103
- package/engine/frontmatter.mjs +11 -11
- package/engine/generate.mjs +44 -39
- package/engine/helpers.mjs +147 -84
- package/engine/homepage.mjs +20 -22
- package/engine/ids.mjs +15 -15
- package/engine/index-records.mjs +4 -4
- package/engine/index.mjs +26 -10
- package/engine/item-compiler.mjs +54 -29
- package/engine/item-docs.mjs +11 -13
- package/engine/item-registry.mjs +13 -13
- package/engine/journals.mjs +24 -15
- package/engine/macros.mjs +3 -3
- package/engine/map-notes.mjs +6 -6
- package/engine/metadata-index.mjs +5 -5
- package/engine/note-claims.mjs +51 -41
- package/engine/note-ids.mjs +5 -5
- package/engine/note-package.mjs +4 -4
- package/engine/note-renames.mjs +3 -3
- package/engine/note-schemas.mjs +3 -8
- package/engine/note-vocabulary.mjs +56 -54
- package/engine/pack-config.mjs +39 -25
- package/engine/pack-router.mjs +0 -0
- package/engine/pdf-build.mjs +464 -0
- package/engine/pdf-fonts.mjs +420 -0
- package/engine/pdf-render.mjs +798 -0
- package/engine/pdf-toc.mjs +525 -0
- package/engine/prose-config.mjs +9 -9
- package/engine/prose-lint.mjs +12 -13
- package/engine/region-events.mjs +2 -2
- package/engine/retired-fields.mjs +24 -22
- package/engine/runtime-only-fields.mjs +1 -1
- package/engine/scene-levels.mjs +1 -1
- package/engine/scenes.mjs +33 -25
- package/engine/schema-check.mjs +21 -21
- package/engine/schema-extract.mjs +1 -1
- package/engine/site-build.mjs +66 -50
- package/engine/site-index.mjs +17 -18
- package/engine/sql-tables.mjs +21 -13
- package/engine/subtype-registry.mjs +5 -4
- package/engine/system-block.mjs +18 -19
- package/engine/systems.mjs +2 -2
- package/engine/web-wikilinks.mjs +41 -38
- package/engine/wikilink-syntax.mjs +16 -16
- package/engine/wikilinks.mjs +40 -42
- package/hm3/actors.mjs +30 -22
- package/hm3/document-subtypes.mjs +5 -5
- package/hm3/index.mjs +2 -2
- package/hm3/item-builders.mjs +1 -1
- package/hm3/item-fields.mjs +3 -3
- package/hm3/items.mjs +28 -8
- package/hm3/template-priority.mjs +2 -2
- package/lang.mjs +3 -3
- package/manifest.mjs +17 -20
- package/package.json +1 -2
- package/release.mjs +66 -6
- package/sohl/actors.mjs +36 -28
- package/sohl/affiliation-standings.mjs +2 -2
- package/sohl/being-info.mjs +5 -5
- package/sohl/default-item-art.mjs +5 -5
- package/sohl/document-subtypes.mjs +5 -5
- package/sohl/index.mjs +3 -3
- package/sohl/item-builders.mjs +7 -7
- package/sohl/item-fields.mjs +8 -9
- package/sohl/items.mjs +28 -6
- package/sohl/kb-passes.mjs +5 -5
- package/sohl/note-schemas.mjs +12 -12
- package/sohl/skill-base.mjs +1 -1
- package/types/content-config.d.mts +91 -39
- package/types/e2e.d.mts +3 -3
- package/types/engine/actor-compiler.d.mts +45 -40
- package/types/engine/address-charset.d.mts +6 -6
- package/types/engine/address-diff.d.mts +60 -6
- package/types/engine/base-compiler.d.mts +27 -21
- package/types/engine/bundle-notes.d.mts +3 -3
- package/types/engine/bundles.d.mts +10 -1
- package/types/engine/code-fences.d.mts +3 -3
- package/types/engine/compendiums.d.mts +3 -3
- package/types/engine/compile-corpus.d.mts +1 -1
- package/types/engine/content-address.d.mts +20 -20
- package/types/engine/content-format-check.d.mts +6 -6
- package/types/engine/content-format.d.mts +2 -2
- package/types/engine/content-html.d.mts +78 -0
- package/types/engine/content-icons.d.mts +139 -92
- package/types/engine/content-index.d.mts +59 -15
- package/types/engine/content-links.d.mts +8 -8
- package/types/engine/content-lint.d.mts +8 -6
- package/types/engine/content-package.d.mts +6 -8
- package/types/engine/content-tables.d.mts +49 -18
- package/types/engine/derived-fields.d.mts +101 -0
- package/types/engine/diagnostics.d.mts +2 -2
- package/types/engine/document-subtypes.d.mts +3 -3
- package/types/engine/field-spec.d.mts +37 -39
- package/types/engine/folder-notes.d.mts +4 -5
- package/types/engine/foreign-catalog.d.mts +3 -3
- package/types/engine/foundry-entries.d.mts +6 -7
- package/types/engine/frontmatter-lint.d.mts +10 -9
- package/types/engine/frontmatter.d.mts +7 -7
- package/types/engine/generate.d.mts +7 -7
- package/types/engine/helpers.d.mts +128 -56
- package/types/engine/homepage.d.mts +16 -18
- package/types/engine/ids.d.mts +13 -13
- package/types/engine/index-records.d.mts +3 -3
- package/types/engine/index.d.mts +6 -0
- package/types/engine/item-compiler.d.mts +21 -5
- package/types/engine/item-docs.d.mts +2 -2
- package/types/engine/item-registry.d.mts +6 -6
- package/types/engine/journals.d.mts +12 -3
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/metadata-index.d.mts +4 -4
- package/types/engine/note-claims.d.mts +30 -22
- package/types/engine/note-ids.d.mts +4 -4
- package/types/engine/note-package.d.mts +1 -1
- package/types/engine/note-renames.d.mts +3 -3
- package/types/engine/note-vocabulary.d.mts +31 -203
- package/types/engine/pack-config.d.mts +7 -7
- package/types/engine/pack-router.d.mts +1 -1
- package/types/engine/pdf-build.d.mts +42 -0
- package/types/engine/pdf-fonts.d.mts +30 -0
- package/types/engine/pdf-render.d.mts +144 -0
- package/types/engine/pdf-toc.d.mts +114 -0
- package/types/engine/prose-config.d.mts +9 -9
- package/types/engine/prose-lint.d.mts +3 -4
- package/types/engine/region-events.d.mts +2 -2
- package/types/engine/retired-fields.d.mts +10 -9
- package/types/engine/scenes.d.mts +10 -1
- package/types/engine/schema-check.d.mts +13 -13
- package/types/engine/site-build.d.mts +52 -25
- package/types/engine/site-index.d.mts +3 -4
- package/types/engine/sql-tables.d.mts +11 -5
- package/types/engine/subtype-registry.d.mts +3 -3
- package/types/engine/system-block.d.mts +3 -3
- package/types/engine/web-wikilinks.d.mts +7 -7
- package/types/engine/wikilink-syntax.d.mts +17 -17
- package/types/engine/wikilinks.d.mts +14 -14
- package/types/hm3/actors.d.mts +9 -1
- package/types/hm3/document-subtypes.d.mts +1 -1
- package/types/hm3/items.d.mts +9 -2
- package/types/hm3/template-priority.d.mts +1 -1
- package/types/manifest.d.mts +8 -8
- package/types/release.d.mts +15 -4
- package/types/sohl/actors.d.mts +12 -4
- package/types/sohl/affiliation-standings.d.mts +2 -2
- package/types/sohl/being-info.d.mts +2 -2
- package/types/sohl/document-subtypes.d.mts +1 -1
- package/types/sohl/items.d.mts +9 -2
- package/types/sohl/note-schemas.d.mts +1 -1
- package/MIGRATING.md +0 -608
package/content-config.mjs
CHANGED
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
* packageKind: systems
|
|
23
23
|
* compatibility: { minimum: "14.359", verified: "14.364" }
|
|
24
24
|
* stats:
|
|
25
|
-
* systemId: sohl
|
|
26
25
|
* lastModifiedBy: sohlbuilder00000
|
|
27
26
|
* itemBuilders: sohl
|
|
28
27
|
* skipDirectories: [Templates]
|
|
@@ -42,44 +41,105 @@
|
|
|
42
41
|
* a consumer's config is data, and the compilers read it.
|
|
43
42
|
*
|
|
44
43
|
* **This module validates; it does not load.** `engine/pack-config.mjs` is what
|
|
45
|
-
* finds a repository's configuration and reads it, and it is where the
|
|
44
|
+
* finds a repository's configuration and reads it, and it is where the four
|
|
46
45
|
* fields absent from the YAML above are derived: `rootDir` (the directory the
|
|
47
|
-
* file sits in), `stats.systemVersion` (the adjacent
|
|
48
|
-
* `itemBuilders` table the name `sohl` stands for. All
|
|
49
|
-
* and this module is deliberately neither — which is also
|
|
50
|
-
* item-builder registry is its own writes
|
|
51
|
-
* `defineConfig` below directly with a
|
|
52
|
-
* Both forms end here, so both are
|
|
46
|
+
* file sits in), `foundryPackage` and `stats.systemVersion` (the adjacent
|
|
47
|
+
* `package.json`), and the `itemBuilders` table the name `sohl` stands for. All
|
|
48
|
+
* four are I/O or code, and this module is deliberately neither — which is also
|
|
49
|
+
* why a consumer whose item-builder registry is its own writes
|
|
50
|
+
* `package-build.config.mjs`, calling `defineConfig` below directly with a
|
|
51
|
+
* `rootDir` of `import.meta.dirname`. Both forms end here, so both are
|
|
52
|
+
* validated and frozen identically.
|
|
53
53
|
*
|
|
54
54
|
* **`rootDir` anchors every path**, so the build reads the same files whatever
|
|
55
|
-
* directory it was launched from
|
|
55
|
+
* directory it was launched from.
|
|
56
56
|
*
|
|
57
57
|
* The Foundry floor is declared here as top-level `compatibility`, and the
|
|
58
58
|
* shipped manifest is generated *from* this file. That reverses an older rule —
|
|
59
59
|
* configuration named where the manifest was and read the floor back out of it —
|
|
60
60
|
* which was right while the manifest was hand-authored and became a round trip
|
|
61
|
-
* through a generated artifact once it was not
|
|
61
|
+
* through a generated artifact once it was not.
|
|
62
62
|
*
|
|
63
63
|
* @module
|
|
64
64
|
*/
|
|
65
65
|
|
|
66
|
+
import fs from "node:fs";
|
|
66
67
|
import path from "node:path";
|
|
68
|
+
import YAML from "yaml";
|
|
67
69
|
|
|
68
70
|
// Leaves with no local imports of their own, so naming them here cannot close
|
|
69
71
|
// a cycle around a consumer's config file (see `engine/pack-config.mjs`).
|
|
70
72
|
import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./engine/address-charset.mjs";
|
|
73
|
+
import { EMPTY_ICON_REGISTRY, checkIconRegistry } from "./engine/content-icons.mjs";
|
|
71
74
|
import { MAP_TYPES, PACK_BY_TYPE } from "./engine/ids.mjs";
|
|
72
75
|
import { ACTOR_TYPES } from "./engine/subtype-registry.mjs";
|
|
73
76
|
import { NOTE_VOCABULARY } from "./engine/note-vocabulary.mjs";
|
|
74
77
|
|
|
75
78
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
+
* What kind of package this is.
|
|
80
|
+
*
|
|
81
|
+
* `systems` and `modules` are the two Foundry answers, and the value is also
|
|
82
|
+
* the directory Foundry installs the package under, which is why they are
|
|
83
|
+
* plural. `documentation` is the answer "not a Foundry package at all": it
|
|
84
|
+
* publishes a site and a book from its notes, installs into no Foundry data
|
|
85
|
+
* directory and compiles no compendium.
|
|
79
86
|
*
|
|
80
87
|
* @satisfies {readonly PackageKind[]}
|
|
81
88
|
*/
|
|
82
|
-
export const PACKAGE_KINDS = /** @type {const} */ (["systems", "modules"]);
|
|
89
|
+
export const PACKAGE_KINDS = /** @type {const} */ (["systems", "modules", "documentation"]);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The kind that compiles no Foundry documents.
|
|
93
|
+
*
|
|
94
|
+
* Spelled once and read wherever a pass asks whether it applies, so the
|
|
95
|
+
* validator, the CLI and the compile passes cannot come to disagree about what
|
|
96
|
+
* the value means.
|
|
97
|
+
*
|
|
98
|
+
* @type {string}
|
|
99
|
+
*/
|
|
100
|
+
export const DOCUMENTATION_KIND = "documentation";
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Whether this package compiles Foundry documents at all.
|
|
104
|
+
*
|
|
105
|
+
* The one question every Foundry-side reader asks — the manifest writer, to
|
|
106
|
+
* decide whether there is a package for Foundry to install, and the pack
|
|
107
|
+
* compilers, to decide whether there is anything to compile.
|
|
108
|
+
*
|
|
109
|
+
* @param {{packageKind: string}} config - A resolved configuration.
|
|
110
|
+
* @returns {boolean} Whether the package compiles Foundry documents.
|
|
111
|
+
*/
|
|
112
|
+
export function compilesFoundryDocuments(config) {
|
|
113
|
+
return config.packageKind !== DOCUMENTATION_KIND;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Every key a documentation package may not declare, and why.
|
|
118
|
+
*
|
|
119
|
+
* The value of the kind is as much in what it refuses as in what it accepts. A
|
|
120
|
+
* key here cannot mean anything in a package that compiles nothing and installs
|
|
121
|
+
* nowhere, so it fails at load naming the key — the loader resolves that name
|
|
122
|
+
* to a line and a column — rather than being read and ignored, which is the
|
|
123
|
+
* failure this contract exists to prevent.
|
|
124
|
+
*
|
|
125
|
+
* `foundryPackage` is on the list for the same reason as the rest, and is the
|
|
126
|
+
* one the YAML loader would otherwise supply: it derives the id from the
|
|
127
|
+
* adjacent `package.json`, and there is no Foundry package here to carry one.
|
|
128
|
+
*
|
|
129
|
+
* @type {Readonly<Record<string, string>>}
|
|
130
|
+
*/
|
|
131
|
+
const DOCUMENTATION_REFUSES = Object.freeze({
|
|
132
|
+
packs: "compiles no compendium, so there are no packs to declare",
|
|
133
|
+
itemBuilders: "compiles no items, so there is no item-type registry to name",
|
|
134
|
+
docs: "compiles no items, so there are no item-field reference pages to frame",
|
|
135
|
+
compatibility:
|
|
136
|
+
"installs into no Foundry data directory, so there is no Foundry core range to support",
|
|
137
|
+
relationships: "is not a Foundry package, so it stands in no relationship to one",
|
|
138
|
+
systems: "compiles no documents, so it ships content for no game system",
|
|
139
|
+
requiresSystem: "compiles no documents, so there is no game system to gate its packs on",
|
|
140
|
+
stats: "compiles no documents, so there is no `_stats` block to stamp",
|
|
141
|
+
foundryPackage: "is not a Foundry package, so it has no Foundry package id",
|
|
142
|
+
});
|
|
83
143
|
|
|
84
144
|
/**
|
|
85
145
|
* The directories the build reads from and writes to, relative to `rootDir`,
|
|
@@ -92,7 +152,7 @@ export const DEFAULT_PATHS = /** @type {const} */ ({
|
|
|
92
152
|
// Where `content-index` writes this package's note index. Under `build/`
|
|
93
153
|
// because it is derived and disposable — regenerating it costs a
|
|
94
154
|
// frontmatter parse — and emphatically not under `stage`, which is mirrored
|
|
95
|
-
// into a Foundry data root
|
|
155
|
+
// into a Foundry data root.
|
|
96
156
|
contentIndex: "build/content-index",
|
|
97
157
|
packJson: "build/packs-json",
|
|
98
158
|
stage: "build/stage/packs",
|
|
@@ -100,7 +160,7 @@ export const DEFAULT_PATHS = /** @type {const} */ ({
|
|
|
100
160
|
// Where a dependency declaring `itemCatalog: true` is unpacked. Under
|
|
101
161
|
// `build/` because it is derived, disposable, and version-keyed.
|
|
102
162
|
foreignCache: "build/cache/foreign",
|
|
103
|
-
// Where a dependency's published content index is fetched to
|
|
163
|
+
// Where a dependency's published content index is fetched to. A
|
|
104
164
|
// sibling of the item catalogue rather than a subdirectory of it: the two
|
|
105
165
|
// are fetched for different dependency sets — a catalogue only where
|
|
106
166
|
// `itemCatalog: true` is declared, an index for *every* declared
|
|
@@ -112,7 +172,7 @@ export const DEFAULT_PATHS = /** @type {const} */ ({
|
|
|
112
172
|
/**
|
|
113
173
|
* The Foundry document types a compendium pack may hold. This is the set the
|
|
114
174
|
* toolchain is able to compile a pack of; a document type Foundry supports but
|
|
115
|
-
* this toolchain does not compile is deliberately absent (
|
|
175
|
+
* this toolchain does not compile is deliberately absent (playlists
|
|
116
176
|
* and roll tables are out of scope).
|
|
117
177
|
*
|
|
118
178
|
* @satisfies {readonly PackDocumentType[]}
|
|
@@ -139,15 +199,11 @@ export const PACK_DOCUMENT_TYPES = /** @type {const} */ ([
|
|
|
139
199
|
* key is **refused**, at the line it was written on, with a message that says
|
|
140
200
|
* the mechanism is gone rather than naming a value to correct.
|
|
141
201
|
*
|
|
142
|
-
*
|
|
202
|
+
* **`landing` is one such key.** It named which note addressed a whole section
|
|
143
203
|
* rather than a page within one — a *landing page*, which therefore had no slug
|
|
144
|
-
* of its own.
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* no note lands anything. The key outlived its mechanism by one release only
|
|
148
|
-
* because both publishing consumers still declared the then-true
|
|
149
|
-
* `landing: readme`, and neither breaking them over a correct statement nor
|
|
150
|
-
* accepting the key in silence was acceptable. Neither declares it now.
|
|
204
|
+
* of its own. There are no sections to address: a section is a Hugo content
|
|
205
|
+
* directory the note format does not carry, a page's address names no
|
|
206
|
+
* directory, and so no note lands anything.
|
|
151
207
|
*
|
|
152
208
|
* @type {Readonly<Record<string, string>>}
|
|
153
209
|
*/
|
|
@@ -168,7 +224,7 @@ export const RETIRED_ADDRESS_KEYS = Object.freeze({
|
|
|
168
224
|
* `thalorna`, whose site is nothing but its content. It is not the package's
|
|
169
225
|
* own mount point: where the package itself is served is the consuming build's
|
|
170
226
|
* knowledge, held in `PACKAGE_BASE` (`engine/content-address.mjs`) and prefixed at
|
|
171
|
-
* resolve time, so it is never recorded here
|
|
227
|
+
* resolve time, so it is never recorded here.
|
|
172
228
|
*
|
|
173
229
|
* It is the whole scheme: `landing`, the key that named which note addressed a
|
|
174
230
|
* whole section, is retired with the sections themselves — see
|
|
@@ -183,7 +239,7 @@ export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
|
|
|
183
239
|
*
|
|
184
240
|
* Every HeroicLands package publishes something: a top-level, human-authored
|
|
185
241
|
* homepage at `https://www.heroiclands.org/<contentPackage>/` saying what the
|
|
186
|
-
* module is, which system it needs and how to install it
|
|
242
|
+
* module is, which system it needs and how to install it. So there is no
|
|
187
243
|
* value here meaning *no web presence at all* — homepage-only is the **floor**,
|
|
188
244
|
* and the default.
|
|
189
245
|
*
|
|
@@ -235,7 +291,7 @@ export function publishesContentPages(config) {
|
|
|
235
291
|
}
|
|
236
292
|
|
|
237
293
|
/**
|
|
238
|
-
* @typedef {"systems" | "modules"} PackageKind
|
|
294
|
+
* @typedef {"systems" | "modules" | "documentation"} PackageKind
|
|
239
295
|
*/
|
|
240
296
|
|
|
241
297
|
/**
|
|
@@ -251,7 +307,7 @@ export function publishesContentPages(config) {
|
|
|
251
307
|
* type** receives its document. The two are orthogonal, and both are needed
|
|
252
308
|
* once a repository groups same-type documents editorially — which it may have
|
|
253
309
|
* to, since a compendium UUID carries its pack name and collapsing such a
|
|
254
|
-
* layout breaks every stored reference
|
|
310
|
+
* layout breaks every stored reference.
|
|
255
311
|
*
|
|
256
312
|
* @typedef {object} PackSpec
|
|
257
313
|
* @property {string} name Pack name — the manifest `name`, and the
|
|
@@ -317,7 +373,7 @@ export function publishesContentPages(config) {
|
|
|
317
373
|
* Inbound, and fetched rather than
|
|
318
374
|
* committed.
|
|
319
375
|
* @property {string} [metadataCache] Where a dependency's published content
|
|
320
|
-
* index is fetched to
|
|
376
|
+
* index is fetched to. Inbound,
|
|
321
377
|
* for *every* declared dependency, not
|
|
322
378
|
* only those supplying a catalogue.
|
|
323
379
|
*/
|
|
@@ -473,13 +529,13 @@ export function publishesContentPages(config) {
|
|
|
473
529
|
* `fields` is what makes the type documentable: a builder function says
|
|
474
530
|
* nothing about the vocabulary it consumes, so a consumer that declares its
|
|
475
531
|
* fields can generate its own authoring reference and check its own notes,
|
|
476
|
-
* while one that does not is simply undocumented rather than broken
|
|
532
|
+
* while one that does not is simply undocumented rather than broken.
|
|
477
533
|
*
|
|
478
534
|
* @typedef {((fm: object) => object)|{system: (fm: object) => object, img?: string, fields?: readonly object[]}} ItemBuilderEntry
|
|
479
535
|
*/
|
|
480
536
|
|
|
481
537
|
/**
|
|
482
|
-
* One **registry** of a declared set, and the system it belongs to
|
|
538
|
+
* One **registry** of a declared set, and the system it belongs to.
|
|
483
539
|
*
|
|
484
540
|
* A repository shipping content for two systems declares one of these per
|
|
485
541
|
* system: the accepted type vocabulary is their union, and a type both declare
|
|
@@ -501,10 +557,20 @@ export function publishesContentPages(config) {
|
|
|
501
557
|
* @property {string} contentPackage Content package name — the address
|
|
502
558
|
* namespace every note in this
|
|
503
559
|
* repository is published under.
|
|
504
|
-
* @property {string} foundryPackage
|
|
560
|
+
* @property {string} [foundryPackage] Foundry package id, as it appears in
|
|
505
561
|
* `system.json` / `module.json`.
|
|
506
|
-
*
|
|
507
|
-
*
|
|
562
|
+
* Refused by a `documentation`
|
|
563
|
+
* package, which ships no Foundry
|
|
564
|
+
* package.
|
|
565
|
+
* @property {PackageKind} packageKind Whether the package is a system, a
|
|
566
|
+
* module, or documentation — the kind
|
|
567
|
+
* that publishes a site and a book
|
|
568
|
+
* while compiling nothing.
|
|
569
|
+
* @property {StatsSpec} [stats] Identity stamped into every
|
|
570
|
+
* document's `_stats`. Required of a
|
|
571
|
+
* package that compiles documents, and
|
|
572
|
+
* refused by a `documentation` one,
|
|
573
|
+
* which compiles none.
|
|
508
574
|
* @property {Record<string, ItemBuilderEntry>|readonly ItemRegistrySpec[]} [itemBuilders]
|
|
509
575
|
* The consumer's
|
|
510
576
|
* item-type registry: each content `type`
|
|
@@ -518,14 +584,13 @@ export function publishesContentPages(config) {
|
|
|
518
584
|
* two systems declares a **list** of
|
|
519
585
|
* `{ system, builders }` registries
|
|
520
586
|
* instead, and the accepted type
|
|
521
|
-
* vocabulary is their union
|
|
587
|
+
* vocabulary is their union.
|
|
522
588
|
* @property {PackSpec[]} packs Packs to compile. More than one entry
|
|
523
589
|
* may share a `type`: a note then names
|
|
524
590
|
* the pack it belongs in with its
|
|
525
591
|
* `pack:` frontmatter, and one pack of
|
|
526
592
|
* the type is marked `default: true` to
|
|
527
|
-
* receive the notes that name none
|
|
528
|
-
* (#1566).
|
|
593
|
+
* receive the notes that name none.
|
|
529
594
|
* @property {PathsInput} [paths] Layout overrides. See {@link DEFAULT_PATHS}.
|
|
530
595
|
* @property {string[]} [skipDirectories] Directory names the content walk ignores
|
|
531
596
|
* wherever they appear (e.g. Obsidian's
|
|
@@ -554,12 +619,20 @@ export function publishesContentPages(config) {
|
|
|
554
619
|
* @typedef {object} ContentBuildConfig
|
|
555
620
|
* @property {string} rootDir
|
|
556
621
|
* @property {string} contentPackage
|
|
557
|
-
* @property {string} foundryPackage
|
|
622
|
+
* @property {string|null} foundryPackage `null` for a `documentation`
|
|
623
|
+
* package, which ships no Foundry package.
|
|
558
624
|
* @property {PackageKind} packageKind
|
|
559
|
-
* @property {string} assetRoot
|
|
560
|
-
*
|
|
625
|
+
* @property {string|null} assetRoot Derived, and **conditional**: the served
|
|
626
|
+
* Foundry asset root,
|
|
627
|
+
* `<packageKind>/<foundryPackage>/assets`,
|
|
628
|
+
* for a package Foundry installs — and
|
|
629
|
+
* `null` for a `documentation` package,
|
|
630
|
+
* which Foundry serves no files for. See
|
|
631
|
+
* {@link module:engine/helpers.resolveImg},
|
|
632
|
+
* the one reader of it.
|
|
561
633
|
* @property {Readonly<ResolvedPaths>} paths
|
|
562
|
-
* @property {Readonly<StatsSpec
|
|
634
|
+
* @property {Readonly<StatsSpec>|null} stats `null` for a `documentation`
|
|
635
|
+
* package, which stamps no `_stats`.
|
|
563
636
|
* @property {Readonly<Record<string, Function>>} itemBuilders Derived: the
|
|
564
637
|
* `system` builder of each entry, whichever
|
|
565
638
|
* of the two spellings declared it.
|
|
@@ -571,12 +644,11 @@ export function publishesContentPages(config) {
|
|
|
571
644
|
* the frontmatter fields each entry
|
|
572
645
|
* declared. Sparse, like `itemArt` — a type
|
|
573
646
|
* absent here compiles normally and is
|
|
574
|
-
* simply undocumented
|
|
647
|
+
* simply undocumented.
|
|
575
648
|
* @property {Readonly<Record<string, Readonly<Record<string, Function>>>>} itemBuildersBySystem
|
|
576
649
|
* Derived: the same builders, kept per
|
|
577
650
|
* declaring system. `{}` for the single
|
|
578
|
-
* registry form, which names no system
|
|
579
|
-
* (#58).
|
|
651
|
+
* registry form, which names no system.
|
|
580
652
|
* @property {Readonly<Record<string, Readonly<Record<string, string>>>>} itemArtBySystem
|
|
581
653
|
* Derived: the default art, per system.
|
|
582
654
|
* @property {Readonly<Record<string, Readonly<Record<string, readonly object[]>>>>} itemFieldsBySystem
|
|
@@ -589,13 +661,17 @@ export function publishesContentPages(config) {
|
|
|
589
661
|
* {@link ContentBuildConfigInput.itemBuilders},
|
|
590
662
|
* unioned across every declared registry, so
|
|
591
663
|
* the accepted item types and the builder
|
|
592
|
-
* tables are one list
|
|
664
|
+
* tables are one list.
|
|
593
665
|
* @property {ReadonlySet<string>} docEntryTypes Derived: every type whose prose
|
|
594
666
|
* compiles into a JournalEntry of its own —
|
|
595
667
|
* the item types, plus `macro`, plus the map
|
|
596
668
|
* types. The one set the compilers and the
|
|
597
669
|
* link-manifest emitter both read.
|
|
598
670
|
* @property {readonly string[]} skipDirectories
|
|
671
|
+
* @property {import("./engine/content-icons.mjs").IconRegistry} icons The
|
|
672
|
+
* fonts this package ships and the names it
|
|
673
|
+
* draws from them; empty when it declares
|
|
674
|
+
* none.
|
|
599
675
|
* @property {readonly Readonly<ResolvedPackSpec>[]} packs
|
|
600
676
|
* @property {readonly string[]} packDirectories Derived: every pack directory
|
|
601
677
|
* the build produces, in compile order —
|
|
@@ -618,9 +694,11 @@ const CONFIG_KEYS = [
|
|
|
618
694
|
"itemBuilders",
|
|
619
695
|
"paths",
|
|
620
696
|
"skipDirectories",
|
|
697
|
+
"icons",
|
|
621
698
|
"packs",
|
|
622
699
|
"docs",
|
|
623
700
|
"site",
|
|
701
|
+
"pdf",
|
|
624
702
|
"compatibility",
|
|
625
703
|
"relationships",
|
|
626
704
|
"systems",
|
|
@@ -644,6 +722,9 @@ const SITE_KEYS = [
|
|
|
644
722
|
"backfillSections",
|
|
645
723
|
];
|
|
646
724
|
const SITE_TREE_KEYS = ["from", "section"];
|
|
725
|
+
const PDF_KEYS = ["title", "subtitle", "document", "out", "front", "fonts", "iconFonts", "binary"];
|
|
726
|
+
const PDF_FONT_KEYS = ["serif", "sans", "mono", "path"];
|
|
727
|
+
const EMPTY_PDF_FONTS = Object.freeze({ serif: "", sans: "", mono: "", path: "" });
|
|
647
728
|
const SECTION_META_KEYS = ["title", "banner", "description", "listType", "listSubType"];
|
|
648
729
|
const DOC_PAGE_KEYS = ["title", "out", "preamble"];
|
|
649
730
|
const RELATIONSHIP_KINDS = ["systems", "requires", "recommends", "conflicts"];
|
|
@@ -668,7 +749,7 @@ const STATS_KEYS = ["lastModifiedBy"];
|
|
|
668
749
|
* How the loader hands {@link defineConfig} the system version it resolved.
|
|
669
750
|
*
|
|
670
751
|
* A **Symbol**, deliberately. `stats.systemVersion` is refused from an authored
|
|
671
|
-
* configuration
|
|
752
|
+
* configuration, but the value still has to reach here from the loader —
|
|
672
753
|
* which is the half that may do I/O, and which reads a system package's version
|
|
673
754
|
* out of the adjacent `package.json`. A string key would be a second spelling of
|
|
674
755
|
* the refused one, forgeable from YAML and reachable by `rejectUnknownKeys`; a
|
|
@@ -692,7 +773,7 @@ function isPlainObject(value) {
|
|
|
692
773
|
* The dotted path is carried on the error as `field` as well as spelled into
|
|
693
774
|
* the message, because the message alone is a good description and a bad
|
|
694
775
|
* locator: the loader that read the file can resolve that path to a line and
|
|
695
|
-
* column, and does (`locateConfigError` in `engine/pack-config.mjs
|
|
776
|
+
* column, and does (`locateConfigError` in `engine/pack-config.mjs`).
|
|
696
777
|
* Attaching it here rather than formatting here is what keeps this module
|
|
697
778
|
* free of I/O — it is the leaf an `.mjs` configuration imports, so it may not
|
|
698
779
|
* reach for the file it is validating.
|
|
@@ -739,14 +820,15 @@ function requireNonEmptyString(value, field) {
|
|
|
739
820
|
* It is the first segment of every canonical address this repository publishes
|
|
740
821
|
* (`package-system-type-shortcode`, so `sohl-none-doc-gear`), and an address is
|
|
741
822
|
* read by counting hyphen-separated segments. So the value carries two
|
|
742
|
-
* obligations that the rest of the configuration does not, and
|
|
823
|
+
* obligations that the rest of the configuration does not, and asks for
|
|
743
824
|
* both to be **enforced rather than assumed** — the alternative is a package
|
|
744
825
|
* whose addresses are simply unreadable, reported nowhere and discovered as
|
|
745
826
|
* links that resolve to nothing.
|
|
746
827
|
*
|
|
747
|
-
* 1.
|
|
748
|
-
* was the one violator, and its keys
|
|
749
|
-
* as
|
|
828
|
+
* 1. _Lowercase alphanumeric_ (`ADDRESS_SEGMENT_PATTERN`), so the hyphen stays
|
|
829
|
+
* purely a separator. `harn-adventures` was the one violator, and its keys
|
|
830
|
+
* read as one segment too many and failed as a `null` return from
|
|
831
|
+
* `readCanonicalKey` — a silence, not an error.
|
|
750
832
|
* 2. _Not a note type_, because a written address is a **partial** one: the
|
|
751
833
|
* shorter forms drop segments from the left, so `skill-clmb` and
|
|
752
834
|
* `sohl-skill-clmb` are both addresses and position alone no longer says
|
|
@@ -754,9 +836,9 @@ function requireNonEmptyString(value, field) {
|
|
|
754
836
|
* by asking whether the name is a known package, and a name in both
|
|
755
837
|
* vocabularies makes one target readable two ways with no defensible pick.
|
|
756
838
|
* Keeping the two disjoint is what lets a name be taken at face value; that
|
|
757
|
-
* the package and the type are
|
|
758
|
-
*
|
|
759
|
-
*
|
|
839
|
+
* the package and the type are not *adjacent* segments, the system sitting
|
|
840
|
+
* between them, changes nothing: the hazard is not adjacency, it is that a
|
|
841
|
+
* short form omits the slots in between.
|
|
760
842
|
* One such collision is structural and cannot be fixed — `sohl` is both a
|
|
761
843
|
* content package and a system id, because Foundry requires a system
|
|
762
844
|
* package's id to *be* its system id, and `sohl-sohl-skill-clmb` is the
|
|
@@ -775,11 +857,12 @@ function requireContentPackage(value, docEntryTypes) {
|
|
|
775
857
|
if (!isAddressSegment(pkg)) {
|
|
776
858
|
fail(
|
|
777
859
|
"contentPackage",
|
|
778
|
-
`is \`${pkg}\`, which is not alphanumeric
|
|
860
|
+
`is \`${pkg}\`, which is not lowercase alphanumeric ` +
|
|
861
|
+
`(${ADDRESS_SEGMENT_PATTERN.source}). It is the first ` +
|
|
779
862
|
`segment of every address this package publishes ` +
|
|
780
863
|
`(\`${pkg}-<system>-<type>-<shortcode>\`), and an address is read by ` +
|
|
781
864
|
`counting hyphen-separated segments — so anything outside ` +
|
|
782
|
-
"
|
|
865
|
+
"that here makes those addresses unreadable rather " +
|
|
783
866
|
"than merely ugly. `harn-adventures` became `harnadventures`",
|
|
784
867
|
);
|
|
785
868
|
}
|
|
@@ -850,7 +933,7 @@ function optionalString(value, field) {
|
|
|
850
933
|
function normalizePack(value, where, nested = false) {
|
|
851
934
|
if (!isPlainObject(value)) fail(where, "must be an object");
|
|
852
935
|
const pack = /** @type {Record<string, unknown>} */ (value);
|
|
853
|
-
// Retired with the YAML it named
|
|
936
|
+
// Retired with the YAML it named. Refused explicitly rather than
|
|
854
937
|
// left to the unknown-key check, because the useful thing to say is not
|
|
855
938
|
// "no such key" but where the folders went: they are notes, and a pack
|
|
856
939
|
// materialises the ones its documents reference.
|
|
@@ -955,6 +1038,128 @@ function normalizePack(value, where, nested = false) {
|
|
|
955
1038
|
return Object.freeze(normalized);
|
|
956
1039
|
}
|
|
957
1040
|
|
|
1041
|
+
/**
|
|
1042
|
+
* A package's icon registry — the fonts it ships and the names it draws from
|
|
1043
|
+
* them.
|
|
1044
|
+
*
|
|
1045
|
+
* **Nothing is supplied by default.** A registry entry is a promise that a
|
|
1046
|
+
* glyph will render, and only the package shipping the font can keep it: the
|
|
1047
|
+
* Game-Icons webfont is built by a consumer from its own templates, and Font
|
|
1048
|
+
* Awesome reaches neither the knowledgebase nor a printed page unless somebody
|
|
1049
|
+
* puts it there. A toolchain that shipped a starter table would be promising on
|
|
1050
|
+
* a consumer's behalf, and a name like `victory-star-tester` is one game
|
|
1051
|
+
* system's vocabulary besides.
|
|
1052
|
+
*
|
|
1053
|
+
* So a package declares both halves, and a package that declares neither names
|
|
1054
|
+
* no icons at all.
|
|
1055
|
+
*
|
|
1056
|
+
* **Two spellings, one shape.** The value is either the registry itself:
|
|
1057
|
+
*
|
|
1058
|
+
* ```yaml
|
|
1059
|
+
* icons:
|
|
1060
|
+
* families:
|
|
1061
|
+
* fontawesome: { class: fa, styles: [solid, regular, brands], describe: Font Awesome Free }
|
|
1062
|
+
* icons:
|
|
1063
|
+
* being: { style: solid, icon: user, label: being }
|
|
1064
|
+
* ```
|
|
1065
|
+
*
|
|
1066
|
+
* or a **path to a file holding it**, relative to this configuration:
|
|
1067
|
+
*
|
|
1068
|
+
* ```yaml
|
|
1069
|
+
* icons: assets/icon-registry.yaml
|
|
1070
|
+
* ```
|
|
1071
|
+
*
|
|
1072
|
+
* The file form is the one a real package wants. A registry is derived from
|
|
1073
|
+
* what the interface actually draws, so it is generated rather than hand-kept —
|
|
1074
|
+
* and a generated document inlined into a hand-edited configuration is a merge
|
|
1075
|
+
* conflict on every regeneration. Kept beside it, the generator owns one file
|
|
1076
|
+
* and the configuration owns the other.
|
|
1077
|
+
*
|
|
1078
|
+
* Validated with {@link module:engine/content-icons.checkIconRegistry}, whose
|
|
1079
|
+
* findings are warnings everywhere else and a **refusal** here: elsewhere the
|
|
1080
|
+
* question is whether one note is wrong, and here it is whether the table every
|
|
1081
|
+
* note is read against is.
|
|
1082
|
+
*
|
|
1083
|
+
* @param {unknown} value - The authored `icons:` value.
|
|
1084
|
+
* @param {string} rootDir - The configuration's own directory, which a relative
|
|
1085
|
+
* path is resolved against.
|
|
1086
|
+
* @returns {import("./engine/content-icons.mjs").IconRegistry} The frozen
|
|
1087
|
+
* registry.
|
|
1088
|
+
*/
|
|
1089
|
+
function normalizeIcons(value, rootDir) {
|
|
1090
|
+
if (value === undefined) return EMPTY_ICON_REGISTRY;
|
|
1091
|
+
|
|
1092
|
+
let declared = value;
|
|
1093
|
+
let where = "icons";
|
|
1094
|
+
if (typeof value === "string") {
|
|
1095
|
+
if (!value.trim()) fail("icons", "is empty — name a file, or write the registry inline");
|
|
1096
|
+
const file = path.resolve(rootDir, value);
|
|
1097
|
+
let text;
|
|
1098
|
+
try {
|
|
1099
|
+
text = fs.readFileSync(file, "utf8");
|
|
1100
|
+
} catch {
|
|
1101
|
+
fail("icons", `names ${value}, which cannot be read from ${rootDir}`);
|
|
1102
|
+
}
|
|
1103
|
+
try {
|
|
1104
|
+
declared = YAML.parse(text);
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
fail("icons", `names ${value}, which is not readable YAML: ${err.message}`);
|
|
1107
|
+
}
|
|
1108
|
+
// A finding says which *file* is wrong, not which key of this one.
|
|
1109
|
+
where = value;
|
|
1110
|
+
if (declared === null || declared === undefined) {
|
|
1111
|
+
fail("icons", `names ${value}, which is empty`);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (!isPlainObject(declared)) {
|
|
1116
|
+
fail(
|
|
1117
|
+
"icons",
|
|
1118
|
+
"must be a registry — `families` and `icons` — or a path to a file holding one",
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const families = declared.families ?? {};
|
|
1123
|
+
const icons = declared.icons ?? {};
|
|
1124
|
+
if (!isPlainObject(families)) fail(`${where}.families`, "must be a mapping of name to family");
|
|
1125
|
+
if (!isPlainObject(icons)) fail(`${where}.icons`, "must be a mapping of name to icon entry");
|
|
1126
|
+
|
|
1127
|
+
for (const name of Object.keys(icons)) {
|
|
1128
|
+
// The name a note writes between the colons. Checked here rather than
|
|
1129
|
+
// left to the note, because an entry nothing can name is a silent
|
|
1130
|
+
// no-op: every use of it reports "no such icon" and the table says
|
|
1131
|
+
// otherwise.
|
|
1132
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
|
|
1133
|
+
fail(
|
|
1134
|
+
`${where}.icons.${name}`,
|
|
1135
|
+
"is not a name a note can write — `:icon-…:` takes lowercase " +
|
|
1136
|
+
"letters, digits and hyphens, the charset an address segment uses",
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const defaultFamily = declared.defaultFamily;
|
|
1142
|
+
if (defaultFamily !== undefined) {
|
|
1143
|
+
if (typeof defaultFamily !== "string" || !(defaultFamily in families)) {
|
|
1144
|
+
fail(
|
|
1145
|
+
`${where}.defaultFamily`,
|
|
1146
|
+
`names \`${defaultFamily}\`, which is not one of the declared families`,
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const findings = checkIconRegistry({ families, icons, defaultFamily }, where);
|
|
1152
|
+
if (findings.length) {
|
|
1153
|
+
fail("icons", findings.map((finding) => finding.message).join("; "));
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
return Object.freeze({
|
|
1157
|
+
families: Object.freeze(families),
|
|
1158
|
+
defaultFamily,
|
|
1159
|
+
icons: Object.freeze(icons),
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
|
|
958
1163
|
/**
|
|
959
1164
|
* Resolve the layout a consumer supplies against its `rootDir`, filling every
|
|
960
1165
|
* unnamed directory from {@link DEFAULT_PATHS}.
|
|
@@ -994,7 +1199,9 @@ function normalizePaths(value, rootDir) {
|
|
|
994
1199
|
}
|
|
995
1200
|
|
|
996
1201
|
/**
|
|
997
|
-
* @param {unknown} value
|
|
1202
|
+
* @param {unknown} value - The authored `stats:` block.
|
|
1203
|
+
* @param {{systemId: string, systemVersion: string}} derived - The package-wide
|
|
1204
|
+
* system and the version it stamps against, both derived by the caller.
|
|
998
1205
|
* @returns {Readonly<StatsSpec>}
|
|
999
1206
|
*/
|
|
1000
1207
|
function normalizeStats(value, derived) {
|
|
@@ -1002,7 +1209,7 @@ function normalizeStats(value, derived) {
|
|
|
1002
1209
|
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1003
1210
|
|
|
1004
1211
|
// **`systemId` and `systemVersion` are derived, and authoring a derived
|
|
1005
|
-
// value is an error rather than an override
|
|
1212
|
+
// value is an error rather than an override.** `systems:` is the
|
|
1006
1213
|
// single source: it says which systems this package stamps against, and
|
|
1007
1214
|
// `requiresSystem` — or a lone declared system — says which one the
|
|
1008
1215
|
// package-wide block takes. A system package answers for itself.
|
|
@@ -1115,7 +1322,7 @@ function normalizeDocs(value) {
|
|
|
1115
1322
|
* One section's landing metadata — what a section says about itself on the
|
|
1116
1323
|
* `_index.md` this build generates for it.
|
|
1117
1324
|
*
|
|
1118
|
-
* A generated landing is the *only* place a section can speak, and
|
|
1325
|
+
* A generated landing is the *only* place a section can speak, and it
|
|
1119
1326
|
* is the only place a section **exists**: a content page is addressed
|
|
1120
1327
|
* `(type, shortcode)` and written flat under the mount, so no page creates a
|
|
1121
1328
|
* directory and nothing else makes `<prefix><section>/` answer. This is
|
|
@@ -1127,18 +1334,18 @@ function normalizeDocs(value) {
|
|
|
1127
1334
|
* written fourteen to twenty times per build against a contract every package
|
|
1128
1335
|
* and every section shares. Unbounded there, a mistyped `descrption:` publishes
|
|
1129
1336
|
* into front matter, is read by nobody, and says nothing to anyone — which is
|
|
1130
|
-
* the failure
|
|
1337
|
+
* the same failure, moved one step downstream where no build can
|
|
1131
1338
|
* see it. So the keys are named here, and the writers emit what this produced
|
|
1132
|
-
* rather than transcribing a second list of their own
|
|
1339
|
+
* rather than transcribing a second list of their own.
|
|
1133
1340
|
*
|
|
1134
1341
|
* `banner` and `description` are optional — the hero images are external assets
|
|
1135
1342
|
* and not every section has one, and a section may reasonably have nothing to
|
|
1136
1343
|
* add to its title. Each is left off entirely rather than written as
|
|
1137
1344
|
* `undefined`, which is not a value YAML can carry.
|
|
1138
1345
|
*
|
|
1139
|
-
* **`listType` / `listSubType` say what the section lists
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1346
|
+
* **`listType` / `listSubType` say what the section lists.** A section's
|
|
1347
|
+
* directory holds nothing but the `_index.md` written here, so a layout
|
|
1348
|
+
* reading Hugo's `.Pages` finds
|
|
1142
1349
|
* no members and renders an empty landing. The membership survives in this map
|
|
1143
1350
|
* and nowhere a theme can reach it, so the landing states it and a layout
|
|
1144
1351
|
* substitutes the equivalent `site.RegularPages` query — the same one `sohl`'s
|
|
@@ -1155,7 +1362,7 @@ function normalizeDocs(value) {
|
|
|
1155
1362
|
* Both are checked as **address segments**, which is the trap this came from:
|
|
1156
1363
|
* a section is named for the URL a consumer chose and a subType is an address
|
|
1157
1364
|
* segment, and the two need not agree — `/sohl/kb/user-guide/` is the section,
|
|
1158
|
-
* `userguide` the subType
|
|
1365
|
+
* `userguide` the subType. Copying the section's name into the
|
|
1159
1366
|
* declaration would select no page at all, and an empty landing reported by
|
|
1160
1367
|
* nobody is the failure being fixed. A `listSubType` with no `listType` is
|
|
1161
1368
|
* refused for the same reason: a subType is only distinguishing *within* a
|
|
@@ -1184,7 +1391,7 @@ function normalizeSectionMeta(value, where) {
|
|
|
1184
1391
|
if (!isAddressSegment(segment)) {
|
|
1185
1392
|
fail(
|
|
1186
1393
|
`${where}.${key}`,
|
|
1187
|
-
`is \`${segment}\`, which is not alphanumeric. It names a ` +
|
|
1394
|
+
`is \`${segment}\`, which is not lowercase alphanumeric. It names a ` +
|
|
1188
1395
|
"content type or subType, and those are address segments " +
|
|
1189
1396
|
`(${ADDRESS_SEGMENT_PATTERN.source}) — not the section's ` +
|
|
1190
1397
|
"own name, which is a URL this site chose and need not " +
|
|
@@ -1309,6 +1516,114 @@ function normalizeSite(value) {
|
|
|
1309
1516
|
});
|
|
1310
1517
|
}
|
|
1311
1518
|
|
|
1519
|
+
/**
|
|
1520
|
+
* The `pdf` section — the book the content tree is published as.
|
|
1521
|
+
*
|
|
1522
|
+
* A third surface beside the packs and the website, and the one that is a
|
|
1523
|
+
* **selection** rather than a rendering of everything: `document:` names the
|
|
1524
|
+
* tree that says which notes the volume carries and in what order, because a
|
|
1525
|
+
* book is an editorial act where a site is an index. That file is the
|
|
1526
|
+
* consumer's, parsed by {@link module:engine/pdf-toc.parseDocumentTree}, and
|
|
1527
|
+
* nothing about its shape is validated here — this block says only where it is.
|
|
1528
|
+
*
|
|
1529
|
+
* **Nothing here is an address or a brand.** The title, the subtitle, the front
|
|
1530
|
+
* matter and the faces are every one of them the publishing repository's to
|
|
1531
|
+
* choose, which is the whole reason they are configuration: the engine that
|
|
1532
|
+
* sets the book must be able to set somebody else's book.
|
|
1533
|
+
*
|
|
1534
|
+
* **Declaring the block is not the switch.** Whether a PDF is built at all is
|
|
1535
|
+
* `publish.site` — `content` builds one, `homepage` does not — so a package
|
|
1536
|
+
* cannot end up with two switches that disagree about whether it publishes its
|
|
1537
|
+
* content tree. See {@link publishesContentPages}.
|
|
1538
|
+
*
|
|
1539
|
+
* @param {unknown} value - The `pdf` block, or `undefined`.
|
|
1540
|
+
* @param {string} rootDir - The repository root configured paths resolve against.
|
|
1541
|
+
* @returns {Readonly<object>|null} It, frozen; `null` when the block is absent.
|
|
1542
|
+
*/
|
|
1543
|
+
function normalizePdf(value, rootDir) {
|
|
1544
|
+
if (value === undefined) return null;
|
|
1545
|
+
if (!isPlainObject(value)) fail("pdf", "must be a mapping");
|
|
1546
|
+
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1547
|
+
rejectUnknownKeys(input, PDF_KEYS, "pdf.");
|
|
1548
|
+
|
|
1549
|
+
// Both required, and required together: a document with no tree has nothing
|
|
1550
|
+
// to print, and a tree with no title produces a file whose name and cover
|
|
1551
|
+
// say nothing about what a reader downloaded.
|
|
1552
|
+
const title = requireNonEmptyString(input.title, "pdf.title");
|
|
1553
|
+
const document = requireNonEmptyString(input.document, "pdf.document");
|
|
1554
|
+
|
|
1555
|
+
const front = [];
|
|
1556
|
+
if (input.front !== undefined) {
|
|
1557
|
+
if (!Array.isArray(input.front)) fail("pdf.front", "must be a list of markdown files");
|
|
1558
|
+
input.front.forEach((entry, i) => {
|
|
1559
|
+
front.push(requireNonEmptyString(entry, `pdf.front[${i}]`));
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
let fonts = EMPTY_PDF_FONTS;
|
|
1564
|
+
if (input.fonts !== undefined) {
|
|
1565
|
+
if (!isPlainObject(input.fonts)) fail("pdf.fonts", "must be a mapping");
|
|
1566
|
+
const declared = /** @type {Record<string, unknown>} */ (input.fonts);
|
|
1567
|
+
rejectUnknownKeys(declared, PDF_FONT_KEYS, "pdf.fonts.");
|
|
1568
|
+
fonts = Object.freeze({
|
|
1569
|
+
// Family *names*, not files: the renderer asks the font stack for a
|
|
1570
|
+
// family, and `path` is where it may look beyond the system's own.
|
|
1571
|
+
serif:
|
|
1572
|
+
declared.serif === undefined ?
|
|
1573
|
+
""
|
|
1574
|
+
: requireNonEmptyString(declared.serif, "pdf.fonts.serif"),
|
|
1575
|
+
sans:
|
|
1576
|
+
declared.sans === undefined ?
|
|
1577
|
+
""
|
|
1578
|
+
: requireNonEmptyString(declared.sans, "pdf.fonts.sans"),
|
|
1579
|
+
mono:
|
|
1580
|
+
declared.mono === undefined ?
|
|
1581
|
+
""
|
|
1582
|
+
: requireNonEmptyString(declared.mono, "pdf.fonts.mono"),
|
|
1583
|
+
path:
|
|
1584
|
+
declared.path === undefined ?
|
|
1585
|
+
""
|
|
1586
|
+
: path.resolve(rootDir, requireNonEmptyString(declared.path, "pdf.fonts.path")),
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// Family name to the font file carrying its glyphs, for `:icon-…:`. A file
|
|
1591
|
+
// rather than a codepoint, because the font's own tables are the only
|
|
1592
|
+
// trustworthy source of which glyph a name resolves to — see
|
|
1593
|
+
// {@link module:engine/content-icons}, which states the style and the name
|
|
1594
|
+
// and deliberately holds no codepoints.
|
|
1595
|
+
const iconFonts = {};
|
|
1596
|
+
if (input.iconFonts !== undefined) {
|
|
1597
|
+
if (!isPlainObject(input.iconFonts)) {
|
|
1598
|
+
fail("pdf.iconFonts", "must be a mapping of icon family to font file");
|
|
1599
|
+
}
|
|
1600
|
+
for (const [family, file] of Object.entries(input.iconFonts)) {
|
|
1601
|
+
iconFonts[family] = path.resolve(
|
|
1602
|
+
rootDir,
|
|
1603
|
+
requireNonEmptyString(file, `pdf.iconFonts.${family}`),
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
return Object.freeze({
|
|
1609
|
+
title,
|
|
1610
|
+
subtitle:
|
|
1611
|
+
input.subtitle === undefined ?
|
|
1612
|
+
""
|
|
1613
|
+
: requireNonEmptyString(input.subtitle, "pdf.subtitle"),
|
|
1614
|
+
document: path.resolve(rootDir, document),
|
|
1615
|
+
out: input.out === undefined ? "" : requireNonEmptyString(input.out, "pdf.out"),
|
|
1616
|
+
front: Object.freeze(front.map((f) => path.resolve(rootDir, f))),
|
|
1617
|
+
fonts,
|
|
1618
|
+
iconFonts: Object.freeze(iconFonts),
|
|
1619
|
+
// Where the Typst binary is, when it is not simply `typst` on PATH.
|
|
1620
|
+
// Named rather than bundled: a native compiler would put a
|
|
1621
|
+
// platform-specific binary in the dependency tree of three repositories
|
|
1622
|
+
// that mostly do not build books.
|
|
1623
|
+
binary: input.binary === undefined ? "" : requireNonEmptyString(input.binary, "pdf.binary"),
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1312
1627
|
/**
|
|
1313
1628
|
* Validate a Foundry version range.
|
|
1314
1629
|
*
|
|
@@ -1339,40 +1654,7 @@ function normalizeCompatibility(value, where, requireMinimum = true) {
|
|
|
1339
1654
|
}
|
|
1340
1655
|
|
|
1341
1656
|
/**
|
|
1342
|
-
*
|
|
1343
|
-
*
|
|
1344
|
-
* Only as far as this package needs to read them: enough that a system
|
|
1345
|
-
* relationship can be found and its `verified` version trusted. The rest is
|
|
1346
|
-
* passed through for the manifest generator to emit.
|
|
1347
|
-
*
|
|
1348
|
-
* @param {unknown} value - The `relationships` block, or `undefined`.
|
|
1349
|
-
* @returns {Readonly<Relationships>} It, frozen; `{}` when absent.
|
|
1350
|
-
*/
|
|
1351
|
-
/**
|
|
1352
|
-
* The systems this package can stamp content against — declaration only (#48).
|
|
1353
|
-
*
|
|
1354
|
-
* **Declaring is not requiring, and that separation is the whole point.** The
|
|
1355
|
-
* only place to state a system version used to be `relationships.systems`, and
|
|
1356
|
-
* that list is a *restriction*: Foundry's `supportsSystem` drops a module from
|
|
1357
|
-
* any world whose system it does not name. So a module shipping content for two
|
|
1358
|
-
* systems — `harn-ensemble` ships an HM3 pack, a SoHL pack and a system-neutral
|
|
1359
|
-
* journals pack — had to choose between naming its systems and remaining
|
|
1360
|
-
* loadable, and choosing the second meant stamping no system version at all on
|
|
1361
|
-
* content that certainly has one.
|
|
1362
|
-
*
|
|
1363
|
-
* Naming a system here restricts nothing. {@link normalizeRequiresSystem} is
|
|
1364
|
-
* what restricts, and it is separate and optional.
|
|
1365
|
-
*
|
|
1366
|
-
* Each entry carries the same `compatibility` shape a relationship does, and
|
|
1367
|
-
* `verified` is what a pack stamps: `_stats.systemVersion` records what the
|
|
1368
|
-
* content was *built against*, not the floor it tolerates.
|
|
1369
|
-
*
|
|
1370
|
-
* @param {unknown} value - The declared `systems:` mapping.
|
|
1371
|
-
* @returns {Readonly<Record<string, Readonly<object>>>} Frozen; `{}` when absent.
|
|
1372
|
-
*/
|
|
1373
|
-
/**
|
|
1374
|
-
* The **package-wide** system, or `null` where the configuration names none
|
|
1375
|
-
* (#48).
|
|
1657
|
+
* The **package-wide** system, or `null` where the configuration names none.
|
|
1376
1658
|
*
|
|
1377
1659
|
* A *system* package is its own system, which is true by construction and needs
|
|
1378
1660
|
* no declaration. A *module* takes the one it requires, or the one system it
|
|
@@ -1387,13 +1669,13 @@ function normalizeCompatibility(value, where, requireMinimum = true) {
|
|
|
1387
1669
|
* exists to remove. Several entries have no single answer and get none.
|
|
1388
1670
|
*
|
|
1389
1671
|
* **Written once and read twice**, which is why it is a function rather than the
|
|
1390
|
-
* expression
|
|
1672
|
+
* expression: the value stamped into `stats.systemId` and the
|
|
1391
1673
|
* value a pack's `system:` is validated against are the same fact, and two
|
|
1392
1674
|
* spellings of it would be free to disagree about exactly the case that has no
|
|
1393
1675
|
* answer.
|
|
1394
1676
|
*
|
|
1395
1677
|
* @param {object} parts - The resolved pieces of the configuration.
|
|
1396
|
-
* @param {string} parts.packageKind -
|
|
1678
|
+
* @param {string} parts.packageKind - One of {@link PACKAGE_KINDS}.
|
|
1397
1679
|
* @param {unknown} parts.foundryPackage - The package id.
|
|
1398
1680
|
* @param {string|null} parts.requiresSystem - The declared gate, if any.
|
|
1399
1681
|
* @param {Readonly<Record<string, object>>} parts.systems - The `systems:` block.
|
|
@@ -1416,6 +1698,28 @@ function packageWideSystemId({
|
|
|
1416
1698
|
return null;
|
|
1417
1699
|
}
|
|
1418
1700
|
|
|
1701
|
+
/**
|
|
1702
|
+
* The systems this package can stamp content against — declaration only.
|
|
1703
|
+
*
|
|
1704
|
+
* **Declaring is not requiring, and that separation is the whole point.** The
|
|
1705
|
+
* only other place to state a system version is `relationships.systems`, and
|
|
1706
|
+
* that list is a *restriction*: Foundry's `supportsSystem` drops a module from
|
|
1707
|
+
* any world whose system it does not name. So a module shipping content for two
|
|
1708
|
+
* systems — `harn-ensemble` ships an HM3 pack, a SoHL pack and a system-neutral
|
|
1709
|
+
* journals pack — had to choose between naming its systems and remaining
|
|
1710
|
+
* loadable, and choosing the second meant stamping no system version at all on
|
|
1711
|
+
* content that certainly has one.
|
|
1712
|
+
*
|
|
1713
|
+
* Naming a system here restricts nothing. {@link normalizeRequiresSystem} is
|
|
1714
|
+
* what restricts, and it is separate and optional.
|
|
1715
|
+
*
|
|
1716
|
+
* Each entry carries the same `compatibility` shape a relationship does, and
|
|
1717
|
+
* `verified` is what a pack stamps: `_stats.systemVersion` records what the
|
|
1718
|
+
* content was *built against*, not the floor it tolerates.
|
|
1719
|
+
*
|
|
1720
|
+
* @param {unknown} value - The declared `systems:` mapping.
|
|
1721
|
+
* @returns {Readonly<Record<string, Readonly<object>>>} Frozen; `{}` when absent.
|
|
1722
|
+
*/
|
|
1419
1723
|
function normalizeSystems(value) {
|
|
1420
1724
|
if (value === undefined || value === null) return Object.freeze({});
|
|
1421
1725
|
if (!isPlainObject(value)) fail("systems", "must be a mapping of id to spec");
|
|
@@ -1458,7 +1762,7 @@ function normalizeSystems(value) {
|
|
|
1458
1762
|
}
|
|
1459
1763
|
|
|
1460
1764
|
/**
|
|
1461
|
-
* The one system this package refuses to load without, or `null
|
|
1765
|
+
* The one system this package refuses to load without, or `null`.
|
|
1462
1766
|
*
|
|
1463
1767
|
* The gate half of the split. Naming a system here emits
|
|
1464
1768
|
* `relationships.systems` for it, which is what Foundry's `supportsSystem`
|
|
@@ -1479,6 +1783,16 @@ function normalizeRequiresSystem(value) {
|
|
|
1479
1783
|
return requireNonEmptyString(value, "requiresSystem");
|
|
1480
1784
|
}
|
|
1481
1785
|
|
|
1786
|
+
/**
|
|
1787
|
+
* Validate the declared relationships.
|
|
1788
|
+
*
|
|
1789
|
+
* Only as far as this package needs to read them: enough that a system
|
|
1790
|
+
* relationship can be found and its `verified` version trusted. The rest is
|
|
1791
|
+
* passed through for the manifest generator to emit.
|
|
1792
|
+
*
|
|
1793
|
+
* @param {unknown} value - The `relationships` block, or `undefined`.
|
|
1794
|
+
* @returns {Readonly<Relationships>} It, frozen; `{}` when absent.
|
|
1795
|
+
*/
|
|
1482
1796
|
function normalizeRelationships(value) {
|
|
1483
1797
|
if (value === undefined) return Object.freeze({});
|
|
1484
1798
|
if (!isPlainObject(value)) fail("relationships", "must be a mapping");
|
|
@@ -1557,7 +1871,7 @@ function normalizePackageBuild(value) {
|
|
|
1557
1871
|
* The registry is *code* a consumer supplies — the only place the configuration
|
|
1558
1872
|
* carries any — because the type list and the builder table have to be the same
|
|
1559
1873
|
* list. They were two, and `trait` sat in the whitelist for a release with no
|
|
1560
|
-
* builder behind it
|
|
1874
|
+
* builder behind it.
|
|
1561
1875
|
*
|
|
1562
1876
|
* **An entry may be written two ways**, and the difference is only whether the
|
|
1563
1877
|
* type brings default art:
|
|
@@ -1637,12 +1951,11 @@ function normalizeOneRegistry(value, at) {
|
|
|
1637
1951
|
}
|
|
1638
1952
|
|
|
1639
1953
|
/**
|
|
1640
|
-
* The declared item-builder registries, and the vocabulary their union gives
|
|
1641
|
-
* (#58).
|
|
1954
|
+
* The declared item-builder registries, and the vocabulary their union gives.
|
|
1642
1955
|
*
|
|
1643
1956
|
* **One registry is a ceiling, not a default.** The accepted type list is the
|
|
1644
1957
|
* registry's keys, which is what makes a type impossible to accept without a
|
|
1645
|
-
* builder behind it
|
|
1958
|
+
* builder behind it — and, with one registry, impossible to accept a
|
|
1646
1959
|
* type a *second* system declares. A tree feeding two systems has both:
|
|
1647
1960
|
* `spell`, `invocation` and `psionic` are HM3's, `mysticalability` and
|
|
1648
1961
|
* `projectile` are SoHL's, and `skill` is both systems' under one name and
|
|
@@ -1768,7 +2081,7 @@ function normalizeItemBuilders(value) {
|
|
|
1768
2081
|
}
|
|
1769
2082
|
|
|
1770
2083
|
/**
|
|
1771
|
-
* The publishing mode, refusing
|
|
2084
|
+
* The publishing mode, refusing a boolean.
|
|
1772
2085
|
*
|
|
1773
2086
|
* A boolean is refused rather than mapped onto the nearest mode, because the
|
|
1774
2087
|
* reading `false` invited — *this package has no web presence* — is exactly the
|
|
@@ -1827,7 +2140,7 @@ function normalizePublish(value) {
|
|
|
1827
2140
|
// A retired key is refused by name, ahead of the vocabulary check: reported
|
|
1828
2141
|
// as merely unrecognized it would read as a misspelling of the one key that
|
|
1829
2142
|
// survives, and the author would correct the spelling rather than learn
|
|
1830
|
-
// that the mechanism is gone
|
|
2143
|
+
// that the mechanism is gone.
|
|
1831
2144
|
for (const key of Object.keys(address)) {
|
|
1832
2145
|
if (Object.hasOwn(RETIRED_ADDRESS_KEYS, key)) {
|
|
1833
2146
|
fail(`publish.address.${key}`, RETIRED_ADDRESS_KEYS[key]);
|
|
@@ -1847,7 +2160,7 @@ function normalizePublish(value) {
|
|
|
1847
2160
|
}
|
|
1848
2161
|
if (prefix.startsWith("/")) {
|
|
1849
2162
|
// A leading slash would make the recorded address package-absolute,
|
|
1850
|
-
// which is exactly the site-absolute shape
|
|
2163
|
+
// which is exactly the site-absolute shape this avoids.
|
|
1851
2164
|
fail("publish.address.prefix", "must not begin with a slash");
|
|
1852
2165
|
}
|
|
1853
2166
|
|
|
@@ -1895,12 +2208,48 @@ export function defineConfig(config) {
|
|
|
1895
2208
|
fail("packageKind", `must be one of: ${PACKAGE_KINDS.join(", ")}`);
|
|
1896
2209
|
}
|
|
1897
2210
|
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
2211
|
+
// A documentation package compiles nothing and installs nowhere, so every
|
|
2212
|
+
// key that describes a Foundry package is refused by name — ahead of the
|
|
2213
|
+
// checks below, which each assume a Foundry package is being described.
|
|
2214
|
+
const documentation = packageKind === DOCUMENTATION_KIND;
|
|
2215
|
+
if (documentation) {
|
|
2216
|
+
for (const [key, why] of Object.entries(DOCUMENTATION_REFUSES)) {
|
|
2217
|
+
if (input[key] === undefined) continue;
|
|
2218
|
+
fail(key, `is refused in a \`${DOCUMENTATION_KIND}\` package, which ${why}`);
|
|
2219
|
+
}
|
|
2220
|
+
// Publishing is what a documentation package is *for*, so the floor
|
|
2221
|
+
// every other package may sit at is not available to it: `homepage`
|
|
2222
|
+
// would leave a package that publishes one authored page, builds no
|
|
2223
|
+
// book, and compiles nothing at all.
|
|
2224
|
+
if (!isPlainObject(input.publish)) {
|
|
2225
|
+
fail(
|
|
2226
|
+
"publish",
|
|
2227
|
+
`is required in a \`${DOCUMENTATION_KIND}\` package: publishing ` +
|
|
2228
|
+
"the content tree is the whole of what it does. Write " +
|
|
2229
|
+
"`publish: {site: content}`",
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
const mode = /** @type {Record<string, unknown>} */ (input.publish).site;
|
|
2233
|
+
if (mode !== "content") {
|
|
2234
|
+
fail(
|
|
2235
|
+
"publish.site",
|
|
2236
|
+
`must be \`content\` in a \`${DOCUMENTATION_KIND}\` package — ` +
|
|
2237
|
+
"`homepage` fences the content surfaces off, and a package " +
|
|
2238
|
+
"that compiles nothing and publishes nothing from its tree " +
|
|
2239
|
+
"would produce a single authored page and no book",
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
if (!documentation) {
|
|
2245
|
+
if (!Array.isArray(input.packs)) fail("packs", "must be an array");
|
|
2246
|
+
if (input.packs.length === 0) fail("packs", "must declare at least one pack");
|
|
2247
|
+
}
|
|
2248
|
+
const declaredPacks = Array.isArray(input.packs) ? input.packs : [];
|
|
2249
|
+
const packs = declaredPacks.map((pack, index) => normalizePack(pack, `packs[${index}]`));
|
|
1901
2250
|
|
|
1902
2251
|
// One list, so the compile order and the directory list cannot disagree —
|
|
1903
|
-
//
|
|
2252
|
+
// as `PACK_CONFIGS` and `SOURCE_PACKS` they would be maintained apart.
|
|
1904
2253
|
const packDirectories = packs.flatMap((pack) => [
|
|
1905
2254
|
pack.name,
|
|
1906
2255
|
...pack.companions.map((companion) => companion.name),
|
|
@@ -1913,7 +2262,7 @@ export function defineConfig(config) {
|
|
|
1913
2262
|
seen.add(name);
|
|
1914
2263
|
}
|
|
1915
2264
|
|
|
1916
|
-
// ── systems: declaring, and requiring, are separate decisions
|
|
2265
|
+
// ── systems: declaring, and requiring, are separate decisions ──────
|
|
1917
2266
|
const systems = normalizeSystems(input.systems);
|
|
1918
2267
|
const requiresSystem = normalizeRequiresSystem(input.requiresSystem);
|
|
1919
2268
|
const declaredSystems = new Set(Object.keys(systems));
|
|
@@ -1934,7 +2283,7 @@ export function defineConfig(config) {
|
|
|
1934
2283
|
// A name that resolves to nothing is a build error rather than a
|
|
1935
2284
|
// fall-through, in the spirit the rest of this file already follows: a pack
|
|
1936
2285
|
// stamping a system nobody declared would stamp `undefined`, which is the
|
|
1937
|
-
// plausible lie
|
|
2286
|
+
// plausible lie.
|
|
1938
2287
|
if (requiresSystem !== null && !declaredSystems.has(requiresSystem)) {
|
|
1939
2288
|
fail(
|
|
1940
2289
|
"requiresSystem",
|
|
@@ -1951,14 +2300,14 @@ export function defineConfig(config) {
|
|
|
1951
2300
|
// verified version `statsForPack` reads, or this package's own
|
|
1952
2301
|
// package-wide system, whose stats answer for every pack of it.
|
|
1953
2302
|
//
|
|
1954
|
-
//
|
|
2303
|
+
// Skipping this entirely when `systems:` is empty or absent
|
|
1955
2304
|
// — `declaredSystems.size &&` guarded it — which left the case the
|
|
1956
2305
|
// comment above was written about wide open. `harn-ensemble` declares
|
|
1957
2306
|
// `system: sohl` and `system: hm3` on its packs, no `systems:` block,
|
|
1958
2307
|
// and no package-wide system, so every pack fell through to a
|
|
1959
2308
|
// package-wide stat that is null: 2,513 compiled actors stamped
|
|
1960
2309
|
// `_stats.systemId: null` in a pack that says `system: sohl` on the
|
|
1961
|
-
// line above. That is the plausible lie
|
|
2310
|
+
// line above. That is the plausible lie, reached by the
|
|
1962
2311
|
// one path this check did not cover, and the `requiresSystem` check ten
|
|
1963
2312
|
// lines up already refuses its own version of it in as many words.
|
|
1964
2313
|
if (!declaredSystems.has(pack.system) && pack.system !== packageWide) {
|
|
@@ -1994,7 +2343,7 @@ export function defineConfig(config) {
|
|
|
1994
2343
|
|
|
1995
2344
|
// Several packs of one document type are allowed — editorial grouping of
|
|
1996
2345
|
// same-type documents is ordinary Foundry practice, and collapsing such a
|
|
1997
|
-
// layout breaks every stored compendium UUID
|
|
2346
|
+
// layout breaks every stored compendium UUID. What is not allowed
|
|
1998
2347
|
// is two candidates for the same undeclared note.
|
|
1999
2348
|
const defaultsByType = new Map();
|
|
2000
2349
|
for (const pack of packs) {
|
|
@@ -2018,7 +2367,10 @@ export function defineConfig(config) {
|
|
|
2018
2367
|
requireNonEmptyString(name, `skipDirectories[${index}]`),
|
|
2019
2368
|
);
|
|
2020
2369
|
|
|
2021
|
-
|
|
2370
|
+
// Refused above for a documentation package, so there is nothing to read
|
|
2371
|
+
// and nothing to derive an asset root or a package-wide system from.
|
|
2372
|
+
const foundryPackage =
|
|
2373
|
+
documentation ? null : requireNonEmptyString(input.foundryPackage, "foundryPackage");
|
|
2022
2374
|
|
|
2023
2375
|
const {
|
|
2024
2376
|
itemBuilders,
|
|
@@ -2029,12 +2381,12 @@ export function defineConfig(config) {
|
|
|
2029
2381
|
itemFieldsBySystem,
|
|
2030
2382
|
itemTypesBySeveralSystems,
|
|
2031
2383
|
} = normalizeItemBuilders(input.itemBuilders);
|
|
2032
|
-
// The union across every declared registry
|
|
2384
|
+
// The union across every declared registry — the flat table already
|
|
2033
2385
|
// holds every key any of them declares, so this stays "the registry's keys"
|
|
2034
|
-
// rather than becoming a second list to keep in step
|
|
2386
|
+
// rather than becoming a second list to keep in step.
|
|
2035
2387
|
const itemTypes = Object.freeze(new Set(Object.keys(itemBuilders)));
|
|
2036
2388
|
// Every note that compiles into a *system-bearing* document publishes its
|
|
2037
|
-
// prose as a documentation JournalEntry, and that includes actors
|
|
2389
|
+
// prose as a documentation JournalEntry, and that includes actors.
|
|
2038
2390
|
// A being was the one such note with no `none` address — its only address
|
|
2039
2391
|
// named the Actor — so nothing a prose link wrote could land on its page.
|
|
2040
2392
|
// `doc` stays out for the reason that actually applies to it: its single
|
|
@@ -2050,37 +2402,50 @@ export function defineConfig(config) {
|
|
|
2050
2402
|
packageKind: /** @type {PackageKind} */ (packageKind),
|
|
2051
2403
|
// Foundry serves a package's files from `<kind>/<id>/`, so this is the
|
|
2052
2404
|
// one place `systems/sohl` (or `modules/sohl-thalorna`) is spelled.
|
|
2053
|
-
|
|
2405
|
+
//
|
|
2406
|
+
// **Conditional on the kind.** `documentation` names no directory
|
|
2407
|
+
// Foundry serves, and there is no package id to put under one either, so
|
|
2408
|
+
// the derivation would read `documentation/null/assets` — an address
|
|
2409
|
+
// that resolves nowhere and would be written into every compiled `img`.
|
|
2410
|
+
// `null` says the package has no asset root instead, and
|
|
2411
|
+
// {@link module:engine/helpers.resolveImg} — the only reader — refuses
|
|
2412
|
+
// rather than rooting a path against nothing.
|
|
2413
|
+
assetRoot: documentation ? null : `${packageKind}/${foundryPackage}/assets`,
|
|
2054
2414
|
paths: normalizePaths(input.paths, rootDir),
|
|
2055
|
-
// The package-wide system, derived
|
|
2415
|
+
// The package-wide system, derived. A **system** package is its
|
|
2056
2416
|
// own system, which is true by construction and needs no declaration. A
|
|
2057
2417
|
// **module** takes the one it requires, or the one system it declares
|
|
2058
2418
|
// when there is exactly one; with several and no gate there is no
|
|
2059
2419
|
// package-wide answer, and each pack carries its own.
|
|
2060
|
-
stats:
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2420
|
+
stats:
|
|
2421
|
+
documentation ? null : (
|
|
2422
|
+
normalizeStats(input.stats, {
|
|
2423
|
+
systemId: packageWideSystemId({
|
|
2424
|
+
packageKind,
|
|
2425
|
+
foundryPackage,
|
|
2426
|
+
requiresSystem,
|
|
2427
|
+
systems,
|
|
2428
|
+
relationshipSystems,
|
|
2429
|
+
}),
|
|
2430
|
+
// Derived here where the answer is pure data — the `verified` of
|
|
2431
|
+
// whichever system the package-wide block takes — and supplied by
|
|
2432
|
+
// the loader otherwise. The loader is the half that may do I/O, and
|
|
2433
|
+
// the two cases needing it are a *system* package (its own
|
|
2434
|
+
// `package.json` version) and a module still deriving from
|
|
2435
|
+
// `relationships.systems`.
|
|
2436
|
+
systemVersion:
|
|
2437
|
+
(() => {
|
|
2438
|
+
const id =
|
|
2439
|
+
requiresSystem ??
|
|
2440
|
+
(Object.keys(systems).length === 1 ?
|
|
2441
|
+
Object.keys(systems)[0]
|
|
2442
|
+
: null);
|
|
2443
|
+
return id ? (systems[id]?.compatibility?.verified ?? null) : null;
|
|
2444
|
+
})() ??
|
|
2445
|
+
(isPlainObject(input.stats) ? input.stats[DERIVED_SYSTEM_VERSION] : null) ??
|
|
2446
|
+
null,
|
|
2447
|
+
})
|
|
2448
|
+
),
|
|
2084
2449
|
itemBuilders,
|
|
2085
2450
|
itemArt,
|
|
2086
2451
|
itemFields,
|
|
@@ -2097,10 +2462,12 @@ export function defineConfig(config) {
|
|
|
2097
2462
|
itemTypes,
|
|
2098
2463
|
docEntryTypes,
|
|
2099
2464
|
skipDirectories: Object.freeze(skipDirectories),
|
|
2465
|
+
icons: normalizeIcons(input.icons, rootDir),
|
|
2100
2466
|
packs: Object.freeze(packs),
|
|
2101
2467
|
packDirectories: Object.freeze(packDirectories),
|
|
2102
2468
|
docs: normalizeDocs(input.docs),
|
|
2103
2469
|
site: normalizeSite(input.site),
|
|
2470
|
+
pdf: normalizePdf(input.pdf, rootDir),
|
|
2104
2471
|
compatibility: normalizeCompatibility(input.compatibility, "compatibility"),
|
|
2105
2472
|
relationships: normalizeRelationships(input.relationships),
|
|
2106
2473
|
systems,
|