@heroiclands/package-build 23.0.0 → 23.1.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 (157) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +1 -1
  3. package/bin/content-commands.mjs +1 -1
  4. package/ci/content-format-reference.mjs +46 -0
  5. package/docs/api.md +124 -141
  6. package/docs/authoring/assets.md +1 -1
  7. package/docs/authoring/dates-and-calendars.md +12 -6
  8. package/docs/authoring/frontmatter.md +2 -2
  9. package/docs/authoring/links-and-markup.md +145 -29
  10. package/docs/commands.md +8 -10
  11. package/docs/configuration.md +13 -11
  12. package/docs/content-format.md +2 -2
  13. package/docs/guides/book.md +59 -4
  14. package/docs/guides/icons.md +4 -5
  15. package/docs/guides/packs.md +6 -0
  16. package/docs/guides/reference-pages.md +21 -1
  17. package/docs/guides/release-and-deploy.md +39 -7
  18. package/docs/guides/site.md +17 -18
  19. package/docs/reference/format-details.md +877 -276
  20. package/docs/reference/note-types.md +278 -91
  21. package/docs/reference/theme-pages.md +58 -97
  22. package/engine/address-values.mjs +124 -4
  23. package/engine/address.mjs +44 -3
  24. package/engine/anchors.mjs +103 -3
  25. package/engine/art-slots.mjs +2 -9
  26. package/engine/base-compiler.mjs +25 -6
  27. package/engine/calendar-artifacts.mjs +4 -1
  28. package/engine/calendar-notes.mjs +166 -102
  29. package/engine/content-format-check.mjs +2 -3
  30. package/engine/content-format.mjs +44 -1
  31. package/engine/content-format.yaml +79 -16
  32. package/engine/content-images.mjs +10 -1
  33. package/engine/content-index.mjs +37 -9
  34. package/engine/content-links.mjs +24 -5
  35. package/engine/content-tables.mjs +24 -3
  36. package/engine/data-keys.mjs +347 -0
  37. package/engine/derived-sections.mjs +322 -0
  38. package/engine/diagnostics.mjs +28 -0
  39. package/engine/event-fields.mjs +187 -0
  40. package/engine/event-rows.mjs +126 -0
  41. package/engine/event-views.mjs +305 -0
  42. package/engine/exported-scene.mjs +29 -53
  43. package/engine/frontmatter-lint.mjs +103 -77
  44. package/engine/frontmatter.mjs +7 -2
  45. package/engine/generate.mjs +53 -0
  46. package/engine/generated-sections.mjs +223 -0
  47. package/engine/heading-attributes.mjs +4 -2
  48. package/engine/helpers.mjs +12 -1
  49. package/engine/holdings.mjs +12 -33
  50. package/engine/ids.mjs +17 -11
  51. package/engine/index.mjs +4 -4
  52. package/engine/infobox-render.mjs +21 -6
  53. package/engine/infobox.mjs +217 -99
  54. package/engine/journals.mjs +104 -47
  55. package/engine/literature-notes.mjs +4 -1
  56. package/engine/literature-works.mjs +21 -13
  57. package/engine/map-graphviz.mjs +4 -18
  58. package/engine/markdown-expressions.mjs +116 -11
  59. package/engine/metadata-index.mjs +26 -0
  60. package/engine/near-miss.mjs +90 -0
  61. package/engine/note-addresses.mjs +75 -19
  62. package/engine/note-dates.mjs +5 -3
  63. package/engine/note-event-terms.mjs +364 -0
  64. package/engine/note-events.mjs +823 -0
  65. package/engine/note-vocabulary.mjs +184 -88
  66. package/engine/office-holders.mjs +20 -54
  67. package/engine/packagebuild-index.mjs +16 -1
  68. package/engine/pdf-build.mjs +97 -60
  69. package/engine/pdf-index.mjs +263 -0
  70. package/engine/pdf-render.mjs +47 -5
  71. package/engine/place-relation-terms.mjs +174 -0
  72. package/engine/place-relations.mjs +72 -160
  73. package/engine/rank-ladder.mjs +14 -41
  74. package/engine/reckoning-markers.mjs +2 -0
  75. package/engine/region-events.mjs +6 -12
  76. package/engine/scenes.mjs +73 -468
  77. package/engine/site-build.mjs +75 -101
  78. package/engine/site-index.mjs +3 -1
  79. package/engine/site-maps.mjs +43 -29
  80. package/engine/site-preview.mjs +40 -31
  81. package/engine/social-ties.mjs +2 -1
  82. package/engine/sql-tables.mjs +212 -9
  83. package/engine/standing-terms.mjs +145 -6
  84. package/engine/standings.mjs +37 -49
  85. package/engine/web-wikilinks.mjs +21 -1
  86. package/engine/wikilink-syntax.mjs +34 -4
  87. package/engine/wikilinks.mjs +27 -2
  88. package/engine/yaml-lint.mjs +7 -4
  89. package/hugo-theme/layouts/partials/infobox/entry.html +4 -1
  90. package/hugo-theme/layouts/partials/page-body.html +0 -3
  91. package/hugo-theme/static/css/style.css +25 -55
  92. package/package.json +1 -2
  93. package/packagebuild-metadata.jsonl +13 -13
  94. package/release.mjs +62 -4
  95. package/sohl/note-schemas.mjs +8 -73
  96. package/types/engine/address-values.d.mts +50 -3
  97. package/types/engine/address.d.mts +21 -0
  98. package/types/engine/anchors.d.mts +49 -2
  99. package/types/engine/art-slots.d.mts +1 -1
  100. package/types/engine/base-compiler.d.mts +13 -4
  101. package/types/engine/calendar-notes.d.mts +7 -0
  102. package/types/engine/content-format.d.mts +52 -0
  103. package/types/engine/content-images.d.mts +5 -1
  104. package/types/engine/content-tables.d.mts +9 -1
  105. package/types/engine/data-keys.d.mts +145 -0
  106. package/types/engine/derived-sections.d.mts +73 -0
  107. package/types/engine/diagnostics.d.mts +15 -0
  108. package/types/engine/event-fields.d.mts +99 -0
  109. package/types/engine/event-rows.d.mts +12 -0
  110. package/types/engine/event-views.d.mts +67 -0
  111. package/types/engine/exported-scene.d.mts +5 -2
  112. package/types/engine/frontmatter-lint.d.mts +3 -3
  113. package/types/engine/generated-sections.d.mts +143 -0
  114. package/types/engine/holdings.d.mts +6 -20
  115. package/types/engine/ids.d.mts +15 -12
  116. package/types/engine/index.d.mts +1 -1
  117. package/types/engine/infobox.d.mts +13 -3
  118. package/types/engine/journals.d.mts +24 -11
  119. package/types/engine/literature-works.d.mts +1 -2
  120. package/types/engine/map-graphviz.d.mts +2 -10
  121. package/types/engine/markdown-expressions.d.mts +12 -3
  122. package/types/engine/near-miss.d.mts +42 -0
  123. package/types/engine/note-addresses.d.mts +7 -1
  124. package/types/engine/note-event-terms.d.mts +68 -0
  125. package/types/engine/note-events.d.mts +71 -0
  126. package/types/engine/note-vocabulary.d.mts +37 -8
  127. package/types/engine/office-holders.d.mts +3 -0
  128. package/types/engine/pdf-index.d.mts +35 -0
  129. package/types/engine/pdf-render.d.mts +8 -2
  130. package/types/engine/place-relation-terms.d.mts +83 -0
  131. package/types/engine/place-relations.d.mts +11 -66
  132. package/types/engine/rank-ladder.d.mts +2 -0
  133. package/types/engine/region-events.d.mts +6 -12
  134. package/types/engine/scenes.d.mts +6 -32
  135. package/types/engine/site-build.d.mts +8 -22
  136. package/types/engine/site-maps.d.mts +15 -7
  137. package/types/engine/sql-tables.d.mts +77 -2
  138. package/types/engine/standing-terms.d.mts +37 -4
  139. package/types/engine/standings.d.mts +14 -9
  140. package/types/engine/wikilink-syntax.d.mts +20 -0
  141. package/types/engine/wikilinks.d.mts +3 -2
  142. package/types/engine/yaml-lint.d.mts +5 -4
  143. package/types/release.d.mts +4 -0
  144. package/docs/guides/government-migration.md +0 -52
  145. package/engine/itinerary-scenes.mjs +0 -166
  146. package/engine/lore-events.mjs +0 -197
  147. package/engine/map-notes.mjs +0 -1309
  148. package/engine/map-raster.mjs +0 -51
  149. package/hugo-theme/layouts/partials/holdings/entry.html +0 -21
  150. package/hugo-theme/layouts/partials/holdings/kind-groups.html +0 -71
  151. package/hugo-theme/layouts/partials/holdings.html +0 -61
  152. package/hugo-theme/layouts/partials/literature-works.html +0 -29
  153. package/hugo-theme/layouts/partials/place-map.html +0 -9
  154. package/types/engine/itinerary-scenes.d.mts +0 -29
  155. package/types/engine/lore-events.d.mts +0 -14
  156. package/types/engine/map-notes.d.mts +0 -297
  157. package/types/engine/map-raster.d.mts +0 -8
package/engine/scenes.mjs CHANGED
@@ -13,142 +13,68 @@
13
13
 
14
14
  /**
15
15
  * Scenes pack compiler — map notes in `assets/content/` → Foundry `Scene`
16
- * documents, and the `Adventure` bundles that make their references resolve.
16
+ * documents, and the `Adventure` bundles that make their pins resolve.
17
17
  *
18
- * The translation itself lives in the framework-free `map-notes.mjs`; this
19
- * module is the pass that walks the tree, resolves what one note says about
20
- * another, and writes the JSON the compendium CLI compiles.
18
+ * A map note carries its Scene as exported from Foundry, at `data.scene`. The
19
+ * build never constructs a Scene: {@link module:engine/exported-scene} passes
20
+ * the export through, applying `data.fixup` and binding pins marked `#anchor`
21
+ * to the note's own journal pages.
21
22
  *
22
23
  * **Two outputs, for two different jobs.**
23
24
  *
24
25
  * - The **`scenes` pack** holds every map note's Scene. It is what a wikilink
25
26
  * to a map addresses, and what a GM browses.
26
- * - The **`adventures` pack** holds one Adventure per *place* — a group of map
27
- * notes sharing a `place:`, defaulting to the note's own shortcode — bundling
28
- * those scenes with the JournalEntries their prose compiled into. A map with
29
- * `locations:` **must** be imported this way: `Adventure#importContent`
30
- * creates with `keepId: true`, and a pin's `Note.entryId` / `pageId`, a
31
- * `teleportToken` destination and a `toggleBehavior` target are all
32
- * id-based. Dragged out of the bare `scenes` pack, a pinned scene lands with
33
- * pins pointing at ids no document in the world carries.
34
- *
35
- * **Cross-references are addresses, never UUIDs.** A note says
36
- * `to: {map: manorsolar, region: stair-landing}`; this pass turns that into
37
- * `Scene.<id>.Region.<id>` from an index built before any scene is compiled,
38
- * because every embedded id is derived from the scene id and the authored key
39
- * rather than stored. It is the same vocabulary the link manifest uses.
27
+ * - The **`adventures` pack** holds one Adventure per *place* — the map notes
28
+ * sharing a `data.place`, defaulting to the note's own shortcode — bundling
29
+ * those scenes with the JournalEntries their prose compiled into. A Scene
30
+ * whose pins open this note's pages **must** be imported this way:
31
+ * `Adventure#importContent` creates with `keepId: true`, and a pin's
32
+ * `Note.entryId` / `pageId` are id-based. Dragged out of the bare `scenes`
33
+ * pack, a pinned scene lands with pins pointing at ids no document in the
34
+ * world carries.
40
35
  *
41
36
  * Not a standalone script — exports the `Scenes` compiler class, imported and
42
- * driven by `packages/package-build/engine/generate.mjs` (via `npm run build:compiledb`).
37
+ * driven by `engine/generate.mjs`.
43
38
  *
44
39
  * The walk itself — filtering by type, expanding tables, converting
45
- * wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
46
- * states only what makes this pass its own.
40
+ * wikilinks, writing the JSON and counting errors — belongs to
41
+ * {@link sohl.utils.packs.BasePackCompiler}; this module states only what makes
42
+ * this pass its own.
47
43
  */
48
44
 
49
- import fs from "fs";
50
- import path from "path";
51
45
  import log from "loglevel";
52
46
 
53
- // The one reader of a system block's own properties, so a scene's effect
54
- // references resolve from the same position the item compiler emits from.
55
- import { blockProperty } from "./system-block.mjs";
56
-
57
- import {
58
- parseMarkdownFile,
59
- sohlField,
60
- resolveName,
61
- slugify,
62
- defaultStats,
63
- folderField,
64
- resolveImg,
65
- } from "./helpers.mjs";
47
+ import { folderField, resolveName, resolveImg } from "./helpers.mjs";
66
48
  import { BasePackCompiler } from "./base-compiler.mjs";
67
49
  // What an Adventure member may carry is one rule, and the module that owns the
68
50
  // Adventure states it: the scenes pass bundles its pinned places, and the
69
51
  // bundles pass compiles a note into one.
70
52
  import { stripAdventureKeys } from "./bundle-notes.mjs";
71
- import { buildJournalEntry, splitPages, journalPageId } from "./journals.mjs";
72
- import { compendiumUuid, makeId, packForType } from "./ids.mjs";
73
- // The record accessors only — see `engine/index-records.mjs` for why they live
74
- // apart from the index that builds them.
75
- import { authoredFrontmatter, isNoteRecord, noteFile } from "./index-records.mjs";
76
- import { packRouter } from "./pack-router.mjs";
77
- import { foundryPackageId } from "./content-package.mjs";
53
+ import { buildJournalEntry } from "./journals.mjs";
54
+ import { isMapType, makeId } from "./ids.mjs";
55
+ import { renderAddress } from "./address.mjs";
78
56
  import { itemDocEntryId } from "./item-docs.mjs";
79
- import { behaviorDocId, buildScene, isMapType, regionDocId } from "./map-notes.mjs";
80
- import { buildItineraryScenes } from "./itinerary-scenes.mjs";
81
- import { emitDiagnostic } from "./diagnostics.mjs";
82
- import { rasterizeMapSvg } from "./map-raster.mjs";
83
- import { artPathname, artSlot, pathnameRoles } from "./art-fields.mjs";
57
+ import { artPathname, pathnameRoles } from "./art-fields.mjs";
84
58
  import { buildExportedScene } from "./exported-scene.mjs";
85
59
 
86
- /**
87
- * Every SoHL action name this build knows about, for the `action:` warning on a
88
- * region trigger.
89
- *
90
- * Deliberately a **superset**, gathered from the localization keys every
91
- * intrinsic action's `title:` points at and from the `shortcode:` / `executor:`
92
- * string literals the action definitions carry. A warning that fires on a real
93
- * action would be worse than one that misses a typo, so the wider net is the
94
- * right one: this only has to recognise the names that exist, not enumerate
95
- * them exactly.
96
- *
97
- * @param {string} repoRoot - The repository root.
98
- * @returns {Set<string>} The known action names.
99
- */
100
- export function collectKnownActionNames(repoRoot) {
101
- const names = new Set();
102
- const langFile = path.join(repoRoot, "lang", "en.json");
103
- if (fs.existsSync(langFile)) {
104
- const lang = JSON.parse(fs.readFileSync(langFile, "utf8"));
105
- for (const key of Object.keys(lang)) {
106
- const at = key.indexOf(".Action.");
107
- if (at < 0) continue;
108
- const [leaf] = key.slice(at + ".Action.".length).split(".");
109
- if (/^[A-Za-z][A-Za-z0-9]*$/.test(leaf)) names.add(leaf);
110
- }
111
- }
112
- const srcRoot = path.join(repoRoot, "src");
113
- const pattern = /(?:shortcode|executor):\s*"([A-Za-z][A-Za-z0-9]*)"/g;
114
- const walk = (dir) => {
115
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
116
- const abs = path.join(dir, entry.name);
117
- if (entry.isDirectory()) walk(abs);
118
- else if (entry.name.endsWith(".ts")) {
119
- const text = fs.readFileSync(abs, "utf8");
120
- for (const m of text.matchAll(pattern)) names.add(m[1]);
121
- }
122
- }
123
- };
124
- if (fs.existsSync(srcRoot)) walk(srcRoot);
125
- return names;
126
- }
127
-
128
60
  /**
129
61
  * Scenes pack compiler.
130
62
  *
131
- * Walks the content tree and compiles every map note into one Scene, resolving
132
- * what one note says about another through an index built before any scene is
133
- * written. It also writes one Adventure per place, bundling those scenes with
134
- * the JournalEntries their prose compiled into, which is what makes a pinned
135
- * scene's id-based references resolve on import.
63
+ * Compiles every map note's exported Scene, and writes one Adventure per place
64
+ * whose scenes carry pins, bundling those scenes with the JournalEntries their
65
+ * prose compiled into.
136
66
  */
137
67
  export class Scenes extends BasePackCompiler {
138
68
  static id = "scenes";
139
69
  static label = "map";
140
- static readsPackOutputOf = Object.freeze(["JournalEntry"]);
141
70
 
142
71
  /**
143
- * A map note's `bgImage` is its background art, and it is **required**: the
144
- * map compiler refuses a note without one. It lands on the scene's level
145
- * rather than on a property spelled `img`, which makes no difference to the
146
- * question this declaration answers — the authored address reaches the
147
- * output. The place Adventure this pass bundles carries it too.
72
+ * A map's Scene is exported from Foundry and names its own art, so no art
73
+ * slot a note authors reaches it.
148
74
  *
149
75
  * @type {readonly string[]}
150
76
  */
151
- static emitsArt = Object.freeze(["bgImage"]);
77
+ static emitsArt = Object.freeze([]);
152
78
 
153
79
  /** @type {string} */
154
80
  adventureDir;
@@ -167,10 +93,7 @@ export class Scenes extends BasePackCompiler {
167
93
  skipDirectories,
168
94
  companionDests = {},
169
95
  folderResolver = () => null,
170
- repoRoot = process.cwd(),
171
- config,
172
96
  packName,
173
- bundleSourceDirs = {},
174
97
  corpus,
175
98
  }) {
176
99
  super({ contentBase, assetsBase, dest, folderResolver, skipDirectories, packName, corpus });
@@ -181,13 +104,6 @@ export class Scenes extends BasePackCompiler {
181
104
  value: companionDests.adventures,
182
105
  writable: false,
183
106
  });
184
- Object.defineProperty(this, "repoRoot", {
185
- value: repoRoot,
186
- writable: false,
187
- });
188
- this.mapConfig = config;
189
- this.packName = packName;
190
- this.journalSourceDirs = bundleSourceDirs.JournalEntry ?? [];
191
107
  }
192
108
 
193
109
  /**
@@ -199,241 +115,21 @@ export class Scenes extends BasePackCompiler {
199
115
  }
200
116
 
201
117
  /**
202
- * The router this pass resolves pack names through.
203
- *
204
- * `generatePack` hands every pass the one router the compile resolved, and
205
- * that is the answer whenever a real compile is running — a second router
206
- * is a second answer to where a document landed, built from whichever
207
- * configuration the working directory offers. A compiler
208
- * constructed directly, as a consumer's or a test's is, has none, and falls
209
- * back exactly as `prepare` falls back to deriving its own corpus.
210
- *
211
- * @returns {object} The pack router.
212
- */
213
- get #packRouter() {
214
- return this.router ?? packRouter();
215
- }
216
-
217
- /**
218
- * Collect every map note in the tree, and every item note's Active Effects.
219
- *
220
- * @returns {{maps: Array<object>, effectsByAddress: Map<string, object>}}
221
- */
222
- #collect() {
223
- const maps = [];
224
- const effectsByAddress = new Map();
225
- // The corpus this compile derived once, not a walk of this pass's own
226
- // — and a note is opened only when this pass needs its *prose*,
227
- // which for a map note means three files in `sohl` rather than 1,685.
228
- for (const record of this.corpus.records) {
229
- if (!isNoteRecord(record)) continue;
230
- // No retired-field test: this pass's own walk — the shared compile
231
- // loop — is where a note still declaring `package:` or
232
- // `draft:` is reported, once. Repeating either check here
233
- // would double the diagnostic or throw past it. A refused note is
234
- // indexed and then never compiled, so it reaches no document.
235
- const fm = authoredFrontmatter(record);
236
- const absPath = noteFile(this.contentBase, record);
237
- // The id is the index's, derived against the configuration this
238
- // build resolved — it was derived here through
239
- // `resolveNoteId(fm)` with no package, which falls back to the
240
- // ambient one. What remains unset is a file with no address.
241
- if (!fm || !fm.id) continue;
242
- // The Active Effects a region behaviour may point at, read from
243
- // the block that carries them onto the document — the one position
244
- // the format accepts for them.
245
- const effects = blockProperty(fm, "sohl", "effects");
246
- if (fm.shortcode && Array.isArray(effects) && effects.length) {
247
- effectsByAddress.set(`${fm.type}-${fm.shortcode}`, {
248
- id: fm.id,
249
- type: fm.type,
250
- // Where the owning item landed, so a region behaviour's
251
- // effect reference addresses the right pack when a
252
- // repository ships several of one type.
253
- // This compile's router, not a freshly built one: a
254
- // second router is a second answer to where the document
255
- // landed, resolved from the working directory.
256
- pack: this.#packRouter.resolveOrNull(fm, packForType(fm.type).docType),
257
- effects,
258
- });
259
- }
260
- if (!isMapType(fm.type)) continue;
261
- // Read here, and only here: the index carries no note body, and a
262
- // map's prose is what this pass compiles into its Scene.
263
- maps.push({ fm, body: parseMarkdownFile(absPath).body, absPath });
264
- }
265
- return { maps, effectsByAddress };
266
- }
267
-
268
- /**
269
- * Index every map's derived region and behaviour ids, keyed by shortcode.
270
- *
271
- * Built before any scene is compiled, because a cross-reference in the
272
- * first note may address the last. Every id is a pure function of the
273
- * scene id and the authored key (or an authored `_id`), so this needs
274
- * nothing but frontmatter.
275
- *
276
- * @param {Array<object>} maps - The collected map notes.
277
- * @returns {Map<string, object>} shortcode → `{sceneId, name, regions}`.
278
- */
279
- #indexMaps(maps) {
280
- const index = new Map();
281
- for (const { fm, absPath } of maps) {
282
- if (!fm.shortcode) {
283
- throw new Error(`Map note missing shortcode: ${absPath}`);
284
- }
285
- if (index.has(fm.shortcode)) {
286
- throw new Error(`Two map notes share the shortcode "${fm.shortcode}"`);
287
- }
288
- const regions = new Map();
289
- for (const [key, spec] of Object.entries(fm.sohl?.regions ?? {})) {
290
- const id = regionDocId(fm.id, key, spec?._id);
291
- const behaviors = new Map();
292
- for (const [bKey, bSpec] of Object.entries(spec?.behaviors ?? {})) {
293
- behaviors.set(bKey, behaviorDocId(id, bKey, bSpec?._id));
294
- }
295
- regions.set(key, { id, behaviors });
296
- }
297
- index.set(fm.shortcode, {
298
- sceneId: fm.id,
299
- name: resolveName(fm),
300
- regions,
301
- });
302
- }
303
- return index;
304
- }
305
-
306
- /**
307
- * Build the three address → UUID resolvers a scene's behaviours need.
308
- *
309
- * @param {Map<string, object>} index - From {@link Scenes#indexMaps}.
310
- * @param {Map<string, object>} effectsByAddress - Item notes with effects.
311
- * @param {string} selfShortcode - The note being compiled, so `map:` may be
312
- * omitted for a reference within the same map.
313
- * @returns {object} `{resolveRegionRef, resolveBehaviorRef, resolveEffectRef}`.
314
- */
315
- #resolvers(index, effectsByAddress, selfShortcode) {
316
- const lookupRegion = (addr, label) => {
317
- if (!addr || typeof addr !== "object" || !addr.region) {
318
- throw new Error(
319
- `${label}: a cross-reference is {map, region} — the target ` +
320
- `map's shortcode and the region's key, never a UUID`,
321
- );
322
- }
323
- const mapKey = addr.map ?? selfShortcode;
324
- const target = index.get(mapKey);
325
- if (!target) {
326
- throw new Error(`${label}: no map note has the shortcode "${mapKey}"`);
327
- }
328
- const region = target.regions.get(addr.region);
329
- if (!region) {
330
- throw new Error(
331
- `${label}: map "${mapKey}" has no region "${addr.region}" — ` +
332
- `it has ${[...target.regions.keys()].join(", ") || "none"}`,
333
- );
334
- }
335
- return { target, region };
336
- };
337
-
338
- return {
339
- resolveRegionRef: (addr, label) => {
340
- const { target, region } = lookupRegion(addr, label);
341
- return `Scene.${target.sceneId}.Region.${region.id}`;
342
- },
343
- resolveBehaviorRef: (addr, label) => {
344
- const { target, region } = lookupRegion(addr, label);
345
- if (!addr.behavior) {
346
- throw new Error(`${label}: a behaviour reference is {map, region, behavior}`);
347
- }
348
- const behaviorId = region.behaviors.get(addr.behavior);
349
- if (!behaviorId) {
350
- throw new Error(
351
- `${label}: region "${addr.region}" has no behaviour ` +
352
- `"${addr.behavior}"`,
353
- );
354
- }
355
- return (
356
- `Scene.${target.sceneId}.Region.${region.id}` + `.RegionBehavior.${behaviorId}`
357
- );
358
- },
359
- resolveEffectRef: (addr, label) => {
360
- if (!addr || typeof addr !== "object" || !addr.item) {
361
- throw new Error(
362
- `${label}: an effect reference is {item, effect} — the ` +
363
- `owning item's \`type-shortcode\` address and the ` +
364
- `effect's name, never a UUID`,
365
- );
366
- }
367
- const item = effectsByAddress.get(addr.item);
368
- if (!item) {
369
- throw new Error(
370
- `${label}: no content note addressed "${addr.item}" ` +
371
- `carries Active Effects`,
372
- );
373
- }
374
- const effect = item.effects.find(
375
- (e) => e.name === addr.effect || e._id === addr.effect,
376
- );
377
- if (!effect?._id) {
378
- throw new Error(
379
- `${label}: "${addr.item}" has no Active Effect named ` +
380
- `"${addr.effect}" with an \`_id\``,
381
- );
382
- }
383
- return `${compendiumUuid(foundryPackageId(), item.type, item.id, item.pack)}.ActiveEffect.${effect._id}`;
384
- },
385
- };
386
- }
387
-
388
- /**
389
- * The heading keys a map pin may name, mapped to the page each compiled to.
390
- *
391
- * A location key matches a heading's `{#anchor}` slug, or the slug of its
392
- * text. Both passes split the *converted* markdown, so the ids agree with
393
- * the journals pack without either reading the other's output.
394
- *
395
- * @param {string} markdown - The converted body.
396
- * @param {string} entryId - The JournalEntry's id.
397
- * @param {string} name - The map's name (its lead page).
398
- * @param {(address: string) => string|undefined} resolveRole - See
399
- * {@link module:engine/journals.splitPages}; threaded through so an
400
- * unanchored map figure's page key agrees with the journal this pass
401
- * derives it against.
402
- * @returns {Map<string, string>} heading key → page id.
403
- */
404
- #pageIds(markdown, entryId, name, resolveRole) {
405
- const pageIds = new Map();
406
- splitPages(markdown, name, resolveRole).forEach((page) => {
407
- const id = journalPageId(entryId, page);
408
- if (page.anchorSlug) pageIds.set(page.anchorSlug, id);
409
- const slug = slugify(page.name);
410
- if (slug && !pageIds.has(slug)) pageIds.set(slug, id);
411
- });
412
- return pageIds;
413
- }
414
-
415
- /**
416
- * Index every cross-reference before the walk: a reference in the first
417
- * note may address the last, and the effects an authored `to:` names live
418
- * on item notes this pass does not otherwise read.
118
+ * Start each compile with no places accumulated.
419
119
  *
420
120
  * @returns {Promise<void>}
421
121
  */
422
122
  async prepare() {
423
123
  await super.prepare();
424
- const { maps, effectsByAddress } = this.#collect();
425
- this.index = this.#indexMaps(maps);
426
- this.effectsByAddress = effectsByAddress;
427
- this.knownActions = collectKnownActionNames(this.repoRoot);
428
- /** place key → `{name, img, scenes: [], journal: []}` */
124
+ /** place key → `{key, name, scenes: [], journal: [], pinned}` */
429
125
  this.places = new Map();
430
126
  }
431
127
 
432
128
  /**
433
- * Compile one map note into a Scene, and accumulate the place it belongs
434
- * to — a map's Scene and the JournalEntry its prose compiles into ship
435
- * together in an Adventure, which is the only import that preserves the
436
- * ids its pins address.
129
+ * Compile one map note's exported Scene, and accumulate the place it
130
+ * belongs to — a map's Scene and the JournalEntry its prose compiles into
131
+ * ship together in an Adventure, which is the only import that preserves
132
+ * the ids its pins address.
437
133
  *
438
134
  * @param {object} fm - The note's frontmatter.
439
135
  * @param {string} markdown - The body, tables expanded and wikilinks
@@ -449,96 +145,29 @@ export class Scenes extends BasePackCompiler {
449
145
  // which derives this same journal's pages the same way.
450
146
  const roles = pathnameRoles(this.linkIndex);
451
147
  const resolveRole = (pathname) => roles.get(pathname);
452
- // The same doc-entry id the journals pass derives, from the
453
- // shared `docEntryTypes` arrangement — so neither
454
- // pass has to read the other's output.
148
+ // The same doc-entry id the journals pass derives, from the shared
149
+ // `docEntryTypes` arrangement — so neither pass has to read the
150
+ // other's output.
455
151
  const entryId = hasBody ? itemDocEntryId(fm.id) : undefined;
456
- const { value: authoredFolder } = folderField(fm);
457
- const folder = this.folderResolver(authoredFolder, { isAddress: true });
458
- const warnings = [];
459
- if (fm.data?.scene && fm.subType === "regionalmap") {
460
- throw new Error("`data.scene` is for battlemap and localmap notes");
461
- }
462
- if (fm.data?.fixup !== undefined && !fm.data?.scene) {
463
- throw new Error("`data.fixup` needs an exported Scene at `data.scene`");
464
- }
465
- const bgImage =
466
- fm.data?.scene ?
467
- null
468
- : this.artPathOf(fm.data?.bgImage, "bgImage", "image", artSlot("bgImage")?.accepts);
469
- if (
470
- bgImage?.toLowerCase().endsWith(".svg") &&
471
- (!["regionalmap", "totm"].includes(fm.subType) ||
472
- (fm.subType === "regionalmap" && !fm.data?.scale))
473
- ) {
474
- throw new Error("an SVG map needs `subType: regionalmap` and `data.scale`");
475
- }
476
- const raster =
477
- bgImage?.toLowerCase().endsWith(".svg") ?
478
- rasterizeMapSvg({
479
- foundryPath: bgImage,
480
- width: fm.sohl?.dimensions?.[0],
481
- height: fm.sohl?.dimensions?.[1],
482
- sceneId: fm.id,
483
- config: this.mapConfig,
484
- })
485
- : null;
486
- const scene =
487
- fm.data?.scene ?
488
- buildExportedScene(fm, markdown, {
489
- journalEntryId: entryId,
490
- stats: this.stats,
491
- resolveAddress: (address) => {
492
- const result = artPathname(this.linkIndex, address, "image");
493
- if (!result.resolved || !result.pathname) {
494
- throw new Error(
495
- `Scene fixup address "${address}" cannot resolve to an asset: ${result.reason ?? "no pathname"}`,
496
- );
497
- }
498
- return resolveImg(result.pathname);
499
- },
500
- })
501
- : buildScene(fm, {
502
- packageId: foundryPackageId(),
503
- // The art resolver, so the map pass turns an address into the path
504
- // each surface serves without holding an index of its own. `accepts`
505
- // is threaded through so `bgImage`, one of the four declared art
506
- // slots, is held to its own accepted set here exactly as it is
507
- // through `artPath`.
508
- art: (value, key, type, accepts) =>
509
- key === "bgImage" && raster ?
510
- raster
511
- : this.artPathOf(value, key, type, accepts),
512
- name,
513
- folder,
514
- stats: this.stats,
515
- journalEntryId: entryId,
516
- // A map note's prose is a derived JournalEntry: it lands in the
517
- // default JournalEntry pack, not in whichever Scene pack the map
518
- // itself was routed to.
519
- // This compile's router, as everywhere else in this pass.
520
- journalPack:
521
- hasBody ?
522
- this.#packRouter.resolve(fm, "JournalEntry")
523
- : this.#packRouter.defaultOf("JournalEntry"),
524
- pageIds:
525
- hasBody ? this.#pageIds(markdown, entryId, name, resolveRole) : new Map(),
526
- knownActions: this.knownActions,
527
- warnings,
528
- ...this.#resolvers(this.index, this.effectsByAddress, fm.shortcode),
529
- });
530
- for (const message of warnings) {
531
- // Named by file, like every other note diagnostic. A map
532
- // warning is about the note's frontmatter, which carries no
533
- // offset, so it names the file and stops there rather than
534
- // pointing at a line it cannot establish.
535
- this.noteWarn(`map "${name}": ${message}`);
536
- }
152
+ const scene = buildExportedScene(fm, markdown, {
153
+ journalEntryId: entryId,
154
+ stats: this.stats,
155
+ resolveAddress: (address) => {
156
+ const result = artPathname(this.linkIndex, address, "image");
157
+ if (!result.resolved || !result.pathname) {
158
+ throw new Error(
159
+ `Scene fixup address "${address}" cannot resolve to an asset: ${result.reason ?? "no pathname"}`,
160
+ );
161
+ }
162
+ return resolveImg(result.pathname);
163
+ },
164
+ });
537
165
  this.writeEntry(scene);
538
166
 
539
- // The journal the pins point at, derived here exactly as the
540
- // journals pass derives it, so the Adventure bundles the same
541
- // document that pack ships.
167
+ // The journal the pins point at, derived here exactly as the journals
168
+ // pass derives it, so the Adventure bundles the same document that
169
+ // pack ships.
170
+ const { value: authoredFolder } = folderField(fm);
542
171
  const journal =
543
172
  hasBody ?
544
173
  buildJournalEntry({
@@ -547,31 +176,35 @@ export class Scenes extends BasePackCompiler {
547
176
  markdown,
548
177
  leadName: name,
549
178
  resolveRole,
550
- // As in the journals pass: an address resolves in the
551
- // pack that emits it, which is what makes the folder
552
- // materialise there too. The id spelling that used
553
- // to cross packs verbatim is retired.
179
+ // As in the journals pass: an address resolves in the pack
180
+ // that emits it, which is what makes the folder
181
+ // materialise there too.
554
182
  folder: this.folderResolver(authoredFolder, { isAddress: true }),
555
183
  })
556
184
  : null;
557
185
 
558
- const placeKey = sohlField(fm, "place", null) || fm.shortcode;
186
+ // The compile hands `data.place` over as its resolved Address, and the
187
+ // rendered form keys the place.
188
+ const place = fm.data?.place;
189
+ const placeKey =
190
+ place && typeof place === "object" && place.type && place.shortcode ?
191
+ renderAddress(place)
192
+ : typeof place === "string" && place ? place
193
+ : fm.shortcode;
559
194
  if (!this.places.has(placeKey)) {
560
195
  this.places.set(placeKey, {
561
196
  key: placeKey,
562
- name: sohlField(fm, "placeName", null) || name,
563
- img: fm.data?.scene ? null : this.artPath(fm, "bgImage"),
197
+ // Named after the first map of the place to compile.
198
+ name,
564
199
  pinned: false,
565
200
  scenes: [],
566
201
  journal: [],
567
202
  });
568
203
  }
569
- const place = this.places.get(placeKey);
570
- place.scenes.push(stripAdventureKeys(scene));
571
- if (journal) place.journal.push(stripAdventureKeys(journal));
572
- if (scene.notes?.length || Object.keys(fm.sohl?.locations ?? {}).length) {
573
- place.pinned = true;
574
- }
204
+ const entry = this.places.get(placeKey);
205
+ entry.scenes.push(stripAdventureKeys(scene));
206
+ if (journal) entry.journal.push(stripAdventureKeys(journal));
207
+ if (Array.isArray(scene.notes) && scene.notes.length) entry.pinned = true;
575
208
  return scene;
576
209
  }
577
210
 
@@ -581,36 +214,8 @@ export class Scenes extends BasePackCompiler {
581
214
  *
582
215
  * @returns {Promise<void>}
583
216
  */
584
- async finish(stats) {
217
+ async finish() {
585
218
  this.adventureCount = 0;
586
- const firstScenePack = this.mapConfig?.packs.find((pack) => pack.type === "Scene")?.name;
587
- if (this.mapConfig && this.packName === firstScenePack) {
588
- const generated = buildItineraryScenes({
589
- config: this.mapConfig,
590
- records: this.corpus.records,
591
- contentBase: this.contentBase,
592
- journalSourceDirs: this.journalSourceDirs,
593
- stats: this.stats,
594
- });
595
- for (const finding of generated.findings) emitDiagnostic(finding);
596
- if (generated.scenes.length) {
597
- const place = {
598
- key: "generated-itineraries",
599
- name: "Itinerary Maps",
600
- img: null,
601
- pinned: true,
602
- scenes: [],
603
- journal: generated.journal,
604
- };
605
- for (const scene of generated.scenes) {
606
- this.writeEntry(scene);
607
- place.scenes.push(stripAdventureKeys(scene));
608
- }
609
- this.places.set(place.key, place);
610
- stats.compiled += generated.scenes.length;
611
- this.compiledCount = stats.compiled;
612
- }
613
- }
614
219
  for (const place of this.places.values()) {
615
220
  // A scene that references nothing ships fine as a plain `scenes`
616
221
  // entry; only pins need the id-preserving import an Adventure gives.
@@ -635,7 +240,7 @@ export class Scenes extends BasePackCompiler {
635
240
  const id = makeId("map-adventure", place.key);
636
241
  return {
637
242
  name: place.name,
638
- img: place.img ?? null,
243
+ img: null,
639
244
  caption: "",
640
245
  description: "",
641
246
  actors: [],