@heroiclands/package-build 20.7.0 → 21.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 (63) hide show
  1. package/CHANGELOG.md +183 -0
  2. package/CONTENT.md +132 -44
  3. package/bin/content-build.mjs +37 -5
  4. package/bin/package-build.mjs +77 -0
  5. package/content-config.mjs +59 -1
  6. package/docs/api.md +149 -19
  7. package/docs/commands.md +75 -0
  8. package/docs/configuration.md +37 -10
  9. package/docs/content-format.md +450 -49
  10. package/engine/content-format.mjs +52 -3
  11. package/engine/content-images.mjs +699 -0
  12. package/engine/dependency-bump.mjs +218 -0
  13. package/engine/frontmatter-lint.mjs +89 -2
  14. package/engine/helpers.mjs +81 -142
  15. package/engine/index.mjs +15 -0
  16. package/engine/infobox-registry.mjs +81 -0
  17. package/engine/infobox-render.mjs +382 -0
  18. package/engine/infobox.mjs +963 -0
  19. package/engine/item-registry.mjs +5 -5
  20. package/engine/journals.mjs +22 -1
  21. package/engine/map-notes.mjs +11 -5
  22. package/engine/metadata-index.mjs +5 -0
  23. package/engine/note-vocabulary.mjs +57 -2
  24. package/engine/pathnames.mjs +374 -0
  25. package/engine/pdf-build.mjs +206 -9
  26. package/engine/pdf-render.mjs +453 -20
  27. package/engine/pdf-toc.mjs +77 -5
  28. package/engine/scenes.mjs +2 -1
  29. package/engine/site-build.mjs +106 -7
  30. package/engine/site-index.mjs +93 -4
  31. package/engine/wikilinks.mjs +93 -0
  32. package/hm3/default-item-art.mjs +14 -15
  33. package/hm3/index.mjs +3 -0
  34. package/hm3/infobox.mjs +64 -0
  35. package/package.json +1 -1
  36. package/sohl/default-item-art.mjs +18 -16
  37. package/sohl/index.mjs +3 -0
  38. package/sohl/infobox.mjs +499 -0
  39. package/types/content-config.d.mts +7 -0
  40. package/types/engine/content-format.d.mts +36 -0
  41. package/types/engine/content-images.d.mts +281 -0
  42. package/types/engine/dependency-bump.d.mts +89 -0
  43. package/types/engine/frontmatter-lint.d.mts +23 -0
  44. package/types/engine/helpers.d.mts +30 -72
  45. package/types/engine/index.d.mts +5 -0
  46. package/types/engine/infobox-registry.d.mts +36 -0
  47. package/types/engine/infobox-render.d.mts +87 -0
  48. package/types/engine/infobox.d.mts +443 -0
  49. package/types/engine/item-registry.d.mts +5 -5
  50. package/types/engine/journals.d.mts +9 -1
  51. package/types/engine/note-vocabulary.d.mts +51 -0
  52. package/types/engine/pathnames.d.mts +189 -0
  53. package/types/engine/pdf-build.d.mts +46 -0
  54. package/types/engine/pdf-render.d.mts +97 -1
  55. package/types/engine/pdf-toc.d.mts +10 -5
  56. package/types/engine/site-build.d.mts +11 -3
  57. package/types/engine/site-index.d.mts +35 -3
  58. package/types/engine/wikilinks.d.mts +22 -0
  59. package/types/hm3/default-item-art.d.mts +5 -6
  60. package/types/hm3/index.d.mts +1 -0
  61. package/types/hm3/infobox.d.mts +22 -0
  62. package/types/sohl/index.d.mts +1 -0
  63. package/types/sohl/infobox.d.mts +145 -0
@@ -0,0 +1,189 @@
1
+ /**
2
+ * What is wrong with an authored pathname, or `""` when nothing is.
3
+ *
4
+ * Config-free, and a sentence rather than a code, so the lint that has a line
5
+ * and a column to attach it to and the resolver that has only a file say the
6
+ * same thing about the same value.
7
+ *
8
+ * @param {string|null|undefined} raw - The pathname, as authored.
9
+ * @returns {string} The problem, as a finding's sentence, or `""`.
10
+ */
11
+ export function pathnameProblem(raw: string | null | undefined): string;
12
+ /**
13
+ * Every content package this build can resolve a pathname against.
14
+ *
15
+ * The package being built; every game system it compiles content for, which
16
+ * Foundry serves from `systems/<id>`; and every other package it declares a
17
+ * relationship with, which states that package's Foundry id and — where the two
18
+ * words differ — what its content is called.
19
+ *
20
+ * @param {object} config - The resolved build configuration.
21
+ * @returns {Map<string, {root: string|null, id: string|null, own: boolean}>} The
22
+ * packages, by content package name. `root` is the Foundry directory the
23
+ * package is served from, `null` where the package ships no Foundry package.
24
+ */
25
+ export function packageAddresses(config: object): Map<string, {
26
+ root: string | null;
27
+ id: string | null;
28
+ own: boolean;
29
+ }>;
30
+ /**
31
+ * Resolve one authored pathname into the address each surface serves.
32
+ *
33
+ * @param {string|null|undefined} raw - The pathname, as authored.
34
+ * @param {object} config - The resolved build configuration. Required rather
35
+ * than defaulted, which is what keeps this module a leaf: it reads a
36
+ * configuration and never loads one, so the lint can import it without a
37
+ * repository to resolve.
38
+ * @returns {PathnameForms|null} The four forms, or `null` when the note names
39
+ * no file at all.
40
+ * @throws {Error} When the pathname is written in Foundry's own spelling, which
41
+ * resolves on one surface and nowhere else.
42
+ */
43
+ export function resolvePathname(raw: string | null | undefined, config: object): PathnameForms | null;
44
+ /**
45
+ * One authored pathname, and the four addresses it resolves to.
46
+ *
47
+ * A note names a file once — in `img:`, in `data.portrait:`, in the body of a
48
+ * markdown image — and four surfaces have to serve it: a Foundry install, this
49
+ * repository's own working tree, the website, and the book. Each addresses the
50
+ * same file differently, so the authored pathname is a *statement of
51
+ * ownership* and every surface derives its own address from it. One statement,
52
+ * four derivations, one rule.
53
+ *
54
+ * ## The rule
55
+ *
56
+ * **The first segment says which package owns the file, when it is followed by
57
+ * `assets/`.** Everything after `assets/` is the *suffix* — the path inside
58
+ * that package's shipped tree, and the one piece every form is built from.
59
+ *
60
+ * | Authored | Owner | Suffix |
61
+ * | --------------------------- | ---------------- | ------------------- |
62
+ * | `sohl/assets/icons/a.svg` | the `sohl` package | `icons/a.svg` |
63
+ * | `images/beings/b.webp` | **this** package | `images/beings/b.webp` |
64
+ *
65
+ * A pathname that does not open with `<package>/assets/` belongs to the package
66
+ * being built, and the whole of it is the suffix. That is the ordinary case and
67
+ * the one nearly every note writes.
68
+ *
69
+ * The four forms, for a `thalorna` note writing `images/map.webp` (`thalorna`
70
+ * ships as the Foundry module `sohl-thalorna`):
71
+ *
72
+ * | Form | Address |
73
+ * | --------- | -------------------------------------------------- |
74
+ * | `foundry` | `modules/sohl-thalorna/assets/images/map.webp` |
75
+ * | `local` | `assets/images/map.webp` |
76
+ * | `web` | `https://cdn.heroiclands.org/thalorna/images/map.webp` |
77
+ * | `pdf` | `assets/images/map.webp` |
78
+ *
79
+ * **`<package>` and `<foundry-id>` are two different names.** The package is
80
+ * `thalorna` — what the content is called, what the website serves it under,
81
+ * and what a note writes. The Foundry id is `sohl-thalorna` — what Foundry
82
+ * installs the module as, and the only place that name appears. They coincide
83
+ * for `sohl` and `hm3`, which is exactly why the two are kept apart here rather
84
+ * than treated as one value.
85
+ *
86
+ * `local` and `pdf` read the same and mean different places: `local` is the file
87
+ * in the owning repository's working tree, `pdf` is where the book stages a copy
88
+ * beside its Typst source. They are derived separately because only one of them
89
+ * is a file a build may open — see {@link PathnameForms.own}.
90
+ *
91
+ * ## What is not a package pathname
92
+ *
93
+ * **An off-install address passes through on every surface**: a URL, a
94
+ * protocol-relative `//host/…`, or a `/`-rooted path, which Foundry serves from
95
+ * the data root and which names no package at all. That is how a note addresses
96
+ * core Foundry art (`/icons/svg/mystery-man.svg`) or a package this build knows
97
+ * nothing about (`/systems/dnd5e/icons/spell.webp`).
98
+ *
99
+ * **A package this build has never heard of keeps its ownership.** The website
100
+ * and the book need only the package's name and the suffix, so both resolve;
101
+ * the Foundry address needs the package's kind and its Foundry id, which only a
102
+ * declared relationship carries, so that one form comes back `null` and the
103
+ * caller that needs it refuses. Reading such a pathname as this package's own
104
+ * would file one package's name inside another's tree and say nothing.
105
+ *
106
+ * **A `systems/…` or `modules/…` pathname is refused.** It is a Foundry address
107
+ * written where an ownership statement belongs: it resolves for Foundry and for
108
+ * nothing else, because neither the website nor the book has any such directory.
109
+ * {@link pathnameProblem} names the replacement, and every surface refuses the
110
+ * value rather than deriving an address from it — a wrong address that resolves
111
+ * to a 404 is the failure this module exists to remove, and inventing one here
112
+ * would reintroduce it one directory along.
113
+ *
114
+ * ## The two empties
115
+ *
116
+ * `null` — or an absent key, which arrives as `undefined` — means **unset**: the
117
+ * note names no file and the caller's default applies. `""` means **blank on
118
+ * purpose**: the note names no file and wants none, so no default may replace
119
+ * it. `resolvePathname` returns `null` for the first and a form object whose
120
+ * every address is `""` for the second, so the two stay distinguishable all the
121
+ * way to the caller.
122
+ *
123
+ * @module
124
+ */
125
+ /**
126
+ * The directory a package ships its files in, and the segment that marks a
127
+ * pathname's first segment as a package name.
128
+ *
129
+ * One constant rather than a literal in six places: it is the second segment of
130
+ * an authored package pathname, the last segment of a Foundry asset root, the
131
+ * whole of the `local` form's prefix, and the directory the book stages into.
132
+ *
133
+ * @type {string}
134
+ */
135
+ export const ASSETS_SEGMENT: string;
136
+ /**
137
+ * The surfaces one authored pathname resolves for.
138
+ *
139
+ * Exported so a test can assert that every form a resolution carries is one of
140
+ * these, and that none is missing — the guard against a fifth surface being
141
+ * added to one caller and forgotten in the resolver.
142
+ *
143
+ * @type {readonly string[]}
144
+ */
145
+ export const PATHNAME_SURFACES: readonly string[];
146
+ /**
147
+ * One authored pathname, resolved.
148
+ */
149
+ export type PathnameForms = {
150
+ /**
151
+ * The pathname exactly as the note wrote it.
152
+ */
153
+ authored: string;
154
+ /**
155
+ * `blank` for `""`, `external`
156
+ * for an address no package owns, `package` for an owned file.
157
+ */
158
+ state: "blank" | "external" | "package";
159
+ /**
160
+ * The content package that owns the file, or
161
+ * `null` when no package does.
162
+ */
163
+ package: string | null;
164
+ /**
165
+ * The path inside that package's shipped tree.
166
+ */
167
+ suffix: string | null;
168
+ /**
169
+ * Whether the owner is the package being built, and so
170
+ * whether `local` names a file this build may open.
171
+ */
172
+ own: boolean;
173
+ /**
174
+ * The address inside a Foundry install.
175
+ */
176
+ foundry: string | null;
177
+ /**
178
+ * The file in the owning repository's tree.
179
+ */
180
+ local: string | null;
181
+ /**
182
+ * The address the website serves.
183
+ */
184
+ web: string | null;
185
+ /**
186
+ * Where the book stages its copy.
187
+ */
188
+ pdf: string | null;
189
+ };
@@ -1,3 +1,49 @@
1
+ /**
2
+ * The file on disk an authored image pathname names, or `null`.
3
+ *
4
+ * Two of the four forms {@link module:engine/pathnames.resolvePathname}
5
+ * derives, used together: `local` is the file in this repository's own tree,
6
+ * and `pdf` is where the book stages a copy of it.
7
+ *
8
+ * **Typst decides where that copy goes.** It resolves a path against its root —
9
+ * the directory holding the source it is given — and refuses to read anything
10
+ * above it. So a file reaches the compiler by being copied under the output
11
+ * directory rather than by widening the root to the whole repository: the
12
+ * emitted `.typ` and everything it opens sit in one directory, which is what
13
+ * makes the source a consumer can compile by hand with no flags, and what keeps
14
+ * a build from touching a path outside its own output.
15
+ *
16
+ * Only a file **this** package ships can be staged. A pathname naming another
17
+ * package's file, or a URL, names something no build here can open — a build
18
+ * reaches no network — and the caller reports it as a picture the book will not
19
+ * carry.
20
+ *
21
+ * @param {string} src - The pathname, as authored.
22
+ * @param {object} config - The resolved configuration.
23
+ * @returns {{from: string, to: string}|null} The file, and where under the
24
+ * output directory it is staged.
25
+ */
26
+ export function stagedImagePath(src: string, config: object): {
27
+ from: string;
28
+ to: string;
29
+ } | null;
30
+ /**
31
+ * Copy every banner the document tree names into the output directory.
32
+ *
33
+ * **A missing banner is not a failure.** A section plate implies a banner per
34
+ * section and art arrives later than rendering does, so a section that names
35
+ * none draws its plate over the book's ink and says nothing about it. One that
36
+ * names a file the build cannot read is a different matter — that is a
37
+ * statement the tree makes and the build cannot honour — and it is reported.
38
+ *
39
+ * @param {object[]} entries - The plan's entries.
40
+ * @param {object} config - The resolved configuration.
41
+ * @param {string} outDir - Where the book is written.
42
+ * @param {object[]} findings - Collected here rather than thrown.
43
+ * @returns {Map<string, string>} Declared path → the staged file's path,
44
+ * relative to the `.typ`.
45
+ */
46
+ export function stageBanners(entries: object[], config: object, outDir: string, findings?: object[]): Map<string, string>;
1
47
  /**
2
48
  * The file name a downloaded book identifies itself by.
3
49
  *
@@ -47,10 +47,16 @@ export function createParser(registry?: object): object;
47
47
  * @param {object} [opts.registry] - The icon registry, when no parser is passed.
48
48
  * @param {Map<string, string>} [opts.links] - Address slug → plan anchor.
49
49
  * @param {Map<string, string>} [opts.glyphs] - Icon name → `{font, char}`.
50
+ * @param {Map<string, string>} [opts.images] - An image's address as authored →
51
+ * the staged file's path, relative to the `.typ`. An address this does not
52
+ * carry has no file the compiler can open, so the figure prints its caption
53
+ * alone — see {@link renderImage}.
50
54
  * @param {number} [opts.headingOffset] - Added to every heading level, so a
51
55
  * note's own `##` nests beneath the entry heading the book gave it.
52
56
  * @param {string} [opts.anchorPrefix] - The entry's anchor, which namespaces
53
57
  * every `{#slug}` the body declares.
58
+ * capital. Set for an entry, which begins a page; not for front matter or a
59
+ * prose file, which carry headings of their own.
54
60
  * @returns {string} Typst markup.
55
61
  */
56
62
  export function markdownToTypst(markdown: string, opts?: {
@@ -58,9 +64,50 @@ export function markdownToTypst(markdown: string, opts?: {
58
64
  registry?: object | undefined;
59
65
  links?: Map<string, string> | undefined;
60
66
  glyphs?: Map<string, string> | undefined;
67
+ images?: Map<string, string> | undefined;
61
68
  headingOffset?: number | undefined;
62
69
  anchorPrefix?: string | undefined;
63
70
  }): string;
71
+ /**
72
+ * The Typst definitions the book's page furniture is drawn with.
73
+ *
74
+ * Emitted once at the head of the document, for the reason the infobox panel's
75
+ * rules are: 2,000 entries each restating the plate, the running foot and the
76
+ * drop cap is a megabyte of repetition, and the one place a reader changes how
77
+ * the book looks should be one place.
78
+ *
79
+ * ## The geometry is stated, not discovered
80
+ *
81
+ * The page is US Letter with a 1.9cm margin, and the plate bleeds off all
82
+ * three edges it touches — so the plate has to know the paper's width and the
83
+ * margin it is escaping. Both are `#let` bindings here rather than numbers
84
+ * repeated down the file, and every other measure is arithmetic on them.
85
+ *
86
+ * ## Three things that cost time to discover, encoded here
87
+ *
88
+ * - **A title inherits the body's justification and hyphenation** unless told
89
+ * otherwise. Both are habits of body text that make a display line look
90
+ * broken, so the plate turns them off inside itself.
91
+ * - **The plate's height follows its title**, and the line count is derived
92
+ * from the title's *natural* width: measuring an already-wrapped block does
93
+ * not report the wrapped height, and a percentage width cannot resolve
94
+ * inside `measure`. So the title arrives as a string to be measured
95
+ * alongside the heading that is actually drawn.
96
+ * - **A float is not breakable.** A table taller than the page placed as one
97
+ * silently piles its rows on top of each other at the foot of the page —
98
+ * no warning, no error. So `book-wide` measures first and gives a table that
99
+ * will not fit its own single-column pages instead.
100
+ *
101
+ * ## The ornament is drawn, not typed
102
+ *
103
+ * The running foot's centre mark is a rotated square rather than a dingbat
104
+ * character, because the faces a consumer names and the faces a build runner
105
+ * carries are not the same set, and a missing glyph on 2,000 feet is a
106
+ * tofu box on every page of the book.
107
+ *
108
+ * @returns {string} Typst markup.
109
+ */
110
+ export function bookTypstPreamble(): string;
64
111
  /**
65
112
  * The whole book, as one Typst document.
66
113
  *
@@ -96,6 +143,46 @@ export function markdownToTypst(markdown: string, opts?: {
96
143
  * `#outline()` needs no depth limit under this model: what prints is decided
97
144
  * per heading, not by how deep the tree happens to go.
98
145
  *
146
+ * ## An entry owns its page, and the page is set in two columns
147
+ *
148
+ * A reference book is consulted rather than read through. An entry beginning
149
+ * halfway down a page is harder to find, cannot carry its own running head
150
+ * honestly, and makes a page number in the contents point at the middle of
151
+ * something else — so every entry opens a page of its own, under a full-bleed
152
+ * plate carrying a kicker and its name.
153
+ *
154
+ * The body is set in **two columns**, the measure a reference work wants and
155
+ * the one every other decision follows from: an image with no width class is a
156
+ * column wide, the infobox flows in the column measure and breaks between its
157
+ * sections, and a table wider than {@link WIDE_TABLE_COLUMNS} spans the page.
158
+ * The columns are the *page's* rather than a `columns()` block's, because only
159
+ * a page with columns can carry a float that spans them — which is what the
160
+ * plate, a wide table and a full-width figure all need.
161
+ *
162
+ * Two columns are print's answer and print's alone: a scrolling page has no
163
+ * fixed viewport, so the website keeps one measure.
164
+ *
165
+ * ## What a section declares, its entries inherit
166
+ *
167
+ * {@link module:engine/pdf-toc.PRESENTATION_KEYS} travels down the document
168
+ * tree, and two of those keys are read here:
169
+ *
170
+ * - **`page`** — `banner:`, the plate's picture; `kicker:`, the line above an
171
+ * entry's name; and `columns:`, the measure the section's pages are set in.
172
+ * - **`footer`** — the name the running foot carries, which is the section's
173
+ * own title when nothing says otherwise.
174
+ *
175
+ * `header` and `infobox` are reserved and read by nothing: the running head is
176
+ * a foot in this design, and which infobox a note draws is decided by the
177
+ * note's type.
178
+ *
179
+ * ## A missing banner is a plate without a picture
180
+ *
181
+ * A section plate implies a banner per section, and art arrives later than
182
+ * rendering does. A section that names no banner — or names one the build
183
+ * cannot read — still gets its plate, its kicker and its title, set over the
184
+ * book's ink.
185
+ *
99
186
  * ## Headings carry the structure, so nothing else has to
100
187
  *
101
188
  * Every section, every prose file and every entry is a real Typst heading at
@@ -113,9 +200,16 @@ export function markdownToTypst(markdown: string, opts?: {
113
200
  * @param {string[]} [opts.front] - Rendered Typst for each front-matter file.
114
201
  * @param {object} [opts.fonts] - `{ serif, sans, mono }` family names.
115
202
  * @param {string} [opts.version] - Stamped on the title page when given.
203
+ * @param {string} [opts.preamble] - Definitions the bodies call, emitted once
204
+ * above the title page. A panel every entry draws is a set of rules stated
205
+ * here rather than repeated 2,500 times.
206
+ * @param {Map<string, string>} [opts.banners] - A banner as the document tree
207
+ * declared it → the staged file's path, relative to the `.typ`. A declared
208
+ * banner this map does not carry has no file the compiler can open, so the
209
+ * plate draws without a picture.
116
210
  * @returns {string} A complete `.typ` document.
117
211
  */
118
- export function renderBook({ plan, bodies, title, subtitle, front, fonts, version, }?: {
212
+ export function renderBook({ plan, bodies, title, subtitle, front, fonts, version, preamble, banners, }?: {
119
213
  plan: object;
120
214
  bodies: Map<string, string>;
121
215
  title: string;
@@ -123,6 +217,8 @@ export function renderBook({ plan, bodies, title, subtitle, front, fonts, versio
123
217
  front?: string[] | undefined;
124
218
  fonts?: object | undefined;
125
219
  version?: string | undefined;
220
+ preamble?: string | undefined;
221
+ banners?: Map<string, string> | undefined;
126
222
  }): string;
127
223
  /**
128
224
  * Point every internal link at a label the document actually declares.
@@ -103,11 +103,16 @@ export function planDocument(nodes: object[], { selections }?: {
103
103
  /**
104
104
  * Presentation a node may declare, and that its descendants inherit.
105
105
  *
106
- * Reserved now although the first release renders none of them, because the
107
- * shape of the file is the thing consumers commit to: a book that has to be
108
- * restructured to gain a running head has the wrong format, not the wrong
109
- * renderer. Inheritance is what makes them worth declaring at all — `Gear` says
110
- * once which infobox its entries use, and nine sections beneath it agree.
106
+ * Inheritance is what makes them worth declaring at all `Gear` says once
107
+ * which banner its entries are plated over, and nine sections beneath it
108
+ * agree.
109
+ *
110
+ * **`page` and `footer` are read by the renderer**, and their shapes are
111
+ * checked here so that a key nothing can act on is an error rather than a
112
+ * silence. `header` and `infobox` are reserved and read by nothing: the
113
+ * running head is a foot in this design, and which infobox a note draws is
114
+ * decided by the note's type. They keep their place in the format because the
115
+ * shape of the file is the thing consumers commit to.
111
116
  *
112
117
  * @type {readonly string[]}
113
118
  */
@@ -237,11 +237,16 @@ export function sectionFrontmatter(meta: object): object;
237
237
  * for.
238
238
  * @param {(data: object, page: object) => void} [options.decorate] - Called
239
239
  * with each page's frontmatter, for whatever a consumer's own pass adds.
240
+ * @param {(src: string) => string} [options.webSrc] - Translates an authored
241
+ * pathname into the address the website serves. Every artwork field goes
242
+ * through it, so a page's `img:` and its body images name the same file the
243
+ * same way.
240
244
  * @returns {object} The frontmatter to write.
241
245
  */
242
- export function pageFrontmatter(page: object, { readmeSections, decorate }: {
246
+ export function pageFrontmatter(page: object, { readmeSections, decorate, webSrc }: {
243
247
  readmeSections?: Record<string, object> | undefined;
244
248
  decorate?: ((data: object, page: object) => void) | undefined;
249
+ webSrc?: ((src: string) => string) | undefined;
245
250
  }): object;
246
251
  /**
247
252
  * Where a page is written, relative to the output root.
@@ -281,13 +286,15 @@ export function pageDestination(page: any): string;
281
286
  *
282
287
  * @param {object[]} pages - Every page.
283
288
  * @param {object} options - Everything the render needs.
284
- * @returns {{written: number, byKind: Record<string, number>, tableErrors: object[], wikiErrors: object[]}}
289
+ * @returns {{written: number, byKind: Record<string, number>, tableErrors: object[],
290
+ * wikiErrors: object[], imageErrors: object[]}}
285
291
  */
286
292
  export function renderPages(pages: object[], options: object): {
287
293
  written: number;
288
294
  byKind: Record<string, number>;
289
295
  tableErrors: object[];
290
296
  wikiErrors: object[];
297
+ imageErrors: object[];
291
298
  };
292
299
  /**
293
300
  * Writes the Hugo sections a published tree declares.
@@ -406,7 +413,7 @@ export function resolveOutputRoot(rootDir: string, out: string): string;
406
413
  * `sql` directive with none prepared is a table error: nothing here runs a
407
414
  * query.
408
415
  * @returns {{gates: object, stats: object|null, tableErrors: object[],
409
- * wikiErrors: object[], manifests: object|null}}
416
+ * wikiErrors: object[], imageErrors: object[], manifests: object|null}}
410
417
  */
411
418
  export function buildSite({ config, outRoot, sqlTables }?: {
412
419
  config?: object | undefined;
@@ -417,6 +424,7 @@ export function buildSite({ config, outRoot, sqlTables }?: {
417
424
  stats: object | null;
418
425
  tableErrors: object[];
419
426
  wikiErrors: object[];
427
+ imageErrors: object[];
420
428
  manifests: object | null;
421
429
  };
422
430
  export { formatUnaddressableFinding };
@@ -49,6 +49,37 @@ export function wikiContext(built: SiteIndex, { src, file, type, errors, foreign
49
49
  file?: string | undefined;
50
50
  foreignIndex?: Map<string, object> | undefined;
51
51
  }): object;
52
+ /**
53
+ * Resolve one infobox reference against a site index.
54
+ *
55
+ * A note writes a reference three ways and all three reach here: a bare
56
+ * **shortcode** (`slntlncmpny`), a short **address** (`affiliation-slntlncmpny`)
57
+ * and a **canonical** one (`sohl-sohl-skill-melee`). The first is the ordinary
58
+ * case and the ambiguous one — a shortcode is unique within a type and not
59
+ * across a tree — so a caller that knows what it expects passes `hint.type`
60
+ * and the lookup is narrowed to it.
61
+ *
62
+ * **Without a hint the types are tried in sorted order**, so two trees holding
63
+ * the same note resolve it the same way. A shortcode two types both claim
64
+ * answers with the first alphabetically, which is a stable wrong answer rather
65
+ * than an unstable one; a caller that cares supplies the hint.
66
+ *
67
+ * The `address` on the answer is the slug form — `type-shortcode` — because
68
+ * that is what the book's own link map is keyed by and what a wikilink is
69
+ * written as.
70
+ *
71
+ * @param {SiteIndex} siteIndex - The index.
72
+ * @param {unknown} ref - The reference, as authored.
73
+ * @param {object} [hint] - `{type}`, where the caller knows it.
74
+ * @returns {{name?: string, url?: string, address?: string, subType?: string}|undefined}
75
+ * The page, or `undefined` where nothing answers.
76
+ */
77
+ export function resolveInfoboxRef(siteIndex: SiteIndex, ref: unknown, hint?: object): {
78
+ name?: string;
79
+ url?: string;
80
+ address?: string;
81
+ subType?: string;
82
+ } | undefined;
52
83
  /**
53
84
  * One page the site will publish, as the index needs to see it.
54
85
  */
@@ -123,13 +154,14 @@ export type SiteIndex = {
123
154
  */
124
155
  sections: Set<string>;
125
156
  /**
126
- * `type:shortcode`
127
- * page, for callers resolving embedded
128
- * references (a being's items, say).
157
+ * `type:shortcode` → page, for callers
158
+ * resolving embedded references (a
159
+ * being's items, say).
129
160
  */
130
161
  refIndex: Map<string, {
131
162
  name: string;
132
163
  url: string;
164
+ subType?: string;
133
165
  }>;
134
166
  /**
135
167
  * Addresses claimed by
@@ -199,6 +199,28 @@ export function convertWikilinks(markdown: string, { type, id, pack, docPack, in
199
199
  anchor?: string;
200
200
  }>;
201
201
  };
202
+ /**
203
+ * Resolve one reference against a compile's address index.
204
+ *
205
+ * The compile-time counterpart of
206
+ * {@link module:engine/site-index.resolveInfoboxRef}: the same three authored
207
+ * forms — a bare shortcode, a short address, a canonical one — answered with
208
+ * what a **compendium** can use. A local target answers with the UUID its own
209
+ * document was addressed by; a foreign one with the UUID its package
210
+ * published.
211
+ *
212
+ * @param {object} index - From {@link buildWikilinkIndex}.
213
+ * @param {unknown} ref - The reference, as authored.
214
+ * @param {object} [hint] - `{type}`, where the caller knows what it expects.
215
+ * @returns {{name?: string, uuid?: string, address?: string, subType?: string}|undefined}
216
+ * The target, or `undefined` where nothing answers.
217
+ */
218
+ export function resolveReference(index: object, ref: unknown, hint?: object): {
219
+ name?: string;
220
+ uuid?: string;
221
+ address?: string;
222
+ subType?: string;
223
+ } | undefined;
202
224
  import { ITEM_PACK } from "./ids.mjs";
203
225
  import { PACK_BY_TYPE } from "./ids.mjs";
204
226
  import { packForType } from "./ids.mjs";
@@ -18,12 +18,11 @@ export function hm3DefaultItemArt(type: string): string;
18
18
  * no `img:` of its own, rather than shipping a mismatched icon. So every type
19
19
  * `item-builders.mjs` declares has a row here.
20
20
  *
21
- * **Paths are fully resolved.** `resolveImg` rewrites a leading `icons/` or
22
- * `images/` to the *consuming package's* asset root, which is not where these
23
- * live: they are shipped by the HM3 system. Written as `systems/hm3/images/…`
24
- * they pass through untouched and address the icons HM3's own compendiums
25
- * already use, so an item compiled from a note looks like its hand-authored
26
- * neighbours.
21
+ * **Paths name the package that owns the file.** These icons are not the
22
+ * consuming package's they are shipped by the HM3 system — so they are
23
+ * written `hm3/assets/…` and each surface derives its own address from that.
24
+ * That addresses the icons HM3's own compendiums already use, so an item
25
+ * compiled from a note looks like its hand-authored neighbours.
27
26
  *
28
27
  * **A one-to-many type gets one default**, because art is keyed by note type
29
28
  * and a note type is what a registry entry addresses. `weapongear` compiles
@@ -4,4 +4,5 @@ export * as documentSubtypes from "./document-subtypes.mjs";
4
4
  export * as items from "./items.mjs";
5
5
  export * as actors from "./actors.mjs";
6
6
  export * as templatePriority from "./template-priority.mjs";
7
+ export * as infobox from "./infobox.mjs";
7
8
  export { HM3_DEFAULT_ITEM_ART, hm3DefaultItemArt } from "./default-item-art.mjs";
@@ -0,0 +1,22 @@
1
+ /** What this system's box is called. @type {string} */
2
+ export const HM3_INFOBOX_TITLE: string;
3
+ /**
4
+ * HM3's presentation overlay: what one of this system's fields is called where
5
+ * humanising its key gives the wrong word.
6
+ *
7
+ * **Not a second field list.** A field it does not mention still gets a row
8
+ * under its own humanised name, so a field added to {@link HM3_ITEM_FIELDS}
9
+ * reaches the box with no edit here.
10
+ *
11
+ * @type {Readonly<Record<string, {label?: string, withheld?: string}>>}
12
+ */
13
+ export const HM3_FIELD_PRESENTATION: Readonly<Record<string, {
14
+ label?: string;
15
+ withheld?: string;
16
+ }>>;
17
+ /**
18
+ * HM3's infobox declaration.
19
+ *
20
+ * @type {object}
21
+ */
22
+ export const HM3_INFOBOX: object;
@@ -3,6 +3,7 @@ export * as documentSubtypes from "./document-subtypes.mjs";
3
3
  export * as items from "./items.mjs";
4
4
  export * as actors from "./actors.mjs";
5
5
  export * as kbPasses from "./kb-passes.mjs";
6
+ export * as infobox from "./infobox.mjs";
6
7
  export { AFFILIATION_STANDINGS } from "./affiliation-standings.mjs";
7
8
  export { DEFAULT_ITEM_ART, defaultItemArt } from "./default-item-art.mjs";
8
9
  export { BEING_TYPE, GEAR_TYPE_TO_KEY, deriveBeingInfo, isBeing } from "./being-info.mjs";