@heroiclands/package-build 20.4.0 → 20.6.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 +288 -0
- package/CONTENT.md +213 -20
- package/README.md +19 -1
- package/bin/content-build.mjs +135 -32
- package/bin/package-build.mjs +46 -13
- package/content-config.mjs +345 -101
- package/docs/api.md +1352 -0
- package/docs/commands.md +1609 -0
- package/docs/configuration.md +1432 -0
- package/docs/content-format.md +16 -6
- package/docs/diagnostics.md +356 -0
- package/docs/getting-started.md +813 -0
- package/docs/project-setup.md +469 -0
- package/engine/actor-compiler.mjs +30 -27
- package/engine/address-diff.mjs +45 -41
- package/engine/base-compiler.mjs +6 -0
- package/engine/bundles.mjs +9 -0
- package/engine/content-address.mjs +9 -9
- package/engine/content-index.mjs +44 -23
- package/engine/content-links.mjs +44 -11
- package/engine/content-lint.mjs +44 -10
- package/engine/content-tables.mjs +32 -27
- package/engine/folder-notes.mjs +4 -2
- package/engine/frontmatter-lint.mjs +35 -38
- package/engine/generate.mjs +5 -0
- package/engine/helpers.mjs +86 -32
- package/engine/index.mjs +12 -2
- package/engine/journals.mjs +9 -0
- package/engine/note-claims.mjs +18 -10
- package/engine/note-schemas.mjs +0 -5
- package/engine/note-vocabulary.mjs +32 -31
- package/engine/pack-config.mjs +26 -12
- 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 +876 -0
- package/engine/pdf-toc.mjs +525 -0
- package/engine/scenes.mjs +14 -5
- package/engine/schema-check.mjs +1 -1
- package/engine/site-build.mjs +21 -3
- package/engine/web-wikilinks.mjs +6 -3
- package/engine/wikilinks.mjs +2 -4
- package/hm3/actors.mjs +8 -0
- package/hm3/items.mjs +8 -0
- package/package.json +1 -1
- package/release.mjs +63 -3
- package/sohl/actors.mjs +8 -0
- package/sohl/items.mjs +8 -0
- package/sohl/note-schemas.mjs +5 -5
- package/types/content-config.d.mts +66 -15
- package/types/engine/actor-compiler.d.mts +34 -30
- package/types/engine/address-diff.d.mts +57 -3
- package/types/engine/base-compiler.d.mts +10 -2
- package/types/engine/bundles.d.mts +9 -0
- package/types/engine/content-address.d.mts +9 -9
- package/types/engine/content-index.d.mts +57 -13
- package/types/engine/content-lint.d.mts +6 -4
- package/types/engine/content-tables.d.mts +49 -18
- package/types/engine/frontmatter-lint.d.mts +3 -2
- package/types/engine/helpers.d.mts +105 -31
- package/types/engine/index.d.mts +4 -0
- package/types/engine/journals.d.mts +9 -0
- package/types/engine/note-claims.d.mts +17 -10
- package/types/engine/note-vocabulary.d.mts +23 -196
- package/types/engine/pack-config.d.mts +4 -4
- 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 +156 -0
- package/types/engine/pdf-toc.d.mts +114 -0
- package/types/engine/scenes.d.mts +10 -1
- package/types/engine/schema-check.d.mts +2 -2
- package/types/engine/site-build.d.mts +34 -6
- package/types/engine/wikilinks.d.mts +2 -3
- package/types/hm3/actors.d.mts +8 -0
- package/types/hm3/items.d.mts +8 -0
- package/types/release.d.mts +15 -4
- package/types/sohl/actors.d.mts +10 -2
- package/types/sohl/items.d.mts +8 -0
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,14 +41,15 @@
|
|
|
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
55
|
* directory it was launched from.
|
|
@@ -76,13 +76,70 @@ import { ACTOR_TYPES } from "./engine/subtype-registry.mjs";
|
|
|
76
76
|
import { NOTE_VOCABULARY } from "./engine/note-vocabulary.mjs";
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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.
|
|
82
86
|
*
|
|
83
87
|
* @satisfies {readonly PackageKind[]}
|
|
84
88
|
*/
|
|
85
|
-
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
|
+
});
|
|
86
143
|
|
|
87
144
|
/**
|
|
88
145
|
* The directories the build reads from and writes to, relative to `rootDir`,
|
|
@@ -234,7 +291,7 @@ export function publishesContentPages(config) {
|
|
|
234
291
|
}
|
|
235
292
|
|
|
236
293
|
/**
|
|
237
|
-
* @typedef {"systems" | "modules"} PackageKind
|
|
294
|
+
* @typedef {"systems" | "modules" | "documentation"} PackageKind
|
|
238
295
|
*/
|
|
239
296
|
|
|
240
297
|
/**
|
|
@@ -500,10 +557,20 @@ export function publishesContentPages(config) {
|
|
|
500
557
|
* @property {string} contentPackage Content package name — the address
|
|
501
558
|
* namespace every note in this
|
|
502
559
|
* repository is published under.
|
|
503
|
-
* @property {string} foundryPackage
|
|
560
|
+
* @property {string} [foundryPackage] Foundry package id, as it appears in
|
|
504
561
|
* `system.json` / `module.json`.
|
|
505
|
-
*
|
|
506
|
-
*
|
|
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.
|
|
507
574
|
* @property {Record<string, ItemBuilderEntry>|readonly ItemRegistrySpec[]} [itemBuilders]
|
|
508
575
|
* The consumer's
|
|
509
576
|
* item-type registry: each content `type`
|
|
@@ -552,12 +619,20 @@ export function publishesContentPages(config) {
|
|
|
552
619
|
* @typedef {object} ContentBuildConfig
|
|
553
620
|
* @property {string} rootDir
|
|
554
621
|
* @property {string} contentPackage
|
|
555
|
-
* @property {string} foundryPackage
|
|
622
|
+
* @property {string|null} foundryPackage `null` for a `documentation`
|
|
623
|
+
* package, which ships no Foundry package.
|
|
556
624
|
* @property {PackageKind} packageKind
|
|
557
|
-
* @property {string} assetRoot
|
|
558
|
-
*
|
|
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.
|
|
559
633
|
* @property {Readonly<ResolvedPaths>} paths
|
|
560
|
-
* @property {Readonly<StatsSpec
|
|
634
|
+
* @property {Readonly<StatsSpec>|null} stats `null` for a `documentation`
|
|
635
|
+
* package, which stamps no `_stats`.
|
|
561
636
|
* @property {Readonly<Record<string, Function>>} itemBuilders Derived: the
|
|
562
637
|
* `system` builder of each entry, whichever
|
|
563
638
|
* of the two spellings declared it.
|
|
@@ -623,6 +698,7 @@ const CONFIG_KEYS = [
|
|
|
623
698
|
"packs",
|
|
624
699
|
"docs",
|
|
625
700
|
"site",
|
|
701
|
+
"pdf",
|
|
626
702
|
"compatibility",
|
|
627
703
|
"relationships",
|
|
628
704
|
"systems",
|
|
@@ -646,6 +722,9 @@ const SITE_KEYS = [
|
|
|
646
722
|
"backfillSections",
|
|
647
723
|
];
|
|
648
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: "" });
|
|
649
728
|
const SECTION_META_KEYS = ["title", "banner", "description", "listType", "listSubType"];
|
|
650
729
|
const DOC_PAGE_KEYS = ["title", "out", "preamble"];
|
|
651
730
|
const RELATIONSHIP_KINDS = ["systems", "requires", "recommends", "conflicts"];
|
|
@@ -746,9 +825,10 @@ function requireNonEmptyString(value, field) {
|
|
|
746
825
|
* whose addresses are simply unreadable, reported nowhere and discovered as
|
|
747
826
|
* links that resolve to nothing.
|
|
748
827
|
*
|
|
749
|
-
* 1.
|
|
750
|
-
* was the one violator, and its keys
|
|
751
|
-
* 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.
|
|
752
832
|
* 2. _Not a note type_, because a written address is a **partial** one: the
|
|
753
833
|
* shorter forms drop segments from the left, so `skill-clmb` and
|
|
754
834
|
* `sohl-skill-clmb` are both addresses and position alone no longer says
|
|
@@ -777,11 +857,12 @@ function requireContentPackage(value, docEntryTypes) {
|
|
|
777
857
|
if (!isAddressSegment(pkg)) {
|
|
778
858
|
fail(
|
|
779
859
|
"contentPackage",
|
|
780
|
-
`is \`${pkg}\`, which is not alphanumeric
|
|
860
|
+
`is \`${pkg}\`, which is not lowercase alphanumeric ` +
|
|
861
|
+
`(${ADDRESS_SEGMENT_PATTERN.source}). It is the first ` +
|
|
781
862
|
`segment of every address this package publishes ` +
|
|
782
863
|
`(\`${pkg}-<system>-<type>-<shortcode>\`), and an address is read by ` +
|
|
783
864
|
`counting hyphen-separated segments — so anything outside ` +
|
|
784
|
-
"
|
|
865
|
+
"that here makes those addresses unreadable rather " +
|
|
785
866
|
"than merely ugly. `harn-adventures` became `harnadventures`",
|
|
786
867
|
);
|
|
787
868
|
}
|
|
@@ -957,18 +1038,6 @@ function normalizePack(value, where, nested = false) {
|
|
|
957
1038
|
return Object.freeze(normalized);
|
|
958
1039
|
}
|
|
959
1040
|
|
|
960
|
-
/**
|
|
961
|
-
* Resolve the layout a consumer supplies against its `rootDir`, filling every
|
|
962
|
-
* unnamed directory from {@link DEFAULT_PATHS}.
|
|
963
|
-
*
|
|
964
|
-
* Configured paths are **relative by contract**: an absolute one would escape
|
|
965
|
-
* the repository the config anchors, which is never what a consumer means and
|
|
966
|
-
* is what made these paths working-directory-dependent in the first place.
|
|
967
|
-
*
|
|
968
|
-
* @param {unknown} value
|
|
969
|
-
* @param {string} rootDir
|
|
970
|
-
* @returns {Readonly<ResolvedPaths>}
|
|
971
|
-
*/
|
|
972
1041
|
/**
|
|
973
1042
|
* A package's icon registry — the fonts it ships and the names it draws from
|
|
974
1043
|
* them.
|
|
@@ -1091,6 +1160,18 @@ function normalizeIcons(value, rootDir) {
|
|
|
1091
1160
|
});
|
|
1092
1161
|
}
|
|
1093
1162
|
|
|
1163
|
+
/**
|
|
1164
|
+
* Resolve the layout a consumer supplies against its `rootDir`, filling every
|
|
1165
|
+
* unnamed directory from {@link DEFAULT_PATHS}.
|
|
1166
|
+
*
|
|
1167
|
+
* Configured paths are **relative by contract**: an absolute one would escape
|
|
1168
|
+
* the repository the config anchors, which is never what a consumer means and
|
|
1169
|
+
* is what made these paths working-directory-dependent in the first place.
|
|
1170
|
+
*
|
|
1171
|
+
* @param {unknown} value
|
|
1172
|
+
* @param {string} rootDir
|
|
1173
|
+
* @returns {Readonly<ResolvedPaths>}
|
|
1174
|
+
*/
|
|
1094
1175
|
function normalizePaths(value, rootDir) {
|
|
1095
1176
|
if (value !== undefined && !isPlainObject(value)) {
|
|
1096
1177
|
fail("paths", "must be an object");
|
|
@@ -1118,7 +1199,9 @@ function normalizePaths(value, rootDir) {
|
|
|
1118
1199
|
}
|
|
1119
1200
|
|
|
1120
1201
|
/**
|
|
1121
|
-
* @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.
|
|
1122
1205
|
* @returns {Readonly<StatsSpec>}
|
|
1123
1206
|
*/
|
|
1124
1207
|
function normalizeStats(value, derived) {
|
|
@@ -1308,7 +1391,7 @@ function normalizeSectionMeta(value, where) {
|
|
|
1308
1391
|
if (!isAddressSegment(segment)) {
|
|
1309
1392
|
fail(
|
|
1310
1393
|
`${where}.${key}`,
|
|
1311
|
-
`is \`${segment}\`, which is not alphanumeric. It names a ` +
|
|
1394
|
+
`is \`${segment}\`, which is not lowercase alphanumeric. It names a ` +
|
|
1312
1395
|
"content type or subType, and those are address segments " +
|
|
1313
1396
|
`(${ADDRESS_SEGMENT_PATTERN.source}) — not the section's ` +
|
|
1314
1397
|
"own name, which is a URL this site chose and need not " +
|
|
@@ -1433,6 +1516,114 @@ function normalizeSite(value) {
|
|
|
1433
1516
|
});
|
|
1434
1517
|
}
|
|
1435
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
|
+
|
|
1436
1627
|
/**
|
|
1437
1628
|
* Validate a Foundry version range.
|
|
1438
1629
|
*
|
|
@@ -1462,38 +1653,6 @@ function normalizeCompatibility(value, where, requireMinimum = true) {
|
|
|
1462
1653
|
return Object.freeze(out);
|
|
1463
1654
|
}
|
|
1464
1655
|
|
|
1465
|
-
/**
|
|
1466
|
-
* Validate the declared relationships.
|
|
1467
|
-
*
|
|
1468
|
-
* Only as far as this package needs to read them: enough that a system
|
|
1469
|
-
* relationship can be found and its `verified` version trusted. The rest is
|
|
1470
|
-
* passed through for the manifest generator to emit.
|
|
1471
|
-
*
|
|
1472
|
-
* @param {unknown} value - The `relationships` block, or `undefined`.
|
|
1473
|
-
* @returns {Readonly<Relationships>} It, frozen; `{}` when absent.
|
|
1474
|
-
*/
|
|
1475
|
-
/**
|
|
1476
|
-
* The systems this package can stamp content against — declaration only.
|
|
1477
|
-
*
|
|
1478
|
-
* **Declaring is not requiring, and that separation is the whole point.** The
|
|
1479
|
-
* only other place to state a system version is `relationships.systems`, and
|
|
1480
|
-
* that list is a *restriction*: Foundry's `supportsSystem` drops a module from
|
|
1481
|
-
* any world whose system it does not name. So a module shipping content for two
|
|
1482
|
-
* systems — `harn-ensemble` ships an HM3 pack, a SoHL pack and a system-neutral
|
|
1483
|
-
* journals pack — had to choose between naming its systems and remaining
|
|
1484
|
-
* loadable, and choosing the second meant stamping no system version at all on
|
|
1485
|
-
* content that certainly has one.
|
|
1486
|
-
*
|
|
1487
|
-
* Naming a system here restricts nothing. {@link normalizeRequiresSystem} is
|
|
1488
|
-
* what restricts, and it is separate and optional.
|
|
1489
|
-
*
|
|
1490
|
-
* Each entry carries the same `compatibility` shape a relationship does, and
|
|
1491
|
-
* `verified` is what a pack stamps: `_stats.systemVersion` records what the
|
|
1492
|
-
* content was *built against*, not the floor it tolerates.
|
|
1493
|
-
*
|
|
1494
|
-
* @param {unknown} value - The declared `systems:` mapping.
|
|
1495
|
-
* @returns {Readonly<Record<string, Readonly<object>>>} Frozen; `{}` when absent.
|
|
1496
|
-
*/
|
|
1497
1656
|
/**
|
|
1498
1657
|
* The **package-wide** system, or `null` where the configuration names none.
|
|
1499
1658
|
*
|
|
@@ -1516,7 +1675,7 @@ function normalizeCompatibility(value, where, requireMinimum = true) {
|
|
|
1516
1675
|
* answer.
|
|
1517
1676
|
*
|
|
1518
1677
|
* @param {object} parts - The resolved pieces of the configuration.
|
|
1519
|
-
* @param {string} parts.packageKind -
|
|
1678
|
+
* @param {string} parts.packageKind - One of {@link PACKAGE_KINDS}.
|
|
1520
1679
|
* @param {unknown} parts.foundryPackage - The package id.
|
|
1521
1680
|
* @param {string|null} parts.requiresSystem - The declared gate, if any.
|
|
1522
1681
|
* @param {Readonly<Record<string, object>>} parts.systems - The `systems:` block.
|
|
@@ -1539,6 +1698,28 @@ function packageWideSystemId({
|
|
|
1539
1698
|
return null;
|
|
1540
1699
|
}
|
|
1541
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
|
+
*/
|
|
1542
1723
|
function normalizeSystems(value) {
|
|
1543
1724
|
if (value === undefined || value === null) return Object.freeze({});
|
|
1544
1725
|
if (!isPlainObject(value)) fail("systems", "must be a mapping of id to spec");
|
|
@@ -1602,6 +1783,16 @@ function normalizeRequiresSystem(value) {
|
|
|
1602
1783
|
return requireNonEmptyString(value, "requiresSystem");
|
|
1603
1784
|
}
|
|
1604
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
|
+
*/
|
|
1605
1796
|
function normalizeRelationships(value) {
|
|
1606
1797
|
if (value === undefined) return Object.freeze({});
|
|
1607
1798
|
if (!isPlainObject(value)) fail("relationships", "must be a mapping");
|
|
@@ -2017,9 +2208,45 @@ export function defineConfig(config) {
|
|
|
2017
2208
|
fail("packageKind", `must be one of: ${PACKAGE_KINDS.join(", ")}`);
|
|
2018
2209
|
}
|
|
2019
2210
|
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
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}]`));
|
|
2023
2250
|
|
|
2024
2251
|
// One list, so the compile order and the directory list cannot disagree —
|
|
2025
2252
|
// as `PACK_CONFIGS` and `SOURCE_PACKS` they would be maintained apart.
|
|
@@ -2140,7 +2367,10 @@ export function defineConfig(config) {
|
|
|
2140
2367
|
requireNonEmptyString(name, `skipDirectories[${index}]`),
|
|
2141
2368
|
);
|
|
2142
2369
|
|
|
2143
|
-
|
|
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");
|
|
2144
2374
|
|
|
2145
2375
|
const {
|
|
2146
2376
|
itemBuilders,
|
|
@@ -2172,37 +2402,50 @@ export function defineConfig(config) {
|
|
|
2172
2402
|
packageKind: /** @type {PackageKind} */ (packageKind),
|
|
2173
2403
|
// Foundry serves a package's files from `<kind>/<id>/`, so this is the
|
|
2174
2404
|
// one place `systems/sohl` (or `modules/sohl-thalorna`) is spelled.
|
|
2175
|
-
|
|
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`,
|
|
2176
2414
|
paths: normalizePaths(input.paths, rootDir),
|
|
2177
2415
|
// The package-wide system, derived. A **system** package is its
|
|
2178
2416
|
// own system, which is true by construction and needs no declaration. A
|
|
2179
2417
|
// **module** takes the one it requires, or the one system it declares
|
|
2180
2418
|
// when there is exactly one; with several and no gate there is no
|
|
2181
2419
|
// package-wide answer, and each pack carries its own.
|
|
2182
|
-
stats:
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
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
|
+
),
|
|
2206
2449
|
itemBuilders,
|
|
2207
2450
|
itemArt,
|
|
2208
2451
|
itemFields,
|
|
@@ -2224,6 +2467,7 @@ export function defineConfig(config) {
|
|
|
2224
2467
|
packDirectories: Object.freeze(packDirectories),
|
|
2225
2468
|
docs: normalizeDocs(input.docs),
|
|
2226
2469
|
site: normalizeSite(input.site),
|
|
2470
|
+
pdf: normalizePdf(input.pdf, rootDir),
|
|
2227
2471
|
compatibility: normalizeCompatibility(input.compatibility, "compatibility"),
|
|
2228
2472
|
relationships: normalizeRelationships(input.relationships),
|
|
2229
2473
|
systems,
|