@heroiclands/package-build 20.3.1 → 20.4.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 +75 -0
- package/CONTENT.md +111 -92
- package/README.md +2 -9
- package/bin/content-build.mjs +70 -61
- package/bin/package-build.mjs +2 -2
- package/config.mjs +6 -8
- package/content-config.mjs +187 -64
- package/docs/content-format.md +57 -68
- package/e2e.mjs +4 -4
- package/engine/actor-compiler.mjs +36 -36
- package/engine/address-charset.mjs +6 -6
- package/engine/address-diff.mjs +12 -11
- package/engine/anchored-sections.mjs +1 -1
- package/engine/anchors.mjs +1 -1
- package/engine/base-compiler.mjs +36 -38
- package/engine/bundle-notes.mjs +4 -4
- package/engine/bundles.mjs +8 -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 +15 -15
- 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 +17 -17
- package/engine/content-links.mjs +30 -30
- package/engine/content-lint.mjs +20 -24
- package/engine/content-package.mjs +7 -9
- package/engine/content-slug.mjs +2 -2
- package/engine/content-tables.mjs +7 -8
- 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 +14 -14
- package/engine/foreign-catalog.mjs +8 -9
- package/engine/foundry-entries.mjs +19 -20
- package/engine/frontmatter-lint.mjs +64 -69
- package/engine/frontmatter.mjs +11 -11
- package/engine/generate.mjs +39 -39
- package/engine/helpers.mjs +63 -54
- package/engine/homepage.mjs +20 -22
- package/engine/ids.mjs +15 -15
- package/engine/index-records.mjs +4 -4
- package/engine/index.mjs +14 -8
- 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 +15 -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 +36 -34
- 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 -3
- package/engine/note-vocabulary.mjs +27 -26
- package/engine/pack-config.mjs +13 -13
- package/engine/pack-router.mjs +0 -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 +19 -20
- package/engine/schema-check.mjs +20 -20
- package/engine/schema-extract.mjs +1 -1
- package/engine/site-build.mjs +45 -47
- 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 +35 -35
- package/engine/wikilink-syntax.mjs +16 -16
- package/engine/wikilinks.mjs +39 -39
- package/hm3/actors.mjs +22 -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 +20 -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 +3 -3
- package/sohl/actors.mjs +28 -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 +20 -6
- package/sohl/kb-passes.mjs +5 -5
- package/sohl/note-schemas.mjs +7 -7
- package/sohl/skill-base.mjs +1 -1
- package/types/content-config.d.mts +25 -24
- package/types/e2e.d.mts +3 -3
- package/types/engine/actor-compiler.d.mts +14 -13
- package/types/engine/address-charset.d.mts +6 -6
- package/types/engine/address-diff.d.mts +3 -3
- package/types/engine/base-compiler.d.mts +17 -19
- package/types/engine/bundle-notes.d.mts +3 -3
- package/types/engine/bundles.d.mts +1 -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 +11 -11
- 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 +2 -2
- package/types/engine/content-links.d.mts +8 -8
- package/types/engine/content-lint.d.mts +2 -2
- package/types/engine/content-package.d.mts +6 -8
- 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 +7 -7
- package/types/engine/frontmatter.d.mts +7 -7
- package/types/engine/generate.d.mts +7 -7
- package/types/engine/helpers.d.mts +25 -27
- 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 +2 -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 +3 -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 +16 -15
- 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 +9 -8
- package/types/engine/pack-config.d.mts +3 -3
- package/types/engine/pack-router.d.mts +1 -1
- 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/schema-check.d.mts +11 -11
- package/types/engine/site-build.d.mts +18 -19
- 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 +13 -12
- package/types/hm3/actors.d.mts +1 -1
- package/types/hm3/document-subtypes.d.mts +1 -1
- package/types/hm3/items.d.mts +1 -2
- package/types/hm3/template-priority.d.mts +1 -1
- package/types/manifest.d.mts +8 -8
- package/types/sohl/actors.d.mts +2 -2
- 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 +1 -2
- package/types/sohl/note-schemas.d.mts +1 -1
- package/MIGRATING.md +0 -608
package/content-config.mjs
CHANGED
|
@@ -52,22 +52,25 @@
|
|
|
52
52
|
* Both forms end here, so both are 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";
|
|
@@ -92,7 +95,7 @@ export const DEFAULT_PATHS = /** @type {const} */ ({
|
|
|
92
95
|
// Where `content-index` writes this package's note index. Under `build/`
|
|
93
96
|
// because it is derived and disposable — regenerating it costs a
|
|
94
97
|
// frontmatter parse — and emphatically not under `stage`, which is mirrored
|
|
95
|
-
// into a Foundry data root
|
|
98
|
+
// into a Foundry data root.
|
|
96
99
|
contentIndex: "build/content-index",
|
|
97
100
|
packJson: "build/packs-json",
|
|
98
101
|
stage: "build/stage/packs",
|
|
@@ -100,7 +103,7 @@ export const DEFAULT_PATHS = /** @type {const} */ ({
|
|
|
100
103
|
// Where a dependency declaring `itemCatalog: true` is unpacked. Under
|
|
101
104
|
// `build/` because it is derived, disposable, and version-keyed.
|
|
102
105
|
foreignCache: "build/cache/foreign",
|
|
103
|
-
// Where a dependency's published content index is fetched to
|
|
106
|
+
// Where a dependency's published content index is fetched to. A
|
|
104
107
|
// sibling of the item catalogue rather than a subdirectory of it: the two
|
|
105
108
|
// are fetched for different dependency sets — a catalogue only where
|
|
106
109
|
// `itemCatalog: true` is declared, an index for *every* declared
|
|
@@ -112,7 +115,7 @@ export const DEFAULT_PATHS = /** @type {const} */ ({
|
|
|
112
115
|
/**
|
|
113
116
|
* The Foundry document types a compendium pack may hold. This is the set the
|
|
114
117
|
* toolchain is able to compile a pack of; a document type Foundry supports but
|
|
115
|
-
* this toolchain does not compile is deliberately absent (
|
|
118
|
+
* this toolchain does not compile is deliberately absent (playlists
|
|
116
119
|
* and roll tables are out of scope).
|
|
117
120
|
*
|
|
118
121
|
* @satisfies {readonly PackDocumentType[]}
|
|
@@ -139,15 +142,11 @@ export const PACK_DOCUMENT_TYPES = /** @type {const} */ ([
|
|
|
139
142
|
* key is **refused**, at the line it was written on, with a message that says
|
|
140
143
|
* the mechanism is gone rather than naming a value to correct.
|
|
141
144
|
*
|
|
142
|
-
*
|
|
145
|
+
* **`landing` is one such key.** It named which note addressed a whole section
|
|
143
146
|
* 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.
|
|
147
|
+
* of its own. There are no sections to address: a section is a Hugo content
|
|
148
|
+
* directory the note format does not carry, a page's address names no
|
|
149
|
+
* directory, and so no note lands anything.
|
|
151
150
|
*
|
|
152
151
|
* @type {Readonly<Record<string, string>>}
|
|
153
152
|
*/
|
|
@@ -168,7 +167,7 @@ export const RETIRED_ADDRESS_KEYS = Object.freeze({
|
|
|
168
167
|
* `thalorna`, whose site is nothing but its content. It is not the package's
|
|
169
168
|
* own mount point: where the package itself is served is the consuming build's
|
|
170
169
|
* knowledge, held in `PACKAGE_BASE` (`engine/content-address.mjs`) and prefixed at
|
|
171
|
-
* resolve time, so it is never recorded here
|
|
170
|
+
* resolve time, so it is never recorded here.
|
|
172
171
|
*
|
|
173
172
|
* It is the whole scheme: `landing`, the key that named which note addressed a
|
|
174
173
|
* whole section, is retired with the sections themselves — see
|
|
@@ -183,7 +182,7 @@ export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
|
|
|
183
182
|
*
|
|
184
183
|
* Every HeroicLands package publishes something: a top-level, human-authored
|
|
185
184
|
* homepage at `https://www.heroiclands.org/<contentPackage>/` saying what the
|
|
186
|
-
* module is, which system it needs and how to install it
|
|
185
|
+
* module is, which system it needs and how to install it. So there is no
|
|
187
186
|
* value here meaning *no web presence at all* — homepage-only is the **floor**,
|
|
188
187
|
* and the default.
|
|
189
188
|
*
|
|
@@ -251,7 +250,7 @@ export function publishesContentPages(config) {
|
|
|
251
250
|
* type** receives its document. The two are orthogonal, and both are needed
|
|
252
251
|
* once a repository groups same-type documents editorially — which it may have
|
|
253
252
|
* to, since a compendium UUID carries its pack name and collapsing such a
|
|
254
|
-
* layout breaks every stored reference
|
|
253
|
+
* layout breaks every stored reference.
|
|
255
254
|
*
|
|
256
255
|
* @typedef {object} PackSpec
|
|
257
256
|
* @property {string} name Pack name — the manifest `name`, and the
|
|
@@ -317,7 +316,7 @@ export function publishesContentPages(config) {
|
|
|
317
316
|
* Inbound, and fetched rather than
|
|
318
317
|
* committed.
|
|
319
318
|
* @property {string} [metadataCache] Where a dependency's published content
|
|
320
|
-
* index is fetched to
|
|
319
|
+
* index is fetched to. Inbound,
|
|
321
320
|
* for *every* declared dependency, not
|
|
322
321
|
* only those supplying a catalogue.
|
|
323
322
|
*/
|
|
@@ -473,13 +472,13 @@ export function publishesContentPages(config) {
|
|
|
473
472
|
* `fields` is what makes the type documentable: a builder function says
|
|
474
473
|
* nothing about the vocabulary it consumes, so a consumer that declares its
|
|
475
474
|
* fields can generate its own authoring reference and check its own notes,
|
|
476
|
-
* while one that does not is simply undocumented rather than broken
|
|
475
|
+
* while one that does not is simply undocumented rather than broken.
|
|
477
476
|
*
|
|
478
477
|
* @typedef {((fm: object) => object)|{system: (fm: object) => object, img?: string, fields?: readonly object[]}} ItemBuilderEntry
|
|
479
478
|
*/
|
|
480
479
|
|
|
481
480
|
/**
|
|
482
|
-
* One **registry** of a declared set, and the system it belongs to
|
|
481
|
+
* One **registry** of a declared set, and the system it belongs to.
|
|
483
482
|
*
|
|
484
483
|
* A repository shipping content for two systems declares one of these per
|
|
485
484
|
* system: the accepted type vocabulary is their union, and a type both declare
|
|
@@ -518,14 +517,13 @@ export function publishesContentPages(config) {
|
|
|
518
517
|
* two systems declares a **list** of
|
|
519
518
|
* `{ system, builders }` registries
|
|
520
519
|
* instead, and the accepted type
|
|
521
|
-
* vocabulary is their union
|
|
520
|
+
* vocabulary is their union.
|
|
522
521
|
* @property {PackSpec[]} packs Packs to compile. More than one entry
|
|
523
522
|
* may share a `type`: a note then names
|
|
524
523
|
* the pack it belongs in with its
|
|
525
524
|
* `pack:` frontmatter, and one pack of
|
|
526
525
|
* the type is marked `default: true` to
|
|
527
|
-
* receive the notes that name none
|
|
528
|
-
* (#1566).
|
|
526
|
+
* receive the notes that name none.
|
|
529
527
|
* @property {PathsInput} [paths] Layout overrides. See {@link DEFAULT_PATHS}.
|
|
530
528
|
* @property {string[]} [skipDirectories] Directory names the content walk ignores
|
|
531
529
|
* wherever they appear (e.g. Obsidian's
|
|
@@ -571,12 +569,11 @@ export function publishesContentPages(config) {
|
|
|
571
569
|
* the frontmatter fields each entry
|
|
572
570
|
* declared. Sparse, like `itemArt` — a type
|
|
573
571
|
* absent here compiles normally and is
|
|
574
|
-
* simply undocumented
|
|
572
|
+
* simply undocumented.
|
|
575
573
|
* @property {Readonly<Record<string, Readonly<Record<string, Function>>>>} itemBuildersBySystem
|
|
576
574
|
* Derived: the same builders, kept per
|
|
577
575
|
* declaring system. `{}` for the single
|
|
578
|
-
* registry form, which names no system
|
|
579
|
-
* (#58).
|
|
576
|
+
* registry form, which names no system.
|
|
580
577
|
* @property {Readonly<Record<string, Readonly<Record<string, string>>>>} itemArtBySystem
|
|
581
578
|
* Derived: the default art, per system.
|
|
582
579
|
* @property {Readonly<Record<string, Readonly<Record<string, readonly object[]>>>>} itemFieldsBySystem
|
|
@@ -589,13 +586,17 @@ export function publishesContentPages(config) {
|
|
|
589
586
|
* {@link ContentBuildConfigInput.itemBuilders},
|
|
590
587
|
* unioned across every declared registry, so
|
|
591
588
|
* the accepted item types and the builder
|
|
592
|
-
* tables are one list
|
|
589
|
+
* tables are one list.
|
|
593
590
|
* @property {ReadonlySet<string>} docEntryTypes Derived: every type whose prose
|
|
594
591
|
* compiles into a JournalEntry of its own —
|
|
595
592
|
* the item types, plus `macro`, plus the map
|
|
596
593
|
* types. The one set the compilers and the
|
|
597
594
|
* link-manifest emitter both read.
|
|
598
595
|
* @property {readonly string[]} skipDirectories
|
|
596
|
+
* @property {import("./engine/content-icons.mjs").IconRegistry} icons The
|
|
597
|
+
* fonts this package ships and the names it
|
|
598
|
+
* draws from them; empty when it declares
|
|
599
|
+
* none.
|
|
599
600
|
* @property {readonly Readonly<ResolvedPackSpec>[]} packs
|
|
600
601
|
* @property {readonly string[]} packDirectories Derived: every pack directory
|
|
601
602
|
* the build produces, in compile order —
|
|
@@ -618,6 +619,7 @@ const CONFIG_KEYS = [
|
|
|
618
619
|
"itemBuilders",
|
|
619
620
|
"paths",
|
|
620
621
|
"skipDirectories",
|
|
622
|
+
"icons",
|
|
621
623
|
"packs",
|
|
622
624
|
"docs",
|
|
623
625
|
"site",
|
|
@@ -668,7 +670,7 @@ const STATS_KEYS = ["lastModifiedBy"];
|
|
|
668
670
|
* How the loader hands {@link defineConfig} the system version it resolved.
|
|
669
671
|
*
|
|
670
672
|
* A **Symbol**, deliberately. `stats.systemVersion` is refused from an authored
|
|
671
|
-
* configuration
|
|
673
|
+
* configuration, but the value still has to reach here from the loader —
|
|
672
674
|
* which is the half that may do I/O, and which reads a system package's version
|
|
673
675
|
* out of the adjacent `package.json`. A string key would be a second spelling of
|
|
674
676
|
* the refused one, forgeable from YAML and reachable by `rejectUnknownKeys`; a
|
|
@@ -692,7 +694,7 @@ function isPlainObject(value) {
|
|
|
692
694
|
* The dotted path is carried on the error as `field` as well as spelled into
|
|
693
695
|
* the message, because the message alone is a good description and a bad
|
|
694
696
|
* 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
|
|
697
|
+
* column, and does (`locateConfigError` in `engine/pack-config.mjs`).
|
|
696
698
|
* Attaching it here rather than formatting here is what keeps this module
|
|
697
699
|
* free of I/O — it is the leaf an `.mjs` configuration imports, so it may not
|
|
698
700
|
* reach for the file it is validating.
|
|
@@ -739,7 +741,7 @@ function requireNonEmptyString(value, field) {
|
|
|
739
741
|
* It is the first segment of every canonical address this repository publishes
|
|
740
742
|
* (`package-system-type-shortcode`, so `sohl-none-doc-gear`), and an address is
|
|
741
743
|
* read by counting hyphen-separated segments. So the value carries two
|
|
742
|
-
* obligations that the rest of the configuration does not, and
|
|
744
|
+
* obligations that the rest of the configuration does not, and asks for
|
|
743
745
|
* both to be **enforced rather than assumed** — the alternative is a package
|
|
744
746
|
* whose addresses are simply unreadable, reported nowhere and discovered as
|
|
745
747
|
* links that resolve to nothing.
|
|
@@ -754,9 +756,9 @@ function requireNonEmptyString(value, field) {
|
|
|
754
756
|
* by asking whether the name is a known package, and a name in both
|
|
755
757
|
* vocabularies makes one target readable two ways with no defensible pick.
|
|
756
758
|
* Keeping the two disjoint is what lets a name be taken at face value; that
|
|
757
|
-
* the package and the type are
|
|
758
|
-
*
|
|
759
|
-
*
|
|
759
|
+
* the package and the type are not *adjacent* segments, the system sitting
|
|
760
|
+
* between them, changes nothing: the hazard is not adjacency, it is that a
|
|
761
|
+
* short form omits the slots in between.
|
|
760
762
|
* One such collision is structural and cannot be fixed — `sohl` is both a
|
|
761
763
|
* content package and a system id, because Foundry requires a system
|
|
762
764
|
* package's id to *be* its system id, and `sohl-sohl-skill-clmb` is the
|
|
@@ -850,7 +852,7 @@ function optionalString(value, field) {
|
|
|
850
852
|
function normalizePack(value, where, nested = false) {
|
|
851
853
|
if (!isPlainObject(value)) fail(where, "must be an object");
|
|
852
854
|
const pack = /** @type {Record<string, unknown>} */ (value);
|
|
853
|
-
// Retired with the YAML it named
|
|
855
|
+
// Retired with the YAML it named. Refused explicitly rather than
|
|
854
856
|
// left to the unknown-key check, because the useful thing to say is not
|
|
855
857
|
// "no such key" but where the folders went: they are notes, and a pack
|
|
856
858
|
// materialises the ones its documents reference.
|
|
@@ -967,6 +969,128 @@ function normalizePack(value, where, nested = false) {
|
|
|
967
969
|
* @param {string} rootDir
|
|
968
970
|
* @returns {Readonly<ResolvedPaths>}
|
|
969
971
|
*/
|
|
972
|
+
/**
|
|
973
|
+
* A package's icon registry — the fonts it ships and the names it draws from
|
|
974
|
+
* them.
|
|
975
|
+
*
|
|
976
|
+
* **Nothing is supplied by default.** A registry entry is a promise that a
|
|
977
|
+
* glyph will render, and only the package shipping the font can keep it: the
|
|
978
|
+
* Game-Icons webfont is built by a consumer from its own templates, and Font
|
|
979
|
+
* Awesome reaches neither the knowledgebase nor a printed page unless somebody
|
|
980
|
+
* puts it there. A toolchain that shipped a starter table would be promising on
|
|
981
|
+
* a consumer's behalf, and a name like `victory-star-tester` is one game
|
|
982
|
+
* system's vocabulary besides.
|
|
983
|
+
*
|
|
984
|
+
* So a package declares both halves, and a package that declares neither names
|
|
985
|
+
* no icons at all.
|
|
986
|
+
*
|
|
987
|
+
* **Two spellings, one shape.** The value is either the registry itself:
|
|
988
|
+
*
|
|
989
|
+
* ```yaml
|
|
990
|
+
* icons:
|
|
991
|
+
* families:
|
|
992
|
+
* fontawesome: { class: fa, styles: [solid, regular, brands], describe: Font Awesome Free }
|
|
993
|
+
* icons:
|
|
994
|
+
* being: { style: solid, icon: user, label: being }
|
|
995
|
+
* ```
|
|
996
|
+
*
|
|
997
|
+
* or a **path to a file holding it**, relative to this configuration:
|
|
998
|
+
*
|
|
999
|
+
* ```yaml
|
|
1000
|
+
* icons: assets/icon-registry.yaml
|
|
1001
|
+
* ```
|
|
1002
|
+
*
|
|
1003
|
+
* The file form is the one a real package wants. A registry is derived from
|
|
1004
|
+
* what the interface actually draws, so it is generated rather than hand-kept —
|
|
1005
|
+
* and a generated document inlined into a hand-edited configuration is a merge
|
|
1006
|
+
* conflict on every regeneration. Kept beside it, the generator owns one file
|
|
1007
|
+
* and the configuration owns the other.
|
|
1008
|
+
*
|
|
1009
|
+
* Validated with {@link module:engine/content-icons.checkIconRegistry}, whose
|
|
1010
|
+
* findings are warnings everywhere else and a **refusal** here: elsewhere the
|
|
1011
|
+
* question is whether one note is wrong, and here it is whether the table every
|
|
1012
|
+
* note is read against is.
|
|
1013
|
+
*
|
|
1014
|
+
* @param {unknown} value - The authored `icons:` value.
|
|
1015
|
+
* @param {string} rootDir - The configuration's own directory, which a relative
|
|
1016
|
+
* path is resolved against.
|
|
1017
|
+
* @returns {import("./engine/content-icons.mjs").IconRegistry} The frozen
|
|
1018
|
+
* registry.
|
|
1019
|
+
*/
|
|
1020
|
+
function normalizeIcons(value, rootDir) {
|
|
1021
|
+
if (value === undefined) return EMPTY_ICON_REGISTRY;
|
|
1022
|
+
|
|
1023
|
+
let declared = value;
|
|
1024
|
+
let where = "icons";
|
|
1025
|
+
if (typeof value === "string") {
|
|
1026
|
+
if (!value.trim()) fail("icons", "is empty — name a file, or write the registry inline");
|
|
1027
|
+
const file = path.resolve(rootDir, value);
|
|
1028
|
+
let text;
|
|
1029
|
+
try {
|
|
1030
|
+
text = fs.readFileSync(file, "utf8");
|
|
1031
|
+
} catch {
|
|
1032
|
+
fail("icons", `names ${value}, which cannot be read from ${rootDir}`);
|
|
1033
|
+
}
|
|
1034
|
+
try {
|
|
1035
|
+
declared = YAML.parse(text);
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
fail("icons", `names ${value}, which is not readable YAML: ${err.message}`);
|
|
1038
|
+
}
|
|
1039
|
+
// A finding says which *file* is wrong, not which key of this one.
|
|
1040
|
+
where = value;
|
|
1041
|
+
if (declared === null || declared === undefined) {
|
|
1042
|
+
fail("icons", `names ${value}, which is empty`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
if (!isPlainObject(declared)) {
|
|
1047
|
+
fail(
|
|
1048
|
+
"icons",
|
|
1049
|
+
"must be a registry — `families` and `icons` — or a path to a file holding one",
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const families = declared.families ?? {};
|
|
1054
|
+
const icons = declared.icons ?? {};
|
|
1055
|
+
if (!isPlainObject(families)) fail(`${where}.families`, "must be a mapping of name to family");
|
|
1056
|
+
if (!isPlainObject(icons)) fail(`${where}.icons`, "must be a mapping of name to icon entry");
|
|
1057
|
+
|
|
1058
|
+
for (const name of Object.keys(icons)) {
|
|
1059
|
+
// The name a note writes between the colons. Checked here rather than
|
|
1060
|
+
// left to the note, because an entry nothing can name is a silent
|
|
1061
|
+
// no-op: every use of it reports "no such icon" and the table says
|
|
1062
|
+
// otherwise.
|
|
1063
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
|
|
1064
|
+
fail(
|
|
1065
|
+
`${where}.icons.${name}`,
|
|
1066
|
+
"is not a name a note can write — `:icon-…:` takes lowercase " +
|
|
1067
|
+
"letters, digits and hyphens, the charset an address segment uses",
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const defaultFamily = declared.defaultFamily;
|
|
1073
|
+
if (defaultFamily !== undefined) {
|
|
1074
|
+
if (typeof defaultFamily !== "string" || !(defaultFamily in families)) {
|
|
1075
|
+
fail(
|
|
1076
|
+
`${where}.defaultFamily`,
|
|
1077
|
+
`names \`${defaultFamily}\`, which is not one of the declared families`,
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const findings = checkIconRegistry({ families, icons, defaultFamily }, where);
|
|
1083
|
+
if (findings.length) {
|
|
1084
|
+
fail("icons", findings.map((finding) => finding.message).join("; "));
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
return Object.freeze({
|
|
1088
|
+
families: Object.freeze(families),
|
|
1089
|
+
defaultFamily,
|
|
1090
|
+
icons: Object.freeze(icons),
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
|
|
970
1094
|
function normalizePaths(value, rootDir) {
|
|
971
1095
|
if (value !== undefined && !isPlainObject(value)) {
|
|
972
1096
|
fail("paths", "must be an object");
|
|
@@ -1002,7 +1126,7 @@ function normalizeStats(value, derived) {
|
|
|
1002
1126
|
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1003
1127
|
|
|
1004
1128
|
// **`systemId` and `systemVersion` are derived, and authoring a derived
|
|
1005
|
-
// value is an error rather than an override
|
|
1129
|
+
// value is an error rather than an override.** `systems:` is the
|
|
1006
1130
|
// single source: it says which systems this package stamps against, and
|
|
1007
1131
|
// `requiresSystem` — or a lone declared system — says which one the
|
|
1008
1132
|
// package-wide block takes. A system package answers for itself.
|
|
@@ -1115,7 +1239,7 @@ function normalizeDocs(value) {
|
|
|
1115
1239
|
* One section's landing metadata — what a section says about itself on the
|
|
1116
1240
|
* `_index.md` this build generates for it.
|
|
1117
1241
|
*
|
|
1118
|
-
* A generated landing is the *only* place a section can speak, and
|
|
1242
|
+
* A generated landing is the *only* place a section can speak, and it
|
|
1119
1243
|
* is the only place a section **exists**: a content page is addressed
|
|
1120
1244
|
* `(type, shortcode)` and written flat under the mount, so no page creates a
|
|
1121
1245
|
* directory and nothing else makes `<prefix><section>/` answer. This is
|
|
@@ -1127,18 +1251,18 @@ function normalizeDocs(value) {
|
|
|
1127
1251
|
* written fourteen to twenty times per build against a contract every package
|
|
1128
1252
|
* and every section shares. Unbounded there, a mistyped `descrption:` publishes
|
|
1129
1253
|
* into front matter, is read by nobody, and says nothing to anyone — which is
|
|
1130
|
-
* the failure
|
|
1254
|
+
* the same failure, moved one step downstream where no build can
|
|
1131
1255
|
* 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
|
|
1256
|
+
* rather than transcribing a second list of their own.
|
|
1133
1257
|
*
|
|
1134
1258
|
* `banner` and `description` are optional — the hero images are external assets
|
|
1135
1259
|
* and not every section has one, and a section may reasonably have nothing to
|
|
1136
1260
|
* add to its title. Each is left off entirely rather than written as
|
|
1137
1261
|
* `undefined`, which is not a value YAML can carry.
|
|
1138
1262
|
*
|
|
1139
|
-
* **`listType` / `listSubType` say what the section lists
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1263
|
+
* **`listType` / `listSubType` say what the section lists.** A section's
|
|
1264
|
+
* directory holds nothing but the `_index.md` written here, so a layout
|
|
1265
|
+
* reading Hugo's `.Pages` finds
|
|
1142
1266
|
* no members and renders an empty landing. The membership survives in this map
|
|
1143
1267
|
* and nowhere a theme can reach it, so the landing states it and a layout
|
|
1144
1268
|
* substitutes the equivalent `site.RegularPages` query — the same one `sohl`'s
|
|
@@ -1155,7 +1279,7 @@ function normalizeDocs(value) {
|
|
|
1155
1279
|
* Both are checked as **address segments**, which is the trap this came from:
|
|
1156
1280
|
* a section is named for the URL a consumer chose and a subType is an address
|
|
1157
1281
|
* segment, and the two need not agree — `/sohl/kb/user-guide/` is the section,
|
|
1158
|
-
* `userguide` the subType
|
|
1282
|
+
* `userguide` the subType. Copying the section's name into the
|
|
1159
1283
|
* declaration would select no page at all, and an empty landing reported by
|
|
1160
1284
|
* nobody is the failure being fixed. A `listSubType` with no `listType` is
|
|
1161
1285
|
* refused for the same reason: a subType is only distinguishing *within* a
|
|
@@ -1349,10 +1473,10 @@ function normalizeCompatibility(value, where, requireMinimum = true) {
|
|
|
1349
1473
|
* @returns {Readonly<Relationships>} It, frozen; `{}` when absent.
|
|
1350
1474
|
*/
|
|
1351
1475
|
/**
|
|
1352
|
-
* The systems this package can stamp content against — declaration only
|
|
1476
|
+
* The systems this package can stamp content against — declaration only.
|
|
1353
1477
|
*
|
|
1354
1478
|
* **Declaring is not requiring, and that separation is the whole point.** The
|
|
1355
|
-
* only place to state a system version
|
|
1479
|
+
* only other place to state a system version is `relationships.systems`, and
|
|
1356
1480
|
* that list is a *restriction*: Foundry's `supportsSystem` drops a module from
|
|
1357
1481
|
* any world whose system it does not name. So a module shipping content for two
|
|
1358
1482
|
* systems — `harn-ensemble` ships an HM3 pack, a SoHL pack and a system-neutral
|
|
@@ -1371,8 +1495,7 @@ function normalizeCompatibility(value, where, requireMinimum = true) {
|
|
|
1371
1495
|
* @returns {Readonly<Record<string, Readonly<object>>>} Frozen; `{}` when absent.
|
|
1372
1496
|
*/
|
|
1373
1497
|
/**
|
|
1374
|
-
* The **package-wide** system, or `null` where the configuration names none
|
|
1375
|
-
* (#48).
|
|
1498
|
+
* The **package-wide** system, or `null` where the configuration names none.
|
|
1376
1499
|
*
|
|
1377
1500
|
* A *system* package is its own system, which is true by construction and needs
|
|
1378
1501
|
* no declaration. A *module* takes the one it requires, or the one system it
|
|
@@ -1387,7 +1510,7 @@ function normalizeCompatibility(value, where, requireMinimum = true) {
|
|
|
1387
1510
|
* exists to remove. Several entries have no single answer and get none.
|
|
1388
1511
|
*
|
|
1389
1512
|
* **Written once and read twice**, which is why it is a function rather than the
|
|
1390
|
-
* expression
|
|
1513
|
+
* expression: the value stamped into `stats.systemId` and the
|
|
1391
1514
|
* value a pack's `system:` is validated against are the same fact, and two
|
|
1392
1515
|
* spellings of it would be free to disagree about exactly the case that has no
|
|
1393
1516
|
* answer.
|
|
@@ -1458,7 +1581,7 @@ function normalizeSystems(value) {
|
|
|
1458
1581
|
}
|
|
1459
1582
|
|
|
1460
1583
|
/**
|
|
1461
|
-
* The one system this package refuses to load without, or `null
|
|
1584
|
+
* The one system this package refuses to load without, or `null`.
|
|
1462
1585
|
*
|
|
1463
1586
|
* The gate half of the split. Naming a system here emits
|
|
1464
1587
|
* `relationships.systems` for it, which is what Foundry's `supportsSystem`
|
|
@@ -1557,7 +1680,7 @@ function normalizePackageBuild(value) {
|
|
|
1557
1680
|
* The registry is *code* a consumer supplies — the only place the configuration
|
|
1558
1681
|
* carries any — because the type list and the builder table have to be the same
|
|
1559
1682
|
* list. They were two, and `trait` sat in the whitelist for a release with no
|
|
1560
|
-
* builder behind it
|
|
1683
|
+
* builder behind it.
|
|
1561
1684
|
*
|
|
1562
1685
|
* **An entry may be written two ways**, and the difference is only whether the
|
|
1563
1686
|
* type brings default art:
|
|
@@ -1637,12 +1760,11 @@ function normalizeOneRegistry(value, at) {
|
|
|
1637
1760
|
}
|
|
1638
1761
|
|
|
1639
1762
|
/**
|
|
1640
|
-
* The declared item-builder registries, and the vocabulary their union gives
|
|
1641
|
-
* (#58).
|
|
1763
|
+
* The declared item-builder registries, and the vocabulary their union gives.
|
|
1642
1764
|
*
|
|
1643
1765
|
* **One registry is a ceiling, not a default.** The accepted type list is the
|
|
1644
1766
|
* registry's keys, which is what makes a type impossible to accept without a
|
|
1645
|
-
* builder behind it
|
|
1767
|
+
* builder behind it — and, with one registry, impossible to accept a
|
|
1646
1768
|
* type a *second* system declares. A tree feeding two systems has both:
|
|
1647
1769
|
* `spell`, `invocation` and `psionic` are HM3's, `mysticalability` and
|
|
1648
1770
|
* `projectile` are SoHL's, and `skill` is both systems' under one name and
|
|
@@ -1768,7 +1890,7 @@ function normalizeItemBuilders(value) {
|
|
|
1768
1890
|
}
|
|
1769
1891
|
|
|
1770
1892
|
/**
|
|
1771
|
-
* The publishing mode, refusing
|
|
1893
|
+
* The publishing mode, refusing a boolean.
|
|
1772
1894
|
*
|
|
1773
1895
|
* A boolean is refused rather than mapped onto the nearest mode, because the
|
|
1774
1896
|
* reading `false` invited — *this package has no web presence* — is exactly the
|
|
@@ -1827,7 +1949,7 @@ function normalizePublish(value) {
|
|
|
1827
1949
|
// A retired key is refused by name, ahead of the vocabulary check: reported
|
|
1828
1950
|
// as merely unrecognized it would read as a misspelling of the one key that
|
|
1829
1951
|
// survives, and the author would correct the spelling rather than learn
|
|
1830
|
-
// that the mechanism is gone
|
|
1952
|
+
// that the mechanism is gone.
|
|
1831
1953
|
for (const key of Object.keys(address)) {
|
|
1832
1954
|
if (Object.hasOwn(RETIRED_ADDRESS_KEYS, key)) {
|
|
1833
1955
|
fail(`publish.address.${key}`, RETIRED_ADDRESS_KEYS[key]);
|
|
@@ -1847,7 +1969,7 @@ function normalizePublish(value) {
|
|
|
1847
1969
|
}
|
|
1848
1970
|
if (prefix.startsWith("/")) {
|
|
1849
1971
|
// A leading slash would make the recorded address package-absolute,
|
|
1850
|
-
// which is exactly the site-absolute shape
|
|
1972
|
+
// which is exactly the site-absolute shape this avoids.
|
|
1851
1973
|
fail("publish.address.prefix", "must not begin with a slash");
|
|
1852
1974
|
}
|
|
1853
1975
|
|
|
@@ -1900,7 +2022,7 @@ export function defineConfig(config) {
|
|
|
1900
2022
|
const packs = input.packs.map((pack, index) => normalizePack(pack, `packs[${index}]`));
|
|
1901
2023
|
|
|
1902
2024
|
// One list, so the compile order and the directory list cannot disagree —
|
|
1903
|
-
//
|
|
2025
|
+
// as `PACK_CONFIGS` and `SOURCE_PACKS` they would be maintained apart.
|
|
1904
2026
|
const packDirectories = packs.flatMap((pack) => [
|
|
1905
2027
|
pack.name,
|
|
1906
2028
|
...pack.companions.map((companion) => companion.name),
|
|
@@ -1913,7 +2035,7 @@ export function defineConfig(config) {
|
|
|
1913
2035
|
seen.add(name);
|
|
1914
2036
|
}
|
|
1915
2037
|
|
|
1916
|
-
// ── systems: declaring, and requiring, are separate decisions
|
|
2038
|
+
// ── systems: declaring, and requiring, are separate decisions ──────
|
|
1917
2039
|
const systems = normalizeSystems(input.systems);
|
|
1918
2040
|
const requiresSystem = normalizeRequiresSystem(input.requiresSystem);
|
|
1919
2041
|
const declaredSystems = new Set(Object.keys(systems));
|
|
@@ -1934,7 +2056,7 @@ export function defineConfig(config) {
|
|
|
1934
2056
|
// A name that resolves to nothing is a build error rather than a
|
|
1935
2057
|
// fall-through, in the spirit the rest of this file already follows: a pack
|
|
1936
2058
|
// stamping a system nobody declared would stamp `undefined`, which is the
|
|
1937
|
-
// plausible lie
|
|
2059
|
+
// plausible lie.
|
|
1938
2060
|
if (requiresSystem !== null && !declaredSystems.has(requiresSystem)) {
|
|
1939
2061
|
fail(
|
|
1940
2062
|
"requiresSystem",
|
|
@@ -1951,14 +2073,14 @@ export function defineConfig(config) {
|
|
|
1951
2073
|
// verified version `statsForPack` reads, or this package's own
|
|
1952
2074
|
// package-wide system, whose stats answer for every pack of it.
|
|
1953
2075
|
//
|
|
1954
|
-
//
|
|
2076
|
+
// Skipping this entirely when `systems:` is empty or absent
|
|
1955
2077
|
// — `declaredSystems.size &&` guarded it — which left the case the
|
|
1956
2078
|
// comment above was written about wide open. `harn-ensemble` declares
|
|
1957
2079
|
// `system: sohl` and `system: hm3` on its packs, no `systems:` block,
|
|
1958
2080
|
// and no package-wide system, so every pack fell through to a
|
|
1959
2081
|
// package-wide stat that is null: 2,513 compiled actors stamped
|
|
1960
2082
|
// `_stats.systemId: null` in a pack that says `system: sohl` on the
|
|
1961
|
-
// line above. That is the plausible lie
|
|
2083
|
+
// line above. That is the plausible lie, reached by the
|
|
1962
2084
|
// one path this check did not cover, and the `requiresSystem` check ten
|
|
1963
2085
|
// lines up already refuses its own version of it in as many words.
|
|
1964
2086
|
if (!declaredSystems.has(pack.system) && pack.system !== packageWide) {
|
|
@@ -1994,7 +2116,7 @@ export function defineConfig(config) {
|
|
|
1994
2116
|
|
|
1995
2117
|
// Several packs of one document type are allowed — editorial grouping of
|
|
1996
2118
|
// same-type documents is ordinary Foundry practice, and collapsing such a
|
|
1997
|
-
// layout breaks every stored compendium UUID
|
|
2119
|
+
// layout breaks every stored compendium UUID. What is not allowed
|
|
1998
2120
|
// is two candidates for the same undeclared note.
|
|
1999
2121
|
const defaultsByType = new Map();
|
|
2000
2122
|
for (const pack of packs) {
|
|
@@ -2029,12 +2151,12 @@ export function defineConfig(config) {
|
|
|
2029
2151
|
itemFieldsBySystem,
|
|
2030
2152
|
itemTypesBySeveralSystems,
|
|
2031
2153
|
} = normalizeItemBuilders(input.itemBuilders);
|
|
2032
|
-
// The union across every declared registry
|
|
2154
|
+
// The union across every declared registry — the flat table already
|
|
2033
2155
|
// 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
|
|
2156
|
+
// rather than becoming a second list to keep in step.
|
|
2035
2157
|
const itemTypes = Object.freeze(new Set(Object.keys(itemBuilders)));
|
|
2036
2158
|
// Every note that compiles into a *system-bearing* document publishes its
|
|
2037
|
-
// prose as a documentation JournalEntry, and that includes actors
|
|
2159
|
+
// prose as a documentation JournalEntry, and that includes actors.
|
|
2038
2160
|
// A being was the one such note with no `none` address — its only address
|
|
2039
2161
|
// named the Actor — so nothing a prose link wrote could land on its page.
|
|
2040
2162
|
// `doc` stays out for the reason that actually applies to it: its single
|
|
@@ -2052,7 +2174,7 @@ export function defineConfig(config) {
|
|
|
2052
2174
|
// one place `systems/sohl` (or `modules/sohl-thalorna`) is spelled.
|
|
2053
2175
|
assetRoot: `${packageKind}/${foundryPackage}/assets`,
|
|
2054
2176
|
paths: normalizePaths(input.paths, rootDir),
|
|
2055
|
-
// The package-wide system, derived
|
|
2177
|
+
// The package-wide system, derived. A **system** package is its
|
|
2056
2178
|
// own system, which is true by construction and needs no declaration. A
|
|
2057
2179
|
// **module** takes the one it requires, or the one system it declares
|
|
2058
2180
|
// when there is exactly one; with several and no gate there is no
|
|
@@ -2097,6 +2219,7 @@ export function defineConfig(config) {
|
|
|
2097
2219
|
itemTypes,
|
|
2098
2220
|
docEntryTypes,
|
|
2099
2221
|
skipDirectories: Object.freeze(skipDirectories),
|
|
2222
|
+
icons: normalizeIcons(input.icons, rootDir),
|
|
2100
2223
|
packs: Object.freeze(packs),
|
|
2101
2224
|
packDirectories: Object.freeze(packDirectories),
|
|
2102
2225
|
docs: normalizeDocs(input.docs),
|