@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.
Files changed (156) hide show
  1. package/CHANGELOG.md +72 -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 +125 -142
  6. package/docs/authoring/assets.md +1 -1
  7. package/docs/authoring/dates-and-calendars.md +12 -6
  8. package/docs/authoring/first-note.md +1 -1
  9. package/docs/authoring/frontmatter.md +2 -2
  10. package/docs/authoring/links-and-markup.md +145 -29
  11. package/docs/commands.md +8 -10
  12. package/docs/configuration.md +13 -11
  13. package/docs/content-format.md +3 -3
  14. package/docs/guides/book.md +59 -4
  15. package/docs/guides/icons.md +4 -5
  16. package/docs/guides/packs.md +6 -0
  17. package/docs/guides/reference-pages.md +21 -1
  18. package/docs/guides/release-and-deploy.md +17 -1
  19. package/docs/guides/site.md +17 -18
  20. package/docs/reference/format-details.md +904 -275
  21. package/docs/reference/note-types.md +282 -91
  22. package/docs/reference/theme-pages.md +58 -66
  23. package/engine/address-values.mjs +124 -4
  24. package/engine/address.mjs +44 -3
  25. package/engine/anchors.mjs +103 -3
  26. package/engine/art-slots.mjs +2 -9
  27. package/engine/base-compiler.mjs +25 -6
  28. package/engine/calendar-artifacts.mjs +4 -1
  29. package/engine/calendar-notes.mjs +166 -102
  30. package/engine/content-format-check.mjs +2 -3
  31. package/engine/content-format.mjs +44 -1
  32. package/engine/content-format.yaml +84 -16
  33. package/engine/content-images.mjs +10 -1
  34. package/engine/content-index.mjs +37 -9
  35. package/engine/content-links.mjs +24 -5
  36. package/engine/content-tables.mjs +24 -3
  37. package/engine/data-keys.mjs +347 -0
  38. package/engine/derived-sections.mjs +322 -0
  39. package/engine/diagnostics.mjs +28 -0
  40. package/engine/event-fields.mjs +187 -0
  41. package/engine/event-rows.mjs +126 -0
  42. package/engine/event-views.mjs +305 -0
  43. package/engine/exported-scene.mjs +29 -53
  44. package/engine/frontmatter-lint.mjs +103 -77
  45. package/engine/frontmatter.mjs +7 -2
  46. package/engine/generate.mjs +53 -0
  47. package/engine/generated-sections.mjs +223 -0
  48. package/engine/heading-attributes.mjs +4 -2
  49. package/engine/helpers.mjs +12 -1
  50. package/engine/holdings.mjs +12 -33
  51. package/engine/ids.mjs +17 -11
  52. package/engine/index.mjs +4 -4
  53. package/engine/infobox-render.mjs +21 -6
  54. package/engine/infobox.mjs +217 -99
  55. package/engine/journals.mjs +104 -47
  56. package/engine/literature-notes.mjs +136 -0
  57. package/engine/literature-works.mjs +161 -0
  58. package/engine/map-graphviz.mjs +4 -18
  59. package/engine/markdown-expressions.mjs +116 -11
  60. package/engine/metadata-index.mjs +34 -0
  61. package/engine/near-miss.mjs +90 -0
  62. package/engine/note-addresses.mjs +75 -19
  63. package/engine/note-dates.mjs +5 -3
  64. package/engine/note-event-terms.mjs +364 -0
  65. package/engine/note-events.mjs +823 -0
  66. package/engine/note-vocabulary.mjs +195 -92
  67. package/engine/office-holders.mjs +20 -54
  68. package/engine/packagebuild-index.mjs +16 -1
  69. package/engine/pdf-build.mjs +97 -60
  70. package/engine/pdf-index.mjs +263 -0
  71. package/engine/pdf-render.mjs +47 -5
  72. package/engine/place-relation-terms.mjs +174 -0
  73. package/engine/place-relations.mjs +72 -160
  74. package/engine/rank-ladder.mjs +14 -41
  75. package/engine/reckoning-markers.mjs +2 -0
  76. package/engine/region-events.mjs +6 -12
  77. package/engine/scenes.mjs +73 -468
  78. package/engine/site-build.mjs +74 -85
  79. package/engine/site-index.mjs +3 -1
  80. package/engine/site-maps.mjs +43 -29
  81. package/engine/site-preview.mjs +40 -31
  82. package/engine/social-ties.mjs +2 -1
  83. package/engine/sql-tables.mjs +212 -9
  84. package/engine/standing-terms.mjs +145 -6
  85. package/engine/standings.mjs +37 -49
  86. package/engine/stub-lint.mjs +6 -0
  87. package/engine/web-wikilinks.mjs +21 -1
  88. package/engine/wikilink-syntax.mjs +34 -4
  89. package/engine/wikilinks.mjs +27 -2
  90. package/hugo-theme/layouts/partials/infobox/entry.html +4 -1
  91. package/hugo-theme/layouts/partials/page-body.html +0 -2
  92. package/hugo-theme/static/css/style.css +25 -55
  93. package/package.json +1 -2
  94. package/packagebuild-metadata.jsonl +13 -13
  95. package/sohl/default-item-art.mjs +6 -6
  96. package/sohl/note-schemas.mjs +8 -73
  97. package/types/engine/address-values.d.mts +50 -3
  98. package/types/engine/address.d.mts +21 -0
  99. package/types/engine/anchors.d.mts +49 -2
  100. package/types/engine/art-slots.d.mts +1 -1
  101. package/types/engine/base-compiler.d.mts +13 -4
  102. package/types/engine/calendar-notes.d.mts +7 -0
  103. package/types/engine/content-format.d.mts +52 -0
  104. package/types/engine/content-images.d.mts +5 -1
  105. package/types/engine/content-tables.d.mts +9 -1
  106. package/types/engine/data-keys.d.mts +145 -0
  107. package/types/engine/derived-sections.d.mts +73 -0
  108. package/types/engine/diagnostics.d.mts +15 -0
  109. package/types/engine/event-fields.d.mts +99 -0
  110. package/types/engine/event-rows.d.mts +12 -0
  111. package/types/engine/event-views.d.mts +67 -0
  112. package/types/engine/exported-scene.d.mts +5 -2
  113. package/types/engine/frontmatter-lint.d.mts +3 -3
  114. package/types/engine/generated-sections.d.mts +143 -0
  115. package/types/engine/holdings.d.mts +6 -20
  116. package/types/engine/ids.d.mts +15 -12
  117. package/types/engine/index.d.mts +1 -1
  118. package/types/engine/infobox.d.mts +13 -3
  119. package/types/engine/journals.d.mts +24 -11
  120. package/types/engine/literature-notes.d.mts +29 -0
  121. package/types/engine/literature-works.d.mts +67 -0
  122. package/types/engine/map-graphviz.d.mts +2 -10
  123. package/types/engine/markdown-expressions.d.mts +12 -3
  124. package/types/engine/near-miss.d.mts +42 -0
  125. package/types/engine/note-addresses.d.mts +7 -1
  126. package/types/engine/note-event-terms.d.mts +68 -0
  127. package/types/engine/note-events.d.mts +71 -0
  128. package/types/engine/note-vocabulary.d.mts +37 -8
  129. package/types/engine/office-holders.d.mts +3 -0
  130. package/types/engine/pdf-index.d.mts +35 -0
  131. package/types/engine/pdf-render.d.mts +8 -2
  132. package/types/engine/place-relation-terms.d.mts +83 -0
  133. package/types/engine/place-relations.d.mts +11 -66
  134. package/types/engine/rank-ladder.d.mts +2 -0
  135. package/types/engine/region-events.d.mts +6 -12
  136. package/types/engine/scenes.d.mts +6 -32
  137. package/types/engine/site-build.d.mts +8 -22
  138. package/types/engine/site-maps.d.mts +15 -7
  139. package/types/engine/sql-tables.d.mts +77 -2
  140. package/types/engine/standing-terms.d.mts +37 -4
  141. package/types/engine/standings.d.mts +14 -9
  142. package/types/engine/wikilink-syntax.d.mts +20 -0
  143. package/types/engine/wikilinks.d.mts +3 -2
  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/place-map.html +0 -9
  153. package/types/engine/itinerary-scenes.d.mts +0 -29
  154. package/types/engine/lore-events.d.mts +0 -14
  155. package/types/engine/map-notes.d.mts +0 -297
  156. package/types/engine/map-raster.d.mts +0 -8
@@ -0,0 +1,223 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * A note's **generated sections**: the H1 sections the build appends after
16
+ * everything the author wrote, each under a fixed anchor.
17
+ *
18
+ * Two families share the one mechanism. The derived lists —
19
+ * {@link module:engine/derived-sections} — say where a place sits, who governs
20
+ * it, what it governs, which works concern the note and what the map from a
21
+ * place shows. The event views — {@link module:engine/event-views} — list the
22
+ * events concerning a note. Each section is Markdown an author could have
23
+ * written, so the website, the Foundry journal and the book receive one text
24
+ * and set it as they set an authored section: Foundry starts a page at the
25
+ * heading, the website a section in its table of contents, the book a section
26
+ * of the entry.
27
+ *
28
+ * Three rules hold for every section:
29
+ *
30
+ * - **An author's own anchor replaces it.** A note whose body declares a prose
31
+ * anchor with the section's slug keeps its own section and is given no
32
+ * generated one.
33
+ * - **A section with nothing to show is not generated.**
34
+ * - **Sections follow the author's text in the order of
35
+ * {@link GENERATED_SECTIONS}.**
36
+ *
37
+ * This module is a leaf: it holds the order, the join and the boundary, and
38
+ * imports nothing, so the table expansion every surface runs can read it
39
+ * without reaching the derivations.
40
+ *
41
+ * @module
42
+ */
43
+
44
+ /**
45
+ * Every generated section, in the order it follows the author's text: where a
46
+ * place sits and who governs it, then what happened, then what was written
47
+ * about it, then the map.
48
+ *
49
+ * @type {ReadonlyArray<Readonly<{slug: string, heading: string}>>}
50
+ */
51
+ export const GENERATED_SECTIONS = Object.freeze(
52
+ [
53
+ { slug: "within", heading: "Within" },
54
+ { slug: "governedby", heading: "Governed by" },
55
+ { slug: "governedplaces", heading: "Governed places" },
56
+ { slug: "chronology", heading: "Chronology" },
57
+ { slug: "events", heading: "Events" },
58
+ { slug: "accounts", heading: "Accounts" },
59
+ { slug: "followed", heading: "What followed" },
60
+ { slug: "insongandstory", heading: "In song and story" },
61
+ { slug: "fromhere", heading: "From here" },
62
+ ].map((section) => Object.freeze(section)),
63
+ );
64
+
65
+ /**
66
+ * The directory, inside a package's own pathname space, that holds the
67
+ * drawings the build makes. A generated section names a drawing as
68
+ * `generated/<name>`, and each surface serves it from where it drew it: the
69
+ * website inlines it, the Foundry compile stages it into the module under
70
+ * `assets/generated/`, and the book draws it beside its Typst source. An
71
+ * authored image is never addressed here.
72
+ *
73
+ * @type {string}
74
+ */
75
+ export const GENERATED_DRAWING_DIR = "generated";
76
+
77
+ /**
78
+ * The pathname a generated section names the map from a place by.
79
+ *
80
+ * @param {string} shortcode - The place.
81
+ * @returns {string} `generated/from-<shortcode>.svg`.
82
+ */
83
+ export function fromHereSource(shortcode) {
84
+ return `${GENERATED_DRAWING_DIR}/${fromHereFile(shortcode)}`;
85
+ }
86
+
87
+ /**
88
+ * The file the map from a place is drawn into, wherever a surface draws it.
89
+ *
90
+ * @param {string} shortcode - The place.
91
+ * @returns {string} `from-<shortcode>.svg`.
92
+ */
93
+ export function fromHereFile(shortcode) {
94
+ return `from-${String(shortcode).toLowerCase()}.svg`;
95
+ }
96
+
97
+ /**
98
+ * The drawing an image pathname names, when it names one the build makes.
99
+ *
100
+ * @param {string} src - An image's address, as the Markdown carries it.
101
+ * @returns {string|null} The drawn file's name, or `null` for any other image.
102
+ */
103
+ export function generatedDrawing(src) {
104
+ const match = /^generated\/(from-[a-z0-9_-]+\.svg)$/.exec(String(src ?? "").trim());
105
+ return match ? match[1] : null;
106
+ }
107
+
108
+ /**
109
+ * Whether an authored image addresses the directory the build's drawings use.
110
+ *
111
+ * @param {string} src - The address, exactly as authored.
112
+ * @returns {string} The problem, as a finding's sentence, or `""`.
113
+ */
114
+ export function generatedDirectoryProblem(src) {
115
+ const s = String(src ?? "").trim();
116
+ if (s === GENERATED_DRAWING_DIR || s.startsWith(`${GENERATED_DRAWING_DIR}/`)) {
117
+ return (
118
+ `\`${s}\` is under \`${GENERATED_DRAWING_DIR}/\`, which holds the drawings ` +
119
+ "the build makes for its generated sections — address an authored image " +
120
+ "anywhere else in the package"
121
+ );
122
+ }
123
+ return "";
124
+ }
125
+
126
+ /**
127
+ * Whether a note's own body declares a prose anchor with this slug.
128
+ *
129
+ * @param {object} record - The note's index record, whose `anchors` were read
130
+ * from the authored body.
131
+ * @param {string} slug - The section's anchor.
132
+ * @returns {boolean} Whether the author has written the section.
133
+ */
134
+ export function authoredSection(record, slug) {
135
+ return (record?.anchors ?? []).some(
136
+ (anchor) => anchor?.kind === "prose" && anchor.slug === slug,
137
+ );
138
+ }
139
+
140
+ /**
141
+ * The heading line of one generated section, with its anchor.
142
+ *
143
+ * @param {string} slug - One of {@link GENERATED_SECTIONS}.
144
+ * @returns {string} `# <Heading> {#<slug>}`.
145
+ */
146
+ export function sectionHeading(slug) {
147
+ const section = GENERATED_SECTIONS.find((one) => one.slug === slug);
148
+ if (!section) throw new Error(`no generated section is named "${slug}"`);
149
+ return `# ${section.heading} {#${section.slug}}`;
150
+ }
151
+
152
+ /**
153
+ * One note's sections, joined in {@link GENERATED_SECTIONS} order.
154
+ *
155
+ * @param {Iterable<Map<string, string>|undefined>} parts - Each a map of slug
156
+ * to that section's Markdown, heading included.
157
+ * @returns {string} The sections, or `""` when there are none.
158
+ */
159
+ export function joinSections(parts) {
160
+ const bySlug = new Map();
161
+ for (const part of parts) for (const [slug, text] of part ?? []) bySlug.set(slug, text);
162
+ return GENERATED_SECTIONS.filter((section) => bySlug.has(section.slug))
163
+ .map((section) => bySlug.get(section.slug).replace(/\s+$/, "") + "\n")
164
+ .join("\n");
165
+ }
166
+
167
+ /**
168
+ * A note's body with its generated sections after it — the one way the two
169
+ * are joined, so the pass that answers their fences and every pass that
170
+ * expands them read the same text.
171
+ *
172
+ * @param {string} body - The authored body.
173
+ * @param {string|undefined} sections - Its sections, from {@link joinSections}.
174
+ * @returns {string} The body the surfaces receive.
175
+ */
176
+ export function appendGeneratedSections(body, sections) {
177
+ if (!sections) return body;
178
+ return `${String(body ?? "").replace(/\s+$/, "")}\n\n${sections}`;
179
+ }
180
+
181
+ /**
182
+ * The line a surface inserts where a note's generated sections begin, when it
183
+ * treats them differently from the author's text. Private-use characters, so
184
+ * no pass between the expansion and the split reads it as markup, and no
185
+ * output carries it: {@link splitGenerated} removes it.
186
+ *
187
+ * @type {string}
188
+ */
189
+ export const GENERATED_BOUNDARY = "generated-sections";
190
+
191
+ /**
192
+ * Mark where a note's generated sections begin.
193
+ *
194
+ * @param {string} markdown - The expanded body.
195
+ * @param {number|undefined} from - The 0-based line the sections start on,
196
+ * from {@link module:engine/content-tables.expandContentTables}.
197
+ * @returns {string} The body, with {@link GENERATED_BOUNDARY} on its own line
198
+ * before the sections; unchanged where there are none.
199
+ */
200
+ export function markGenerated(markdown, from) {
201
+ if (from === undefined || from < 0) return markdown;
202
+ const lines = String(markdown).split("\n");
203
+ if (from > lines.length) return markdown;
204
+ lines.splice(from, 0, GENERATED_BOUNDARY);
205
+ return lines.join("\n");
206
+ }
207
+
208
+ /**
209
+ * Split a body at its {@link GENERATED_BOUNDARY}.
210
+ *
211
+ * @param {string} markdown - A body {@link markGenerated} marked.
212
+ * @returns {{authored: string, generated: string}} The author's text, and the
213
+ * generated sections after it — `""` when there are none.
214
+ */
215
+ export function splitGenerated(markdown) {
216
+ const lines = String(markdown ?? "").split("\n");
217
+ const at = lines.indexOf(GENERATED_BOUNDARY);
218
+ if (at < 0) return { authored: String(markdown ?? ""), generated: "" };
219
+ return {
220
+ authored: lines.slice(0, at).join("\n"),
221
+ generated: lines.slice(at + 1).join("\n"),
222
+ };
223
+ }
@@ -305,9 +305,11 @@ export function withheldSections(source, bodyLine = 1) {
305
305
  sections.push({ start: index, end: starts[at + 1] ?? lines.length, heading });
306
306
  }
307
307
  // A heading that opens no page is not among the openings at all, so the
308
- // class is looked for again across every heading the body states.
308
+ // class is looked for again across every heading the body states. The
309
+ // openings are keyed by body line index and a heading carries its file
310
+ // line, so the body's first line is subtracted to compare the two.
309
311
  for (const heading of scanHeadingAttributes(source, bodyLine).headings) {
310
- if (openings.has(heading.line - 1)) continue;
312
+ if (openings.has(heading.line - bodyLine)) continue;
311
313
  if (!heading.classes.includes(WITHHELD_CLASS)) continue;
312
314
  errors.push({
313
315
  line: heading.line,
@@ -920,7 +920,18 @@ export function buildContentLinkIndex(
920
920
  // discarded here — the walk yields the body and nothing read it.
921
921
  // Read from the record rather than from a second reading of the
922
922
  // note's headings — the one-anchor-reader rule.
923
- anchors: new Set((record.anchors ?? []).map((anchor) => anchor.slug)),
923
+ anchors: new Set(
924
+ (record.anchors ?? [])
925
+ .filter((anchor) => anchor.kind === "prose")
926
+ .map((anchor) => anchor.slug),
927
+ ),
928
+ // The note's event ids, which a wikilink may not name: a link to
929
+ // one is refused with its own message rather than as unknown.
930
+ eventAnchors: new Set(
931
+ (record.anchors ?? [])
932
+ .filter((anchor) => anchor.kind === "event")
933
+ .map((anchor) => anchor.slug),
934
+ ),
924
935
  anchorUuids: record.foundry?.note?.anchors,
925
936
  docAnchorUuids: documentation?.foundry?.note?.anchors,
926
937
  });
@@ -14,32 +14,17 @@
14
14
  /**
15
15
  * Derive containment and government lists from place facts. Geography comes
16
16
  * only from `data.parents`; government comes only from `data.government`.
17
- * Affiliation domains never supply government, and neither graph expands
18
- * through geographic or affiliation ancestors. Government identity preserves the Address package, type and shortcode.
17
+ * Neither graph expands through geographic or affiliation ancestors. Government identity preserves the Address package, type and shortcode.
19
18
  * Its system normalizes to `note` because Item and journal documents share one page.
20
- * Historical holdings API names remain for callers migrating their imports.
19
+ * The lists become a note's generated **Within**, **Governed by** and
20
+ * **Governed places** sections — see {@link module:engine/derived-sections}.
21
21
  * @module
22
22
  */
23
23
  import { isAddressTuple, ownDocumentSystem, parseAddress, renderAddress } from "./address.mjs";
24
24
  import { positionOfFrontmatterPath } from "./diagnostics.mjs";
25
25
  import { readCanonicalKey } from "./content-address.mjs";
26
26
 
27
- /** Derived keys, including retired keys removed from generated pages.
28
- * @type {readonly string[]}
29
- */
30
- export const HOLDINGS_KEYS = Object.freeze([
31
- "contains",
32
- "governed_by",
33
- "governed_places",
34
- "held_by",
35
- "holdings",
36
- ]);
37
- /** @deprecated Government advisories apply to every inhabited place subtype.
38
- * @type {readonly string[]}
39
- */
40
- export const HELD_SUBTYPES = Object.freeze(["settlement", "site", "structure"]);
41
-
42
- /** @typedef {{title:string,url?:string,type:string,subType?:string}} HoldingsEntry */
27
+ /** @typedef {{title:string,url?:string,address:string,type:string,subType?:string}} HoldingsEntry */
43
28
  /**
44
29
  * @typedef {object} HoldingsNode
45
30
  * @property {string} shortcode
@@ -56,7 +41,7 @@ export const HELD_SUBTYPES = Object.freeze(["settlement", "site", "structure"]);
56
41
  */
57
42
  /** @typedef {{contains?:HoldingsEntry[],governed_by?:HoldingsEntry[],governed_places?:HoldingsEntry[]}} Holdings */
58
43
 
59
- /** Historical shortcode projection for callers that explicitly need one.
44
+ /** The shortcodes a `parents` value names, bare or qualified.
60
45
  * @param {unknown} value
61
46
  * @returns {string[]}
62
47
  */
@@ -124,6 +109,7 @@ function entryOf(node) {
124
109
  return {
125
110
  title: node.title,
126
111
  ...(listed(node) ? { url: node.url } : {}),
112
+ address: ownKey(node),
127
113
  type: node.type,
128
114
  ...(node.subType === undefined ? {} : { subType: node.subType }),
129
115
  };
@@ -138,10 +124,13 @@ function bySubTypeThenTitle(a, b) {
138
124
 
139
125
  /** Read local facts without converting government omission into anarchy.
140
126
  * @param {object} fm
141
- * @param {{title:string,url?:string,package?:string,system?:string,governmentSystem?:string}} page
127
+ * @param {{title:string,url?:string,package?:string,system?:string,canonical?:string,governmentSystem?:string}} page
142
128
  * @returns {HoldingsNode|null}
143
129
  */
144
- export function holdingsNode(fm, { title, url, package: pkg, system, governmentSystem }) {
130
+ export function holdingsNode(
131
+ fm,
132
+ { title, url, package: pkg, system, canonical, governmentSystem },
133
+ ) {
145
134
  const type = String(fm?.type ?? "");
146
135
  if (type !== "place" && type !== "affiliation") return null;
147
136
  const shortcode = String(fm?.shortcode ?? "").toLowerCase();
@@ -155,6 +144,7 @@ export function holdingsNode(fm, { title, url, package: pkg, system, governmentS
155
144
  url,
156
145
  ...(pkg ? { package: pkg } : {}),
157
146
  ...(system ? { system } : {}),
147
+ ...(canonical ? { canonical } : {}),
158
148
  ...(governmentSystem ? { governmentSystem } : {}),
159
149
  parents:
160
150
  type === "place" ?
@@ -294,14 +284,3 @@ export function checkGovernment(note) {
294
284
  },
295
285
  ];
296
286
  }
297
-
298
- /**
299
- * The historical API name for the government advisory. Tenure is no longer checked.
300
- * @deprecated Use checkGovernment.
301
- * @param {object} note - The place note.
302
- * @param {object} [_opts] - Historical options, no longer needed.
303
- * @returns {object[]} Government advisory findings.
304
- */
305
- export function checkHeld(note, _opts = {}) {
306
- return checkGovernment(note);
307
- }
package/engine/ids.mjs CHANGED
@@ -40,25 +40,31 @@ export function makeId(namespace, value) {
40
40
  }
41
41
 
42
42
  /**
43
- * Every content type that compiles into a Foundry `Scene` — a **map note**.
44
- * The three differ only in derived canvas defaults, which is the map
45
- * compiler's business; everything else treats them alike.
43
+ * Every content type that compiles into a Foundry `Scene` — a **map note**,
44
+ * which carries its Scene as exported from Foundry.
46
45
  *
47
- * Declared in this leaf module because several passes that must not depend on
48
- * the map compiler need it: the pack router below, and the doc-carrying type
49
- * set in `item-docs.mjs` (a map note's prose becomes a JournalEntry, exactly as
50
- * an item's or a macro's does).
46
+ * Declared in this leaf module because several passes need it: the pack router
47
+ * below, the scenes pass, and the doc-carrying type set in `item-docs.mjs` (a
48
+ * map note's prose becomes a JournalEntry, exactly as an item's or a macro's
49
+ * does).
51
50
  *
52
51
  * @type {ReadonlySet<string>}
53
52
  */
54
53
  export const MAP_TYPES = Object.freeze(new Set(["map"]));
55
54
 
56
55
  /**
57
- * The map subTypes, which differ only in the canvas defaults derived for them.
56
+ * Whether a content note's type compiles into a Scene.
58
57
  *
59
- * As three *types* they would cost three entries in the pack
60
- * router, three in the claims set and three in every consumer's section config
61
- * — for one idea that the specification had always described as one type.
58
+ * @param {string} [type] - The note's `type` frontmatter.
59
+ * @returns {boolean} True for a map type.
60
+ */
61
+ export function isMapType(type) {
62
+ return MAP_TYPES.has(String(type));
63
+ }
64
+
65
+ /**
66
+ * The map subTypes. A subType says what kind of map a note is; every one
67
+ * carries its Scene as exported from Foundry.
62
68
  *
63
69
  * @type {readonly string[]}
64
70
  */
package/engine/index.mjs CHANGED
@@ -280,8 +280,8 @@ export * as journals from "./journals.mjs";
280
280
  /** The Macro compiler. */
281
281
  export * as macros from "./macros.mjs";
282
282
 
283
- /** The map-note schema a Scene is authored in. */
284
- export * as mapNotes from "./map-notes.mjs";
283
+ /** An exported Scene's compile: its fixups, keys and anchored pins. */
284
+ export * as exportedScene from "./exported-scene.mjs";
285
285
 
286
286
  /** The Scene and Adventure compiler. */
287
287
  export * as scenes from "./scenes.mjs";
@@ -303,8 +303,8 @@ export * as compendiums from "./compendiums.mjs";
303
303
 
304
304
  // The region-event vocabulary stays flat as well as namespaced: the Foundry
305
305
  // runtime imports these three by name through
306
- // `@heroiclands/package-build/engine/region-events`, and they were part of this
307
- // barrel's surface before the pipeline arrived.
306
+ // `@heroiclands/package-build/engine/region-events`, and this barrel carries
307
+ // them by name too.
308
308
  export {
309
309
  CURATED_REGION_EVENTS,
310
310
  EXCLUDED_REGION_EVENTS,
@@ -116,12 +116,17 @@ function valueToHtml(row, link) {
116
116
  /**
117
117
  * One entry of a `runin` group or a `list` section as HTML.
118
118
  *
119
+ * An entry's optional `lead` is plain text set before it, so a line such as a
120
+ * membership — `War Chief, Hárár (5), of Vrystwald Tribes` — links the body's
121
+ * name and nothing else.
122
+ *
119
123
  * @param {object} entry - The entry.
120
124
  * @param {(value: object) => string} link - How this medium draws a link.
121
125
  * @returns {string} HTML.
122
126
  */
123
127
  function entryToHtml(entry, link) {
124
- return entry?.url || entry?.address ? link(entry) : escapeHtml(entry?.text);
128
+ const lead = entry?.lead ? escapeHtml(entry.lead) : "";
129
+ return lead + (entry?.url || entry?.address ? link(entry) : escapeHtml(entry?.text));
125
130
  }
126
131
 
127
132
  /**
@@ -265,6 +270,19 @@ function valueToTypst(row, link) {
265
270
  }
266
271
  }
267
272
 
273
+ /**
274
+ * One entry of a `runin` group or a `list` section as Typst: its `lead` as
275
+ * plain text, then the entry, linked where the book can reach it.
276
+ *
277
+ * @param {object} entry - The entry.
278
+ * @param {(value: object) => string} link - How the book draws a link.
279
+ * @returns {string} Typst markup.
280
+ */
281
+ function entryToTypst(entry, link) {
282
+ const lead = entry?.lead ? escapeTypst(entry.lead) : "";
283
+ return lead + (entry?.url || entry?.address ? link(entry) : escapeTypst(entry?.text));
284
+ }
285
+
268
286
  /**
269
287
  * One section as Typst, whole and unbreakable.
270
288
  *
@@ -306,16 +324,13 @@ function sectionToTypst(section, link, lead = "") {
306
324
  } else if (section.layout === "runin") {
307
325
  for (const group of section.groups ?? []) {
308
326
  const entries = (group.entries ?? [])
309
- .map((entry) =>
310
- entry?.url || entry?.address ? link(entry) : escapeTypst(entry?.text),
311
- )
327
+ .map((entry) => entryToTypst(entry, link))
312
328
  .join(", ");
313
329
  body.push(`#infobox-runin("${escapeTypstString(group.label ?? "")}")[${entries}]`);
314
330
  }
315
331
  } else {
316
332
  for (const entry of section.entries ?? []) {
317
- const text = entry?.url || entry?.address ? link(entry) : escapeTypst(entry?.text);
318
- body.push(`#infobox-runin("")[${text}]`);
333
+ body.push(`#infobox-runin("")[${entryToTypst(entry, link)}]`);
319
334
  }
320
335
  }
321
336