@heroiclands/package-build 21.1.0 → 22.0.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 (115) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/CONTENT.md +107 -3
  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/bin/package-build.mjs +6 -5
  25. package/ci/emit-asset-index.mjs +32 -0
  26. package/content-config.mjs +30 -1
  27. package/docs/api.md +103 -27
  28. package/docs/commands.md +13 -11
  29. package/docs/configuration.md +16 -7
  30. package/docs/content-format.md +523 -111
  31. package/engine/actor-compiler.mjs +58 -9
  32. package/engine/art-fields.mjs +292 -0
  33. package/engine/asset-index.mjs +397 -0
  34. package/engine/asset-types.mjs +192 -0
  35. package/engine/base-compiler.mjs +69 -1
  36. package/engine/bundles.mjs +5 -5
  37. package/engine/compile-corpus.mjs +4 -0
  38. package/engine/content-address.mjs +14 -2
  39. package/engine/content-charset.mjs +5 -1
  40. package/engine/content-embeds.mjs +314 -0
  41. package/engine/content-html.mjs +5 -1
  42. package/engine/content-icons.mjs +5 -1
  43. package/engine/content-images.mjs +26 -5
  44. package/engine/content-index.mjs +68 -33
  45. package/engine/content-links.mjs +164 -24
  46. package/engine/dependency-bump.mjs +14 -16
  47. package/engine/frontmatter-lint.mjs +36 -23
  48. package/engine/helpers.mjs +83 -31
  49. package/engine/index-records.mjs +63 -4
  50. package/engine/index.mjs +15 -0
  51. package/engine/infobox-registry.mjs +50 -3
  52. package/engine/infobox.mjs +12 -8
  53. package/engine/item-compiler.mjs +10 -10
  54. package/engine/journals.mjs +3 -0
  55. package/engine/macros.mjs +16 -13
  56. package/engine/map-notes.mjs +29 -22
  57. package/engine/metadata-index.mjs +44 -36
  58. package/engine/note-vocabulary.mjs +71 -29
  59. package/engine/pack-router.mjs +14 -0
  60. package/engine/packagebuild-index.mjs +124 -0
  61. package/engine/packages.mjs +83 -0
  62. package/engine/pathnames.mjs +74 -0
  63. package/engine/pdf-build.mjs +133 -8
  64. package/engine/pdf-render.mjs +49 -12
  65. package/engine/scenes.mjs +12 -9
  66. package/engine/site-build.mjs +43 -16
  67. package/engine/site-index.mjs +10 -1
  68. package/engine/subtype-registry.mjs +21 -0
  69. package/engine/web-wikilinks.mjs +46 -12
  70. package/engine/wikilink-syntax.mjs +20 -2
  71. package/engine/wikilinks.mjs +15 -2
  72. package/hm3/actors.mjs +11 -38
  73. package/manifest.mjs +67 -13
  74. package/package.json +5 -2
  75. package/packagebuild-metadata.jsonl +13 -0
  76. package/sohl/actors.mjs +12 -86
  77. package/sohl/note-schemas.mjs +4 -16
  78. package/types/content-config.d.mts +8 -0
  79. package/types/engine/actor-compiler.d.mts +29 -0
  80. package/types/engine/art-fields.d.mts +192 -0
  81. package/types/engine/asset-index.d.mts +92 -0
  82. package/types/engine/asset-types.d.mts +110 -0
  83. package/types/engine/base-compiler.d.mts +42 -1
  84. package/types/engine/compile-corpus.d.mts +4 -1
  85. package/types/engine/content-charset.d.mts +4 -0
  86. package/types/engine/content-embeds.d.mts +218 -0
  87. package/types/engine/content-html.d.mts +4 -0
  88. package/types/engine/content-icons.d.mts +4 -0
  89. package/types/engine/content-images.d.mts +15 -2
  90. package/types/engine/content-index.d.mts +27 -15
  91. package/types/engine/dependency-bump.d.mts +2 -2
  92. package/types/engine/frontmatter-lint.d.mts +11 -14
  93. package/types/engine/helpers.d.mts +7 -2
  94. package/types/engine/index-records.d.mts +44 -3
  95. package/types/engine/index.d.mts +5 -0
  96. package/types/engine/infobox-registry.d.mts +36 -1
  97. package/types/engine/infobox.d.mts +6 -4
  98. package/types/engine/macros.d.mts +9 -5
  99. package/types/engine/map-notes.d.mts +10 -6
  100. package/types/engine/metadata-index.d.mts +2 -17
  101. package/types/engine/note-vocabulary.d.mts +30 -0
  102. package/types/engine/packagebuild-index.d.mts +56 -0
  103. package/types/engine/packages.d.mts +62 -0
  104. package/types/engine/pathnames.d.mts +41 -81
  105. package/types/engine/pdf-build.d.mts +58 -1
  106. package/types/engine/pdf-render.d.mts +3 -1
  107. package/types/engine/scenes.d.mts +2 -1
  108. package/types/engine/site-build.d.mts +5 -1
  109. package/types/engine/site-index.d.mts +4 -1
  110. package/types/engine/subtype-registry.d.mts +20 -0
  111. package/types/engine/web-wikilinks.d.mts +14 -4
  112. package/types/engine/wikilink-syntax.d.mts +13 -1
  113. package/types/engine/wikilinks.d.mts +7 -1
  114. package/types/manifest.d.mts +42 -1
  115. package/types/sohl/actors.d.mts +0 -1
@@ -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 };
@@ -57,8 +57,8 @@ export function lockedVersion(lock: object, name: string): string | undefined;
57
57
  * Take the newest published version of one or more dependencies.
58
58
  *
59
59
  * npm performs the resolution — so a bump that changes the dependency set is
60
- * as correct as one that moves three lines — and both JSON files are restored
61
- * to the indentation they already used.
60
+ * as correct as one that moves three lines — and both JSON files are written
61
+ * back with the indentation they already used.
62
62
  *
63
63
  * @param {object} options - Options.
64
64
  * @param {string} options.rootDir - The repository root holding `package.json`.
@@ -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";
@@ -6,6 +6,38 @@
6
6
  * toolchain ships none for it.
7
7
  */
8
8
  export function infoboxFor(system: string | undefined): object | undefined;
9
+ /**
10
+ * Whether one system compiles a document for one note.
11
+ *
12
+ * This is what a system box's _available_ asserts, and it is the compile's own
13
+ * question rather than a reading of the frontmatter. A note carrying no block
14
+ * is not a note a system has nothing for: where the pack compiling its document
15
+ * declares no `system:`, the document is built from `data:` and the field
16
+ * defaults and ships exactly like any other.
17
+ *
18
+ * Three statements answer it, and they are the three the compile itself
19
+ * follows:
20
+ *
21
+ * 1. **The map** says which document class this system makes of the note's
22
+ * type. No row, no document — and no box either, which is why a caller
23
+ * reaching here already has one.
24
+ * 2. **The router** says which pack that document goes to, read from the pack
25
+ * list this build is driven by.
26
+ * 3. **That pack's `system:`** decides the rest. Declaring one, it writes that
27
+ * system's data and takes only notes that say something about it — the rule
28
+ * {@link module:engine/base-compiler.BasePackCompiler#eligibleFor} applies,
29
+ * asked here from outside. Declaring none, it is compiled by the fallback
30
+ * pass, which needs no block and answers for
31
+ * {@link module:engine/subtype-registry.DEFAULT_DOCUMENT_SUBTYPES} alone —
32
+ * so a tree with no HM3 pack ships no HM3 document however a note is
33
+ * written.
34
+ *
35
+ * @param {object} fm - The note's frontmatter.
36
+ * @param {object} map - The system's note-type → document-subtype map.
37
+ * @param {object} router - The pack router this build is driven by.
38
+ * @returns {boolean} True when this system compiles a document for this note.
39
+ */
40
+ export function compilesSystemDocument(fm: object, map: object, router: object): boolean;
9
41
  /**
10
42
  * Every box one note carries, wired to the registries this toolchain ships.
11
43
  *
@@ -17,12 +49,15 @@ export function infoboxFor(system: string | undefined): object | undefined;
17
49
  * @param {object} [options] - Options.
18
50
  * @param {(ref: unknown, hint?: object) => object|undefined} [options.resolve] -
19
51
  * Resolves a reference to `{name, url?, uuid?, address?, subType?}`.
52
+ * @param {object} [options.router] - The pack router deciding which system
53
+ * compiles a document for this note. Defaults to the consuming repository's.
20
54
  * @returns {object[]} The boxes, in the order every medium renders them.
21
55
  * @throws {Error} When the built set disagrees with what the note's type maps
22
56
  * to — see {@link module:engine/infobox.assertInfoboxSet}.
23
57
  */
24
- export function noteInfoboxes(fm: object, { resolve }?: {
58
+ export function noteInfoboxes(fm: object, { resolve, router }?: {
25
59
  resolve?: ((ref: unknown, hint?: object) => object | undefined) | undefined;
60
+ router?: object | undefined;
26
61
  }): object[];
27
62
  /**
28
63
  * Every system's infobox declaration, in the order a page shows them.
@@ -246,9 +246,11 @@ export function systemRowsSection(fm: object, fields: readonly object[], { block
246
246
  * build ships, which decide the box set.
247
247
  * @param {readonly object[]} [options.providers] - The systems' infobox
248
248
  * declarations, keyed by `system`.
249
- * @param {(fm: object, block: string) => boolean} options.carriesBlock -
250
- * Whether the note says anything about a system, which decides
251
- * {@link NOT_AVAILABLE}.
249
+ * @param {(fm: object, map: object) => boolean} options.compilesDocument -
250
+ * Whether that system compiles a document for this note, which decides
251
+ * {@link NOT_AVAILABLE}. Asked of the routing and the passes rather than of
252
+ * the frontmatter: a pack declaring no `system:` compiles a note from `data:`
253
+ * and the field defaults, so the block's presence is not the question.
252
254
  * @param {(field: object, fm: object, opts: object) => object} options.resolveField -
253
255
  * Resolves one declared field against the note.
254
256
  * @param {(ref: unknown) => object|undefined} [options.resolve] - Resolves a
@@ -259,7 +261,7 @@ export function systemRowsSection(fm: object, fields: readonly object[], { block
259
261
  export function buildInfoboxes(fm: object, options: {
260
262
  maps: readonly object[];
261
263
  providers?: readonly object[] | undefined;
262
- carriesBlock: (fm: object, block: string) => boolean;
264
+ compilesDocument: (fm: object, map: object) => boolean;
263
265
  resolveField: (field: object, fm: object, opts: object) => object;
264
266
  resolve?: ((ref: unknown) => object | undefined) | undefined;
265
267
  vocabulary?: object | undefined;