@heroiclands/package-build 22.22.0 → 22.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/content-commands.mjs +51 -14
  3. package/ci/content-format-reference.mjs +1 -1
  4. package/content-config.mjs +80 -2
  5. package/docs/api.md +85 -72
  6. package/docs/authoring/assets.md +14 -0
  7. package/docs/authoring/dates-and-calendars.md +37 -1
  8. package/docs/authoring/links-and-markup.md +112 -31
  9. package/docs/commands.md +12 -10
  10. package/docs/configuration.md +90 -13
  11. package/docs/content-format.md +1 -1
  12. package/docs/guides/book.md +5 -4
  13. package/docs/reference/format-details.md +323 -60
  14. package/docs/reference/note-types.md +9 -9
  15. package/engine/anchors.mjs +16 -10
  16. package/engine/art-fields.mjs +118 -0
  17. package/engine/art-slots.mjs +7 -1
  18. package/engine/asset-index.mjs +160 -10
  19. package/engine/asset-replacement.mjs +93 -0
  20. package/engine/asset-types.mjs +240 -0
  21. package/engine/base-compiler.mjs +54 -2
  22. package/engine/bundles.mjs +13 -1
  23. package/engine/content-blocks.mjs +4 -4
  24. package/engine/content-embeds.mjs +19 -0
  25. package/engine/content-figures.mjs +468 -0
  26. package/engine/content-footnotes.mjs +10 -2
  27. package/engine/content-format.yaml +1 -1
  28. package/engine/content-images.mjs +126 -25
  29. package/engine/content-index.mjs +36 -10
  30. package/engine/field-reference.mjs +31 -10
  31. package/engine/field-spec.mjs +10 -8
  32. package/engine/foreign-catalog.mjs +143 -18
  33. package/engine/frontmatter-lint.mjs +73 -1
  34. package/engine/heading-attributes.mjs +31 -18
  35. package/engine/helpers.mjs +97 -30
  36. package/engine/infobox-registry.mjs +4 -1
  37. package/engine/infobox.mjs +47 -8
  38. package/engine/itinerary-scenes.mjs +4 -10
  39. package/engine/journals.mjs +264 -39
  40. package/engine/lore-events.mjs +197 -0
  41. package/engine/markdown-expressions.mjs +257 -7
  42. package/engine/note-dates.mjs +305 -12
  43. package/engine/note-vocabulary.mjs +11 -5
  44. package/engine/pathnames.mjs +36 -0
  45. package/engine/pdf-build.mjs +357 -34
  46. package/engine/pdf-images.mjs +64 -0
  47. package/engine/pdf-render.mjs +160 -36
  48. package/engine/scenes.mjs +16 -4
  49. package/engine/site-build.mjs +127 -13
  50. package/engine/site-index.mjs +72 -1
  51. package/engine/site-preview.mjs +2 -0
  52. package/engine/system-block.mjs +44 -16
  53. package/hm3/actors.mjs +2 -2
  54. package/manifest.mjs +30 -24
  55. package/package.json +1 -1
  56. package/packagebuild-metadata.jsonl +13 -13
  57. package/sohl/item-fields.mjs +4 -3
  58. package/types/content-config.d.mts +48 -1
  59. package/types/engine/anchors.d.mts +9 -4
  60. package/types/engine/art-fields.d.mts +64 -0
  61. package/types/engine/asset-index.d.mts +43 -0
  62. package/types/engine/asset-replacement.d.mts +22 -0
  63. package/types/engine/asset-types.d.mts +18 -0
  64. package/types/engine/base-compiler.d.mts +0 -17
  65. package/types/engine/content-embeds.d.mts +16 -0
  66. package/types/engine/content-figures.d.mts +101 -0
  67. package/types/engine/content-footnotes.d.mts +7 -0
  68. package/types/engine/content-images.d.mts +78 -12
  69. package/types/engine/content-index.d.mts +8 -2
  70. package/types/engine/field-reference.d.mts +21 -0
  71. package/types/engine/field-spec.d.mts +20 -16
  72. package/types/engine/foreign-catalog.d.mts +39 -0
  73. package/types/engine/heading-attributes.d.mts +13 -8
  74. package/types/engine/helpers.d.mts +23 -7
  75. package/types/engine/infobox-registry.d.mts +4 -1
  76. package/types/engine/infobox.d.mts +6 -0
  77. package/types/engine/journals.d.mts +31 -9
  78. package/types/engine/lore-events.d.mts +14 -0
  79. package/types/engine/markdown-expressions.d.mts +23 -2
  80. package/types/engine/note-dates.d.mts +64 -1
  81. package/types/engine/pathnames.d.mts +26 -0
  82. package/types/engine/pdf-build.d.mts +28 -6
  83. package/types/engine/pdf-images.d.mts +52 -0
  84. package/types/engine/pdf-render.d.mts +12 -0
  85. package/types/engine/site-build.d.mts +14 -3
  86. package/types/engine/site-index.d.mts +54 -0
  87. package/types/engine/system-block.d.mts +26 -2
  88. package/types/hm3/actors.d.mts +18 -0
  89. package/types/manifest.d.mts +19 -23
  90. package/engine/content-captions.mjs +0 -230
  91. package/types/engine/content-captions.d.mts +0 -58
@@ -66,10 +66,12 @@ import {
66
66
  renderAddress,
67
67
  isAddressTuple,
68
68
  ownDocumentSystem,
69
+ expandAddress,
69
70
  } from "./address.mjs";
70
71
  import { NOTE_SYSTEM } from "./systems.mjs";
71
72
  import { contentPackage } from "./content-package.mjs";
72
73
  import { reckoningContext } from "./reckoning-markers.mjs";
74
+ import { presentAmongRecords } from "./being-age.mjs";
73
75
  // The declared tag vocabulary, which is where `draft` is stated.
74
76
  import { isDraftNote } from "./note-vocabulary.mjs";
75
77
 
@@ -277,7 +279,12 @@ export function buildSiteIndex(
277
279
 
278
280
  return {
279
281
  contentPackage: ownPackage,
280
- dateContext: reckoningContext({ notes: records }),
282
+ // Carried on the same context `resolvedDateFields` reads: a recurring
283
+ // event's `next` is computed against this present.
284
+ dateContext: {
285
+ ...reckoningContext({ notes: records }),
286
+ present: presentAmongRecords(records),
287
+ },
281
288
  referenceTargets: buildReferenceTargets(records, new Map([...foreignReferences, ...index])),
282
289
  index,
283
290
  ambiguous,
@@ -288,6 +295,70 @@ export function buildSiteIndex(
288
295
  };
289
296
  }
290
297
 
298
+ /**
299
+ * The address keys one page's figures are reachable by — the same two forms
300
+ * {@link buildSiteIndex} assigns the page itself, so a `ref` crossing into it
301
+ * resolves by exactly the address a wikilink would use.
302
+ *
303
+ * @param {string} pkg - The page's own content package.
304
+ * @param {string} type - The note's `type`.
305
+ * @param {string} shortcode - The note's `shortcode`.
306
+ * @returns {Set<string>} The short `type/shortcode` key and the canonical
307
+ * `package-system-type-shortcode` one.
308
+ */
309
+ export function figureIndexKeys(pkg, type, shortcode) {
310
+ const system = ownDocumentSystem(type);
311
+ const keys = new Set([
312
+ `${type}/${shortcode}`.toLowerCase(),
313
+ canonicalKey(pkg, system, type, shortcode),
314
+ ]);
315
+ if (system !== NOTE_SYSTEM) keys.add(canonicalKey(pkg, NOTE_SYSTEM, type, shortcode));
316
+ return keys;
317
+ }
318
+
319
+ /**
320
+ * Resolve a `ref` call's cross-note address to the target's own figures and
321
+ * its own address — the `figures.note` a `ref` helper's render context reads,
322
+ * built once and threaded into every page.
323
+ *
324
+ * Reads the written address exactly as a wikilink does
325
+ * ({@link readQualifier}, {@link expandAddress}), so a `ref` crossing into
326
+ * another note and a `[[…]]` link to the same note agree on what resolves and
327
+ * what does not: a target naming no known type or no fetched package's index
328
+ * is unresolved for both, and one address names one note for both.
329
+ *
330
+ * @param {string} target - The address as written, anchor already removed —
331
+ * {@link module:engine/wikilink-syntax.ParsedWikilink.target}.
332
+ * @param {object} options
333
+ * @param {Set<string>} options.contentTypes - Every type this build reads as
334
+ * an address qualifier.
335
+ * @param {Set<string>} options.packages - Every package an address may name.
336
+ * @param {Set<string>} options.noIndexPackages - Packages declared
337
+ * `contentIndex: false`.
338
+ * @param {string} options.contentPackage - The package a target with no
339
+ * package segment defaults to.
340
+ * @param {Map<string, {url: string}>} options.siteIndexMap - {@link SiteIndex}'s
341
+ * own `index`, read for the target's existence and its URL.
342
+ * @param {Map<string, Map<string, object>>} options.figuresByAddress - Every
343
+ * note's figures, by id, keyed as {@link figureIndexKeys} keys the note.
344
+ * @returns {{url: string, figures: Map<string, object>}|undefined} The
345
+ * target's own address and figures, or `undefined` when the address names
346
+ * no note this build resolves.
347
+ */
348
+ export function resolveCrossNoteFigures(
349
+ target,
350
+ { contentTypes, packages, noIndexPackages, contentPackage, siteIndexMap, figuresByAddress },
351
+ ) {
352
+ const read = readQualifier(target, contentTypes, packages, noIndexPackages);
353
+ if (!read || read.reason) return undefined;
354
+ const key = expandAddress(read, { package: contentPackage, system: NOTE_SYSTEM });
355
+ const shortKey = `${read.itemDoc ? "doc" : ""}${read.type}/${read.shortcode}`.toLowerCase();
356
+ const entry = siteIndexMap.get(key) ?? siteIndexMap.get(shortKey);
357
+ if (!entry) return undefined;
358
+ const figures = figuresByAddress.get(key) ?? figuresByAddress.get(shortKey) ?? new Map();
359
+ return { url: entry.url, figures };
360
+ }
361
+
291
362
  /**
292
363
  * The per-page context a wikilink resolver takes.
293
364
  *
@@ -270,6 +270,8 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
270
270
  const pos = positionOfLiteral(text, error.src, error.occurrence);
271
271
  findings.push({ ...error, ...pos, severity: "error" });
272
272
  }
273
+ for (const error of result.embedErrors)
274
+ findings.push({ ...error, severity: "error" });
273
275
  if (findings.length) return { ok: false, findings };
274
276
  const edges = snapshot.rendered.edges.filter(([source]) => source !== original.url);
275
277
  for (const hit of result.resolved) if (hit.url) edges.push([page.url, hit.url]);
@@ -104,21 +104,24 @@
104
104
  *
105
105
  * A field's `name` is both its identity and the shared property it draws from,
106
106
  * and those coincide only while the two vocabularies agree about what the
107
- * spelling means. They do not always. An `affiliation` item's `system.title` is
108
- * the style of address an office carries — "Ajaw", "Warden"; a note's top-level
109
- * `title` is the note's own heading, which the site emitter publishes. Two
110
- * unrelated quantities, one spelling, and step 3 fed the first from the second.
107
+ * spelling means — or while the top-level vocabulary admits the spelling at
108
+ * all. They do not always. An `affiliation` item's `system.title` is the style
109
+ * of address an office carries — "Ajaw", "Warden"; a note's top-level `title`
110
+ * is refused outright by the closed top-level vocabulary, which has no `title`
111
+ * entry. Nothing relates them, and step 3 fed the first from the second anyway.
111
112
  *
112
113
  * It was not a harmless coincidence either, because step 3 answers **without**
113
114
  * applying `field.default` — only step 2 does — so an authored `title: null`
114
115
  * reached the field's coercion unguarded and shipped as the literal string
115
116
  * `"null"` in fifteen documents.
116
117
  *
117
- * So a field may declare `topLevelMeans`: what the top-level key of that name
118
- * means *instead*. Declaring it removes the whole shared level — step 3 and the
119
- * retiring 3b alike, since both read the note's top level and the objection is
120
- * to that level, not to a spelling — leaving the two positions that describe
121
- * the document rather than the note. It is deliberately
118
+ * So a field may declare `topLevelMeans`: why the note's top-level key of this
119
+ * name is not this field's source — declared wherever that key's spelling
120
+ * means something else at the note level, or the closed top-level vocabulary
121
+ * refuses it outright. Declaring it removes the whole shared level — step 3
122
+ * and the retiring 3b alike, since both read the note's top level and the
123
+ * objection is to that level, not to a spelling — leaving the two positions
124
+ * that describe the document rather than the note. It is deliberately
122
125
  * a per-field opt-out rather than a change to the order — step 3 is right
123
126
  * wherever the two levels state the same quantity, which is nearly everywhere —
124
127
  * and its value is the reason rather than a bare flag, so the collision is
@@ -362,22 +365,47 @@ export function blockProperty(fm, block, key, defaultValue = undefined) {
362
365
  }
363
366
 
364
367
  /**
365
- * A system override followed by a shared `data:` value. The top-level value
366
- * remains readable while content repositories move their authored fields.
368
+ * A system override followed by a shared `data:` value followed by the
369
+ * retiring top-level spelling `data:` gathered it off, reporting which one
370
+ * answered.
371
+ *
372
+ * {@link module:engine/field-spec.resolveFieldValue}'s counterpart for a
373
+ * universal `data:` key — `pack` and `packFolder`, declared for every note
374
+ * type rather than per type, so neither is a `FieldSpec` that resolver takes.
375
+ * The order is the same shape: a system's own override, then the shared
376
+ * `data:` position, then the bare top-level key a tree authored before
377
+ * `data:` existed. The top-level value stays readable while a tree still on
378
+ * it moves its authored fields.
367
379
  *
368
380
  * @param {object} fm - The note's frontmatter.
369
381
  * @param {string} block - The system block key.
370
382
  * @param {string} key - The shared property.
371
383
  * @param {any} [defaultValue] - Returned when no position declares it.
372
- * @returns {any} The selected value.
384
+ * @returns {{value: any, from: FieldSource}} The value, and where it came
385
+ * from.
373
386
  */
374
- export function blockDataProperty(fm, block, key, defaultValue = undefined) {
387
+ export function resolveDataProperty(fm, block, key, defaultValue = undefined) {
375
388
  const declared = systemBlock(fm, block)?.[key];
376
- if (declared !== undefined && declared !== null) return declared;
389
+ if (declared !== undefined && declared !== null) return { value: declared, from: "block" };
377
390
  const shared = isMapping(fm?.data) ? fm.data[key] : undefined;
378
- if (shared !== undefined && shared !== null) return shared;
391
+ if (shared !== undefined && shared !== null) return { value: shared, from: "shared" };
379
392
  const legacy = isMapping(fm) ? fm[key] : undefined;
380
- return legacy === undefined || legacy === null ? defaultValue : legacy;
393
+ if (legacy !== undefined && legacy !== null) return { value: legacy, from: "topLevel" };
394
+ return { value: defaultValue, from: "default" };
395
+ }
396
+
397
+ /**
398
+ * {@link resolveDataProperty}'s value alone, for a caller that only compiles
399
+ * a document and has no use for which position answered.
400
+ *
401
+ * @param {object} fm - The note's frontmatter.
402
+ * @param {string} block - The system block key.
403
+ * @param {string} key - The shared property.
404
+ * @param {any} [defaultValue] - Returned when no position declares it.
405
+ * @returns {any} The selected value.
406
+ */
407
+ export function blockDataProperty(fm, block, key, defaultValue = undefined) {
408
+ return resolveDataProperty(fm, block, key, defaultValue).value;
381
409
  }
382
410
 
383
411
  /**
package/hm3/actors.mjs CHANGED
@@ -78,7 +78,7 @@ import {
78
78
  *
79
79
  * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
80
80
  */
81
- const ACTOR_FIELDS = Object.freeze([
81
+ export const ACTOR_FIELDS = Object.freeze([
82
82
  {
83
83
  name: "species",
84
84
  to: "species",
@@ -97,7 +97,7 @@ const ACTOR_FIELDS = Object.freeze([
97
97
  *
98
98
  * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
99
99
  */
100
- const CHARACTER_FIELDS = Object.freeze([
100
+ export const CHARACTER_FIELDS = Object.freeze([
101
101
  {
102
102
  name: "data.gender",
103
103
  legacyKey: "gender",
package/manifest.mjs CHANGED
@@ -48,6 +48,7 @@
48
48
  * @module
49
49
  */
50
50
 
51
+ import { RELATIONSHIP_KEYS } from "./content-config.mjs";
51
52
  import fs from "node:fs/promises";
52
53
  import fsSync from "node:fs";
53
54
  import path from "node:path";
@@ -390,33 +391,38 @@ export function packFolderFindings({ packFolders, packs = [] }) {
390
391
  }
391
392
 
392
393
  /**
393
- * Relationship keys that direct the **build**, rather than describe the
394
- * package.
395
- *
396
- * `relationships` is the one manifest block with a second reader.
397
- * `@heroiclands/package-build` consumes it too, and v1.8.0 added
398
- * `itemCatalog: true` as an opt-in on a declared dependency: it selects that
399
- * package's Item packs as a resolution
400
- * source for the actors pass. That is an instruction to the build, not a fact
401
- * about the shipped package — Foundry's relationship schema does not define
402
- * it, and someone reading a published manifest cannot tell a build directive
403
- * from a declaration about what the package needs.
404
- *
405
- * So the block is filtered rather than copied whole. The rule is the
406
- * distinction, not the name: a key listed here answers *how is this built?*,
407
- * and every key that survives answers *what does this package depend on?*.
408
- * `itemCatalog` is the first build-time key to land on a relationship and is
409
- * unlikely to be the last.
410
- *
411
- * A list is enough, and needs no prefix agreed between the two packages,
412
- * because the input is already closed: package-build normalises a relationship
413
- * to `id`, `type`, `manifest`, `compatibility` and its own build keys, and
414
- * rejects anything else at configuration time. A key that reaches here is one
415
- * the toolchain itself put there.
394
+ * The relationship keys Foundry's own schema names.
395
+ *
396
+ * `RelatedPackage` declares exactly these (`common/packages/base-package.mjs`),
397
+ * so a key outside them is one the build put there for itself.
398
+ *
399
+ * @type {readonly string[]}
400
+ */
401
+ export const FOUNDRY_RELATIONSHIP_KEYS = Object.freeze([
402
+ "id",
403
+ "type",
404
+ "manifest",
405
+ "compatibility",
406
+ "reason",
407
+ ]);
408
+
409
+ /**
410
+ * The relationship keys the published manifest drops.
411
+ *
412
+ * Derived rather than listed: every key this package accepts on a relationship
413
+ * that Foundry's schema does not name is one the build reads for itself, so a
414
+ * key added to the configuration is dropped from the manifest without a second
415
+ * list being kept in step.
416
+ *
417
+ * The filter stays a denylist for the reason the pass-through below gives — a
418
+ * key Foundry adds later reaches the manifest unread rather than being stripped
419
+ * by an allowlist that has not heard of it.
416
420
  *
417
421
  * @type {readonly string[]}
418
422
  */
419
- export const BUILD_ONLY_RELATIONSHIP_KEYS = Object.freeze(["itemCatalog"]);
423
+ export const BUILD_ONLY_RELATIONSHIP_KEYS = Object.freeze(
424
+ RELATIONSHIP_KEYS.filter((key) => !FOUNDRY_RELATIONSHIP_KEYS.includes(key)),
425
+ );
420
426
 
421
427
  /**
422
428
  * The `relationships` block as published — every declared dependency, with the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "22.22.0",
3
+ "version": "22.23.0",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -1,13 +1,13 @@
1
- {"address":{"canonical":"packagebuild-none-image-afflictionbnr"},"asset":{"ai":"","attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/afflictionbnr.webp","source":""},"package":"packagebuild","shortcode":"afflictionbnr","type":"image"}
2
- {"address":{"canonical":"packagebuild-none-image-armorclothingbnr"},"asset":{"ai":"","attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/armorclothingbnr.webp","source":""},"package":"packagebuild","shortcode":"armorclothingbnr","type":"image"}
3
- {"address":{"canonical":"packagebuild-none-image-attributebnr"},"asset":{"ai":"","attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/attributebnr.webp","source":""},"package":"packagebuild","shortcode":"attributebnr","type":"image"}
4
- {"address":{"canonical":"packagebuild-none-image-containerbnr"},"asset":{"ai":"","attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/containerbnr.webp","source":""},"package":"packagebuild","shortcode":"containerbnr","type":"image"}
5
- {"address":{"canonical":"packagebuild-none-image-devdocsbnr"},"asset":{"ai":"","attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/devdocsbnr.webp","source":""},"package":"packagebuild","shortcode":"devdocsbnr","type":"image"}
6
- {"address":{"canonical":"packagebuild-none-image-miscgearbnr"},"asset":{"ai":"","attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/miscgearbnr.webp","source":""},"package":"packagebuild","shortcode":"miscgearbnr","type":"image"}
7
- {"address":{"canonical":"packagebuild-none-image-mysticalabilitybnr"},"asset":{"ai":"","attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/mysticalabilitybnr.webp","source":""},"package":"packagebuild","shortcode":"mysticalabilitybnr","type":"image"}
8
- {"address":{"canonical":"packagebuild-none-image-projectilebnr"},"asset":{"ai":"","attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/projectilebnr.webp","source":""},"package":"packagebuild","shortcode":"projectilebnr","type":"image"}
9
- {"address":{"canonical":"packagebuild-none-image-rulesbnr"},"asset":{"ai":"","attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/rulesbnr.webp","source":""},"package":"packagebuild","shortcode":"rulesbnr","type":"image"}
10
- {"address":{"canonical":"packagebuild-none-image-skillbnr"},"asset":{"ai":"","attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/skillbnr.webp","source":""},"package":"packagebuild","shortcode":"skillbnr","type":"image"}
11
- {"address":{"canonical":"packagebuild-none-image-traumabnr"},"asset":{"ai":"","attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/traumabnr.webp","source":""},"package":"packagebuild","shortcode":"traumabnr","type":"image"}
12
- {"address":{"canonical":"packagebuild-none-image-userguidebnr"},"asset":{"ai":"","attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/userguidebnr.webp","source":""},"package":"packagebuild","shortcode":"userguidebnr","type":"image"}
13
- {"address":{"canonical":"packagebuild-none-image-weapongearbnr"},"asset":{"ai":"","attribution":"Tom Rodriguez","license":"CC-BY-SA-4.0","notes":"","path":"images/banners/weapongearbnr.webp","source":""},"package":"packagebuild","shortcode":"weapongearbnr","type":"image"}
1
+ {"address":{"canonical":"packagebuild-none-image-afflictionbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/afflictionbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"afflictionbnr","type":"image"}
2
+ {"address":{"canonical":"packagebuild-none-image-armorclothingbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/armorclothingbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"armorclothingbnr","type":"image"}
3
+ {"address":{"canonical":"packagebuild-none-image-attributebnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/attributebnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"attributebnr","type":"image"}
4
+ {"address":{"canonical":"packagebuild-none-image-containerbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/containerbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"containerbnr","type":"image"}
5
+ {"address":{"canonical":"packagebuild-none-image-devdocsbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/devdocsbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"devdocsbnr","type":"image"}
6
+ {"address":{"canonical":"packagebuild-none-image-miscgearbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/miscgearbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"miscgearbnr","type":"image"}
7
+ {"address":{"canonical":"packagebuild-none-image-mysticalabilitybnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/mysticalabilitybnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"mysticalabilitybnr","type":"image"}
8
+ {"address":{"canonical":"packagebuild-none-image-projectilebnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/projectilebnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"projectilebnr","type":"image"}
9
+ {"address":{"canonical":"packagebuild-none-image-rulesbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/rulesbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"rulesbnr","type":"image"}
10
+ {"address":{"canonical":"packagebuild-none-image-skillbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/skillbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"skillbnr","type":"image"}
11
+ {"address":{"canonical":"packagebuild-none-image-traumabnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/traumabnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"traumabnr","type":"image"}
12
+ {"address":{"canonical":"packagebuild-none-image-userguidebnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/userguidebnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"userguidebnr","type":"image"}
13
+ {"address":{"canonical":"packagebuild-none-image-weapongearbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/weapongearbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"weapongearbnr","type":"image"}
@@ -391,9 +391,10 @@ export const ITEM_FIELDS = Object.freeze({
391
391
  ...STRING,
392
392
  default: "",
393
393
  topLevelMeans:
394
- "An office's style of address belongs to the membership that " +
395
- "holds the office, so nothing outside the item supplies it. " +
396
- "Author it at `sohl.system.title` on the item, or at the " +
394
+ "A top-level `title` is refused outright, since an office's " +
395
+ "style of address belongs to the membership that holds the " +
396
+ "office and nothing outside the item supplies it. Author it " +
397
+ "at `sohl.system.title` on the item, or at the " +
397
398
  "`system.title` of the entry in a being's `sohl.items`.",
398
399
  describe: "The style of address the office carries.",
399
400
  },
@@ -126,6 +126,16 @@ export const RETIRED_ADDRESS_KEYS: Readonly<Record<string, string>>;
126
126
  export const DEFAULT_ADDRESS_SCHEME: Readonly<{
127
127
  prefix: "";
128
128
  }>;
129
+ /**
130
+ * Every key a declared relationship may carry.
131
+ *
132
+ * Read by the manifest writer as well as by the configuration check: the keys
133
+ * Foundry's own relationship schema does not name are the build's own, and the
134
+ * published manifest carries none of them.
135
+ *
136
+ * @type {readonly string[]}
137
+ */
138
+ export const RELATIONSHIP_KEYS: readonly string[];
129
139
  /**
130
140
  * How the loader hands {@link defineConfig} the system version it resolved.
131
141
  *
@@ -274,7 +284,8 @@ export type PathsInput = {
274
284
  unpack?: string | undefined;
275
285
  /**
276
286
  * Where a dependency declaring
277
- * `itemCatalog: true` is unpacked.
287
+ * `itemCatalog: true` or
288
+ * `assetArchive: true` is unpacked.
278
289
  * Inbound, and fetched rather than
279
290
  * committed.
280
291
  */
@@ -438,6 +449,21 @@ export type RelationshipSpec = {
438
449
  * `_stats.systemVersion` is stamped from.
439
450
  */
440
451
  compatibility?: CompatibilitySpec | undefined;
452
+ /**
453
+ * Whether `deps fetch` unpacks this
454
+ * dependency's release archive and extracts
455
+ * its Item packs, so the actors pass can
456
+ * resolve embedded items this repository
457
+ * does not hold. Needs a `manifest`.
458
+ */
459
+ itemCatalog?: boolean | undefined;
460
+ /**
461
+ * Whether `deps fetch` unpacks this
462
+ * dependency's release archive for its
463
+ * asset bytes alone, building no item
464
+ * catalogue from it. Needs a `manifest`.
465
+ */
466
+ assetArchive?: boolean | undefined;
441
467
  /**
442
468
  * Whether `deps fetch` fetches this
443
469
  * dependency's content index. Default
@@ -449,6 +475,16 @@ export type RelationshipSpec = {
449
475
  * index.
450
476
  */
451
477
  contentIndex?: boolean | undefined;
478
+ /**
479
+ * Whether this dependency's asset
480
+ * tree answers this package's own asset
481
+ * addresses ahead of its local record.
482
+ * Default `false`. Refuses `contentIndex:
483
+ * false` on the same entry, since a
484
+ * replacement with no fetched index to
485
+ * check against resolves nothing.
486
+ */
487
+ assetReplacement?: boolean | undefined;
452
488
  };
453
489
  /**
454
490
  * How a generated documentation page is framed in the repository publishing it.
@@ -628,6 +664,12 @@ export type ContentBuildConfigInput = {
628
664
  * `Templates`). Default `[]`.
629
665
  */
630
666
  skipDirectories?: string[] | undefined;
667
+ /**
668
+ * Whether `lint` refuses an asset
669
+ * record of this package carrying
670
+ * `ai: true`. Default `false`.
671
+ */
672
+ forbidGeneratedArt?: boolean | undefined;
631
673
  /**
632
674
  * Reserved for
633
675
  * `@heroiclands/package-build`, which
@@ -764,6 +806,11 @@ export type ContentBuildConfig = {
764
806
  */
765
807
  docEntryTypes: ReadonlySet<string>;
766
808
  skipDirectories: readonly string[];
809
+ /**
810
+ * Whether `lint` refuses an asset
811
+ * record of this package carrying `ai: true`.
812
+ */
813
+ forbidGeneratedArt: boolean;
767
814
  /**
768
815
  * The
769
816
  * fonts this package ships and the names it
@@ -1,17 +1,22 @@
1
1
  /**
2
- * The `{#slug}` and `:::caption {#slug}` anchors a note declares.
2
+ * The `{#slug}` and `:::figure {#slug}` anchors a note declares.
3
3
  *
4
- * A bare `#` heading starts a journal page without declaring a slug. Captioned
5
- * blocks declare a slug and start an addressable journal page.
4
+ * A bare `#` heading starts a journal page without declaring a slug, and so
5
+ * does a figure fence that declares no id. A fence that declares one declares a
6
+ * slug and starts an addressable journal page.
6
7
  *
7
8
  * @param {string} body - The note's markdown body, frontmatter already removed.
8
9
  * @param {number} [bodyLine] - The 1-based file line the body starts on, from
9
10
  * `parseMarkdownFile`. Anchors are reported at their position in the **file**,
10
11
  * so an editor can jump straight to one; passing nothing numbers from the body.
12
+ * @param {(address: string) => string|undefined} [resolveRole] - From a
13
+ * picture's address to the role its asset declares, so a figure anchor's
14
+ * `name` reads `Map 1` rather than `Figure 1` where it is due — see
15
+ * {@link module:engine/content-figures.scanFigures}.
11
16
  * @returns {Array<{slug: string, name: string, level: number, line: number}>}
12
17
  * In document order.
13
18
  */
14
- export function collectAnchors(body: string, bodyLine?: number): Array<{
19
+ export function collectAnchors(body: string, bodyLine?: number, resolveRole?: (address: string) => string | undefined): Array<{
15
20
  slug: string;
16
21
  name: string;
17
22
  level: number;
@@ -61,6 +61,29 @@ export function assetAddressIndex(records?: readonly object[], { config, foreign
61
61
  } | undefined;
62
62
  types?: Iterable<string> | undefined;
63
63
  }): object;
64
+ /**
65
+ * What a renderer needs to size one picture, keyed by the pathname its
66
+ * address resolves to.
67
+ *
68
+ * An image reaches a renderer as the resolved pathname an embed or an authored
69
+ * body image already carries — see {@link readAssetAddress} — never as the
70
+ * address that produced it, so the lookup a renderer wants is by pathname, not
71
+ * by {@link assetAddressIndex}'s own `assets` map.
72
+ *
73
+ * @param {readonly object[]} records - Asset records, local or foreign.
74
+ * @returns {Map<string, {type: string, role?: string, width: number|"", height: number|""}>}
75
+ * One entry per addressable file, carrying only what a renderer sizes a
76
+ * picture from. `role` is omitted when the record states none; `width` and
77
+ * `height` are always present, blank (`""`) for a vector — the record's own
78
+ * convention, carried through rather than collapsed into an absence a
79
+ * renderer could not tell apart from "no asset resolved at all".
80
+ */
81
+ export function assetImageInfoByPathname(records?: readonly object[]): Map<string, {
82
+ type: string;
83
+ role?: string;
84
+ width: number | "";
85
+ height: number | "";
86
+ }>;
64
87
  /**
65
88
  * The asset one authored value names, or why it names none.
66
89
  *
@@ -116,6 +139,47 @@ export function resolveArtRecord(index: object, value: unknown, defaultType: str
116
139
  path: string;
117
140
  };
118
141
  } | null;
142
+ /**
143
+ * Every asset's role, keyed by the pathname it resolves to — the form a body
144
+ * carries once an embed's address has been rewritten into the ordinary image
145
+ * every surface renders ({@link module:engine/content-embeds.resolveEmbeds}).
146
+ *
147
+ * This is the lookup a `:::figure` fence's `map` counter reaches through —
148
+ * see {@link module:engine/content-figures.scanFigures}'s `resolveRole` —
149
+ * for a caller that reads a body after that rewrite: a Foundry journal, an
150
+ * item or actor's documentation, and the book.
151
+ *
152
+ * @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex},
153
+ * or the equivalent the site and the book build.
154
+ * @returns {Map<string, string>} Pathname → role, one entry per asset that
155
+ * declares one.
156
+ */
157
+ export function pathnameRoles(index: object): Map<string, string>;
158
+ /**
159
+ * Flag a note whose art slot names a picture that is not the size that slot
160
+ * is cut to.
161
+ *
162
+ * The size is stated by the slot itself, in
163
+ * {@link module:engine/art-slots.ART_SLOTS}, so it is written once and a slot
164
+ * that states none asks nothing of the picture it names. Today the hero image
165
+ * is the one slot with a fixed strip to fill; a picture written in prose is
166
+ * fitted to the room it has and is never measured here.
167
+ *
168
+ * A vector carries no pixel size to compare, and neither does a raster whose
169
+ * header cannot be read; neither is flagged.
170
+ *
171
+ * @param {readonly object[]} records - The corpus, notes and assets together.
172
+ * @param {object} [opts]
173
+ * @param {object} [opts.config] - The resolved build configuration.
174
+ * @param {string} [opts.assetsBase] - Where the asset roots sit, so a finding
175
+ * names the file from the working directory as every other finding does.
176
+ * @returns {object[]} A finding per picture off its slot's size,
177
+ * `severity: "error"`.
178
+ */
179
+ export function checkArtSlotSizes(records?: readonly object[], { config, assetsBase }?: {
180
+ config?: object | undefined;
181
+ assetsBase?: string | undefined;
182
+ }): object[];
119
183
  /**
120
184
  * The pathname an art value names, in the form an authored one takes.
121
185
  *
@@ -19,6 +19,16 @@ export function collectAssetRecords(assetsBase: string, { contentPackage, proble
19
19
  contentPackage: string;
20
20
  problems?: object[] | undefined;
21
21
  }): Array<Record<string, any>>;
22
+ /**
23
+ * The closed set of roles a picture may declare.
24
+ *
25
+ * Shared with the field declaration below and with every medium that sizes a
26
+ * picture by its role, so the vocabulary cannot drift between the record that
27
+ * carries it and the renderer that reads it.
28
+ *
29
+ * @type {readonly string[]}
30
+ */
31
+ export const ASSET_ROLES: readonly string[];
22
32
  /**
23
33
  * The file a directory records provenance for its subtree in.
24
34
  *
@@ -47,6 +57,18 @@ export const PROVENANCE_SIDECAR_SUFFIX: string;
47
57
  * state this key. Omitting one is a finding rather than a blank, because a
48
58
  * record resolves wholesale: the nearest one is the whole answer, so a key it
49
59
  * leaves out is not inherited from above but simply absent.
60
+ * @property {"boolean"|"integer"} [type] - The value's own type, for a field
61
+ * whose answer is not a string. Omitted for every string field; `ai` carries
62
+ * the YAML boolean itself rather than a stringified copy, and `width` and
63
+ * `height` carry the pixel count the walk measured. Each is refused rather
64
+ * than coerced when the value does not match.
65
+ * @property {readonly string[]} [values] - The closed set a provenance
66
+ * declaration must choose one of; a value outside it is a finding and the
67
+ * key is left blank, the same treatment an `ai` value of the wrong shape
68
+ * gets. `role` is the one field that carries it.
69
+ * @property {string} [onlyType] - The one asset type this field may be
70
+ * declared for. A provenance record stating it for another type is a
71
+ * finding, and the value is dropped rather than carried through.
50
72
  * @property {string} describe - One line, for the author-facing reference.
51
73
  */
52
74
  /**
@@ -105,6 +127,27 @@ export type AssetRecordField = {
105
127
  * leaves out is not inherited from above but simply absent.
106
128
  */
107
129
  required?: boolean | undefined;
130
+ /**
131
+ * - The value's own type, for a field
132
+ * whose answer is not a string. Omitted for every string field; `ai` carries
133
+ * the YAML boolean itself rather than a stringified copy, and `width` and
134
+ * `height` carry the pixel count the walk measured. Each is refused rather
135
+ * than coerced when the value does not match.
136
+ */
137
+ type?: "boolean" | "integer" | undefined;
138
+ /**
139
+ * - The closed set a provenance
140
+ * declaration must choose one of; a value outside it is a finding and the
141
+ * key is left blank, the same treatment an `ai` value of the wrong shape
142
+ * gets. `role` is the one field that carries it.
143
+ */
144
+ values?: readonly string[] | undefined;
145
+ /**
146
+ * - The one asset type this field may be
147
+ * declared for. A provenance record stating it for another type is a
148
+ * finding, and the value is dropped rather than carried through.
149
+ */
150
+ onlyType?: string | undefined;
108
151
  /**
109
152
  * - One line, for the author-facing reference.
110
153
  */