@heroiclands/package-build 0.6.1 → 3.0.1

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.
Files changed (127) hide show
  1. package/CHANGELOG-content-build.md +965 -0
  2. package/CHANGELOG.md +68 -0
  3. package/CONTENT.md +824 -0
  4. package/MIGRATING.md +127 -0
  5. package/README.md +29 -17
  6. package/bin/content-build.mjs +1082 -0
  7. package/bin/package-build.mjs +2 -2
  8. package/bin/report.mjs +2 -2
  9. package/config.mjs +21 -18
  10. package/content-config.mjs +1424 -0
  11. package/coverage.mjs +1 -1
  12. package/e2e.mjs +1 -1
  13. package/engine/abbreviations.mjs +0 -0
  14. package/engine/base-compiler.mjs +608 -0
  15. package/engine/code-fences.mjs +311 -0
  16. package/engine/compendiums.mjs +353 -0
  17. package/engine/content-address.mjs +141 -0
  18. package/engine/content-links.mjs +438 -0
  19. package/engine/content-lint.mjs +204 -0
  20. package/engine/content-package.mjs +67 -0
  21. package/engine/content-slug.mjs +169 -0
  22. package/engine/content-tables.mjs +1216 -0
  23. package/engine/content-tree.mjs +56 -0
  24. package/engine/diagnostics.mjs +270 -0
  25. package/engine/field-reference.mjs +242 -0
  26. package/engine/field-spec.mjs +218 -0
  27. package/engine/foreign-catalog.mjs +450 -0
  28. package/engine/foreign-manifests.mjs +129 -0
  29. package/engine/frontmatter-lint.mjs +343 -0
  30. package/engine/frontmatter.mjs +258 -0
  31. package/engine/generate.mjs +305 -0
  32. package/engine/helpers.mjs +809 -0
  33. package/engine/ids.mjs +192 -0
  34. package/engine/index.mjs +142 -0
  35. package/engine/item-docs.mjs +153 -0
  36. package/engine/item-registry.mjs +129 -0
  37. package/engine/journals.mjs +374 -0
  38. package/engine/kb-manifest.mjs +473 -0
  39. package/engine/macros.mjs +333 -0
  40. package/engine/manifest-emit.mjs +339 -0
  41. package/engine/map-notes.mjs +1302 -0
  42. package/engine/pack-config.mjs +479 -0
  43. package/engine/pack-router.mjs +254 -0
  44. package/engine/prose-config.mjs +238 -0
  45. package/engine/prose-lint.mjs +294 -0
  46. package/engine/region-events.mjs +79 -0
  47. package/engine/scene-levels.mjs +420 -0
  48. package/engine/scenes.mjs +528 -0
  49. package/engine/site-build.mjs +778 -0
  50. package/engine/site-index.mjs +332 -0
  51. package/engine/web-wikilinks.mjs +330 -0
  52. package/engine/wikilink-syntax.mjs +105 -0
  53. package/engine/wikilinks.mjs +645 -0
  54. package/index.mjs +42 -25
  55. package/lang.mjs +1 -1
  56. package/manifest.mjs +79 -5
  57. package/markdownlint-config.mjs +45 -0
  58. package/package.json +78 -35
  59. package/prettier-config.mjs +34 -0
  60. package/sohl/actors.mjs +581 -0
  61. package/sohl/affiliation-standings.mjs +44 -0
  62. package/sohl/being-info.mjs +173 -0
  63. package/sohl/default-item-art.mjs +82 -0
  64. package/sohl/index.mjs +55 -0
  65. package/sohl/item-builders.mjs +114 -0
  66. package/sohl/item-fields.mjs +867 -0
  67. package/sohl/items.mjs +197 -0
  68. package/sohl/kb-passes.mjs +207 -0
  69. package/sohl/note-schemas.mjs +322 -0
  70. package/templates.mjs +1 -1
  71. package/types/config.d.mts +1 -1
  72. package/types/content-config.d.mts +571 -0
  73. package/types/engine/abbreviations.d.mts +44 -0
  74. package/types/engine/base-compiler.d.mts +320 -0
  75. package/types/engine/code-fences.d.mts +102 -0
  76. package/types/engine/compendiums.d.mts +77 -0
  77. package/types/engine/content-address.d.mts +61 -0
  78. package/types/engine/content-links.d.mts +87 -0
  79. package/types/engine/content-lint.d.mts +47 -0
  80. package/types/engine/content-package.d.mts +36 -0
  81. package/types/engine/content-slug.d.mts +59 -0
  82. package/types/engine/content-tables.d.mts +131 -0
  83. package/types/engine/content-tree.d.mts +12 -0
  84. package/types/engine/diagnostics.d.mts +152 -0
  85. package/types/engine/field-reference.d.mts +20 -0
  86. package/types/engine/field-spec.d.mts +181 -0
  87. package/types/engine/foreign-catalog.d.mts +103 -0
  88. package/types/engine/foreign-manifests.d.mts +43 -0
  89. package/types/engine/frontmatter-lint.d.mts +74 -0
  90. package/types/engine/frontmatter.d.mts +99 -0
  91. package/types/engine/generate.d.mts +56 -0
  92. package/types/engine/helpers.d.mts +348 -0
  93. package/types/engine/ids.d.mts +126 -0
  94. package/types/engine/index.d.mts +33 -0
  95. package/types/engine/item-docs.d.mts +73 -0
  96. package/types/engine/item-registry.d.mts +57 -0
  97. package/types/engine/journals.d.mts +121 -0
  98. package/types/engine/kb-manifest.d.mts +214 -0
  99. package/types/engine/macros.d.mts +201 -0
  100. package/types/engine/manifest-emit.d.mts +137 -0
  101. package/types/engine/map-notes.d.mts +283 -0
  102. package/types/engine/pack-config.d.mts +77 -0
  103. package/types/engine/pack-router.d.mts +53 -0
  104. package/types/engine/prose-config.d.mts +121 -0
  105. package/types/engine/prose-lint.d.mts +59 -0
  106. package/types/engine/region-events.d.mts +43 -0
  107. package/types/engine/scene-levels.d.mts +36 -0
  108. package/types/engine/scenes.d.mts +48 -0
  109. package/types/engine/site-build.d.mts +214 -0
  110. package/types/engine/site-index.d.mts +141 -0
  111. package/types/engine/web-wikilinks.d.mts +67 -0
  112. package/types/engine/wikilink-syntax.d.mts +101 -0
  113. package/types/engine/wikilinks.d.mts +172 -0
  114. package/types/index.d.mts +3 -0
  115. package/types/manifest.d.mts +52 -4
  116. package/types/markdownlint-config.d.mts +8 -0
  117. package/types/prettier-config.d.mts +1 -0
  118. package/types/sohl/actors.d.mts +65 -0
  119. package/types/sohl/affiliation-standings.d.mts +22 -0
  120. package/types/sohl/being-info.d.mts +78 -0
  121. package/types/sohl/default-item-art.d.mts +28 -0
  122. package/types/sohl/index.d.mts +7 -0
  123. package/types/sohl/item-builders.d.mts +11 -0
  124. package/types/sohl/item-fields.d.mts +20 -0
  125. package/types/sohl/items.d.mts +19 -0
  126. package/types/sohl/kb-passes.d.mts +71 -0
  127. package/types/sohl/note-schemas.d.mts +7 -0
package/engine/ids.mjs ADDED
@@ -0,0 +1,192 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * Deterministic document ids, derived by hashing rather than stored.
16
+ *
17
+ * Several pack-build passes must agree on an id without being able to see each
18
+ * other's output — the items pass and the journals pass on an item doc's entry
19
+ * id, a section link and the page it addresses on that page's id. They agree by
20
+ * deriving the id from the same inputs, so the derivation has to be reachable
21
+ * from every one of them.
22
+ *
23
+ * That is why this is its own module and not part of `helpers.mjs`: link
24
+ * resolution needs it, and `helpers.mjs` imports the link resolver. A leaf with
25
+ * no local imports can be depended on from anywhere without a cycle.
26
+ */
27
+
28
+ import crypto from "crypto";
29
+
30
+ /**
31
+ * Stable 16-char hex id derived from `${namespace}:${value}`. Use for deriving
32
+ * page ids from heading text when no explicit id is supplied.
33
+ *
34
+ * @param {string} namespace - Keeps unrelated derivations from colliding.
35
+ * @param {string} value - The input the id is a function of.
36
+ * @returns {string} A 16-character hexadecimal Foundry id.
37
+ */
38
+ export function makeId(namespace, value) {
39
+ return crypto
40
+ .createHash("sha1")
41
+ .update(`${namespace}:${value}`)
42
+ .digest("hex")
43
+ .slice(0, 16);
44
+ }
45
+
46
+ /**
47
+ * Every content type that compiles into a Foundry `Scene` — a **map note**
48
+ * (#1525). The three differ only in derived canvas defaults, which is the map
49
+ * compiler's business; everything else treats them alike.
50
+ *
51
+ * Declared in this leaf module because several passes that must not depend on
52
+ * the map compiler need it: the pack router below, and the doc-carrying type
53
+ * set in `item-docs.mjs` (a map note's prose becomes a JournalEntry, exactly as
54
+ * an item's or a macro's does).
55
+ *
56
+ * @type {ReadonlySet<string>}
57
+ */
58
+ export const MAP_TYPES = Object.freeze(
59
+ new Set(["battlemap", "localmap", "regionalmap"]),
60
+ );
61
+
62
+ /**
63
+ * Content type → the pack its documents compile into, and the document type
64
+ * that pack holds.
65
+ *
66
+ * These are pack **names**, not addresses. The package that owns the pack is
67
+ * supplied by the caller, because it is a property of the repository doing the
68
+ * building and not of the content: the same notes compiled by a different
69
+ * repository belong to a different package. Baking the package into these
70
+ * values is what made every link emitted by `sohl-thalorna` address the `sohl`
71
+ * system (#1498) — correct here only by coincidence.
72
+ *
73
+ * @type {Readonly<Record<string, {pack: string, docType: string}>>}
74
+ */
75
+ export const PACK_BY_TYPE = Object.freeze({
76
+ doc: { pack: "journals", docType: "JournalEntry" },
77
+ macro: { pack: "macros", docType: "Macro" },
78
+ being: { pack: "actors", docType: "Actor" },
79
+ ...Object.fromEntries(
80
+ [...MAP_TYPES].map((type) => [
81
+ type,
82
+ { pack: "scenes", docType: "Scene" },
83
+ ]),
84
+ ),
85
+ });
86
+
87
+ /**
88
+ * Content types that no longer exist, and what replaced each one.
89
+ *
90
+ * `character` and `creature` were retired in favour of the single `being` they
91
+ * had always compiled into (SoHL#1580). They are recorded here rather than
92
+ * simply deleted because deleting them is the one change that fails *quietly*:
93
+ * every type not named in {@link PACK_BY_TYPE} falls through to the open item
94
+ * set below, so a note or a link left on the old spelling would be routed to
95
+ * the items pack — a wrong answer, arrived at silently, which is exactly the
96
+ * failure mode the open-set default exists to avoid for real item types.
97
+ *
98
+ * Keeping the names lets {@link assertTypeNotRetired} say what happened and
99
+ * what to write instead. Entries stay for as long as content in the wild might
100
+ * still carry them.
101
+ *
102
+ * @type {Readonly<Record<string, string>>}
103
+ */
104
+ export const RETIRED_TYPES = Object.freeze({
105
+ character: "being",
106
+ creature: "being",
107
+ });
108
+
109
+ /**
110
+ * Throw if `type` names a retired content type.
111
+ *
112
+ * @param {string} type - The note's declared `type`, or a link's qualifier.
113
+ * @param {string} [where] - What carries it — a file path, a link target —
114
+ * appended to the message so the reader can go straight to it.
115
+ * @throws {Error} Naming the replacement type.
116
+ */
117
+ export function assertTypeNotRetired(type, where) {
118
+ const replacement = RETIRED_TYPES[type];
119
+ if (!replacement) return;
120
+ throw new Error(
121
+ `Content type "${type}" was retired in favour of "${replacement}"` +
122
+ (where ? ` — ${where}` : "") +
123
+ `. Both compiled to the same document, so the fix is mechanical: ` +
124
+ `write "${replacement}".`,
125
+ );
126
+ }
127
+
128
+ /** Where every other content type compiles: the items pack. */
129
+ export const ITEM_PACK = Object.freeze({ pack: "items", docType: "Item" });
130
+
131
+ /**
132
+ * The pack a type's documents live in, in the conventional one-pack-per-type
133
+ * layout.
134
+ *
135
+ * Item types are the open set — a new one is added whenever the system grows a
136
+ * document type — so they are the **default** rather than an enumerated list. A
137
+ * hand-maintained list is what made an entire content directory silently
138
+ * unlinkable once (#1276); nothing to maintain, nothing to forget.
139
+ *
140
+ * The `docType` is the authority: it is a property of the *content type* and
141
+ * holds however a repository names or splits its packs. The `pack` is the
142
+ * conventional name only — a repository may rename its packs, or ship several
143
+ * of one type (#1566), in which case the pack a particular note's document
144
+ * lands in comes from `engine/pack-router.mjs` and is passed to
145
+ * {@link compendiumUuid} explicitly. This module stays free of the
146
+ * configuration so the link resolver above it can stay pure.
147
+ *
148
+ * @param {string} type - The target note's `type`.
149
+ * @returns {{pack: string, docType: string}} The pack and document type.
150
+ * @throws {Error} If `type` names a retired content type — see
151
+ * {@link RETIRED_TYPES}. The open-set default would otherwise route it to the
152
+ * items pack and say nothing.
153
+ */
154
+ export function packForType(type) {
155
+ assertTypeNotRetired(type);
156
+ return PACK_BY_TYPE[type] ?? ITEM_PACK;
157
+ }
158
+
159
+ /**
160
+ * A document's full compendium UUID.
161
+ *
162
+ * This is the one place a UUID is spelled. Every link is resolved by looking up
163
+ * an address computed here — never by concatenating a prefix at the point of
164
+ * use, which is how the package came to be hard-coded in two separate files.
165
+ *
166
+ * @param {string} packageId - The Foundry package that ships the pack, e.g.
167
+ * `sohl`. A system id or a module id; Foundry addresses both the same way.
168
+ * @param {string} type - The note's content `type`.
169
+ * @param {string} id - The document's id.
170
+ * @param {string} [packName] - The pack the document actually landed in, from
171
+ * the pack router. Supplied wherever the note is known, because a repository
172
+ * may ship several packs of one type and a UUID carries the pack name
173
+ * (#1566). Omitted only where there is no note to route — the conventional
174
+ * name from {@link packForType} then stands in.
175
+ * @returns {string} `Compendium.<packageId>.<pack>.<DocumentType>.<id>`
176
+ */
177
+ export function compendiumUuid(packageId, type, id, packName) {
178
+ const { pack, docType } = packForType(type);
179
+ return `Compendium.${packageId}.${packName || pack}.${docType}.${id}`;
180
+ }
181
+
182
+ /**
183
+ * The UUID of a JournalEntry page.
184
+ *
185
+ * @param {string} entryUuid - The owning entry's UUID, from
186
+ * {@link compendiumUuid}.
187
+ * @param {string} pageId - The page's id.
188
+ * @returns {string} The page's UUID.
189
+ */
190
+ export function pageUuid(entryUuid, pageId) {
191
+ return `${entryUuid}.JournalEntryPage.${pageId}`;
192
+ }
@@ -0,0 +1,142 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * The package-agnostic half of the toolchain: everything that knows how a
16
+ * HeroicLands content tree is shaped, but nothing about any particular game
17
+ * system's data model.
18
+ *
19
+ * The content walk, frontmatter parsing, table generation, wikilink
20
+ * resolution, id and folder derivation, the link manifest and the web-address
21
+ * rule, `BasePackCompiler`, and the generic Foundry document compilers
22
+ * (journals, macros, scenes) all live here (#1512).
23
+ *
24
+ * **Each module is re-exported as its own namespace, not flattened.** Several
25
+ * of them deliberately re-export a neighbour's symbol so a caller keeps one
26
+ * import path (`helpers` re-exports the frontmatter readers and `makeId`;
27
+ * `wikilinks` re-exports the pack router). Flattened, every such name would
28
+ * become an ambiguous star export and vanish from this barrel silently. Each
29
+ * module is also reachable as its own entry point —
30
+ * `@heroiclands/package-build/engine/<module>` — which is how a build that
31
+ * needs one thing avoids loading the whole pipeline.
32
+ *
33
+ * @module
34
+ */
35
+
36
+ /** Deterministic document ids, the conventional pack map, and compendium UUIDs. */
37
+ export * as ids from "./ids.mjs";
38
+
39
+ /** Fenced-code detection, so a rewrite never edits a code block. */
40
+ export * as codeFences from "./code-fences.mjs";
41
+
42
+ /** The `sohl:` frontmatter readers, shared by every content package. */
43
+ export * as frontmatter from "./frontmatter.mjs";
44
+
45
+ /** The content walk's note census — the empty-tree guard's evidence. */
46
+ export * as contentTree from "./content-tree.mjs";
47
+
48
+ /** The consuming repository's resolved `package-build.config.yaml`. */
49
+ export * as packConfig from "./pack-config.mjs";
50
+
51
+ /** Which pack of a document type a note's document lands in (#1566). */
52
+ export * as packRouter from "./pack-router.mjs";
53
+
54
+ /** The content package a build compiles, and the Foundry package it ships in. */
55
+ export * as contentPackage from "./content-package.mjs";
56
+
57
+ /** The shipped Foundry manifest: locating it, reading it, guarding its id. */
58
+
59
+ /** The URL a content note is published at — the one web-address rule. */
60
+ export * as contentSlug from "./content-slug.mjs";
61
+
62
+ /** Section and address derivation on top of {@link contentSlug}. */
63
+ export * as contentAddress from "./content-address.mjs";
64
+
65
+ /** Whether a vendored manifest can still be addressed, not merely read. */
66
+ export * as foreignManifests from "./foreign-manifests.mjs";
67
+
68
+ /** The cross-package link manifest: reader, writer, and canonical keys. */
69
+ export * as kbManifest from "./kb-manifest.mjs";
70
+
71
+ /** Deriving this package's own link manifest from its content tree. */
72
+ export * as manifestEmit from "./manifest-emit.mjs";
73
+
74
+ /** Publishing a content tree as a website: the pass, and its integrity gates. */
75
+ export * as siteBuild from "./site-build.mjs";
76
+
77
+ /** Address rules every content tree is linted against: shape, uniqueness, alias. */
78
+ export * as contentLint from "./content-lint.mjs";
79
+
80
+ /** Resolving every link in a tree, and the ones that land nowhere. */
81
+ export * as contentLinks from "./content-links.mjs";
82
+
83
+ /** Wikilinks resolved to a **web URL** — the site half of the pair below. */
84
+ export * as webWikilinks from "./web-wikilinks.mjs";
85
+
86
+ /** Dataview-style content tables, expanded into markdown at compile time. */
87
+ export * as contentTables from "./content-tables.mjs";
88
+
89
+ /** Markdown parsing, stats, folders, images, and the wikilink index. */
90
+ export * as helpers from "./helpers.mjs";
91
+
92
+ /** The consumer's resolved item-type registry: the whitelist and its builders. */
93
+ export * as itemRegistry from "./item-registry.mjs";
94
+
95
+ /** Which types carry documentation of their own, and where it is addressed. */
96
+ export * as itemDocs from "./item-docs.mjs";
97
+
98
+ /** Wikilinks resolved to a **Foundry UUID**: qualifiers, the index, rewriting. */
99
+ export * as wikilinks from "./wikilinks.mjs";
100
+
101
+ /** What a `[[…]]` is, before either resolver decides where it points. */
102
+ export * as wikilinkSyntax from "./wikilink-syntax.mjs";
103
+
104
+ /** The address index a site build resolves its wikilinks against. */
105
+ export * as siteIndex from "./site-index.mjs";
106
+
107
+ /** The shape every pack compiler shares. */
108
+ export * as baseCompiler from "./base-compiler.mjs";
109
+
110
+ /** The JournalEntry compiler, and the page/anchor derivation it shares. */
111
+ export * as journals from "./journals.mjs";
112
+
113
+ /** The Macro compiler. */
114
+ export * as macros from "./macros.mjs";
115
+
116
+ /** The map-note schema a Scene is authored in. */
117
+ export * as mapNotes from "./map-notes.mjs";
118
+
119
+ /** The Scene and Adventure compiler. */
120
+ export * as scenes from "./scenes.mjs";
121
+
122
+ /** The compiled-pack Scene/Level integrity guard. */
123
+ export * as sceneLevels from "./scene-levels.mjs";
124
+
125
+ /** Pack JSON generation — the pass that turns a content tree into documents. */
126
+ export * as generate from "./generate.mjs";
127
+
128
+ /** Compile, unpack, and clean the LevelDB packs. */
129
+ export * as compendiums from "./compendiums.mjs";
130
+
131
+ // The region-event vocabulary stays flat as well as namespaced: the Foundry
132
+ // runtime imports these three by name through
133
+ // `@heroiclands/package-build/engine/region-events`, and they were part of this
134
+ // barrel's surface before the pipeline arrived (#1510).
135
+ export {
136
+ CURATED_REGION_EVENTS,
137
+ EXCLUDED_REGION_EVENTS,
138
+ REGION_EVENT_TO_TRIGGER,
139
+ } from "./region-events.mjs";
140
+
141
+ /** The curated region-event vocabulary, shared with the Foundry runtime. */
142
+ export * as regionEvents from "./region-events.mjs";
@@ -0,0 +1,153 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * **Item docs** — an item's prose compiled as a JournalEntry, with the item
16
+ * keeping only a pointer to it.
17
+ *
18
+ * An item note's body describes what the thing *is*. That is documentation, and
19
+ * documentation belongs in the journals pack, so each item note compiles into a
20
+ * JournalEntry and the item's `system.docHtml` becomes nothing but a `@UUID`
21
+ * link to that entry's first page — the description-as-pointer convention
22
+ * (#1356), which {@link sohl.utils.descriptionLinkTarget} recognises and
23
+ * Display Description follows.
24
+ *
25
+ * The prose then exists once. Previously every actor carrying an item carried
26
+ * its own copy of that item's description: 7.59 MB across the actors pack, of
27
+ * which only 133 KB was distinct text (#1348). Nothing about actors changes —
28
+ * they embed whatever the item carries, and what the item carries is now a link.
29
+ *
30
+ * **Two passes, no shared state.** The items pass writes the pointer; the
31
+ * journals pass writes the entry it points at. Neither can see the other's
32
+ * output, so both derive the same ids from the item note's own id — the same
33
+ * technique {@link anchorPageId} uses to let a section link and its page agree.
34
+ *
35
+ * **The shape generalises.** A `macro` note is the same arrangement: it
36
+ * compiles into a Macro, and its prose into a JournalEntry addressed
37
+ * `docmacro/<shortcode>` (#1514). So is a **map note**, which compiles into a
38
+ * Scene and whose prose becomes the place description its map pins point at
39
+ * (#1525). {@link docEntryTypes} is the one set both the compilers and the
40
+ * link manifest read to know which types work this way.
41
+ *
42
+ * Plain ESM with no Foundry and no filesystem access, so it is unit-testable.
43
+ */
44
+
45
+ import { compendiumUuid, makeId, pageUuid } from "./ids.mjs";
46
+ import { loadPackConfig } from "./pack-config.mjs";
47
+ import { itemTypes } from "./item-registry.mjs";
48
+ import { packRouter } from "./pack-router.mjs";
49
+
50
+ /**
51
+ * Every content type that compiles into an item — and therefore into an item
52
+ * doc. Re-exported here rather than restated because both passes need it: the
53
+ * items pass to know what to compile, the journals pass to know whose prose it
54
+ * is holding.
55
+ *
56
+ * It is the consuming repository's `itemBuilders` keys, resolved once in
57
+ * `item-registry.mjs` alongside the builder lookup the Item compiler dispatches
58
+ * through — one object, so the whitelist and the table cannot disagree
59
+ * (#1504/#1563).
60
+ */
61
+ export { itemTypes };
62
+
63
+ /**
64
+ * Every content type whose **prose compiles into a JournalEntry of its own**,
65
+ * addressed by the virtual `doc<type>` qualifier.
66
+ *
67
+ * Every item type, plus `macro` — a macro note's body documents the script the
68
+ * note also compiles into a Macro (#1514), which is the same shape as an item
69
+ * and its description: one note, two documents, the prose living in the
70
+ * journals pack.
71
+ *
72
+ * **One set, read by the compiler and the emitter alike.** The journals pass
73
+ * decides what to compile from it, and the link manifest decides what to
74
+ * publish a `doc<type>` entry for. Held apart, the two drift into a manifest
75
+ * that asserts documentation nothing compiled — or a compiled entry no
76
+ * consumer can address. It is composed exactly once, in `defineConfig`, and
77
+ * read from there — never recomposed at a call site.
78
+ *
79
+ * `doc` notes and actors are absent: each is a single document, so it has no
80
+ * separate documentation to address.
81
+ *
82
+ * An accessor rather than a hoisted constant, so that importing this module
83
+ * needs no configuration (#2).
84
+ *
85
+ * @returns {ReadonlySet<string>} The configured doc-carrying types.
86
+ */
87
+ export function docEntryTypes() {
88
+ return loadPackConfig().docEntryTypes;
89
+ }
90
+
91
+ /**
92
+ * Whether a content note's type is one whose prose becomes a JournalEntry of
93
+ * its own.
94
+ *
95
+ * @param {string} type - The note's `type` frontmatter.
96
+ * @returns {boolean} True for an item type, for `macro` and for a map type;
97
+ * false for `doc` and for actors.
98
+ */
99
+ export function hasDocEntry(type) {
100
+ return docEntryTypes().has(String(type));
101
+ }
102
+
103
+ /**
104
+ * The id of the JournalEntry a note's prose compiles into — an item's, or a
105
+ * macro's.
106
+ *
107
+ * Derived from the note's own id so that the pass writing the document and the
108
+ * pass writing its documentation agree without either reading the other's
109
+ * output. It is deliberately not the note's id itself: the two documents are
110
+ * distinct, live in different packs, and sharing an id would make either one's
111
+ * UUID ambiguous to read.
112
+ *
113
+ * The `"item-doc"` hash namespace is **frozen**: it is baked into every entry
114
+ * id already shipped, and every `@UUID` pointing at one. It names where the
115
+ * derivation started, not what may use it.
116
+ *
117
+ * @param {string} itemId - The note's `id` frontmatter.
118
+ * @returns {string} A 16-character Foundry id.
119
+ */
120
+ export function itemDocEntryId(itemId) {
121
+ return makeId("item-doc", itemId);
122
+ }
123
+
124
+ /**
125
+ * The description an item carries in place of its prose: a `@UUID` link to the
126
+ * first page of its item doc, and nothing else.
127
+ *
128
+ * "Nothing else" is the whole convention — a description that is *only* a link
129
+ * is a pointer, and anything alongside it would make it ordinary prose that
130
+ * happens to contain a link, which the runtime would then show verbatim.
131
+ *
132
+ * @param {string} packageId - The Foundry package shipping the journals pack.
133
+ * Supplied rather than assumed (#1498).
134
+ * @param {string} itemId - The item note's `id` frontmatter.
135
+ * @param {string} name - The item's name, used as the link's label. It shows
136
+ * only if the target ever fails to resolve, where a broken link naming the
137
+ * item beats a bare UUID.
138
+ * @param {string} firstPageId - The id of the entry's first page, from
139
+ * {@link journalPageId}.
140
+ * @returns {string} The pointer to store in `system.docHtml`.
141
+ */
142
+ export function itemDocPointer(packageId, itemId, name, firstPageId) {
143
+ // An item doc is a *derived* document: it lands in the default
144
+ // JournalEntry pack whatever Item pack the item itself was routed to
145
+ // (#1566).
146
+ const entryUuid = compendiumUuid(
147
+ packageId,
148
+ "doc",
149
+ itemDocEntryId(itemId),
150
+ packRouter().defaultOf("JournalEntry"),
151
+ );
152
+ return `@UUID[${pageUuid(entryUuid, firstPageId)}]{${name}}`;
153
+ }
@@ -0,0 +1,129 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * **The resolved item-type registry** — the consuming repository's
16
+ * `itemBuilders` table, and the type whitelist derived from its keys.
17
+ *
18
+ * Both are read from the one resolved configuration, so they are literally the
19
+ * same object's keys and values: a type cannot be whitelisted for compilation
20
+ * without the builder that compiles it, which is the guarantee #1504 exists
21
+ * for. The Item compiler dispatches through {@link itemBuilder}, so the table a
22
+ * consumer configured is the table its notes compile with — the whitelist and
23
+ * the dispatch used to come from different places, and a consumer supplying its
24
+ * own registry got the types it asked for and the builders it did not (#1563).
25
+ *
26
+ * **The registry itself is a consumer's, and stays a leaf.** SoHL's lives in
27
+ * `@heroiclands/package-build/sohl/item-builders`; the consumer names it in
28
+ * `package-build.config.yaml`. That module must never read the resolved
29
+ * configuration — it is loaded *during* the resolution such a read would be
30
+ * asking for, whether the loader requires it by name for a data config or a
31
+ * code config imports it directly, so a read from there would close a cycle
32
+ * around the configuration's own evaluation. Data travels *into*
33
+ * configuration; only modules like this one, which nothing on that path
34
+ * imports, read back out of it.
35
+ *
36
+ * @module
37
+ */
38
+
39
+ import { loadPackConfig } from "./pack-config.mjs";
40
+ import { resolveImg } from "./helpers.mjs";
41
+
42
+ /**
43
+ * Every content type that compiles into an item — and therefore into an item
44
+ * doc. Read by the Item compiler to know what to claim, and by the journals
45
+ * pass to know whose prose it is holding.
46
+ *
47
+ * **Derived, never authored.** These are the keys of the consuming
48
+ * repository's `itemBuilders` registry, so the whitelist and the builder table
49
+ * are the same list and cannot drift apart. They already had: `trait` was
50
+ * whitelisted long after the item type was retired (#651), with no builder
51
+ * behind it, so every `type: trait` note passed the gate and then failed to
52
+ * compile (#1504).
53
+ *
54
+ * An accessor rather than a hoisted constant, so that importing this module
55
+ * needs no configuration (#2).
56
+ *
57
+ * @returns {ReadonlySet<string>} The configured item types.
58
+ */
59
+ export function itemTypes() {
60
+ return loadPackConfig().itemTypes;
61
+ }
62
+
63
+ /**
64
+ * The builder the consuming repository registered for an item type.
65
+ *
66
+ * Unreachable through the compiler — its whitelist *is* this registry's keys —
67
+ * so a throw here means a caller invented a type. It names the type rather than
68
+ * failing as an anonymous `is not a function` (#1504).
69
+ *
70
+ * @param {string} type - The note's `type` frontmatter.
71
+ * @returns {(fm: object) => object} The builder for that type.
72
+ * @throws {Error} When the configuration registers no builder for `type`.
73
+ */
74
+ export function itemBuilder(type) {
75
+ const builder = /** @type {Record<string, Function>} */ (
76
+ loadPackConfig().itemBuilders
77
+ )[type];
78
+ if (typeof builder !== "function") {
79
+ throw new Error(
80
+ `No builder registered for item type "${type}" — add one to the ` +
81
+ `\`itemBuilders\` registry this repository declares in ` +
82
+ `package-build.config.yaml, or stop declaring the type.`,
83
+ );
84
+ }
85
+ return /** @type {(fm: object) => object} */ (builder);
86
+ }
87
+
88
+ /**
89
+ * The default art for an item type — the image a note of that type is given
90
+ * when it carries no `img:` of its own.
91
+ *
92
+ * Read from the consuming repository's `itemBuilders` registry, the same place
93
+ * the type itself is declared, so a consumer's own type can bring art a
94
+ * SoHL-owned table could never hold. Art used to be looked up in
95
+ * `sohl/default-item-art.mjs` instead: a type was configurable while its
96
+ * default art was not, so a second consumer's items compiled only if every one
97
+ * of its notes set `img:` (#7).
98
+ *
99
+ * **Still fail-fast.** A type with neither a note-level `img:` nor paired art
100
+ * aborts the pack build rather than shipping a mismatched icon — the contract
101
+ * `defaultItemArt` was written for. Only the error's *owner* changed: it now
102
+ * names the registry the consumer declares and can add to.
103
+ *
104
+ * **Resolved by the same rule a note's `img:` is.** The path goes through
105
+ * {@link resolveImg}, so `icons/relic.svg` means the consumer's own asset root
106
+ * in the registry exactly as it does on a note, and an already-served path
107
+ * (`systems/sohl/assets/…`, as every SoHL default is) passes through untouched.
108
+ * One spelling, one meaning, wherever it is written.
109
+ *
110
+ * @param {string} type - the item type.
111
+ * @returns {string} The default image path for that type.
112
+ * @throws {Error} When the type's registry entry pairs no `img`.
113
+ */
114
+ export function itemArt(type) {
115
+ const art = /** @type {Record<string, string|undefined>} */ (
116
+ loadPackConfig().itemArt
117
+ )[type];
118
+ if (!art) {
119
+ throw new Error(
120
+ `No default art for item type "${type}" — the note carries no ` +
121
+ `\`img:\`, and the \`itemBuilders\` entry for "${type}" in this ` +
122
+ `repository's package-build.config.yaml pairs none with its ` +
123
+ `builder. Write the entry as ` +
124
+ `\`${type}: { system: <builder>, img: "<path>" }\`, or give the ` +
125
+ `note an \`img:\` of its own.`,
126
+ );
127
+ }
128
+ return resolveImg(art);
129
+ }