@heroiclands/package-build 21.0.0 → 21.2.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 (112) hide show
  1. package/CHANGELOG.md +228 -0
  2. package/CONTENT.md +109 -7
  3. package/assets/fonts/libertinus/LibertinusMono-Regular.otf +0 -0
  4. package/assets/fonts/libertinus/LibertinusSans-Bold.otf +0 -0
  5. package/assets/fonts/libertinus/LibertinusSans-Italic.otf +0 -0
  6. package/assets/fonts/libertinus/LibertinusSans-Regular.otf +0 -0
  7. package/assets/fonts/libertinus/OFL.txt +94 -0
  8. package/assets/fonts/libertinus/provenance.yaml +11 -0
  9. package/assets/images/banners/afflictionbnr.webp +0 -0
  10. package/assets/images/banners/armorclothingbnr.webp +0 -0
  11. package/assets/images/banners/attributebnr.webp +0 -0
  12. package/assets/images/banners/containerbnr.webp +0 -0
  13. package/assets/images/banners/devdocsbnr.webp +0 -0
  14. package/assets/images/banners/miscgearbnr.webp +0 -0
  15. package/assets/images/banners/mysticalabilitybnr.webp +0 -0
  16. package/assets/images/banners/projectilebnr.webp +0 -0
  17. package/assets/images/banners/rulesbnr.webp +0 -0
  18. package/assets/images/banners/skillbnr.webp +0 -0
  19. package/assets/images/banners/traumabnr.webp +0 -0
  20. package/assets/images/banners/userguidebnr.webp +0 -0
  21. package/assets/images/banners/weapongearbnr.webp +0 -0
  22. package/assets/images/provenance.yaml +4 -0
  23. package/bin/content-build.mjs +19 -5
  24. package/ci/emit-asset-index.mjs +32 -0
  25. package/content-config.mjs +30 -1
  26. package/docs/api.md +100 -26
  27. package/docs/configuration.md +16 -7
  28. package/docs/content-format.md +512 -109
  29. package/engine/actor-compiler.mjs +58 -9
  30. package/engine/art-fields.mjs +292 -0
  31. package/engine/asset-index.mjs +397 -0
  32. package/engine/asset-types.mjs +192 -0
  33. package/engine/base-compiler.mjs +69 -1
  34. package/engine/bundles.mjs +5 -5
  35. package/engine/compile-corpus.mjs +4 -0
  36. package/engine/content-address.mjs +14 -2
  37. package/engine/content-charset.mjs +5 -1
  38. package/engine/content-embeds.mjs +314 -0
  39. package/engine/content-html.mjs +5 -1
  40. package/engine/content-icons.mjs +5 -1
  41. package/engine/content-images.mjs +26 -5
  42. package/engine/content-index.mjs +68 -33
  43. package/engine/content-links.mjs +164 -24
  44. package/engine/frontmatter-lint.mjs +36 -23
  45. package/engine/helpers.mjs +83 -31
  46. package/engine/index-records.mjs +63 -4
  47. package/engine/index.mjs +15 -0
  48. package/engine/infobox-registry.mjs +50 -3
  49. package/engine/infobox-render.mjs +4 -3
  50. package/engine/infobox.mjs +12 -8
  51. package/engine/item-compiler.mjs +10 -10
  52. package/engine/journals.mjs +3 -0
  53. package/engine/macros.mjs +16 -13
  54. package/engine/map-notes.mjs +29 -22
  55. package/engine/metadata-index.mjs +44 -36
  56. package/engine/note-vocabulary.mjs +71 -29
  57. package/engine/pack-router.mjs +14 -0
  58. package/engine/packagebuild-index.mjs +124 -0
  59. package/engine/packages.mjs +83 -0
  60. package/engine/pathnames.mjs +74 -0
  61. package/engine/pdf-build.mjs +133 -10
  62. package/engine/pdf-render.mjs +41 -41
  63. package/engine/scenes.mjs +12 -9
  64. package/engine/site-build.mjs +43 -16
  65. package/engine/site-index.mjs +10 -1
  66. package/engine/subtype-registry.mjs +21 -0
  67. package/engine/web-wikilinks.mjs +46 -12
  68. package/engine/wikilink-syntax.mjs +20 -2
  69. package/engine/wikilinks.mjs +15 -2
  70. package/hm3/actors.mjs +11 -38
  71. package/hm3/infobox.mjs +1 -1
  72. package/package.json +5 -2
  73. package/packagebuild-metadata.jsonl +13 -0
  74. package/sohl/actors.mjs +12 -86
  75. package/sohl/infobox.mjs +1 -1
  76. package/sohl/note-schemas.mjs +4 -16
  77. package/types/content-config.d.mts +8 -0
  78. package/types/engine/actor-compiler.d.mts +29 -0
  79. package/types/engine/art-fields.d.mts +192 -0
  80. package/types/engine/asset-index.d.mts +92 -0
  81. package/types/engine/asset-types.d.mts +110 -0
  82. package/types/engine/base-compiler.d.mts +42 -1
  83. package/types/engine/compile-corpus.d.mts +4 -1
  84. package/types/engine/content-charset.d.mts +4 -0
  85. package/types/engine/content-embeds.d.mts +218 -0
  86. package/types/engine/content-html.d.mts +4 -0
  87. package/types/engine/content-icons.d.mts +4 -0
  88. package/types/engine/content-images.d.mts +15 -2
  89. package/types/engine/content-index.d.mts +27 -15
  90. package/types/engine/frontmatter-lint.d.mts +11 -14
  91. package/types/engine/helpers.d.mts +7 -2
  92. package/types/engine/index-records.d.mts +44 -3
  93. package/types/engine/index.d.mts +5 -0
  94. package/types/engine/infobox-registry.d.mts +36 -1
  95. package/types/engine/infobox.d.mts +6 -4
  96. package/types/engine/macros.d.mts +9 -5
  97. package/types/engine/map-notes.d.mts +10 -6
  98. package/types/engine/metadata-index.d.mts +2 -17
  99. package/types/engine/note-vocabulary.d.mts +30 -0
  100. package/types/engine/packagebuild-index.d.mts +56 -0
  101. package/types/engine/packages.d.mts +62 -0
  102. package/types/engine/pathnames.d.mts +41 -81
  103. package/types/engine/pdf-build.d.mts +58 -1
  104. package/types/engine/pdf-render.d.mts +3 -3
  105. package/types/engine/scenes.d.mts +2 -1
  106. package/types/engine/site-build.d.mts +5 -1
  107. package/types/engine/site-index.d.mts +4 -1
  108. package/types/engine/subtype-registry.d.mts +20 -0
  109. package/types/engine/web-wikilinks.d.mts +14 -4
  110. package/types/engine/wikilink-syntax.d.mts +13 -1
  111. package/types/engine/wikilinks.d.mts +7 -1
  112. package/types/sohl/actors.d.mts +0 -1
@@ -129,6 +129,11 @@ export class BasePackCompiler {
129
129
  /**
130
130
  * @param {object} options
131
131
  * @param {string} options.contentBase - Root of the content tree.
132
+ * @param {string} [options.assetsBase] - The asset roots' parent, whose
133
+ * files the art addresses resolve against. Stated beside `contentBase`
134
+ * because the two move independently: a caller compiling a tree it
135
+ * assembled states where that tree's files are, and one compiling the
136
+ * repository's own leaves it to the configuration.
132
137
  * @param {string} options.dest - Where this pass writes its JSON.
133
138
  * @param {readonly string[]} options.skipDirectories - Directories the walk
134
139
  * never descends into. Required: see {@link assertStatedScope}.
@@ -148,8 +153,9 @@ export class BasePackCompiler {
148
153
  * over, derived once and shared by every pass. A pass handed none derives
149
154
  * its own in `prepare`, and reports that corpus's problems itself.
150
155
  */
151
- constructor({ contentBase, dest, skipDirectories, folderResolver, packName, packSystem, docType, router, routingReporter, corpus, }?: {
156
+ constructor({ contentBase, assetsBase, dest, skipDirectories, folderResolver, packName, packSystem, docType, router, routingReporter, corpus, }?: {
152
157
  contentBase: string;
158
+ assetsBase?: string | undefined;
153
159
  dest: string;
154
160
  skipDirectories: readonly string[];
155
161
  folderResolver?: ((address: string | null) => string | null) | undefined;
@@ -164,6 +170,12 @@ export class BasePackCompiler {
164
170
  });
165
171
  /** @type {string} */
166
172
  contentBase: string;
173
+ /**
174
+ * The asset roots' parent, whose files the art addresses resolve against.
175
+ *
176
+ * @type {string|undefined}
177
+ */
178
+ assetsBase: string | undefined;
167
179
  /** @type {string} */
168
180
  outputDir: string;
169
181
  /** @type {(path: string|null) => string|null} */
@@ -348,6 +360,35 @@ export class BasePackCompiler {
348
360
  * that does not convert.
349
361
  */
350
362
  convertBody(fm: object, body: string): string;
363
+ /**
364
+ * The Foundry path one authored art address names, or `null`.
365
+ *
366
+ * **The two empties survive.** `null` and an absent key mean *no art named*,
367
+ * so the caller's own default applies with nullish coalescing; `""` means
368
+ * *ship blank on purpose* and no default may replace it. That is
369
+ * {@link module:engine/helpers.resolveImg}'s rule, reached through it rather
370
+ * than restated, which is also what puts an art address and a body image
371
+ * through one ownership rule.
372
+ *
373
+ * An address nothing answers is reported against the note and treated as
374
+ * unnamed, so the document takes its default rather than shipping a path
375
+ * that installs nowhere.
376
+ *
377
+ * @param {unknown} value - The value as authored.
378
+ * @param {string} key - The key it was authored at, for the message.
379
+ * @param {string} type - The asset type a bare value takes.
380
+ * @returns {string|null} The Foundry-relative path, `""` for a deliberate
381
+ * blank, or `null` where the note names none.
382
+ */
383
+ artPathOf(value: unknown, key: string, type: string): string | null;
384
+ /**
385
+ * The Foundry path one art slot of a note names, or `null`.
386
+ *
387
+ * @param {object} fm - The note's frontmatter.
388
+ * @param {string} key - The slot's key under `data:`.
389
+ * @returns {string|null} As {@link BasePackCompiler#artPathOf}.
390
+ */
391
+ artPath(fm: object, key: string): string | null;
351
392
  /**
352
393
  * Reports a warning about the note being compiled.
353
394
  *
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * @param {object} opts - Options.
5
5
  * @param {string} opts.contentBase - Root of the content tree.
6
+ * @param {string} [opts.assetsBase] - The asset roots' parent, whose files the
7
+ * art addresses resolve against. Defaults to the configured one.
6
8
  * @param {readonly string[]} opts.skipDirectories - The scope, stated by the
7
9
  * caller as every corpus read requires.
8
10
  * @param {object} opts.router - The pack router this compile resolved. Shared
@@ -17,8 +19,9 @@
17
19
  * sqlTables: Map<string, object[]>|undefined, problems: object[]}>} The corpus,
18
20
  * its indexes, and the notes it could not record.
19
21
  */
20
- export function buildCompileCorpus({ contentBase, skipDirectories, router, config, problems, }: {
22
+ export function buildCompileCorpus({ contentBase, assetsBase, skipDirectories, router, config, problems, }: {
21
23
  contentBase: string;
24
+ assetsBase?: string | undefined;
22
25
  skipDirectories: readonly string[];
23
26
  router: object;
24
27
  config?: object | undefined;
@@ -78,6 +78,10 @@ export function checkText(text: string, file: string): Array<{
78
78
  * theoretical exclusion — it was the first thing a run over `sohl-thalorna`
79
79
  * reported before the skip existed.
80
80
  *
81
+ * A finding names its file **relative to the working directory**, which is
82
+ * where a reader is standing and what `formatDiagnostic` emits. The content
83
+ * root is where the walk starts, not what a path is measured from.
84
+ *
81
85
  * @param {string} contentBase - Root of the content tree.
82
86
  * @param {object} [opts]
83
87
  * @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore
@@ -0,0 +1,218 @@
1
+ /**
2
+ * One embed, parsed.
3
+ *
4
+ * @typedef {object} ParsedEmbed
5
+ * @property {string} all - The embed exactly as authored, directive included.
6
+ * @property {string} inner - The whole interior of the brackets.
7
+ * @property {string} written - The link part as authored, anchor included. An
8
+ * embed names a file and a file has no sections, so an anchor is carried into
9
+ * the address rather than stripped off it — where it fails the address charset
10
+ * and is reported as what it is.
11
+ * @property {string|null} display - The text after `|`, `null` when unlabelled.
12
+ * @property {boolean} labelled - Whether a `|` was present at all.
13
+ * @property {string} directive - The `{…}` as written, or `""`.
14
+ * @property {number} index - Where the embed begins in the body.
15
+ * @property {number} length - How much of the body it occupies.
16
+ * @property {boolean} block - Whether it stands alone in its own paragraph.
17
+ */
18
+ /**
19
+ * Every embed in one body, in source order.
20
+ *
21
+ * Code is skipped, because an embed shown as an example is prose *about* an
22
+ * embed — which is what makes this module's own documentation writable.
23
+ *
24
+ * @param {string} body - The note's markdown, without its frontmatter.
25
+ * @returns {ParsedEmbed[]} One entry per embed.
26
+ */
27
+ export function embedsIn(body: string): ParsedEmbed[];
28
+ /**
29
+ * What one embed resolves to, or why it does not.
30
+ *
31
+ * The reasons are {@link module:engine/wikilink-syntax.LINK_FINDING_REASONS},
32
+ * the vocabulary every resolver shares, so an author meets one wording for one
33
+ * mistake whichever build they ran first.
34
+ *
35
+ * @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex},
36
+ * or the equivalent the site and the book build.
37
+ * @param {ParsedEmbed} embed - The embed.
38
+ * @returns {{pathname: string}|{reason: string, target: string, type?: string}}
39
+ * The authored pathname the picture is at, or the finding.
40
+ */
41
+ export function resolveEmbed(index: object, embed: ParsedEmbed): {
42
+ pathname: string;
43
+ } | {
44
+ reason: string;
45
+ target: string;
46
+ type?: string;
47
+ };
48
+ /**
49
+ * What is wrong with one embed's directive or placement, if anything.
50
+ *
51
+ * Separate from the link findings beside it because the two speak different
52
+ * vocabularies: a link finding is a `reason` from the closed set every resolver
53
+ * shares, and this is an image's own complaint about a brace or a paragraph.
54
+ * Collapsing them would put a sentence where a `reason` belongs.
55
+ *
56
+ * @param {ParsedEmbed} embed - The embed.
57
+ * @returns {Array<{link: string, offset: number, message: string}>} One entry
58
+ * per defect. `link` is the embed exactly as authored, which is what lets a
59
+ * caller with no offsets locate it by searching the note for the literal.
60
+ */
61
+ export function embedProblems(embed: ParsedEmbed): Array<{
62
+ link: string;
63
+ offset: number;
64
+ message: string;
65
+ }>;
66
+ /**
67
+ * Rewrite every embed in one body into the image each surface already renders.
68
+ *
69
+ * An embed that does not resolve is left **exactly as authored** and reported,
70
+ * so a missing picture degrades to visible literal text rather than to a broken
71
+ * `src` or a swallowed paragraph. So is one whose directive does not parse: a
72
+ * directive holding a problem is not honoured at all, and the braces reaching
73
+ * the page as their own literal text is how the mistake is visible without a
74
+ * log.
75
+ *
76
+ * Call this **before** wikilink resolution. The rewrite consumes the `!` along
77
+ * with the brackets, which is what stops the link pass reading an embed's
78
+ * interior as an ordinary link to a note that does not exist.
79
+ *
80
+ * @param {string} body - The note's markdown, tables already expanded.
81
+ * @param {object} ctx
82
+ * @param {object} ctx.index - The address index assets resolve through.
83
+ * @returns {{markdown: string, unresolved: Array<{link: string, target: string,
84
+ * offset: number, reason: string, type?: string}>,
85
+ * problems: Array<{link: string, offset: number, message: string}>,
86
+ * images: Array<{link: string, offset: number, pathname: string}>}} The body,
87
+ * the embeds that named nothing, the directives that could not be honoured,
88
+ * and the pathname each embed that did resolve now names. Every `offset` is
89
+ * 0-based in `body`, which is what lets a caller report a line and a column
90
+ * and tell two identical embeds apart.
91
+ *
92
+ * **`images` is how a surface holds an embed to its own rule.** The rewrite
93
+ * is surface-agnostic — a file the website serves and the book stages is not
94
+ * always one a Foundry install carries — so a caller that cares asks about
95
+ * the pathname while it still knows which embed produced it, rather than
96
+ * searching a rewritten body for a literal the note never wrote.
97
+ */
98
+ export function resolveEmbeds(body: string, { index }: {
99
+ index: object;
100
+ }): {
101
+ markdown: string;
102
+ unresolved: Array<{
103
+ link: string;
104
+ target: string;
105
+ offset: number;
106
+ reason: string;
107
+ type?: string;
108
+ }>;
109
+ problems: Array<{
110
+ link: string;
111
+ offset: number;
112
+ message: string;
113
+ }>;
114
+ images: Array<{
115
+ link: string;
116
+ offset: number;
117
+ pathname: string;
118
+ }>;
119
+ };
120
+ /**
121
+ * Every defect in one note's embeds, located.
122
+ *
123
+ * {@link resolveEmbeds} with its two lists turned into the one shape a finding
124
+ * takes, for a caller that wants the report and not the rewrite.
125
+ *
126
+ * **Errors, not warnings**, for the reason an image's are: `reportFindings`
127
+ * fails on an error and not on a warning, so a refusal reported as advisory
128
+ * publishes anyway, looking exactly like a directive that worked.
129
+ *
130
+ * @param {string} body - The note's markdown, without its frontmatter.
131
+ * @param {string} file - The note's path, for the finding.
132
+ * @param {object} [opts]
133
+ * @param {number} [opts.bodyLine=1] - The 1-based file line the body starts on.
134
+ * @param {number} [opts.bodyColumn=1] - The 1-based file column it starts at.
135
+ * @param {object} [opts.index] - The address index assets resolve through.
136
+ * @returns {Array<{file: string, line: number, column: number|undefined,
137
+ * severity: "error", message: string}>} One finding per defect, in source
138
+ * order.
139
+ */
140
+ export function checkEmbeds(body: string, file: string, { bodyLine, bodyColumn, index }?: {
141
+ bodyLine?: number | undefined;
142
+ bodyColumn?: number | undefined;
143
+ index?: object | undefined;
144
+ }): Array<{
145
+ file: string;
146
+ line: number;
147
+ column: number | undefined;
148
+ severity: "error";
149
+ message: string;
150
+ }>;
151
+ /**
152
+ * The type a bare shortcode takes, supplied by the syntax itself.
153
+ *
154
+ * A picture is what `!` asks for, so `image` is what an unqualified embed names.
155
+ * An icon is reached by stating it — `![[icon-anvil|An anvil]]` — because the two
156
+ * have separate shortcode namespaces and neither is derivable from the other.
157
+ *
158
+ * @type {string}
159
+ */
160
+ export const EMBED_DEFAULT_TYPE: string;
161
+ /**
162
+ * An embed, as authored, with the directive it may carry.
163
+ *
164
+ * The interior admits no `]` or newline, exactly as a wikilink's does: an embed
165
+ * is written on one line, and an unclosed `![[` is a typo rather than licence to
166
+ * consume the rest of the document looking for a closer.
167
+ *
168
+ * The directive is `{…}` immediately after the closing `]]`, holding no newline,
169
+ * for the reason {@link module:engine/content-images.IMAGE_PATTERN} gives: a
170
+ * brace that opens and never closes on its line is prose.
171
+ *
172
+ * @type {RegExp}
173
+ */
174
+ export const EMBED_PATTERN: RegExp;
175
+ /**
176
+ * One embed, parsed.
177
+ */
178
+ export type ParsedEmbed = {
179
+ /**
180
+ * - The embed exactly as authored, directive included.
181
+ */
182
+ all: string;
183
+ /**
184
+ * - The whole interior of the brackets.
185
+ */
186
+ inner: string;
187
+ /**
188
+ * - The link part as authored, anchor included. An
189
+ * embed names a file and a file has no sections, so an anchor is carried into
190
+ * the address rather than stripped off it — where it fails the address charset
191
+ * and is reported as what it is.
192
+ */
193
+ written: string;
194
+ /**
195
+ * - The text after `|`, `null` when unlabelled.
196
+ */
197
+ display: string | null;
198
+ /**
199
+ * - Whether a `|` was present at all.
200
+ */
201
+ labelled: boolean;
202
+ /**
203
+ * - The `{…}` as written, or `""`.
204
+ */
205
+ directive: string;
206
+ /**
207
+ * - Where the embed begins in the body.
208
+ */
209
+ index: number;
210
+ /**
211
+ * - How much of the body it occupies.
212
+ */
213
+ length: number;
214
+ /**
215
+ * - Whether it stands alone in its own paragraph.
216
+ */
217
+ block: boolean;
218
+ };
@@ -39,6 +39,10 @@ export function checkHtml(body: string, file: string, { bodyLine, bodyColumn }?:
39
39
  * whole: it is not a note, but a stray `.md` in the tree carrying markup is the
40
40
  * same problem for the same reason.
41
41
  *
42
+ * A finding names its file **relative to the working directory**, which is
43
+ * where a reader is standing and what `formatDiagnostic` emits. The content
44
+ * root is where the walk starts, not what a path is measured from.
45
+ *
42
46
  * @param {string} contentBase - Root of the content tree.
43
47
  * @param {object} [opts]
44
48
  * @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore
@@ -111,6 +111,10 @@ export function checkIconRegistry(registry: {
111
111
  * with nothing imported between them. The cost is one extra pass over the tree,
112
112
  * which is the cheaper half of a lint that already parses every note.
113
113
  *
114
+ * A finding names its file **relative to the working directory**, which is
115
+ * where a reader is standing and what `formatDiagnostic` emits. The content
116
+ * root is where the walk starts, not what a path is measured from.
117
+ *
114
118
  * @param {string} contentBase - Root of the content tree.
115
119
  * @param {object} [opts]
116
120
  * @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore.
@@ -123,13 +123,19 @@ export function imageSourcesIn(body: string): string[];
123
123
  * @param {object} [opts]
124
124
  * @param {number} [opts.bodyLine=1] - The 1-based file line the body starts on.
125
125
  * @param {number} [opts.bodyColumn=1] - The 1-based file column it starts at.
126
+ * @param {object} [opts.config] - The resolved build configuration. Supplied,
127
+ * an address is also held to the one surface a pathname can be dead on
128
+ * without any other pass noticing — see the Foundry address below. Omitted,
129
+ * the config-free checks run alone, which is what lets a caller with no
130
+ * repository to resolve still read a body.
126
131
  * @returns {Array<{file: string, line: number, column: number|undefined,
127
132
  * severity: "error", message: string}>} One finding per defect, in source
128
133
  * order.
129
134
  */
130
- export function checkImages(body: string, file: string, { bodyLine, bodyColumn }?: {
135
+ export function checkImages(body: string, file: string, { bodyLine, bodyColumn, config }?: {
131
136
  bodyLine?: number | undefined;
132
137
  bodyColumn?: number | undefined;
138
+ config?: object | undefined;
133
139
  }): Array<{
134
140
  file: string;
135
141
  line: number;
@@ -143,16 +149,23 @@ export function checkImages(body: string, file: string, { bodyLine, bodyColumn }
143
149
  * Its own walk, like the icon and HTML checks beside it, so all three stay
144
150
  * leaves with nothing imported between them.
145
151
  *
152
+ * A finding names its file **relative to the working directory**, which is
153
+ * where a reader is standing and what `formatDiagnostic` emits. The content
154
+ * root is where the walk starts, not what a path is measured from.
155
+ *
146
156
  * @param {string} contentBase - Root of the content tree.
147
157
  * @param {object} [opts]
148
158
  * @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore
149
159
  * in addition to the dot-directories always skipped.
160
+ * @param {object} [opts.config] - The resolved build configuration, passed to
161
+ * {@link checkImages} so an address is held to the Foundry surface too.
150
162
  * @returns {{findings: Array<{file: string, line: number, column: number|undefined,
151
163
  * severity: "error", message: string}>, files: number}} The findings, and how
152
164
  * many files were read.
153
165
  */
154
- export function lintContentImages(contentBase: string, { skipDirectories }?: {
166
+ export function lintContentImages(contentBase: string, { skipDirectories, config }?: {
155
167
  skipDirectories?: readonly string[] | undefined;
168
+ config?: object | undefined;
156
169
  }): {
157
170
  findings: Array<{
158
171
  file: string;
@@ -31,16 +31,6 @@ export function noteAddress(frontmatter: Record<string, any>, contentPackage: st
31
31
  slug: string;
32
32
  canonical: string;
33
33
  } | null;
34
- /**
35
- * Recursively sort an object's keys, so serialization is order-independent.
36
- *
37
- * Arrays keep their order — it is authored — but every object inside one is
38
- * sorted too. Anything that is not a plain object is returned as it is.
39
- *
40
- * @param {unknown} value - The value to normalize.
41
- * @returns {unknown} The value with every plain object's keys in sorted order.
42
- */
43
- export function sortKeysDeep(value: unknown): unknown;
44
34
  /**
45
35
  * A note's display name reduced to printable 7-bit ASCII.
46
36
  *
@@ -129,12 +119,22 @@ export function buildIndexRecord({ frontmatter, relPath, absPath, contentPackage
129
119
  * An item note yields two records — the item, and the documentation journal
130
120
  * that is a document in its own right.
131
121
  *
122
+ * **The asset roots are walked in the same pass.** A package's addressable files
123
+ * sit beside `content/` rather than inside it, and they publish into the same
124
+ * index under the same address grammar — so there is no second walk, no second
125
+ * artifact, and no notion of an "art module" anywhere in the toolchain. A
126
+ * package whose tree holds only assets is one by consequence.
127
+ *
132
128
  * @param {string} contentBase - The content tree to walk.
133
129
  * @param {object} options - Options.
134
130
  * @param {string} options.contentPackage - The package the tree compiles as.
135
131
  * @param {readonly string[]} options.skipDirectories - The walk's scope, stated
136
132
  * by the caller. An absent one is the caller's omission, and
137
133
  * {@link module:engine/helpers.walkMarkdownTree} throws on it.
134
+ * @param {string} [options.assetsBase] - The package's asset directory, holding
135
+ * the three asset roots. Omitted, no asset is indexed — which is what a caller
136
+ * walking a bare content fixture wants, and what an asset-free package gets
137
+ * anyway.
138
138
  * @param {object} [options.manifest] - The package manifest, which the Foundry
139
139
  * entries are derived against.
140
140
  * @param {object[]} [options.problems] - Supplied by a **reader**: a note that
@@ -144,9 +144,10 @@ export function buildIndexRecord({ frontmatter, relPath, absPath, contentPackage
144
144
  * @returns {Array<Record<string, any>>} The records, in a total order that does
145
145
  * not depend on directory-read order.
146
146
  */
147
- export function collectContentIndex(contentBase: string, { contentPackage, skipDirectories, manifest, problems }: {
147
+ export function collectContentIndex(contentBase: string, { contentPackage, skipDirectories, assetsBase, manifest, problems }: {
148
148
  contentPackage: string;
149
149
  skipDirectories: readonly string[];
150
+ assetsBase?: string | undefined;
150
151
  manifest?: object | undefined;
151
152
  problems?: object[] | undefined;
152
153
  }): Array<Record<string, any>>;
@@ -170,6 +171,10 @@ export function serializeContentIndex(records: Array<Record<string, any>>): stri
170
171
  *
171
172
  * @param {object} [opts]
172
173
  * @param {string} [opts.contentBase] - The tree, defaulting to the configured one.
174
+ * @param {string} [opts.assetsBase] - The asset roots' parent, defaulting to
175
+ * the configured one. Stated separately from `contentBase` because the two
176
+ * move independently — a caller walking an assembled fixture tree says where
177
+ * that fixture's files are.
173
178
  * @param {object} [opts.config] - Resolved configuration, defaulting to ambient.
174
179
  * @param {readonly string[]} [opts.skipDirectories] - The walk's scope, for a
175
180
  * caller that resolved one of its own; defaults to the resolved
@@ -183,8 +188,9 @@ export function serializeContentIndex(records: Array<Record<string, any>>): stri
183
188
  * exist.
184
189
  * @returns {object[]} One record per note, plus one per documentation entry.
185
190
  */
186
- export function indexRecordsFor({ contentBase, config, skipDirectories, problems }?: {
191
+ export function indexRecordsFor({ contentBase, assetsBase, config, skipDirectories, problems, }?: {
187
192
  contentBase?: string | undefined;
193
+ assetsBase?: string | undefined;
188
194
  config?: object | undefined;
189
195
  skipDirectories?: readonly string[] | undefined;
190
196
  problems?: object[] | undefined;
@@ -198,8 +204,9 @@ export function indexRecordsFor({ contentBase, config, skipDirectories, problems
198
204
  * @param {string} [options.outDir] - Where to write; defaults to the configured
199
205
  * `paths.contentIndex`.
200
206
  * @param {object} [options.config] - A resolved configuration; loaded when omitted.
201
- * @returns {{file: string, notes: number, bytes: number}} Where it was written,
202
- * how many notes it holds, and its size.
207
+ * @returns {{file: string, notes: number, assets: number, records: number,
208
+ * bytes: number}} Where it was written, how many notes and how many assets it
209
+ * holds, how many records that is in all, and its size.
203
210
  * @throws {Error} When the content tree is absent, or when it yields no note at
204
211
  * all — an empty index is indistinguishable from a mis-pointed tree, and a
205
212
  * reader would take it as the authoritative statement that this package has
@@ -212,11 +219,16 @@ export function emitContentIndex({ contentBase, outDir, config }?: {
212
219
  }): {
213
220
  file: string;
214
221
  notes: number;
222
+ assets: number;
223
+ records: number;
215
224
  bytes: number;
216
225
  };
217
226
  import { collectAnchors } from "./anchors.mjs";
218
227
  import { authoredFrontmatter } from "./index-records.mjs";
219
228
  import { DERIVED_KEYS } from "./index-records.mjs";
229
+ import { isAssetRecord } from "./index-records.mjs";
220
230
  import { isNoteRecord } from "./index-records.mjs";
221
231
  import { noteFile } from "./index-records.mjs";
222
- export { collectAnchors, authoredFrontmatter, DERIVED_KEYS, isNoteRecord, noteFile };
232
+ import { recordPath } from "./index-records.mjs";
233
+ import { sortKeysDeep } from "./index-records.mjs";
234
+ export { collectAnchors, authoredFrontmatter, DERIVED_KEYS, isAssetRecord, isNoteRecord, noteFile, recordPath, sortKeysDeep };
@@ -240,27 +240,24 @@ export const UNIVERSAL_KEYS: ReadonlySet<string>;
240
240
  */
241
241
  export const DEFAULT_SYSTEM_BLOCKS: Readonly<Record<string, SystemBlockSpec>>;
242
242
  /**
243
- * The frontmatter fields that name artwork, and so resolve through
244
- * {@link module:engine/helpers.resolveImg}.
243
+ * The frontmatter fields that name artwork.
245
244
  *
246
- * Both, always: a being carries `img` and `portrait` independently — the token
247
- * art and the sheet portrait and a rule about how the translator reads an
248
- * empty value belongs to the translator, not to whichever key happens to be
249
- * more common. Eleven `sohl-kethira-basic` beings write `portrait: ""` and no
250
- * note in any tree writes `img: ""` on a being; a check keyed on `img` alone
251
- * would have called that tree clean.
245
+ * {@link module:engine/art-fields.ART_SLOTS} is the declaration; this is that
246
+ * list in the shape the checks below read, so the linter states no art key of
247
+ * its own and a slot added there is checked here with no second edit. All four
248
+ * are authored under `data:`, which is what `inData` records.
252
249
  *
253
- * **Each carries where it is authored**, because the two no longer agree. The
254
- * specification puts an actor's portrait under `data:` and leaves its token art
255
- * at the note's top level, so `portrait` has a third position to read and `img`
256
- * does not — and a check that read only the two they share would pass a
257
- * `data.portrait: ""` it could not see.
250
+ * `document` separates the three that reach a compiled document from `banner`,
251
+ * which reaches none by design. The inert-art check reads it: a key that is
252
+ * *meant* to reach no document is not an inert key, and reporting every note
253
+ * that names a hero image would bury the finding that matters.
258
254
  *
259
- * @type {readonly {key: string, inData: boolean}[]}
255
+ * @type {readonly {key: string, inData: boolean, document: boolean}[]}
260
256
  */
261
257
  export const ART_FIELDS: readonly {
262
258
  key: string;
263
259
  inData: boolean;
260
+ document: boolean;
264
261
  }[];
265
262
  /**
266
263
  * What one system block accepts beyond the keys every block carries.
@@ -385,6 +385,9 @@ export function buildContentLinkIndex(contentBase: string, router?: object, { sk
385
385
  * in, addressing a self-link the same way.
386
386
  * @param {object} ctx.index - The address index every link resolves through.
387
387
  * @param {string} ctx.name - The note, for the message.
388
+ * @param {object} [ctx.config] - The resolved build configuration, which is
389
+ * what says which packages this build can address a file in. Loaded when
390
+ * omitted, exactly as an art field's resolution loads it.
388
391
  * @param {string} [ctx.file] - The note's file, so a report names it.
389
392
  * @param {number} [ctx.bodyLine] - 1-based file line of the body's first line.
390
393
  * @param {number} [ctx.bodyColumn] - 1-based file column of the same character.
@@ -392,17 +395,19 @@ export function buildContentLinkIndex(contentBase: string, router?: object, { sk
392
395
  * authored line each body line came from, from {@link expandNoteTables}.
393
396
  * @returns {{markdown: string, unresolved: Array<object>}}
394
397
  * @throws {Error} On any link that does not resolve — an unlabelled one, a
395
- * target that is not an address, or an address nothing publishes. The error
398
+ * target that is not an address, or an address nothing publishes and on a
399
+ * body image whose pathname names no file a Foundry install serves. The error
396
400
  * carries `file` and `position`, so a caller reports it in the same form
397
401
  * rather than re-deriving one.
398
402
  */
399
- export function convertNoteWikilinks(body: string, { type, id, pack, docPack, index, name, file, bodyLine, bodyColumn, lineMap }: {
403
+ export function convertNoteWikilinks(body: string, { type, id, pack, docPack, index, name, file, bodyLine, bodyColumn, lineMap, config }: {
400
404
  type: string;
401
405
  id: string;
402
406
  pack: string;
403
407
  docPack: string;
404
408
  index: object;
405
409
  name: string;
410
+ config?: object | undefined;
406
411
  file?: string | undefined;
407
412
  bodyLine?: number | undefined;
408
413
  bodyColumn?: number | undefined;
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Recursively sort an object's keys, so serialization is order-independent.
3
+ *
4
+ * Arrays keep their order — it is authored — but every object inside one is
5
+ * sorted too. Anything that is not a plain object is returned as it is.
6
+ *
7
+ * @param {unknown} value - The value to normalize.
8
+ * @returns {unknown} The value with every plain object's keys in sorted order.
9
+ */
10
+ export function sortKeysDeep(value: unknown): unknown;
1
11
  /**
2
12
  * The file a record was read from, as an absolute path.
3
13
  *
@@ -36,19 +46,50 @@ export function noteFile(contentBase: string, record: object): string;
36
46
  */
37
47
  export function authoredFrontmatter(record: Record<string, any>): Record<string, any>;
38
48
  /**
39
- * Whether a record is a note's, rather than a documentation journal's.
49
+ * Whether a record addresses a **file** rather than a note.
50
+ *
51
+ * The index holds two record shapes in one file, and this is how a reader tells
52
+ * them apart. An asset record carries no frontmatter, no anchors and no
53
+ * `foundry` block — a `.webp` declares nothing about itself — so every pass that
54
+ * reads a note's fields has to skip it, and the `asset` block is what marks it.
55
+ *
56
+ * Asked of the block rather than of `type`, so a fourth asset type needs no
57
+ * edit here: what makes a record an asset's is that it describes a file, and the
58
+ * block is the description.
59
+ *
60
+ * @param {Record<string, any>} record - An index record.
61
+ * @returns {boolean} True for an asset's record.
62
+ */
63
+ export function isAssetRecord(record: Record<string, any>): boolean;
64
+ /**
65
+ * Whether a record is a note's, rather than a documentation journal's or an
66
+ * asset's.
40
67
  *
41
68
  * An item note yields two records — itself and the JournalEntry its prose
42
69
  * compiles into — and the second is a document, not a note: it has no file of
43
70
  * its own to read, no frontmatter an author wrote, and its `type` is the
44
71
  * virtual `doc<type>` that `readQualifier` resolves rather than a type any tree
45
- * declares. A reader enumerating the corpus wants the notes; one resolving an
46
- * address wants both.
72
+ * declares. An asset's record is not a note either, for the stronger reason that
73
+ * nobody authored it at all. A reader enumerating the corpus wants the notes;
74
+ * one resolving an address wants all three.
47
75
  *
48
76
  * @param {Record<string, any>} record - An index record.
49
77
  * @returns {boolean} True for a note's own record.
50
78
  */
51
79
  export function isNoteRecord(record: Record<string, any>): boolean;
80
+ /**
81
+ * The path a record names inside its package, whichever shape it is.
82
+ *
83
+ * The two shapes state it differently and honestly: a note names the `.md` it
84
+ * was parsed from, relative to the content root, while an asset names the file
85
+ * it *is*, relative to the package's asset directory. Both are paths within one
86
+ * package, so one total order covers the whole index — which is what keeps the
87
+ * artifact byte-stable across a rebuild.
88
+ *
89
+ * @param {Record<string, any>} record - An index record.
90
+ * @returns {string} The path, or `""` for a record naming neither.
91
+ */
92
+ export function recordPath(record: Record<string, any>): string;
52
93
  /**
53
94
  * The keys the content index adds to a record, which a note therefore may not
54
95
  * carry itself.
@@ -27,12 +27,17 @@ export * as noteRenames from "./note-renames.mjs";
27
27
  export * as metadataIndex from "./metadata-index.mjs";
28
28
  export * as foundryEntries from "./foundry-entries.mjs";
29
29
  export * as contentIndex from "./content-index.mjs";
30
+ export * as assetTypes from "./asset-types.mjs";
31
+ export * as assetIndex from "./asset-index.mjs";
32
+ export * as packages from "./packages.mjs";
33
+ export * as packagebuildIndex from "./packagebuild-index.mjs";
30
34
  export * as siteBuild from "./site-build.mjs";
31
35
  export * as contentLint from "./content-lint.mjs";
32
36
  export * as contentCharset from "./content-charset.mjs";
33
37
  export * as contentIcons from "./content-icons.mjs";
34
38
  export * as contentHtml from "./content-html.mjs";
35
39
  export * as contentImages from "./content-images.mjs";
40
+ export * as contentEmbeds from "./content-embeds.mjs";
36
41
  export * as pathnames from "./pathnames.mjs";
37
42
  export * as contentLinks from "./content-links.mjs";
38
43
  export * as webWikilinks from "./web-wikilinks.mjs";