@heroiclands/package-build 22.22.0 → 22.23.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 (91) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/content-commands.mjs +51 -14
  3. package/ci/content-format-reference.mjs +1 -1
  4. package/content-config.mjs +80 -2
  5. package/docs/api.md +85 -72
  6. package/docs/authoring/assets.md +14 -0
  7. package/docs/authoring/dates-and-calendars.md +37 -1
  8. package/docs/authoring/links-and-markup.md +112 -31
  9. package/docs/commands.md +12 -10
  10. package/docs/configuration.md +90 -13
  11. package/docs/content-format.md +1 -1
  12. package/docs/guides/book.md +5 -4
  13. package/docs/reference/format-details.md +323 -60
  14. package/docs/reference/note-types.md +9 -9
  15. package/engine/anchors.mjs +16 -10
  16. package/engine/art-fields.mjs +118 -0
  17. package/engine/art-slots.mjs +7 -1
  18. package/engine/asset-index.mjs +160 -10
  19. package/engine/asset-replacement.mjs +93 -0
  20. package/engine/asset-types.mjs +240 -0
  21. package/engine/base-compiler.mjs +54 -2
  22. package/engine/bundles.mjs +13 -1
  23. package/engine/content-blocks.mjs +4 -4
  24. package/engine/content-embeds.mjs +19 -0
  25. package/engine/content-figures.mjs +468 -0
  26. package/engine/content-footnotes.mjs +10 -2
  27. package/engine/content-format.yaml +1 -1
  28. package/engine/content-images.mjs +126 -25
  29. package/engine/content-index.mjs +36 -10
  30. package/engine/field-reference.mjs +31 -10
  31. package/engine/field-spec.mjs +10 -8
  32. package/engine/foreign-catalog.mjs +143 -18
  33. package/engine/frontmatter-lint.mjs +73 -1
  34. package/engine/heading-attributes.mjs +31 -18
  35. package/engine/helpers.mjs +97 -30
  36. package/engine/infobox-registry.mjs +4 -1
  37. package/engine/infobox.mjs +47 -8
  38. package/engine/itinerary-scenes.mjs +4 -10
  39. package/engine/journals.mjs +264 -39
  40. package/engine/lore-events.mjs +197 -0
  41. package/engine/markdown-expressions.mjs +257 -7
  42. package/engine/note-dates.mjs +305 -12
  43. package/engine/note-vocabulary.mjs +11 -5
  44. package/engine/pathnames.mjs +36 -0
  45. package/engine/pdf-build.mjs +357 -34
  46. package/engine/pdf-images.mjs +64 -0
  47. package/engine/pdf-render.mjs +160 -36
  48. package/engine/scenes.mjs +16 -4
  49. package/engine/site-build.mjs +127 -13
  50. package/engine/site-index.mjs +72 -1
  51. package/engine/site-preview.mjs +2 -0
  52. package/engine/system-block.mjs +44 -16
  53. package/hm3/actors.mjs +2 -2
  54. package/manifest.mjs +30 -24
  55. package/package.json +1 -1
  56. package/packagebuild-metadata.jsonl +13 -13
  57. package/sohl/item-fields.mjs +4 -3
  58. package/types/content-config.d.mts +48 -1
  59. package/types/engine/anchors.d.mts +9 -4
  60. package/types/engine/art-fields.d.mts +64 -0
  61. package/types/engine/asset-index.d.mts +43 -0
  62. package/types/engine/asset-replacement.d.mts +22 -0
  63. package/types/engine/asset-types.d.mts +18 -0
  64. package/types/engine/base-compiler.d.mts +0 -17
  65. package/types/engine/content-embeds.d.mts +16 -0
  66. package/types/engine/content-figures.d.mts +101 -0
  67. package/types/engine/content-footnotes.d.mts +7 -0
  68. package/types/engine/content-images.d.mts +78 -12
  69. package/types/engine/content-index.d.mts +8 -2
  70. package/types/engine/field-reference.d.mts +21 -0
  71. package/types/engine/field-spec.d.mts +20 -16
  72. package/types/engine/foreign-catalog.d.mts +39 -0
  73. package/types/engine/heading-attributes.d.mts +13 -8
  74. package/types/engine/helpers.d.mts +23 -7
  75. package/types/engine/infobox-registry.d.mts +4 -1
  76. package/types/engine/infobox.d.mts +6 -0
  77. package/types/engine/journals.d.mts +31 -9
  78. package/types/engine/lore-events.d.mts +14 -0
  79. package/types/engine/markdown-expressions.d.mts +23 -2
  80. package/types/engine/note-dates.d.mts +64 -1
  81. package/types/engine/pathnames.d.mts +26 -0
  82. package/types/engine/pdf-build.d.mts +28 -6
  83. package/types/engine/pdf-images.d.mts +52 -0
  84. package/types/engine/pdf-render.d.mts +12 -0
  85. package/types/engine/site-build.d.mts +14 -3
  86. package/types/engine/site-index.d.mts +54 -0
  87. package/types/engine/system-block.d.mts +26 -2
  88. package/types/hm3/actors.d.mts +18 -0
  89. package/types/manifest.d.mts +19 -23
  90. package/engine/content-captions.mjs +0 -230
  91. package/types/engine/content-captions.d.mts +0 -58
@@ -51,14 +51,17 @@ export function compilesSystemDocument(fm: object, map: object, router: object):
51
51
  * @param {object} [options.router] - The pack router deciding which system
52
52
  * compiles a document for this note. Defaults to the consuming repository's.
53
53
  * @param {object} [options.dates] - Calendar conversion context.
54
+ * @param {string} [options.contentPackage] - This build's content package,
55
+ * the default a note box's short-form Address resolves against.
54
56
  * @returns {object[]} The boxes, in the order every medium renders them.
55
57
  * @throws {Error} When the built set disagrees with what the note's type maps
56
58
  * to — see {@link module:engine/infobox.assertInfoboxSet}.
57
59
  */
58
- export function noteInfoboxes(fm: object, { resolve, router, dates }?: {
60
+ export function noteInfoboxes(fm: object, { resolve, router, dates, contentPackage }?: {
59
61
  resolve?: ((ref: unknown, hint?: object) => object | undefined) | undefined;
60
62
  router?: object | undefined;
61
63
  dates?: object | undefined;
64
+ contentPackage?: string | undefined;
62
65
  }): object[];
63
66
  /**
64
67
  * Every system's infobox declaration, in the order a page shows them.
@@ -154,12 +154,15 @@ export function linkValue(ref: unknown, resolve: (ref: unknown, hint?: object) =
154
154
  * @param {object} [options] - Options.
155
155
  * @param {(ref: unknown) => object|undefined} [options.resolve] - Resolves a
156
156
  * reference to `{name, url?, uuid?, address?}`.
157
+ * @param {string} [options.contentPackage] - This build's content package,
158
+ * the default a relation row's short-form Address resolves against.
157
159
  * @param {object} [options.vocabulary] - The note vocabulary to read.
158
160
  * @param {object} [options.presentation] - The overlay to read.
159
161
  * @returns {object} The box.
160
162
  */
161
163
  export function noteInfobox(fm: object, options?: {
162
164
  resolve?: ((ref: unknown) => object | undefined) | undefined;
165
+ contentPackage?: string | undefined;
163
166
  vocabulary?: object | undefined;
164
167
  presentation?: object | undefined;
165
168
  }): object;
@@ -268,6 +271,8 @@ export function systemRowsSection(fm: object, fields: readonly object[], { block
268
271
  * Resolves one declared field against the note.
269
272
  * @param {(ref: unknown) => object|undefined} [options.resolve] - Resolves a
270
273
  * reference to `{name, url?, uuid?, address?}`.
274
+ * @param {string} [options.contentPackage] - This build's content package,
275
+ * the default a note box's short-form Address resolves against.
271
276
  * @param {object} [options.vocabulary] - The note vocabulary to read.
272
277
  * @returns {object[]} The boxes.
273
278
  */
@@ -277,6 +282,7 @@ export function buildInfoboxes(fm: object, options: {
277
282
  compilesDocument: (fm: object, map: object) => boolean;
278
283
  resolveField: (field: object, fm: object, opts: object) => object;
279
284
  resolve?: ((ref: unknown) => object | undefined) | undefined;
285
+ contentPackage?: string | undefined;
280
286
  vocabulary?: object | undefined;
281
287
  }): object[];
282
288
  /**
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Splits a markdown body into pages by top-level H1 headings. Fenced
3
- * code blocks are respected so `# foo` inside ``` blocks doesn't trigger
4
- * a split. Content before the first H1 (if non-empty) becomes a leading
5
- * page. Each H1 yields a page whose name is the heading
3
+ * code blocks are respected so `# foo` inside ``` or ~~~ blocks doesn't
4
+ * trigger a split. Content before the first H1 (if non-empty) becomes a
5
+ * leading page. Each H1 yields a page whose name is the heading
6
6
  * text (with any `{#anchor-id}` suffix stripped out and surfaced as
7
7
  * `anchorSlug`). The classes the heading declares come along as `classes`, so
8
8
  * a surface that honours one has it without reading the suffix again.
@@ -13,16 +13,26 @@
13
13
  * calling that page "Introduction" would label the description as a preamble to
14
14
  * nothing.
15
15
  *
16
+ * A figure fence carries its own record along onto the page it starts, as
17
+ * `figure` — `null` for a page a heading started. {@link buildPages} reads it
18
+ * to build the `image` or `text` page a fence begins; nothing else sets it.
19
+ *
16
20
  * @param {string} body - Markdown body to split.
17
21
  * @param {string} [leadName] - Name of the page before the first heading.
22
+ * @param {(address: string) => string|undefined} [resolveRole] - From a
23
+ * picture's address to the role its asset declares, so a figure page's
24
+ * name reads `Map 1` rather than `Figure 1` where it is due — see
25
+ * {@link module:engine/content-figures.scanFigures}.
18
26
  * @returns {Array<{name: string, anchorSlug: string|null, level: number,
19
- * classes: string[], markdown: string}>} Pages in document order.
27
+ * classes: string[], figure: object|null, markdown: string}>} Pages in
28
+ * document order.
20
29
  */
21
- export function splitPages(body: string, leadName?: string): Array<{
30
+ export function splitPages(body: string, leadName?: string, resolveRole?: (address: string) => string | undefined): Array<{
22
31
  name: string;
23
32
  anchorSlug: string | null;
24
33
  level: number;
25
34
  classes: string[];
35
+ figure: object | null;
26
36
  markdown: string;
27
37
  }>;
28
38
  /**
@@ -101,15 +111,22 @@ export function journalPageId(entryId: string, page: {
101
111
  * @param {string} noteName - The note, for error messages.
102
112
  * A page whose heading carries `.secret` is given to the GM alone, by the one
103
113
  * ownership value Foundry reads as "nobody but a GM". Every other page states
104
- * no ownership and inherits the journal's.
105
- *
114
+ * no ownership and inherits the journal's. A figure-started page states none
115
+ * either: {@link scanFigures} validates a figure's classes against its own
116
+ * closed vocabulary, which carries no `.secret`.
117
+ *
118
+ * @param {Array<object>} figures - From {@link scanFigures}, numbered across
119
+ * the whole note. Read by a page a heading started; a page a figure started
120
+ * carries its own figure already, from {@link splitPages}.
121
+ * @param {(address: string) => string|undefined} [resolveRole] - See
122
+ * {@link splitPages}.
106
123
  * @returns {Array<{_id: string, name: string, type: string,
107
124
  * title: {show: boolean, level: number},
108
125
  * text: {format: number, content: string}, _key: string,
109
126
  * ownership?: {default: number}}>} The page documents, in order.
110
127
  * @throws {Error} When the note has no content at all, or repeats an anchor.
111
128
  */
112
- export function buildPages(rawPages: Array<object>, entryId: string, noteName: string, captions: any): Array<{
129
+ export function buildPages(rawPages: Array<object>, entryId: string, noteName: string, figures: Array<object>, resolveRole?: (address: string) => string | undefined): Array<{
113
130
  _id: string;
114
131
  name: string;
115
132
  type: string;
@@ -156,9 +173,13 @@ export function buildPages(rawPages: Array<object>, entryId: string, noteName: s
156
173
  * module:engine/draft-notice}. It leads the first authored page,
157
174
  * because a reader deciding whether to rely on the entry has to be told
158
175
  * before they read it rather than after.
176
+ * @param {(address: string) => string|undefined} [params.resolveRole] - From
177
+ * a picture's address to the role its asset declares, so a figure page's
178
+ * name reads `Map 1` rather than `Figure 1` where it is due — see
179
+ * {@link module:engine/content-figures.scanFigures}.
159
180
  * @returns {object} The JournalEntry document, keyed for the pack.
160
181
  */
161
- export function buildJournalEntry({ id, name, markdown, leadName, folder, stats, infoboxes, notice, }: {
182
+ export function buildJournalEntry({ id, name, markdown, leadName, folder, stats, infoboxes, notice, resolveRole, }: {
162
183
  id: string;
163
184
  name: string;
164
185
  markdown: string;
@@ -171,6 +192,7 @@ export function buildJournalEntry({ id, name, markdown, leadName, folder, stats,
171
192
  html: string;
172
193
  }[] | undefined;
173
194
  notice?: string | undefined;
195
+ resolveRole?: ((address: string) => string | undefined) | undefined;
174
196
  }): object;
175
197
  /**
176
198
  * Journals pack compiler.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Validate one `lore` note's `data.events`.
3
+ *
4
+ * Registered as the `events` field's own `check` in `note-vocabulary.mjs`, so
5
+ * it runs on every `lore` note regardless of `subType` — an occasion is not a
6
+ * calendar fact, and nothing here is scoped to `subType: calendar`.
7
+ *
8
+ * @param {object} note - The note, as the link index hands it over.
9
+ * @param {{index?: object}} [options]
10
+ * @returns {object[]} Findings, each positioned at the entry that earned it.
11
+ */
12
+ export function checkLoreEvents(note: object, { index }?: {
13
+ index?: object;
14
+ }): object[];
@@ -3,12 +3,33 @@ export function sqlQueriesInMarkdown(body: any, fm?: {}): any[];
3
3
  /**
4
4
  * Expand scalar frontmatter references and registered functions in Markdown prose.
5
5
  * @param {string} body
6
- * @param {{fm?: object, dates?: object, sqlResults?: Map<string, object>, file?: string, bodyLine?: number}} [options]
6
+ * @param {{fm?: object, dates?: object, sqlResults?: Map<string, object>,
7
+ * figures?: {get: (id: string) => {label: string, caption: string,
8
+ * hasCaption: boolean}|undefined, note?: (address: string) =>
9
+ * {url: string|null, figures: Map<string, object>}|undefined,
10
+ * link?: Function}, file?: string, bodyLine?: number}} [options] -
11
+ * `figures` is the `ref` helper's own view of the corpus: `get` reads this
12
+ * note's `:::figure` fences by id, and `note` resolves another note's the
13
+ * way a wikilink would — omitted, a cross-note address is refused rather
14
+ * than looked up. `link` overrides how a resolved reference renders; the
15
+ * default is a Markdown link to the target's own anchor.
7
16
  */
8
- export function renderMarkdownExpressions(body: string, { fm, dates, sqlResults, file, bodyLine }?: {
17
+ export function renderMarkdownExpressions(body: string, { fm, dates, sqlResults, figures, file, bodyLine }?: {
9
18
  fm?: object;
10
19
  dates?: object;
11
20
  sqlResults?: Map<string, object>;
21
+ figures?: {
22
+ get: (id: string) => {
23
+ label: string;
24
+ caption: string;
25
+ hasCaption: boolean;
26
+ } | undefined;
27
+ note?: (address: string) => {
28
+ url: string | null;
29
+ figures: Map<string, object>;
30
+ } | undefined;
31
+ link?: Function;
32
+ };
12
33
  file?: string;
13
34
  bodyLine?: number;
14
35
  }): {
@@ -91,8 +91,71 @@ export function formatNoteDate(date: any, era: any, daysPerYear: any, formatName
91
91
  text: string;
92
92
  prose: string | null;
93
93
  } | null;
94
+ /**
95
+ * The occurrences of one recurring series overlapping a bound.
96
+ *
97
+ * **No materialized occurrence list.** An annual series anchored centuries
98
+ * back has thousands of occurrences to the present, and the only questions a
99
+ * build asks are "is this year one of them" and "which is next" — so an
100
+ * omitted bound answers with a single occurrence rather than every one a live
101
+ * series could have:
102
+ *
103
+ * - `to` omitted: the first occurrence at or after `from` (default, the
104
+ * anchor's own year) — "next".
105
+ * - `from` omitted, `to` given: the last occurrence at or before `to` —
106
+ * "the greatest occurrence at or before `until`".
107
+ * - Both given: every occurrence between them, inclusive — a membership
108
+ * probe is `from === to`.
109
+ *
110
+ * **The lower bound is not optional.** A candidate year before the anchor's
111
+ * own year is never an occurrence, however a bare modulo would read it: `%`
112
+ * takes the sign of its dividend in both JavaScript and SQL, so a year
113
+ * exactly one period before the anchor would otherwise match. `k` is clamped
114
+ * at zero rather than left to run negative.
115
+ *
116
+ * **The period advances the canonical year, never the era-relative year.**
117
+ * `date.canonicalYear` is already on the continuous axis `canonicalYear`
118
+ * builds — era year `-1` sits immediately before era year `1`, but canonical
119
+ * year `0` is an ordinary integer between them — so stepping by whole
120
+ * multiples of `every` on `canonicalYear` crosses an epoch with no special
121
+ * case. Counting in era-relative years instead would need a correction at
122
+ * every crossing and silently loses one year each time it is forgotten.
123
+ *
124
+ * **Precision, approximation and the day of year carry through untouched.**
125
+ * Every occurrence is the anchor's own record with only `canonicalYear`
126
+ * advanced, so a day-precision anchor's `canonicalDay` — including one inside
127
+ * a short intercalary month — names the same day of every occurrence, and an
128
+ * approximate anchor's occurrences are approximate too.
129
+ *
130
+ * @param {object} date - The anchor's resolved record
131
+ * ({@link parseNoteDate}), carrying a real `canonicalYear`. A year-0 "any
132
+ * year" anchor carries none and has no series through this function — its
133
+ * `recurs` is refused, and its own next occurrence is computed directly
134
+ * from the day of year alone.
135
+ * @param {{every?: number, on?: object[]}} [recurs] - The declared
136
+ * recurrence: a whole-year period, or an enumeration of further resolved
137
+ * date records (each already carrying its own `canonicalYear`). Neither
138
+ * admits the other.
139
+ * @param {object} [bounds]
140
+ * @param {number} [bounds.from] - The lower bound, inclusive, as a canonical
141
+ * year.
142
+ * @param {number} [bounds.to] - The upper bound, inclusive, as a canonical
143
+ * year.
144
+ * @returns {object[]} Occurrence records, in increasing order.
145
+ */
146
+ export function occurrencesOf(date: object, recurs?: {
147
+ every?: number;
148
+ on?: object[];
149
+ }, { from, to }?: {
150
+ from?: number | undefined;
151
+ to?: number | undefined;
152
+ }): object[];
94
153
  /** Normalize a note's declared dates for indexes and generated pages. */
95
- export function resolvedDateFields(fm: any, context: any): {};
154
+ export function resolvedDateFields(fm: any, context: any): {
155
+ events: any;
156
+ } | {
157
+ events?: undefined;
158
+ };
96
159
  /**
97
160
  * The literal that says a thing happened and the date is not recorded.
98
161
  *
@@ -82,6 +82,32 @@ export function packageAddresses(config: object): Map<string, {
82
82
  * resolves on one surface and nowhere else.
83
83
  */
84
84
  export function resolvePathname(raw: string | null | undefined, config: object): PathnameForms | null;
85
+ /**
86
+ * The pathname one authored image address keys an asset's role and pixel
87
+ * size under — `<package>/assets/<suffix>`, the same key
88
+ * {@link module:engine/art-fields.assetImageInfoByPathname} builds from the
89
+ * asset record itself.
90
+ *
91
+ * **Why this exists apart from `resolvePathname`'s own forms.** An embed
92
+ * rewrites to that exact pathname — see
93
+ * {@link module:engine/content-embeds.resolveEmbeds} — but an author's own
94
+ * body image is free to write the bare, own-package form instead
95
+ * (`images/m.webp`), which names the identical file without naming the
96
+ * package. The two forms have to key the same lookup, and `resolvePathname`
97
+ * already answers which package a bare form belongs to; this just restates
98
+ * that answer in the one shape a renderer looks a picture's role up by.
99
+ *
100
+ * **Never throws.** A lookup is a best-effort read, not a validation — the
101
+ * pathname lint is what refuses a Foundry-spelled address — so a value
102
+ * {@link resolvePathname} refuses outright answers `null` here as well, the
103
+ * same as a blank or an external one.
104
+ *
105
+ * @param {string} src - The address, as authored or as an embed resolved it.
106
+ * @param {object} config - The resolved build configuration.
107
+ * @returns {string|null} The pathname, or `null` for an address naming no
108
+ * asset record to look up.
109
+ */
110
+ export function assetPathnameKey(src: string, config: object): string | null;
85
111
  /**
86
112
  * The directory a package ships its files in, and the segment that marks a
87
113
  * pathname's first segment as a package name.
@@ -13,17 +13,34 @@
13
13
  * makes the source a consumer can compile by hand with no flags, and what keeps
14
14
  * a build from touching a path outside its own output.
15
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.
16
+ * A file **this** package ships stages directly. A pathname naming another
17
+ * package's file stages instead from a declared replacement's cached archive,
18
+ * when `opts.resolveReplacement` — the pure function
19
+ * {@link module:engine/asset-replacement.resolveAssetReplacement} —
20
+ * answers it; `opts.foreignIndex` is the merged index that answer is read
21
+ * against. A pathname naming a package no replacement carries it for, or a
22
+ * URL, names something no build here can open — a build reaches no network —
23
+ * and the caller reports it as a picture the book will not carry.
20
24
  *
21
25
  * @param {string} src - The pathname, as authored.
22
26
  * @param {object} config - The resolved configuration.
27
+ * @param {object} [opts]
28
+ * @param {Map<string, object>} [opts.foreignIndex] - Every fetched
29
+ * dependency's own records, keyed by canonical address — the index a
30
+ * replacement's record is read out of.
31
+ * @param {Function} [opts.resolveReplacement] - {@link
32
+ * module:engine/asset-replacement.resolveAssetReplacement}, or an
33
+ * equivalent a caller injects for a test. Omitted, a foreign pathname this
34
+ * package does not own stays declined exactly as it always has.
23
35
  * @returns {{from: string, to: string}|null} The file, and where under the
24
36
  * output directory it is staged.
37
+ * @throws {Error} When a declared replacement answers the address but its
38
+ * archive was never fetched.
25
39
  */
26
- export function stagedImagePath(src: string, config: object): {
40
+ export function stagedImagePath(src: string, config: object, opts?: {
41
+ foreignIndex?: Map<string, object> | undefined;
42
+ resolveReplacement?: Function | undefined;
43
+ }): {
27
44
  from: string;
28
45
  to: string;
29
46
  } | null;
@@ -66,13 +83,18 @@ export function pdfFileName(artifact: string, version: string): string;
66
83
  * @param {string} [opts.version] - Stamped on the title page and the file name.
67
84
  * @param {boolean} [opts.compile] - Whether to run Typst. False leaves the
68
85
  * `.typ` source, which is what the unit tests read.
86
+ * @param {Function} [opts.resolveReplacement] - {@link
87
+ * module:engine/asset-replacement.resolveAssetReplacement}, for tests.
88
+ * Loaded from that module otherwise, and only when a relationship declares
89
+ * `assetReplacement: true` — a build with none never imports it.
69
90
  * @returns {Promise<object>} `{ built, reason, findings, typ, pdf, stats }`.
70
91
  */
71
- export function buildPdf({ config, out, version, compile }?: {
92
+ export function buildPdf({ config, out, version, compile, resolveReplacement, }?: {
72
93
  config?: object | undefined;
73
94
  out?: string | undefined;
74
95
  version?: string | undefined;
75
96
  compile?: boolean | undefined;
97
+ resolveReplacement?: Function | undefined;
76
98
  }): Promise<object>;
77
99
  /**
78
100
  * The command line the compile runs, as data.
@@ -1,3 +1,33 @@
1
+ /**
2
+ * The width the book draws one picture at, in inches, when nothing authored
3
+ * narrows it to a named `size=`.
4
+ *
5
+ * > drawn width = min(the role's slot, natural pixels ÷ the print floor)
6
+ *
7
+ * **Every size here is a maximum, never a target**: a picture is never drawn
8
+ * larger than it was made. The print floor is {@link PDF_PAGE.dpi}, the same
9
+ * constant the page geometry above is set against, so a role's slot and the
10
+ * page it is drawn on cannot disagree about what a pixel is worth.
11
+ *
12
+ * An icon-type address takes the nominal icon width whatever role it might
13
+ * otherwise carry — `role` never reaches one, since the asset record refuses
14
+ * it there. A vector carries no pixel count, so nothing narrows its role's
15
+ * slot. A picture declaring no role answers `undefined`: the medium's
16
+ * ordinary size, decided elsewhere by a named `size=` or by its own natural
17
+ * pixels within whatever it is set in.
18
+ *
19
+ * @param {object} [asset] - What {@link module:engine/art-fields.assetImageInfoByPathname}
20
+ * records for the picture's address.
21
+ * @param {string} [asset.type] - The asset's type — `icon` or `image`.
22
+ * @param {string} [asset.role] - The picture's declared role.
23
+ * @param {number|""} [asset.width] - The file's own pixel width; blank for a vector.
24
+ * @returns {number|undefined} The drawn width, in inches.
25
+ */
26
+ export function bookImageWidthIn({ type, role, width }?: {
27
+ type?: string | undefined;
28
+ role?: string | undefined;
29
+ width?: number | "" | undefined;
30
+ }): number | undefined;
1
31
  /**
2
32
  * Optimize staged raster images without changing their authored files.
3
33
  *
@@ -36,3 +66,25 @@ export const pdfTextWidth: number;
36
66
  /** Usable height of a portrait page in inches. */
37
67
  export const pdfTextHeight: number;
38
68
  export function pdfColumnWidth(columns?: number): number;
69
+ /**
70
+ * The fraction of the page's one-column measure a picture's role may fill at
71
+ * most — one table, read by {@link bookImageWidthIn}, so a policy change here
72
+ * is the one line it costs rather than an edit to every note drawing a role.
73
+ *
74
+ * A `portrait` or an `emblem` names a subject or a mark, which sits beside
75
+ * prose rather than replacing a page of it; a `banner`, a `plate` and a `map`
76
+ * are each meant to be looked at in full. The measure is the page's own text
77
+ * width, {@link pdfTextWidth}, so a role's slot does not shrink when a page
78
+ * happens to be set in two columns — it is what the picture is *for* that
79
+ * sets the slot, not the layout it lands in.
80
+ *
81
+ * @type {Readonly<Record<string, number>>}
82
+ */
83
+ export const BOOK_ROLE_SLOTS: Readonly<Record<string, number>>;
84
+ /**
85
+ * The book's nominal width for an icon-type picture, in inches, whatever its
86
+ * file holds — almost every icon is an SVG with no pixel count to measure.
87
+ *
88
+ * @type {number}
89
+ */
90
+ export const BOOK_ICON_WIDTH_IN: number;
@@ -69,6 +69,12 @@ export function stripHtmlComments(markdown: string): string;
69
69
  * the staged file's path, relative to the `.typ`. An address this does not
70
70
  * carry has no file the compiler can open, so the figure prints its caption
71
71
  * alone — see {@link renderImage}.
72
+ * @param {Map<string, {type: string, role?: string, width: number|"", height: number|""}>}
73
+ * [opts.assets] - An image's address as authored → what
74
+ * {@link module:engine/art-fields.assetImageInfoByPathname} records for it,
75
+ * which is what sizes a picture carrying no named `size=` — see
76
+ * {@link module:engine/pdf-images.bookImageWidthIn}. An address missing from
77
+ * this map draws at the medium's ordinary size, the same as today.
72
78
  * @param {number} [opts.headingOffset] - Added to every heading level, so a
73
79
  * note's own `##` nests beneath the entry heading the book gave it.
74
80
  * @param {string} [opts.anchorPrefix] - The entry's anchor, which namespaces
@@ -101,6 +107,12 @@ export function markdownToTypst(markdown: string, opts?: {
101
107
  links?: Map<string, string> | undefined;
102
108
  glyphs?: Map<string, string> | undefined;
103
109
  images?: Map<string, string> | undefined;
110
+ assets?: Map<string, {
111
+ type: string;
112
+ role?: string;
113
+ width: number | "";
114
+ height: number | "";
115
+ }> | undefined;
104
116
  headingOffset?: number | undefined;
105
117
  anchorPrefix?: string | undefined;
106
118
  findings?: object[] | undefined;
@@ -237,7 +237,7 @@ export function pageDestination(page: {
237
237
  bundle?: boolean | undefined;
238
238
  }): string;
239
239
  /** Transform one content page without writing the site mount. */
240
- export function renderSitePage(page: any, { index, foreign, universe, pass, decorate, linkable, sqlTables, config, artIndex, }: {
240
+ export function renderSitePage(page: any, { index, foreign, universe, pass, decorate, linkable, sqlTables, config, artIndex, figuresByAddress, }: {
241
241
  index: any;
242
242
  foreign: any;
243
243
  universe: any;
@@ -247,6 +247,7 @@ export function renderSitePage(page: any, { index, foreign, universe, pass, deco
247
247
  sqlTables: any;
248
248
  config: any;
249
249
  artIndex: any;
250
+ figuresByAddress?: Map<any, any> | undefined;
250
251
  }): {
251
252
  page: any;
252
253
  body: string;
@@ -280,6 +281,13 @@ export function renderSitePage(page: any, { index, foreign, universe, pass, deco
280
281
  }[];
281
282
  wikiErrors: any[];
282
283
  imageErrors: any[];
284
+ embedErrors: {
285
+ file: string;
286
+ line: number;
287
+ column: number | undefined;
288
+ severity: "error";
289
+ message: string;
290
+ }[];
283
291
  };
284
292
  /**
285
293
  * Renders and writes every page.
@@ -316,7 +324,7 @@ export function renderSitePage(page: any, { index, foreign, universe, pass, deco
316
324
  * it by wikilink, and its own markdown links name content pages. `maps` is
317
325
  * each drawing by the URL of the page that carries it.
318
326
  * @returns {{written: number, byKind: Record<string, number>, tableErrors: object[],
319
- * wikiErrors: object[], imageErrors: object[],
327
+ * wikiErrors: object[], imageErrors: object[], embedErrors: object[],
320
328
  * related: Map<string, import("./related-pages.mjs").Related>,
321
329
  * maps: number}} `related`
322
330
  * is keyed by page URL, and holds the homepage's block beside every content
@@ -329,6 +337,7 @@ export function renderPages(pages: object[], options: object): {
329
337
  tableErrors: object[];
330
338
  wikiErrors: object[];
331
339
  imageErrors: object[];
340
+ embedErrors: object[];
332
341
  related: Map<string, import("./related-pages.mjs").Related>;
333
342
  maps: number;
334
343
  };
@@ -361,7 +370,8 @@ export function sitePageDecorator(config: any, index: any): (data: any, page: an
361
370
  * `sql` directive with none prepared is a table error: nothing here runs a
362
371
  * query.
363
372
  * @returns {{gates: object, stats: object|null, tableErrors: object[],
364
- * wikiErrors: object[], imageErrors: object[], mapFindings: object[],
373
+ * wikiErrors: object[], imageErrors: object[], embedErrors: object[],
374
+ * mapFindings: object[],
365
375
  * manifests: object|null}} `mapFindings` is what drawing the maps found —
366
376
  * warnings, never a reason to fail the build.
367
377
  */
@@ -374,6 +384,7 @@ export function buildSite({ config, sqlTables }?: {
374
384
  tableErrors: object[];
375
385
  wikiErrors: object[];
376
386
  imageErrors: object[];
387
+ embedErrors: object[];
377
388
  mapFindings: object[];
378
389
  manifests: object | null;
379
390
  };
@@ -24,6 +24,60 @@ export function buildSiteIndex(entries: readonly SiteEntry[], { foreignIndex, no
24
24
  records?: object[] | undefined;
25
25
  foreignReferences?: Map<string, object> | undefined;
26
26
  }): SiteIndex;
27
+ /**
28
+ * The address keys one page's figures are reachable by — the same two forms
29
+ * {@link buildSiteIndex} assigns the page itself, so a `ref` crossing into it
30
+ * resolves by exactly the address a wikilink would use.
31
+ *
32
+ * @param {string} pkg - The page's own content package.
33
+ * @param {string} type - The note's `type`.
34
+ * @param {string} shortcode - The note's `shortcode`.
35
+ * @returns {Set<string>} The short `type/shortcode` key and the canonical
36
+ * `package-system-type-shortcode` one.
37
+ */
38
+ export function figureIndexKeys(pkg: string, type: string, shortcode: string): Set<string>;
39
+ /**
40
+ * Resolve a `ref` call's cross-note address to the target's own figures and
41
+ * its own address — the `figures.note` a `ref` helper's render context reads,
42
+ * built once and threaded into every page.
43
+ *
44
+ * Reads the written address exactly as a wikilink does
45
+ * ({@link readQualifier}, {@link expandAddress}), so a `ref` crossing into
46
+ * another note and a `[[…]]` link to the same note agree on what resolves and
47
+ * what does not: a target naming no known type or no fetched package's index
48
+ * is unresolved for both, and one address names one note for both.
49
+ *
50
+ * @param {string} target - The address as written, anchor already removed —
51
+ * {@link module:engine/wikilink-syntax.ParsedWikilink.target}.
52
+ * @param {object} options
53
+ * @param {Set<string>} options.contentTypes - Every type this build reads as
54
+ * an address qualifier.
55
+ * @param {Set<string>} options.packages - Every package an address may name.
56
+ * @param {Set<string>} options.noIndexPackages - Packages declared
57
+ * `contentIndex: false`.
58
+ * @param {string} options.contentPackage - The package a target with no
59
+ * package segment defaults to.
60
+ * @param {Map<string, {url: string}>} options.siteIndexMap - {@link SiteIndex}'s
61
+ * own `index`, read for the target's existence and its URL.
62
+ * @param {Map<string, Map<string, object>>} options.figuresByAddress - Every
63
+ * note's figures, by id, keyed as {@link figureIndexKeys} keys the note.
64
+ * @returns {{url: string, figures: Map<string, object>}|undefined} The
65
+ * target's own address and figures, or `undefined` when the address names
66
+ * no note this build resolves.
67
+ */
68
+ export function resolveCrossNoteFigures(target: string, { contentTypes, packages, noIndexPackages, contentPackage, siteIndexMap, figuresByAddress }: {
69
+ contentTypes: Set<string>;
70
+ packages: Set<string>;
71
+ noIndexPackages: Set<string>;
72
+ contentPackage: string;
73
+ siteIndexMap: Map<string, {
74
+ url: string;
75
+ }>;
76
+ figuresByAddress: Map<string, Map<string, object>>;
77
+ }): {
78
+ url: string;
79
+ figures: Map<string, object>;
80
+ } | undefined;
27
81
  /**
28
82
  * The per-page context a wikilink resolver takes.
29
83
  *
@@ -95,8 +95,32 @@ export function sharedProperty(fm: object, source: string, defaultValue?: any):
95
95
  */
96
96
  export function blockProperty(fm: object, block: string, key: string, defaultValue?: any): any;
97
97
  /**
98
- * A system override followed by a shared `data:` value. The top-level value
99
- * remains readable while content repositories move their authored fields.
98
+ * A system override followed by a shared `data:` value followed by the
99
+ * retiring top-level spelling `data:` gathered it off, reporting which one
100
+ * answered.
101
+ *
102
+ * {@link module:engine/field-spec.resolveFieldValue}'s counterpart for a
103
+ * universal `data:` key — `pack` and `packFolder`, declared for every note
104
+ * type rather than per type, so neither is a `FieldSpec` that resolver takes.
105
+ * The order is the same shape: a system's own override, then the shared
106
+ * `data:` position, then the bare top-level key a tree authored before
107
+ * `data:` existed. The top-level value stays readable while a tree still on
108
+ * it moves its authored fields.
109
+ *
110
+ * @param {object} fm - The note's frontmatter.
111
+ * @param {string} block - The system block key.
112
+ * @param {string} key - The shared property.
113
+ * @param {any} [defaultValue] - Returned when no position declares it.
114
+ * @returns {{value: any, from: FieldSource}} The value, and where it came
115
+ * from.
116
+ */
117
+ export function resolveDataProperty(fm: object, block: string, key: string, defaultValue?: any): {
118
+ value: any;
119
+ from: FieldSource;
120
+ };
121
+ /**
122
+ * {@link resolveDataProperty}'s value alone, for a caller that only compiles
123
+ * a document and has no use for which position answered.
100
124
  *
101
125
  * @param {object} fm - The note's frontmatter.
102
126
  * @param {string} block - The system block key.
@@ -1,3 +1,21 @@
1
+ /**
2
+ * Native HM3 facts shared by its actor subtypes.
3
+ *
4
+ * Species is native text. The note's lore Address in `data.species` is
5
+ * independent and supplies no native value.
6
+ *
7
+ * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
8
+ */
9
+ export const ACTOR_FIELDS: readonly import("../engine/field-spec.mjs").FieldSpec[];
10
+ /**
11
+ * The two `data:` facts HM3 declares on a `character` and not on a `creature`.
12
+ *
13
+ * Both name their shared source and their legacy in-block key, for the reason
14
+ * {@link ACTOR_FIELDS} does.
15
+ *
16
+ * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
17
+ */
18
+ export const CHARACTER_FIELDS: readonly import("../engine/field-spec.mjs").FieldSpec[];
1
19
  /**
2
20
  * HM3's Actor compile pass.
3
21
  *