@heroiclands/package-build 23.1.1 → 23.2.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 (247) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/bin/content-commands.mjs +68 -149
  3. package/bin/package-build.mjs +56 -10
  4. package/ci/check-banners.mjs +42 -6
  5. package/ci/confirm-npm-version.mjs +135 -0
  6. package/ci/content-format-reference.mjs +14 -5
  7. package/config.mjs +1 -0
  8. package/container.mjs +185 -4
  9. package/content-config.mjs +333 -210
  10. package/deploy.mjs +116 -10
  11. package/docs/api.md +238 -246
  12. package/docs/authoring/assets.md +11 -5
  13. package/docs/authoring/frontmatter.md +1 -1
  14. package/docs/authoring/links-and-markup.md +87 -0
  15. package/docs/commands.md +138 -99
  16. package/docs/configuration.md +277 -125
  17. package/docs/content-format.md +2 -0
  18. package/docs/diagnostics.md +48 -13
  19. package/docs/guides/book.md +341 -21
  20. package/docs/guides/e2e.md +13 -2
  21. package/docs/guides/icons.md +14 -4
  22. package/docs/guides/module-project.md +18 -8
  23. package/docs/guides/release-and-deploy.md +32 -0
  24. package/docs/guides/site.md +5 -2
  25. package/docs/project-setup.md +30 -10
  26. package/docs/reference/format-details.md +373 -170
  27. package/docs/reference/note-types.md +128 -109
  28. package/docs/reference/theme-pages.md +31 -6
  29. package/e2e.mjs +140 -28
  30. package/engine/actor-compiler.mjs +22 -31
  31. package/engine/address-charset.mjs +4 -17
  32. package/engine/address-fields.mjs +1 -1
  33. package/engine/address-render.mjs +12 -1
  34. package/engine/address-values.mjs +6 -0
  35. package/engine/address.mjs +46 -33
  36. package/engine/art-fields.mjs +30 -5
  37. package/engine/art-slots.mjs +10 -1
  38. package/engine/asset-bindings.mjs +12 -1
  39. package/engine/base-compiler.mjs +34 -65
  40. package/engine/being-age.mjs +2 -7
  41. package/engine/being-measurements.mjs +12 -1
  42. package/engine/calendar-artifacts.mjs +6 -0
  43. package/engine/calendar-choice.mjs +12 -1
  44. package/engine/calendar-format.mjs +12 -1
  45. package/engine/calendar-human.mjs +12 -1
  46. package/engine/compile-corpus.mjs +3 -1
  47. package/engine/content-address.mjs +9 -12
  48. package/engine/content-alerts.mjs +13 -1
  49. package/engine/content-blocks.mjs +14 -3
  50. package/engine/content-embeds.mjs +11 -3
  51. package/engine/content-figures.mjs +9 -0
  52. package/engine/content-footnotes.mjs +12 -1
  53. package/engine/content-format-check.mjs +11 -262
  54. package/engine/content-format.yaml +5 -2
  55. package/engine/content-images.mjs +3 -0
  56. package/engine/content-index.mjs +443 -20
  57. package/engine/content-links.mjs +29 -35
  58. package/engine/content-lint.mjs +2 -3
  59. package/engine/content-poetry.mjs +12 -1
  60. package/engine/content-slug.mjs +2 -14
  61. package/engine/content-spans.mjs +13 -1
  62. package/engine/content-tables.mjs +12 -1
  63. package/engine/culture-choice.mjs +210 -13
  64. package/engine/data-keys.mjs +19 -16
  65. package/engine/date-conversion.mjs +12 -1
  66. package/engine/declaration-check.mjs +9 -0
  67. package/engine/derived-fields.mjs +6 -7
  68. package/engine/diagnostics.mjs +34 -4
  69. package/engine/docs-checks.mjs +9 -0
  70. package/engine/document-subtypes.mjs +47 -34
  71. package/engine/event-views.mjs +139 -14
  72. package/engine/exported-scene.mjs +92 -28
  73. package/engine/extension-attributes.mjs +12 -1
  74. package/engine/field-spec.mjs +11 -102
  75. package/engine/folder-notes.mjs +0 -64
  76. package/engine/foreign-catalog.mjs +2 -1
  77. package/engine/format-generated.mjs +9 -0
  78. package/engine/foundry-entries.mjs +10 -28
  79. package/engine/frontmatter-lint.mjs +83 -392
  80. package/engine/frontmatter.mjs +20 -70
  81. package/engine/generate.mjs +10 -15
  82. package/engine/heading-attributes.mjs +12 -1
  83. package/engine/helpers.mjs +41 -102
  84. package/engine/holdings.mjs +1 -1
  85. package/engine/homepage.mjs +3 -10
  86. package/engine/ids.mjs +1 -167
  87. package/engine/index-records.mjs +6 -4
  88. package/engine/index.mjs +0 -3
  89. package/engine/infobox-render.mjs +15 -3
  90. package/engine/infobox.mjs +13 -14
  91. package/engine/item-compiler.mjs +4 -10
  92. package/engine/item-docs.mjs +3 -5
  93. package/engine/item-registry.mjs +2 -10
  94. package/engine/journals.mjs +1 -5
  95. package/engine/literature-notes.mjs +4 -12
  96. package/engine/macros.mjs +1 -2
  97. package/engine/map-graphviz.mjs +12 -1
  98. package/engine/markdown-expressions.mjs +12 -1
  99. package/engine/markdownlint-run.mjs +567 -0
  100. package/engine/metadata-index.mjs +42 -12
  101. package/engine/note-addresses.mjs +143 -35
  102. package/engine/note-claims.mjs +14 -28
  103. package/engine/note-dates.mjs +3 -3
  104. package/engine/note-frontmatter.mjs +6 -0
  105. package/engine/note-ids.mjs +8 -14
  106. package/engine/note-package.mjs +8 -25
  107. package/engine/note-state.mjs +1 -3
  108. package/engine/note-vocabulary.mjs +148 -84
  109. package/engine/number-words.mjs +12 -1
  110. package/engine/pack-config.mjs +15 -25
  111. package/engine/pack-router.mjs +10 -16
  112. package/engine/pathnames.mjs +8 -22
  113. package/engine/pdf-build.mjs +773 -121
  114. package/engine/pdf-coverage.mjs +188 -0
  115. package/engine/pdf-index.mjs +54 -7
  116. package/engine/pdf-pages.mjs +247 -0
  117. package/engine/pdf-render.mjs +114 -4
  118. package/engine/pdf-toc.mjs +29 -32
  119. package/engine/project-init.mjs +46 -6
  120. package/engine/prose-config.mjs +1 -1
  121. package/engine/prose-lint.mjs +52 -88
  122. package/engine/readability-lint.mjs +9 -0
  123. package/engine/reckoning-markers.mjs +12 -1
  124. package/engine/reference-targets.mjs +7 -0
  125. package/engine/runtime-only-fields.mjs +5 -8
  126. package/engine/scene-levels.mjs +2 -2
  127. package/engine/scenes.mjs +1 -1
  128. package/engine/shortcode-references.mjs +7 -0
  129. package/engine/site-build.mjs +30 -16
  130. package/engine/site-index.mjs +10 -16
  131. package/engine/site-preview.mjs +6 -0
  132. package/engine/site-root.mjs +8 -1
  133. package/engine/social-tie-terms.mjs +12 -1
  134. package/engine/social-ties.mjs +3 -0
  135. package/engine/sql-tables.mjs +56 -20
  136. package/engine/standing-terms.mjs +11 -1
  137. package/engine/standings.mjs +11 -27
  138. package/engine/system-block.mjs +39 -141
  139. package/engine/system-vocabulary.mjs +6 -0
  140. package/engine/walk-files.mjs +69 -0
  141. package/engine/web-wikilinks.mjs +23 -3
  142. package/engine/wikilink-syntax.mjs +32 -21
  143. package/engine/wikilinks.mjs +27 -16
  144. package/githooks/attribution.mjs +8 -0
  145. package/githooks/commit-message.mjs +9 -0
  146. package/hm3/actors.mjs +9 -48
  147. package/hm3/default-item-art.mjs +9 -9
  148. package/hm3/item-fields.mjs +2 -9
  149. package/hm3/template-priority.mjs +6 -7
  150. package/hugo-theme/data/banner-kinds.yaml +78 -0
  151. package/hugo-theme/data/banners.yaml +2 -3
  152. package/hugo-theme/layouts/404.html +3 -3
  153. package/hugo-theme/layouts/partials/banner-url.html +31 -7
  154. package/hugo-theme/layouts/partials/hero-banner.html +13 -14
  155. package/hugo-theme/layouts/partials/related.html +1 -1
  156. package/hugo-theme/layouts/partials/warn-missing-banner.html +1 -1
  157. package/hugo-theme/static/css/style.css +1 -1
  158. package/labels.mjs +2 -2
  159. package/manifest.mjs +11 -4
  160. package/markdownlint-config.mjs +1 -1
  161. package/package.json +6 -3
  162. package/release.mjs +21 -14
  163. package/sohl/actors.mjs +16 -27
  164. package/sohl/default-item-art.mjs +3 -3
  165. package/sohl/document-subtypes.mjs +1 -16
  166. package/sohl/infobox.mjs +2 -3
  167. package/sohl/item-builders.mjs +5 -8
  168. package/sohl/item-fields.mjs +5 -35
  169. package/sohl/items.mjs +0 -4
  170. package/sohl/kb-passes.mjs +3 -3
  171. package/types/container.d.mts +116 -5
  172. package/types/content-config.d.mts +83 -35
  173. package/types/deploy.d.mts +42 -6
  174. package/types/e2e.d.mts +35 -3
  175. package/types/engine/address-charset.d.mts +4 -17
  176. package/types/engine/address.d.mts +17 -8
  177. package/types/engine/art-fields.d.mts +7 -4
  178. package/types/engine/art-slots.d.mts +6 -0
  179. package/types/engine/base-compiler.d.mts +15 -17
  180. package/types/engine/content-address.d.mts +9 -12
  181. package/types/engine/content-embeds.d.mts +4 -2
  182. package/types/engine/content-format-check.d.mts +0 -64
  183. package/types/engine/content-index.d.mts +82 -3
  184. package/types/engine/content-links.d.mts +2 -4
  185. package/types/engine/culture-choice.d.mts +51 -8
  186. package/types/engine/data-keys.d.mts +2 -2
  187. package/types/engine/derived-fields.d.mts +1 -2
  188. package/types/engine/diagnostics.d.mts +27 -4
  189. package/types/engine/document-subtypes.d.mts +12 -31
  190. package/types/engine/event-views.d.mts +14 -2
  191. package/types/engine/exported-scene.d.mts +3 -2
  192. package/types/engine/field-spec.d.mts +13 -122
  193. package/types/engine/folder-notes.d.mts +0 -39
  194. package/types/engine/foreign-catalog.d.mts +2 -1
  195. package/types/engine/foundry-entries.d.mts +4 -5
  196. package/types/engine/frontmatter-lint.d.mts +2 -2
  197. package/types/engine/frontmatter.d.mts +19 -45
  198. package/types/engine/generate.d.mts +1 -1
  199. package/types/engine/helpers.d.mts +25 -42
  200. package/types/engine/homepage.d.mts +3 -10
  201. package/types/engine/ids.d.mts +0 -114
  202. package/types/engine/index-records.d.mts +6 -4
  203. package/types/engine/index.d.mts +0 -1
  204. package/types/engine/infobox-render.d.mts +7 -1
  205. package/types/engine/infobox.d.mts +3 -2
  206. package/types/engine/item-docs.d.mts +1 -1
  207. package/types/engine/item-registry.d.mts +2 -4
  208. package/types/engine/literature-notes.d.mts +1 -1
  209. package/types/engine/markdownlint-run.d.mts +40 -0
  210. package/types/engine/metadata-index.d.mts +9 -1
  211. package/types/engine/note-addresses.d.mts +27 -2
  212. package/types/engine/note-claims.d.mts +3 -5
  213. package/types/engine/note-ids.d.mts +2 -4
  214. package/types/engine/note-package.d.mts +1 -1
  215. package/types/engine/note-state.d.mts +1 -3
  216. package/types/engine/note-vocabulary.d.mts +28 -21
  217. package/types/engine/pack-config.d.mts +2 -6
  218. package/types/engine/pack-router.d.mts +4 -4
  219. package/types/engine/pathnames.d.mts +3 -3
  220. package/types/engine/pdf-build.d.mts +98 -12
  221. package/types/engine/pdf-coverage.d.mts +60 -0
  222. package/types/engine/pdf-index.d.mts +34 -5
  223. package/types/engine/pdf-pages.d.mts +143 -0
  224. package/types/engine/pdf-render.d.mts +45 -1
  225. package/types/engine/pdf-toc.d.mts +12 -14
  226. package/types/engine/prose-config.d.mts +1 -1
  227. package/types/engine/prose-lint.d.mts +11 -10
  228. package/types/engine/runtime-only-fields.d.mts +2 -2
  229. package/types/engine/site-build.d.mts +9 -5
  230. package/types/engine/site-index.d.mts +4 -5
  231. package/types/engine/site-root.d.mts +1 -1
  232. package/types/engine/sql-tables.d.mts +31 -2
  233. package/types/engine/standing-terms.d.mts +2 -1
  234. package/types/engine/standings.d.mts +7 -9
  235. package/types/engine/system-block.d.mts +10 -44
  236. package/types/engine/walk-files.d.mts +15 -0
  237. package/types/engine/wikilink-syntax.d.mts +18 -19
  238. package/types/hm3/actors.d.mts +1 -2
  239. package/types/hm3/item-fields.d.mts +2 -4
  240. package/types/hm3/template-priority.d.mts +6 -7
  241. package/types/manifest.d.mts +3 -2
  242. package/types/release.d.mts +8 -7
  243. package/types/sohl/actors.d.mts +2 -1
  244. package/types/sohl/document-subtypes.d.mts +1 -2
  245. package/types/sohl/item-fields.d.mts +3 -7
  246. package/engine/retired-fields.mjs +0 -631
  247. package/types/engine/retired-fields.d.mts +0 -354
package/engine/ids.mjs CHANGED
@@ -134,163 +134,6 @@ export const PACK_BY_TYPE = Object.freeze({
134
134
  ),
135
135
  });
136
136
 
137
- /**
138
- * Content types that no longer exist, and what replaced each one.
139
- *
140
- * `character` and `creature` were retired in favour of the single `being` they
141
- * had always compiled into. They are recorded here rather than
142
- * simply deleted because deleting them is the one change that fails *quietly*:
143
- * every type not named in {@link PACK_BY_TYPE} falls through to the open item
144
- * set below, so a note or a link left on the old spelling would be routed to
145
- * the items pack — a wrong answer, arrived at silently, which is exactly the
146
- * failure mode the open-set default exists to avoid for real item types.
147
- *
148
- * Keeping the names lets {@link assertTypeNotRetired} say what happened and
149
- * what to write instead. Entries stay for as long as content in the wild might
150
- * still carry them.
151
- *
152
- * @type {Readonly<Record<string, string>>}
153
- */
154
- export const RETIRED_TYPES = Object.freeze({
155
- character: "being",
156
- creature: "being",
157
- // The three map spellings, retired in favour of the single `map` whose
158
- // subType they became. Recorded rather than deleted for the same
159
- // reason as the two above: an unnamed type falls through to the open item
160
- // set, so a note or link left on one would be routed to the items pack.
161
- battlemap: "map",
162
- localmap: "map",
163
- regionalmap: "map",
164
- });
165
-
166
- /**
167
- * Throw if `type` names a retired content type.
168
- *
169
- * @param {string} type - The note's declared `type`, or a link's qualifier.
170
- * @param {string} [where] - What carries it — a file path, a link target —
171
- * appended to the message so the reader can go straight to it.
172
- * @throws {Error} Naming the replacement type.
173
- */
174
- export function assertTypeNotRetired(type, where) {
175
- const replacement = RETIRED_TYPES[type];
176
- if (!replacement) return;
177
- throw new Error(
178
- `Content type "${type}" was retired in favour of "${replacement}"` +
179
- (where ? ` — ${where}` : "") +
180
- `. Both compiled to the same document, so the fix is mechanical: ` +
181
- `write "${replacement}".`,
182
- );
183
- }
184
-
185
- /* -------------------------------------------------------------------- */
186
- /* Renamed in favour of another type — the retirement window */
187
- /* -------------------------------------------------------------------- */
188
-
189
- /**
190
- * Content types that were **renamed**, and what each is called now.
191
- *
192
- * A retired type and a renamed one are different cases, and only the first can
193
- * be refused. `character` above was retired *outright* — nothing a note wrote
194
- * made it right, so {@link assertTypeNotRetired} throws. A renamed type has a
195
- * replacement, the two spellings mean the same thing, and a note carrying the
196
- * old one compiles into exactly the document it always did: refusing it would
197
- * fail a build over a note that is not wrong.
198
- *
199
- * So these retire in the three steps `package:` took, and this table is
200
- * the **first**: both spellings resolve, the current one is canonical, and the
201
- * retired one is *reported* — never refused. The sweep of the content trees and
202
- * the refusal come after, once no tree writes the old name. That is the same
203
- * rule `RETIRED_FIELD_ALIASES` states for a renamed *field*, and for the same
204
- * reason: a consumer must be able to adopt the new toolchain before its content
205
- * moves, and there are some 31,000 references to move.
206
- *
207
- * **Why these three, and why this way round.** The specification renamed
208
- * `armorgear`, `concoctiongear` and `projectilegear` to `armor`, `concoction`
209
- * and `projectile`, on the argument that the suffix named the *SoHL document
210
- * subtype* a note compiled into rather than the thing the note is about. That
211
- * rename is **reversed**: the gear-suffixed spellings are the vocabulary, and
212
- * the bare ones are what retires.
213
- *
214
- * Nothing had adopted the bare spellings, which is what makes the reversal
215
- * cheap and the direction obvious: across all five content trees, **every note
216
- * still authors the suffix** — 331 `armorgear` and 18 `projectilegear` in
217
- * `sohl`, 71 `concoctiongear` in `thalorna`, and not one note anywhere writes
218
- * `armor`, `concoction` or `projectile`. The rename produced 349 warnings and
219
- * zero adopters.
220
- *
221
- * `weapongear` never moved, and the reversal restores the consistency its
222
- * staying put had broken: all five gear types spell the suffix again.
223
- *
224
- * **Keyed by the retired spelling**, which is the opposite of
225
- * `RETIRED_FIELD_ALIASES`. The direction follows the operation: a field alias
226
- * is scoped by the schema that declares the *current* name, so that is what a
227
- * reader asks with; a type alias is applied to a value read off a note, so what
228
- * a reader has in hand is the retired spelling and what it needs is the
229
- * replacement.
230
- *
231
- * **The address is not normalised.** A note's canonical address — and therefore
232
- * its document id — carries its `type` as authored, so both spellings keep the
233
- * addresses they already publish and a tree that has not swept compiles
234
- * byte-identically. Renaming the type in a note *is* an address move, which is
235
- * the sweep's business and not this table's.
236
- *
237
- * @type {Readonly<Record<string, string>>}
238
- */
239
- export const RENAMED_TYPES = Object.freeze({
240
- armor: "armorgear",
241
- concoction: "concoctiongear",
242
- projectile: "projectilegear",
243
- });
244
-
245
- /**
246
- * The current spelling of a content type: itself, or what it was renamed to.
247
- *
248
- * Every table keyed by note type is keyed by the **current** name, and every
249
- * lookup goes through this — which is the whole of the window's behaviour, in
250
- * one function, so no two readers can disagree about which vocabulary a note
251
- * is held to.
252
- *
253
- * A non-string passes through untouched: callers hand this whatever the
254
- * frontmatter carried, and inventing a type for a number would hide the
255
- * missing-`type:` finding that belongs to the linter.
256
- *
257
- * @param {any} type - The type as authored, or as a link spells it.
258
- * @returns {any} The current spelling.
259
- */
260
- export function currentType(type) {
261
- if (typeof type !== "string") return type;
262
- return RENAMED_TYPES[type] ?? type;
263
- }
264
-
265
- /**
266
- * What a note declaring a renamed type is told, in one place.
267
- *
268
- * Shared by every reporter, because an author meets whichever runs first and
269
- * they should read the same. It names the type to write rather than a value to
270
- * correct — no value makes the retired spelling right — and it says the note
271
- * compiles either way, so a reader knows this is a rename to schedule rather
272
- * than a build to unbreak.
273
- *
274
- * @param {string} retired - The spelling the note used.
275
- * @param {string} current - What to write instead.
276
- * @param {string} [where] - What carries it — a file path, a link target —
277
- * appended to the message. Omit it where the caller emits through a
278
- * diagnostic, whose locator already starts the line.
279
- * @returns {string} The message, unpunctuated at the end as a finding is.
280
- */
281
- export function renamedTypeMessage(retired, current, where) {
282
- return (
283
- `content type "${retired}" was renamed to "${current}" — the suffix ` +
284
- `named a SoHL document subtype rather than the thing the note is ` +
285
- `about` +
286
- (where ? ` — ${where}` : "") +
287
- `. Both spellings compile to the same document, so the note is not ` +
288
- `wrong; write "${current}", and move every \`(type, shortcode)\` ` +
289
- `reference and wikilink to it in the same change. "${retired}" is ` +
290
- `refused in a later release`
291
- );
292
- }
293
-
294
137
  /** Where every other content type compiles: the items pack. */
295
138
  export const ITEM_PACK = Object.freeze({ pack: "items", docType: "Item" });
296
139
 
@@ -313,18 +156,9 @@ export const ITEM_PACK = Object.freeze({ pack: "items", docType: "Item" });
313
156
  *
314
157
  * @param {string} type - The target note's `type`.
315
158
  * @returns {{pack: string, docType: string}} The pack and document type.
316
- * @throws {Error} If `type` names a retired content type — see
317
- * {@link RETIRED_TYPES}. The open-set default would otherwise route it to the
318
- * items pack and say nothing.
319
159
  */
320
160
  export function packForType(type) {
321
- assertTypeNotRetired(type);
322
- // Through {@link currentType} like every other type-keyed lookup, so the
323
- // rule holds without exception. None of today's renamed types is named in
324
- // `PACK_BY_TYPE` — all three are items and take the default — but a router
325
- // that read the authored spelling would be the one table left to remember
326
- // when the next rename lands.
327
- return PACK_BY_TYPE[currentType(type)] ?? ITEM_PACK;
161
+ return PACK_BY_TYPE[type] ?? ITEM_PACK;
328
162
  }
329
163
 
330
164
  /**
@@ -110,15 +110,17 @@ export function noteFile(contentBase, record) {
110
110
  }
111
111
 
112
112
  /**
113
- * The note's own frontmatter, as authored, from an index record.
113
+ * The note's frontmatter from an index record, without the keys the index
114
+ * derives.
114
115
  *
115
116
  * The inverse of the record's spread, and exact rather than best-effort: a
116
117
  * record is the note's frontmatter plus {@link DERIVED_KEYS}, and a note that
117
118
  * authors one of those keys fails the walk — so removing them cannot remove
118
119
  * anything the note wrote. That enforced pairing is what lets a pass read the
119
- * corpus from the index and still lint, route or compile what the *author*
120
- * typed, rather than reasoning about `address:` and `anchors:` as though
121
- * someone had written them.
120
+ * corpus from the index without reasoning about `address:` and `anchors:` as
121
+ * though someone had written them. The record's `data.culture` is the note's
122
+ * resolved culture, so a pass reading the frontmatter sees an inherited culture
123
+ * as well as a written one.
122
124
  *
123
125
  * Lives beside the list it is the inverse of, so the two cannot drift.
124
126
  *
package/engine/index.mjs CHANGED
@@ -69,9 +69,6 @@ export * as contentPackage from "./content-package.mjs";
69
69
  /** Which content package a note belongs to, and refusing one that says so. */
70
70
  export * as notePackage from "./note-package.mjs";
71
71
 
72
- /** Frontmatter fields a note may no longer declare, and the refusal of them. */
73
- export * as retiredFields from "./retired-fields.mjs";
74
-
75
72
  /** Schema fields a note may never declare, because play writes them. */
76
73
  export * as runtimeOnlyFields from "./runtime-only-fields.mjs";
77
74
 
@@ -36,7 +36,12 @@
36
36
 
37
37
  import { encodeAddresses } from "./address-values.mjs";
38
38
  import { sectionHolds } from "./infobox.mjs";
39
- import { absolutePageUrl, escapeTypst, escapeTypstString } from "./pdf-render.mjs";
39
+ import {
40
+ absolutePageUrl,
41
+ crossBookReference,
42
+ escapeTypst,
43
+ escapeTypstString,
44
+ } from "./pdf-render.mjs";
40
45
 
41
46
  /**
42
47
  * How many cells an attribute grid packs into a row.
@@ -239,12 +244,19 @@ export function sectionHasContent(section) {
239
244
  * @param {Map<string, string>} links - Address slug → the book's anchor.
240
245
  * @param {(anchor: string) => string} labelFor - The anchor's Typst label.
241
246
  * @param {string} [site] - The absolute address this book's pages are served at.
247
+ * @param {{get: (key: string) => (object|undefined)}} [elsewhere] - The notes
248
+ * another book is home to. One of them is not a link — a PDF link cannot
249
+ * leave its file — and is set as
250
+ * {@link module:engine/pdf-render.crossBookReference}.
242
251
  * @returns {string} Typst markup.
243
252
  */
244
- export function linkToTypst(value, links, labelFor, site) {
253
+ export function linkToTypst(value, links, labelFor, site, elsewhere) {
245
254
  const text = escapeTypst(value?.text ?? "");
246
- const anchor = value?.address ? links?.get(encodeAddresses(value.address)) : undefined;
255
+ const address = value?.address ? encodeAddresses(value.address) : undefined;
256
+ const anchor = address ? links?.get(address) : undefined;
247
257
  if (anchor) return `#link(<${labelFor(anchor)}>)[${text}]`;
258
+ const away = address ? elsewhere?.get(address) : undefined;
259
+ if (away) return crossBookReference(text, away);
248
260
  if (value?.url)
249
261
  return `#link("${escapeTypstString(absolutePageUrl(value.url, site))}")[${text}]`;
250
262
  return text;
@@ -114,11 +114,10 @@
114
114
 
115
115
  import { isAddressTuple, parseAddress, renderAddress } from "./address.mjs";
116
116
  import { getFrontmatter } from "./frontmatter.mjs";
117
- import { currentType } from "./ids.mjs";
118
117
  import { NOTE_VOCABULARY, dataFields } from "./note-vocabulary.mjs";
119
- import { readAliasedField } from "./retired-fields.mjs";
120
118
  import { subtypeRow } from "./document-subtypes.mjs";
121
119
  import { authoredKey } from "./system-block.mjs";
120
+ import { cultureNamesItself } from "./culture-choice.mjs";
122
121
  import { formatDateInCalendar, formatNoteDate, parseNoteDate } from "./note-dates.mjs";
123
122
  import { displayBeingHeight, displayBeingWeight } from "./being-measurements.mjs";
124
123
  import {
@@ -317,6 +316,7 @@ export const NOTE_FIELD_PRESENTATION = Object.freeze({
317
316
  // Scene itself, not a fact a summary row can carry.
318
317
  "map.scene": Object.freeze({ withheld: "canvas machinery, not a summary row" }),
319
318
  "map.fixup": Object.freeze({ withheld: "canvas machinery, not a summary row" }),
319
+ "map.thumbnail": Object.freeze({ withheld: "an image, which the box never carries" }),
320
320
 
321
321
  assocSkill: Object.freeze({ label: "Skill" }),
322
322
  assocAffiliation: Object.freeze({ label: "Affiliation" }),
@@ -385,7 +385,7 @@ export const NOTE_FIELD_PRESENTATION = Object.freeze({
385
385
  */
386
386
  export function overlayFor(presentation, type, name) {
387
387
  const key = authoredKey(name);
388
- return presentation?.[`${currentType(type)}.${key}`] ?? presentation?.[key] ?? {};
388
+ return presentation?.[`${type}.${key}`] ?? presentation?.[key] ?? {};
389
389
  }
390
390
 
391
391
  /** What a composed group's row is called. @type {Readonly<Record<string, string>>} */
@@ -622,9 +622,9 @@ export const MEMBERSHIPS_SECTION = Object.freeze({ id: "affiliations", label: "A
622
622
  * Each entry is the line a reader meets — `War Chief, Hárár (5), of Vrystwald
623
623
  * Tribes` — with the body's name as the link and the standing before it as the
624
624
  * entry's `lead`, drawn as plain text. The rung's title is read from the body's
625
- * own ladder, carried on the reference as its `standings` digest, so a renamed
625
+ * own ladder, carried on the reference as its `standings` digest, so renaming a
626
626
  * rung renames every line pointing at it. A body that publishes no page keeps
627
- * its name as plain text, and a membership in the list form is the name alone.
627
+ * its name as plain text, and a membership stating no standing is the name alone.
628
628
  *
629
629
  * The entries are a `list` section of their own rather than one `links` row,
630
630
  * because a line already holds commas and a `links` row joins its values with
@@ -916,7 +916,7 @@ function declaredKeyRows(fields, raw) {
916
916
  *
917
917
  * The declared string is appended **verbatim**, which is what lets a symbol
918
918
  * hug its number (`160d`) and a word stand off it (`1.1 lbs`). The row's kind
919
- * becomes `text`: a number with a unit on it is no longer a number, and saying
919
+ * becomes `text`: a number with a unit on it is not a number, and saying
920
920
  * otherwise would invite a medium to format it as one.
921
921
  *
922
922
  * @param {string} kind - The row's kind.
@@ -960,7 +960,8 @@ export function linkValue(ref, resolve, hint) {
960
960
  * order, minus what {@link NOTE_FIELD_PRESENTATION} withholds and minus every
961
961
  * field the note left empty. A being's top-level `subType` adds its profile
962
962
  * type. A type that declares no `data:` fields still gets the box, because
963
- * `Name` is a fact about every note.
963
+ * `Name` is a fact about every note. `data.culture` is the resolved culture,
964
+ * inherited ones included, and is left out where it names the note itself.
964
965
  *
965
966
  * @param {object} fm - The note's frontmatter.
966
967
  * @param {object} [options] - Options.
@@ -1022,12 +1023,10 @@ function noteBox(
1022
1023
  for (const field of dataFields(fm?.type, vocabulary) ?? []) {
1023
1024
  const overlay = overlayFor(presentation, fm?.type, field.name);
1024
1025
  if (overlay.withheld) continue;
1025
- // The retired spelling underneath, so a note part-way through a rename
1026
- // still shows its value rather than losing the row with nothing said.
1027
- // The current name wins, which is the whole of the retirement window's
1028
- // behaviour and is `readAliasedField`'s answer, not a second one.
1029
- const own = getFrontmatter(data, field.name, undefined);
1030
- let raw = own !== undefined ? own : readAliasedField(fm, field.name, { inData: true });
1026
+ let raw = getFrontmatter(data, field.name, undefined);
1027
+ // A culture note resolves to itself, and a row linking it to itself
1028
+ // says nothing.
1029
+ if (field.name === "culture" && cultureNamesItself(fm, raw, { contentPackage })) continue;
1031
1030
  if (
1032
1031
  ((fm?.type === "being" && ["born", "died"].includes(field.name)) ||
1033
1032
  (fm?.type === "place" && field.name === "present")) &&
@@ -1333,7 +1332,7 @@ export function buildInfoboxes(fm, options) {
1333
1332
  sections: [],
1334
1333
  };
1335
1334
  if (available && provider) {
1336
- const type = currentType(fm.type);
1335
+ const type = fm.type;
1337
1336
  const ctx = {
1338
1337
  block: map.block,
1339
1338
  resolve,
@@ -57,7 +57,6 @@ import { itemDocEntryId, itemDocPointer } from "./item-docs.mjs";
57
57
  // configuration, so the types this pass claims and the builders it compiles
58
58
  // them with are one table — the consuming repository's, not this package's.
59
59
  import { itemTypes, itemBuilder, itemArt, itemFields } from "./item-registry.mjs";
60
- import { currentType } from "./ids.mjs";
61
60
  // Which Foundry Item subtype a note's `type` compiles into. Looked up in the
62
61
  // system's declared map, never inferred from the type itself.
63
62
  import { documentSubtype, subtypeRow } from "./document-subtypes.mjs";
@@ -198,10 +197,7 @@ export class SystemItemCompiler extends BasePackCompiler {
198
197
  * @returns {boolean} True for a whitelisted item type.
199
198
  */
200
199
  selects(fm) {
201
- // Through {@link currentType}: the registry is keyed by the current
202
- // spelling, and a note still on a renamed one compiles unchanged
203
- // during the retirement window.
204
- if (!fm.type || !itemTypes().has(currentType(fm.type))) return false;
200
+ if (!fm.type || !itemTypes().has(fm.type)) return false;
205
201
  const map = /** @type {typeof SystemItemCompiler} */ (this.constructor).documentSubtypes;
206
202
  const row = subtypeRow(/** @type {never} */ (map), fm.type);
207
203
  return !row || row.document === "Item";
@@ -382,10 +378,8 @@ export class SystemItemCompiler extends BasePackCompiler {
382
378
  });
383
379
 
384
380
  const effects = blockProperty(fm, system, "effects");
385
- // Read through the system block like every other item field. There is
386
- // one spelling: `packFolder` names a folder note by its address, the
387
- // `folder:` id spelling having been retired with the per-pack YAML it
388
- // resolved against.
381
+ // Read through the system block like every other item field.
382
+ // `packFolder` names a folder note by its address.
389
383
  const folder = this.folderResolver(blockDataProperty(fm, system, "packFolder", null), {
390
384
  isAddress: true,
391
385
  });
@@ -415,7 +409,7 @@ export class SystemItemCompiler extends BasePackCompiler {
415
409
 
416
410
  /** @inheritdoc */
417
411
  onCompiled(fm) {
418
- this.counts[currentType(fm.type)]++;
412
+ this.counts[fm.type]++;
419
413
  }
420
414
 
421
415
  /** @inheritdoc */
@@ -22,10 +22,8 @@
22
22
  * which {@link sohl.utils.descriptionLinkTarget} recognises and
23
23
  * Display Description follows.
24
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. Nothing about actors changes —
28
- * they embed whatever the item carries, and what the item carries is now a link.
25
+ * The prose then exists once, not as a copy on every actor carrying the item.
26
+ * Actors embed whatever the item carries, and what the item carries is now a link.
29
27
  *
30
28
  * **Two passes, no shared state.** The items pass writes the pointer; the
31
29
  * journals pass writes the entry it points at. Neither can see the other's
@@ -106,7 +104,7 @@ export function itemDocEntryId(itemId) {
106
104
  * item beats a bare UUID.
107
105
  * @param {string} pageId - The id of the selected entry page, from
108
106
  * {@link journalPageId}.
109
- * @param {object} [fm] - The source note, used to route GM-only prose.
107
+ * @param {object} [fm] - The source note, which routes GM-only prose.
110
108
  * @returns {string} The pointer to store in `system.docHtml`.
111
109
  */
112
110
  export function itemDocPointer(packageId, itemId, name, pageId, fm) {
@@ -40,7 +40,6 @@
40
40
  * @module
41
41
  */
42
42
 
43
- import { currentType } from "./ids.mjs";
44
43
  import { loadPackConfig } from "./pack-config.mjs";
45
44
  import { resolveImg } from "./helpers.mjs";
46
45
 
@@ -51,10 +50,8 @@ import { resolveImg } from "./helpers.mjs";
51
50
  *
52
51
  * **Derived, never authored.** These are the keys of the consuming
53
52
  * repository's `itemBuilders` registry, so the whitelist and the builder table
54
- * are the same list and cannot drift apart. They already had: `trait` was
55
- * whitelisted long after the item type was retired, with no builder
56
- * behind it, so every `type: trait` note passed the gate and then failed to
57
- * compile.
53
+ * are the same list and cannot drift apart. A whitelist naming a type with no builder would
54
+ * let every note of that type pass the gate and then fail to compile.
58
55
  *
59
56
  * An accessor rather than a hoisted constant, so that importing this module
60
57
  * needs no configuration.
@@ -84,11 +81,6 @@ export function itemTypes() {
84
81
  */
85
82
  function lookup(what, table, type, system) {
86
83
  const config = loadPackConfig();
87
- // The registry is keyed by the current spelling of a note type, and a note
88
- // still on a renamed one resolves through it unchanged. One place, because
89
- // every type-keyed table this module reaches — builders, art, fields — is
90
- // indexed here.
91
- type = currentType(type);
92
84
  if (system !== undefined) {
93
85
  const perSystem = /** @type {Record<string, Record<string, unknown>>} */ (
94
86
  config[`${table}BySystem`]
@@ -796,15 +796,11 @@ export class Journals extends BasePackCompiler {
796
796
  // sits under the same heading a reader found the item under.
797
797
  //
798
798
  // An address is resolved wherever it is written, including here — and
799
- // resolving it *here* is what cures the defect this comment used to
800
- // describe. A folder note has one definition and one address, so the
799
+ // resolving it *here* keeps the folders agreeing. A folder note has one definition and one address, so the
801
800
  // journals pack materialises the very folder the items pack does, by
802
801
  // the same id. There is no second folder file left to disagree
803
802
  // with the first, and so no arrangement to assume: the mirroring
804
803
  // failure is unrepresentable rather than merely reported.
805
- //
806
- // The id spelling used to cross packs verbatim here, on the assumption
807
- // both declared it — the arrangement retired with the YAML.
808
804
  const { value: authoredFolder } = folderField(fm);
809
805
  const folder = this.folderResolver(authoredFolder, { isAddress: true });
810
806
 
@@ -13,7 +13,9 @@
13
13
 
14
14
  /**
15
15
  * A work a people tells, sings or writes: the `literature` lore subType and the
16
- * four optional facts such a note may state about itself.
16
+ * three optional facts only such a note may state about itself. Whose work it
17
+ * is, `data.culture`, is a field every lore note but a culture note may state,
18
+ * declared in `culture-choice.mjs`.
17
19
  *
18
20
  * `DataFieldSpec` declares the keys a type accepts, not the subType that may
19
21
  * write them, so {@link checkLiteratureNote} scopes these keys to `literature`
@@ -23,7 +25,6 @@
23
25
  */
24
26
 
25
27
  import { isAddressTuple, parseAddress, renderAddress } from "./address.mjs";
26
- import { checkCultureChoice } from "./culture-choice.mjs";
27
28
  import { positionOfFrontmatterPath } from "./diagnostics.mjs";
28
29
 
29
30
  /** The `lore` subType whose notes are works of literature. */
@@ -66,20 +67,11 @@ export function checkLanguageChoice(note, { index } = {}) {
66
67
  }
67
68
 
68
69
  /**
69
- * The fields a `literature` note may state. Every one is optional.
70
+ * The fields only a `literature` note may state. Every one is optional.
70
71
  *
71
72
  * @type {ReadonlyArray<object>}
72
73
  */
73
74
  export const LITERATURE_FIELDS = Object.freeze([
74
- {
75
- name: "culture",
76
- shape: "an Address",
77
- kind: "address",
78
- ref: "lore",
79
- accepts: ["lore"],
80
- check: checkCultureChoice,
81
- describe: "The people whose work this is, as a culture lore note.",
82
- },
83
75
  {
84
76
  name: "form",
85
77
  shape: "string",
package/engine/macros.mjs CHANGED
@@ -264,8 +264,7 @@ export function buildMacroEntry(fm, { command, folder = null, stats = defaultSta
264
264
  // into chat instead of running.
265
265
  type: resolveMacroType(fm, name),
266
266
  author: null,
267
- // Nullish, not `||`: a macro note that names no art gets the
268
- // shared default, one that writes `icon: ""` ships blank on purpose.
267
+ // A macro note that names no art gets the shared default.
269
268
  img: (img === undefined ? null : img) ?? DEFAULT_MACRO_IMG,
270
269
  scope: resolveMacroScope(fm, name),
271
270
  command,
@@ -1,4 +1,15 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
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
+ */
2
13
 
3
14
  /** Render DOT maps with the Graphviz runtime installed by npm. */
4
15
 
@@ -1,4 +1,15 @@
1
- /* SPDX-License-Identifier: GPL-3.0-or-later */
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
+ */
2
13
 
3
14
  import Handlebars from "handlebars";
4
15