@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
@@ -146,26 +146,14 @@ const BEING_FIELDS = Object.freeze([
146
146
  * business; their authored vocabulary is the same, so they share one
147
147
  * declaration rather than three copies that could drift.
148
148
  *
149
- * `img` is the one required field — the compiler refuses a map note without it,
150
- * since a scene with no background is not a map. It was spelled `image` and
151
- * read from the `sohl:` block; both were read while the trees were
152
- * swept; that retirement has since completed and `image` is gone.
149
+ * `data.bgImage` is the one required piece of art — the compiler refuses a map
150
+ * note without it, since a scene with no background is not a map. It is
151
+ * declared in the note vocabulary rather than here, because it is an art slot
152
+ * like every other and a Scene has no `img` for the shared key to reach.
153
153
  *
154
154
  * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
155
155
  */
156
156
  const MAP_FIELDS = Object.freeze([
157
- {
158
- name: "img",
159
- ...STRING,
160
- required: true,
161
- // Art is not system-specific — a Scene is a core Foundry document, and
162
- // every other note type carries its `img` at the note's top level.
163
- shared: true,
164
- describe:
165
- "The scene's background image. Owned by whichever package its first segment " +
166
- "names — `systems/…` and `modules/…` are emitted unchanged, anything else is " +
167
- "this package's own and is rooted under its assets.",
168
- },
169
157
  {
170
158
  name: "dimensions",
171
159
  ...LIST,
@@ -66,6 +66,7 @@ export const PACKAGE_KINDS: readonly ["systems", "modules", "documentation"];
66
66
  export const DOCUMENTATION_KIND: string;
67
67
  export namespace DEFAULT_PATHS {
68
68
  let content: "assets/content";
69
+ let assets: "assets";
69
70
  let contentIndex: "build/content-index";
70
71
  let packJson: "build/packs-json";
71
72
  let stage: "build/stage/packs";
@@ -299,6 +300,12 @@ export type PathsInput = {
299
300
  * Content tree root.
300
301
  */
301
302
  content?: string | undefined;
303
+ /**
304
+ * The asset roots' parent — the directory
305
+ * holding `icons/`, `images/` and
306
+ * `audio/`.
307
+ */
308
+ assets?: string | undefined;
302
309
  /**
303
310
  * Where `content-index` writes this
304
311
  * package's note index. Outbound, and a
@@ -338,6 +345,7 @@ export type PathsInput = {
338
345
  */
339
346
  export type ResolvedPaths = {
340
347
  content: string;
348
+ assets: string;
341
349
  contentIndex: string;
342
350
  packJson: string;
343
351
  stage: string;
@@ -182,6 +182,29 @@ export class SystemActorCompiler extends BasePackCompiler {
182
182
  itemsSourceDirs?: never[] | undefined;
183
183
  foreignSourceDirs?: never[] | undefined;
184
184
  });
185
+ /**
186
+ * The actor's two pieces of art, resolved, with the being default beneath
187
+ * them.
188
+ *
189
+ * **The default is chosen from the note's tags**, which only a compiler can
190
+ * read: a `character` falls back to one file and a `creature` to another,
191
+ * and both are addresses in the package that ships them. A tree whose index
192
+ * cannot answer that address — a dependency not yet fetched — falls through
193
+ * to the subtype's own default, so the document is never left with no art.
194
+ *
195
+ * `tokenIcon` unset follows `icon`, and the fallback is applied after
196
+ * resolution rather than before: a note naming an icon and no token icon
197
+ * means "the token wears the profile art", not "the token wears the
198
+ * default".
199
+ *
200
+ * @param {object} fm - The note's frontmatter.
201
+ * @param {string} fallback - The subtype's own default art path.
202
+ * @returns {{img: string, token: string}} The two paths.
203
+ */
204
+ actorArt(fm: object, fallback: string): {
205
+ img: string;
206
+ token: string;
207
+ };
185
208
  /** @type {readonly string[]} */
186
209
  itemsSourceDirs: readonly string[];
187
210
  foreignSourceDirs: any;
@@ -260,6 +283,12 @@ export class SystemActorCompiler extends BasePackCompiler {
260
283
  * every embedded id exactly where it was.
261
284
  * Returns null if the descriptor cannot be resolved.
262
285
  *
286
+ * **An entry's `data:` is the authoring container, not a document field.**
287
+ * Its art slot is read here and the container itself goes no further, so an
288
+ * entry writing `data.icon` reaches the document as a resolved `img` and an
289
+ * entry writing none takes its type's default — the same two rules an item
290
+ * note compiles under, reached by the same functions.
291
+ *
263
292
  * @param {Map<string, object>} itemsMap - The predefined items, by address.
264
293
  * @param {string} actorId - The owning actor's id, seeding embedded ids.
265
294
  * @param {string} type - The **note** type the reference names.
@@ -0,0 +1,192 @@
1
+ /**
2
+ * The art slot one key names, or `undefined`.
3
+ *
4
+ * @param {unknown} key - The key under `data:`.
5
+ * @returns {ArtSlot|undefined} The slot.
6
+ */
7
+ export function artSlot(key: unknown): ArtSlot | undefined;
8
+ /**
9
+ * The address an authored art value names.
10
+ *
11
+ * @param {string} value - The value as authored.
12
+ * @param {string} defaultType - The type the field declares.
13
+ * @returns {string} A written address, which may be partial.
14
+ */
15
+ export function artTarget(value: string, defaultType: string): string;
16
+ /**
17
+ * The address space an asset reference resolves against, from a corpus.
18
+ *
19
+ * Shaped exactly as {@link module:engine/wikilinks.buildWikilinkIndex}'s result
20
+ * is in the parts a resolver reads, so the site, the book and the pack compilers
21
+ * answer one authored address the same way.
22
+ *
23
+ * **The note types belong in `types` as well as the asset ones.** Without them
24
+ * `being-thorn` does not parse as an address at all, and an embed naming a note
25
+ * is reported as an unknown type on one surface and as the wrong kind of type on
26
+ * another — one mistake, two verdicts, which is what the shared vocabulary
27
+ * exists to prevent.
28
+ *
29
+ * @param {readonly object[]} records - The corpus, from
30
+ * {@link module:engine/content-index.indexRecordsFor}.
31
+ * @param {object} [opts]
32
+ * @param {object} [opts.config] - The resolved build configuration.
33
+ * @param {{index?: Map<string, object>, packages?: Iterable<string>}} [opts.foreign] -
34
+ * The vendored indexes a dependency published.
35
+ * @param {Iterable<string>} [opts.types] - The note types this tree knows.
36
+ * @returns {object} The index.
37
+ */
38
+ export function assetAddressIndex(records?: readonly object[], { config, foreign, types }?: {
39
+ config?: object | undefined;
40
+ foreign?: {
41
+ index?: Map<string, object>;
42
+ packages?: Iterable<string>;
43
+ } | undefined;
44
+ types?: Iterable<string> | undefined;
45
+ }): object;
46
+ /**
47
+ * The asset one authored value names, or why it names none.
48
+ *
49
+ * The whole lookup in one place, because two callers need it and they need
50
+ * different halves of the answer: an art slot needs the record, and an embed
51
+ * needs to tell an address that resolves to nothing from one that reaches the
52
+ * wrong *kind* of type. Those are different mistakes with different fixes, and a
53
+ * single `null` would collapse them into one message.
54
+ *
55
+ * Local files answer first and foreign ones after, which is an ordering of maps
56
+ * rather than a precedence rule: an address carries its own package, so the two
57
+ * cannot both hold one.
58
+ *
59
+ * @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex},
60
+ * or the equivalent the site and the book build.
61
+ * @param {unknown} value - The value as authored.
62
+ * @param {string} defaultType - The type a bare value takes.
63
+ * @returns {{record: {package: string, asset: {path: string}}, pathname: string}
64
+ * |{record: null, reason: string, type?: string}} The asset and the pathname
65
+ * it is at, or a reason from
66
+ * {@link module:engine/wikilink-syntax.LINK_FINDING_REASONS}.
67
+ */
68
+ export function readAssetAddress(index: object, value: unknown, defaultType: string): {
69
+ record: {
70
+ package: string;
71
+ asset: {
72
+ path: string;
73
+ };
74
+ };
75
+ pathname: string;
76
+ } | {
77
+ record: null;
78
+ reason: string;
79
+ type?: string;
80
+ };
81
+ /**
82
+ * The index record an art value resolves to, or `null`.
83
+ *
84
+ * @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex}.
85
+ * @param {unknown} value - The value as authored.
86
+ * @param {string} defaultType - The type the field declares.
87
+ * @returns {{package: string, asset: {path: string}}|null} The record.
88
+ */
89
+ export function resolveArtRecord(index: object, value: unknown, defaultType: string): {
90
+ package: string;
91
+ asset: {
92
+ path: string;
93
+ };
94
+ } | null;
95
+ /**
96
+ * The pathname an art value names, in the form an authored one takes.
97
+ *
98
+ * Handing the result to {@link module:engine/helpers.resolveImg} is what puts an
99
+ * art address and a body image through one ownership rule. The two empties
100
+ * survive it unchanged: `null` and an absent key mean *no art named, apply the
101
+ * default*, and `""` means *ship blank on purpose*.
102
+ *
103
+ * @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex}.
104
+ * @param {unknown} value - The value as authored.
105
+ * @param {string} defaultType - The type the field declares.
106
+ * @returns {{pathname: string|null, resolved: boolean}} The pathname, and
107
+ * whether an address was actually answered — which tells a caller applying a
108
+ * default apart from one whose address named nothing.
109
+ */
110
+ export function artPathname(index: object, value: unknown, defaultType: string): {
111
+ pathname: string | null;
112
+ resolved: boolean;
113
+ };
114
+ /**
115
+ * The default art address a being's own tags choose, or `null`.
116
+ *
117
+ * `character` means a **person**, not a human, and `creature` everything else;
118
+ * a being carries exactly one of the two, which is what makes the choice a
119
+ * lookup rather than a precedence rule.
120
+ *
121
+ * @param {object} fm - The note's frontmatter.
122
+ * @returns {string|null} The address, or `null` for a note carrying neither tag.
123
+ */
124
+ export function beingDefaultArt(fm: object): string | null;
125
+ /**
126
+ * What an unresolved art address is reported as.
127
+ *
128
+ * One wording, so the four compilers that can meet the case do not each invent
129
+ * their own.
130
+ *
131
+ * @param {string} key - The key that was authored.
132
+ * @param {unknown} value - The value it carried.
133
+ * @param {string} defaultType - The type the field declares.
134
+ * @returns {string} The message.
135
+ */
136
+ export function unresolvedArtMessage(key: string, value: unknown, defaultType: string): string;
137
+ /**
138
+ * One art slot: the key a note authors, and the type a bare value takes.
139
+ *
140
+ * @typedef {object} ArtSlot
141
+ * @property {string} key - The key under `data:`.
142
+ * @property {string} type - The asset type a bare shortcode defaults to.
143
+ * @property {boolean} document - Whether the slot reaches a compiled document.
144
+ * @property {string} describe - One line, for the author-facing reference.
145
+ */
146
+ /**
147
+ * The four art slots, in the order the specification tabulates them.
148
+ *
149
+ * `banner` is the one that reaches no compiled document: it is the page's hero
150
+ * image, read by the site and by the book's section plates and by nothing else.
151
+ * That is what `document: false` states, and it is why the inert-art check
152
+ * skips it — a key that is *meant* to reach no document is not an inert key.
153
+ *
154
+ * @type {readonly ArtSlot[]}
155
+ */
156
+ export const ART_SLOTS: readonly ArtSlot[];
157
+ /**
158
+ * The art a being falls back to, by the kind it is tagged.
159
+ *
160
+ * Both files ship in `sohl`, under `assets/icons/other/`, and both are named
161
+ * here as addresses rather than as paths for the reason every art reference is:
162
+ * the address resolves to a record that carries the owning package, so a
163
+ * package borrowing the default gets the same file the system ships.
164
+ *
165
+ * **Only the compiler can choose between them**, because only the compiler
166
+ * reads the note's tags. A schema default is the last resort beneath this one,
167
+ * and covers a world document created by hand, which no note describes.
168
+ *
169
+ * @type {Readonly<Record<string, string>>}
170
+ */
171
+ export const BEING_DEFAULT_ART: Readonly<Record<string, string>>;
172
+ /**
173
+ * One art slot: the key a note authors, and the type a bare value takes.
174
+ */
175
+ export type ArtSlot = {
176
+ /**
177
+ * - The key under `data:`.
178
+ */
179
+ key: string;
180
+ /**
181
+ * - The asset type a bare shortcode defaults to.
182
+ */
183
+ type: string;
184
+ /**
185
+ * - Whether the slot reaches a compiled document.
186
+ */
187
+ document: boolean;
188
+ /**
189
+ * - One line, for the author-facing reference.
190
+ */
191
+ describe: string;
192
+ };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Read a package's asset trees into index records.
3
+ *
4
+ * @param {string} assetsBase - The package's asset directory, the three roots'
5
+ * parent. A directory that does not exist yields nothing: a package with no
6
+ * art is ordinary.
7
+ * @param {object} options - Options.
8
+ * @param {string} options.contentPackage - The package the trees belong to.
9
+ * @param {object[]} [options.problems] - Supplied by a **reader**: a file that
10
+ * cannot be addressed is pushed here as a diagnostic and skipped. Omitted, it
11
+ * throws — the contract the emitter needs, since an index missing an asset
12
+ * asserts that the address does not exist.
13
+ * @returns {Array<Record<string, any>>} One record per addressable file, in walk
14
+ * order; the caller imposes the index's total order.
15
+ * @throws {Error} When a file cannot be addressed and no `problems` array was
16
+ * supplied. `file` rides on the error.
17
+ */
18
+ export function collectAssetRecords(assetsBase: string, { contentPackage, problems }: {
19
+ contentPackage: string;
20
+ problems?: object[] | undefined;
21
+ }): Array<Record<string, any>>;
22
+ /**
23
+ * The file a directory records provenance for its subtree in.
24
+ *
25
+ * @type {string}
26
+ */
27
+ export const PROVENANCE_FILE: string;
28
+ /**
29
+ * The suffix a per-file provenance sidecar carries.
30
+ *
31
+ * Appended to the **whole** filename, extension included — `anvil.svg.yaml`,
32
+ * not `anvil.yaml` — because the address holds one file whose format is free to
33
+ * change, and a sidecar named after the address alone would be orphaned the day
34
+ * an SVG became a WebP without anything saying so.
35
+ *
36
+ * @type {string}
37
+ */
38
+ export const PROVENANCE_SIDECAR_SUFFIX: string;
39
+ /**
40
+ * One field of the `asset` block.
41
+ *
42
+ * @typedef {object} AssetRecordField
43
+ * @property {string} name - The key inside `asset`.
44
+ * @property {"walk"|"provenance"} from - Where the value comes from: the walk
45
+ * itself, or the provenance record resolved for the address.
46
+ * @property {string} describe - One line, for the author-facing reference.
47
+ */
48
+ /**
49
+ * Every field an `asset` block carries, in the order it is documented.
50
+ *
51
+ * **The emitter builds a record from this list**, rather than from a literal
52
+ * that a second list somewhere else would have to be kept in step with. So a
53
+ * field added here is emitted, a field removed here stops being emitted, and the
54
+ * completeness guard derives what it checks from the same declaration instead of
55
+ * hand-copying it.
56
+ *
57
+ * Every field is always present, blank where nothing states it. A fixed record
58
+ * shape is what lets a consumer read `asset.license` without branching on
59
+ * whether the package happened to record one, and blank is the honest answer to
60
+ * "what does this package say about where this file came from" when it says
61
+ * nothing.
62
+ *
63
+ * @type {readonly AssetRecordField[]}
64
+ */
65
+ export const ASSET_RECORD_FIELDS: readonly AssetRecordField[];
66
+ /**
67
+ * The keys a provenance file may declare.
68
+ *
69
+ * Derived from {@link ASSET_RECORD_FIELDS} rather than restated, so the file
70
+ * format and the record cannot disagree about which keys exist.
71
+ *
72
+ * @type {ReadonlySet<string>}
73
+ */
74
+ export const PROVENANCE_KEYS: ReadonlySet<string>;
75
+ /**
76
+ * One field of the `asset` block.
77
+ */
78
+ export type AssetRecordField = {
79
+ /**
80
+ * - The key inside `asset`.
81
+ */
82
+ name: string;
83
+ /**
84
+ * - Where the value comes from: the walk
85
+ * itself, or the provenance record resolved for the address.
86
+ */
87
+ from: "walk" | "provenance";
88
+ /**
89
+ * - One line, for the author-facing reference.
90
+ */
91
+ describe: string;
92
+ };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Whether a type name addresses a file rather than a note.
3
+ *
4
+ * The one test the rewrite scoping rests on: a rewrite rule may substitute an
5
+ * asset and nothing else, so a fourth asset type is covered by this answer
6
+ * rather than by editing a list somewhere else.
7
+ *
8
+ * @param {unknown} type - The type name.
9
+ * @returns {boolean} True for `icon`, `image` or `audio`.
10
+ */
11
+ export function isAssetType(type: unknown): boolean;
12
+ /**
13
+ * The asset type a root directory declares, or `undefined`.
14
+ *
15
+ * @param {unknown} root - A directory name below `paths.assets`.
16
+ * @returns {AssetType|undefined} The type it holds.
17
+ */
18
+ export function assetTypeOfRoot(root: unknown): AssetType | undefined;
19
+ /**
20
+ * Whether a filename can be an address at all.
21
+ *
22
+ * A shortcode is lowercase alphanumerics, so a version string, a hyphen or a
23
+ * date stamp in a basename means the file cannot be addressed. The build says so
24
+ * rather than inventing a shortcode for it.
25
+ *
26
+ * @param {string} shortcode - The basename with its extension removed.
27
+ * @returns {boolean} Whether it matches {@link ADDRESS_SEGMENT_PATTERN}.
28
+ */
29
+ export function isAssetShortcode(shortcode: string): boolean;
30
+ /**
31
+ * The `<system>` segment every asset address carries.
32
+ *
33
+ * Spelled here rather than imported from `engine/systems.mjs` so this module
34
+ * stays a leaf. The two are held to one value by the address round-trip guard,
35
+ * which reads both.
36
+ *
37
+ * @type {string}
38
+ */
39
+ export const ASSET_SYSTEM: string;
40
+ /**
41
+ * File extensions that make a file a picture, lowercase and dot-led.
42
+ *
43
+ * Foundry's own `IMAGE_FILE_EXTENSIONS`, because these files are installed into
44
+ * a Foundry data directory and a format Foundry will not display is not one this
45
+ * toolchain should hand it an address for.
46
+ *
47
+ * @type {readonly string[]}
48
+ */
49
+ export const IMAGE_EXTENSIONS: readonly string[];
50
+ /**
51
+ * File extensions that make a file a sound, lowercase and dot-led.
52
+ *
53
+ * Foundry's own `AUDIO_FILE_EXTENSIONS`, for the reason above.
54
+ *
55
+ * @type {readonly string[]}
56
+ */
57
+ export const AUDIO_EXTENSIONS: readonly string[];
58
+ /**
59
+ * One asset type: what it is called, which directory holds it, and which files
60
+ * in that directory are assets of it.
61
+ *
62
+ * @typedef {object} AssetType
63
+ * @property {string} type - The type name, and the third segment of an address.
64
+ * @property {string} root - The directory below `paths.assets` that holds it.
65
+ * @property {readonly string[]} extensions - Lowercase, dot-led.
66
+ * @property {string} describe - One line, for the author-facing reference.
67
+ */
68
+ /**
69
+ * The three asset types, in address order.
70
+ *
71
+ * The directory is named for what it holds and the type for what an address
72
+ * reaches, so the two differ by a letter and the mapping is **declared** rather
73
+ * than derived from the name.
74
+ *
75
+ * `icon` and `image` are two types rather than one because an icon has to stay
76
+ * coherent drawn into a 32×32 slot while an image is unbounded — a fitness
77
+ * property of the asset itself. They therefore have separate shortcode
78
+ * namespaces, and `icon-anvil` and `image-anvil` are different addresses.
79
+ *
80
+ * @type {readonly AssetType[]}
81
+ */
82
+ export const ASSET_TYPES: readonly AssetType[];
83
+ /**
84
+ * Every asset type name.
85
+ *
86
+ * @type {ReadonlySet<string>}
87
+ */
88
+ export const ASSET_TYPE_NAMES: ReadonlySet<string>;
89
+ /**
90
+ * One asset type: what it is called, which directory holds it, and which files
91
+ * in that directory are assets of it.
92
+ */
93
+ export type AssetType = {
94
+ /**
95
+ * - The type name, and the third segment of an address.
96
+ */
97
+ type: string;
98
+ /**
99
+ * - The directory below `paths.assets` that holds it.
100
+ */
101
+ root: string;
102
+ /**
103
+ * - Lowercase, dot-led.
104
+ */
105
+ extensions: readonly string[];
106
+ /**
107
+ * - One line, for the author-facing reference.
108
+ */
109
+ describe: string;
110
+ };
@@ -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