@heroiclands/package-build 22.28.0 → 23.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/README.md +1 -1
- package/bin/content-commands.mjs +1 -1
- package/ci/content-format-reference.mjs +46 -0
- package/docs/api.md +125 -142
- package/docs/authoring/assets.md +1 -1
- package/docs/authoring/dates-and-calendars.md +12 -6
- package/docs/authoring/first-note.md +1 -1
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +145 -29
- package/docs/commands.md +8 -10
- package/docs/configuration.md +13 -11
- package/docs/content-format.md +3 -3
- package/docs/guides/book.md +59 -4
- package/docs/guides/icons.md +4 -5
- package/docs/guides/packs.md +6 -0
- package/docs/guides/reference-pages.md +21 -1
- package/docs/guides/release-and-deploy.md +17 -1
- package/docs/guides/site.md +17 -18
- package/docs/reference/format-details.md +904 -275
- package/docs/reference/note-types.md +282 -91
- package/docs/reference/theme-pages.md +58 -66
- package/engine/address-values.mjs +124 -4
- package/engine/address.mjs +44 -3
- package/engine/anchors.mjs +103 -3
- package/engine/art-slots.mjs +2 -9
- package/engine/base-compiler.mjs +25 -6
- package/engine/calendar-artifacts.mjs +4 -1
- package/engine/calendar-notes.mjs +166 -102
- package/engine/content-format-check.mjs +2 -3
- package/engine/content-format.mjs +44 -1
- package/engine/content-format.yaml +84 -16
- package/engine/content-images.mjs +10 -1
- package/engine/content-index.mjs +37 -9
- package/engine/content-links.mjs +24 -5
- package/engine/content-tables.mjs +24 -3
- package/engine/data-keys.mjs +347 -0
- package/engine/derived-sections.mjs +322 -0
- package/engine/diagnostics.mjs +28 -0
- package/engine/event-fields.mjs +187 -0
- package/engine/event-rows.mjs +126 -0
- package/engine/event-views.mjs +305 -0
- package/engine/exported-scene.mjs +29 -53
- package/engine/frontmatter-lint.mjs +103 -77
- package/engine/frontmatter.mjs +7 -2
- package/engine/generate.mjs +53 -0
- package/engine/generated-sections.mjs +223 -0
- package/engine/heading-attributes.mjs +4 -2
- package/engine/helpers.mjs +12 -1
- package/engine/holdings.mjs +12 -33
- package/engine/ids.mjs +17 -11
- package/engine/index.mjs +4 -4
- package/engine/infobox-render.mjs +21 -6
- package/engine/infobox.mjs +217 -99
- package/engine/journals.mjs +104 -47
- package/engine/literature-notes.mjs +136 -0
- package/engine/literature-works.mjs +161 -0
- package/engine/map-graphviz.mjs +4 -18
- package/engine/markdown-expressions.mjs +116 -11
- package/engine/metadata-index.mjs +34 -0
- package/engine/near-miss.mjs +90 -0
- package/engine/note-addresses.mjs +75 -19
- package/engine/note-dates.mjs +5 -3
- package/engine/note-event-terms.mjs +364 -0
- package/engine/note-events.mjs +823 -0
- package/engine/note-vocabulary.mjs +195 -92
- package/engine/office-holders.mjs +20 -54
- package/engine/packagebuild-index.mjs +16 -1
- package/engine/pdf-build.mjs +97 -60
- package/engine/pdf-index.mjs +263 -0
- package/engine/pdf-render.mjs +47 -5
- package/engine/place-relation-terms.mjs +174 -0
- package/engine/place-relations.mjs +72 -160
- package/engine/rank-ladder.mjs +14 -41
- package/engine/reckoning-markers.mjs +2 -0
- package/engine/region-events.mjs +6 -12
- package/engine/scenes.mjs +73 -468
- package/engine/site-build.mjs +74 -85
- package/engine/site-index.mjs +3 -1
- package/engine/site-maps.mjs +43 -29
- package/engine/site-preview.mjs +40 -31
- package/engine/social-ties.mjs +2 -1
- package/engine/sql-tables.mjs +212 -9
- package/engine/standing-terms.mjs +145 -6
- package/engine/standings.mjs +37 -49
- package/engine/stub-lint.mjs +6 -0
- package/engine/web-wikilinks.mjs +21 -1
- package/engine/wikilink-syntax.mjs +34 -4
- package/engine/wikilinks.mjs +27 -2
- package/hugo-theme/layouts/partials/infobox/entry.html +4 -1
- package/hugo-theme/layouts/partials/page-body.html +0 -2
- package/hugo-theme/static/css/style.css +25 -55
- package/package.json +1 -2
- package/packagebuild-metadata.jsonl +13 -13
- package/sohl/default-item-art.mjs +6 -6
- package/sohl/note-schemas.mjs +8 -73
- package/types/engine/address-values.d.mts +50 -3
- package/types/engine/address.d.mts +21 -0
- package/types/engine/anchors.d.mts +49 -2
- package/types/engine/art-slots.d.mts +1 -1
- package/types/engine/base-compiler.d.mts +13 -4
- package/types/engine/calendar-notes.d.mts +7 -0
- package/types/engine/content-format.d.mts +52 -0
- package/types/engine/content-images.d.mts +5 -1
- package/types/engine/content-tables.d.mts +9 -1
- package/types/engine/data-keys.d.mts +145 -0
- package/types/engine/derived-sections.d.mts +73 -0
- package/types/engine/diagnostics.d.mts +15 -0
- package/types/engine/event-fields.d.mts +99 -0
- package/types/engine/event-rows.d.mts +12 -0
- package/types/engine/event-views.d.mts +67 -0
- package/types/engine/exported-scene.d.mts +5 -2
- package/types/engine/frontmatter-lint.d.mts +3 -3
- package/types/engine/generated-sections.d.mts +143 -0
- package/types/engine/holdings.d.mts +6 -20
- package/types/engine/ids.d.mts +15 -12
- package/types/engine/index.d.mts +1 -1
- package/types/engine/infobox.d.mts +13 -3
- package/types/engine/journals.d.mts +24 -11
- package/types/engine/literature-notes.d.mts +29 -0
- package/types/engine/literature-works.d.mts +67 -0
- package/types/engine/map-graphviz.d.mts +2 -10
- package/types/engine/markdown-expressions.d.mts +12 -3
- package/types/engine/near-miss.d.mts +42 -0
- package/types/engine/note-addresses.d.mts +7 -1
- package/types/engine/note-event-terms.d.mts +68 -0
- package/types/engine/note-events.d.mts +71 -0
- package/types/engine/note-vocabulary.d.mts +37 -8
- package/types/engine/office-holders.d.mts +3 -0
- package/types/engine/pdf-index.d.mts +35 -0
- package/types/engine/pdf-render.d.mts +8 -2
- package/types/engine/place-relation-terms.d.mts +83 -0
- package/types/engine/place-relations.d.mts +11 -66
- package/types/engine/rank-ladder.d.mts +2 -0
- package/types/engine/region-events.d.mts +6 -12
- package/types/engine/scenes.d.mts +6 -32
- package/types/engine/site-build.d.mts +8 -22
- package/types/engine/site-maps.d.mts +15 -7
- package/types/engine/sql-tables.d.mts +77 -2
- package/types/engine/standing-terms.d.mts +37 -4
- package/types/engine/standings.d.mts +14 -9
- package/types/engine/wikilink-syntax.d.mts +20 -0
- package/types/engine/wikilinks.d.mts +3 -2
- package/docs/guides/government-migration.md +0 -52
- package/engine/itinerary-scenes.mjs +0 -166
- package/engine/lore-events.mjs +0 -197
- package/engine/map-notes.mjs +0 -1309
- package/engine/map-raster.mjs +0 -51
- package/hugo-theme/layouts/partials/holdings/entry.html +0 -21
- package/hugo-theme/layouts/partials/holdings/kind-groups.html +0 -71
- package/hugo-theme/layouts/partials/holdings.html +0 -61
- package/hugo-theme/layouts/partials/place-map.html +0 -9
- package/types/engine/itinerary-scenes.d.mts +0 -29
- package/types/engine/lore-events.d.mts +0 -14
- package/types/engine/map-notes.d.mts +0 -297
- 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
|
|
16
|
+
* documents, and the `Adventure` bundles that make their pins resolve.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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* —
|
|
27
|
-
*
|
|
28
|
-
* those scenes with the JournalEntries their prose compiled into. A
|
|
29
|
-
*
|
|
30
|
-
* creates with `keepId: true`, and a pin's
|
|
31
|
-
* `
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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 `
|
|
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
|
|
46
|
-
* states only what makes
|
|
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
|
-
|
|
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
|
|
72
|
-
import {
|
|
73
|
-
|
|
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 {
|
|
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
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
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
|
|
144
|
-
*
|
|
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([
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
434
|
-
* to — a map's Scene and the JournalEntry its prose compiles into
|
|
435
|
-
* together in an Adventure, which is the only import that preserves
|
|
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
|
-
//
|
|
454
|
-
//
|
|
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
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
//
|
|
541
|
-
//
|
|
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
|
-
//
|
|
552
|
-
// materialise there too.
|
|
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
|
-
|
|
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
|
-
|
|
563
|
-
|
|
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
|
|
570
|
-
|
|
571
|
-
if (journal)
|
|
572
|
-
if (scene.notes
|
|
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(
|
|
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:
|
|
243
|
+
img: null,
|
|
639
244
|
caption: "",
|
|
640
245
|
description: "",
|
|
641
246
|
actors: [],
|