@heroiclands/package-build 22.22.0 → 22.24.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 (140) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -0
  3. package/bin/content-commands.mjs +51 -14
  4. package/ci/content-format-reference.mjs +1 -1
  5. package/content-config.mjs +80 -2
  6. package/docs/api.md +85 -72
  7. package/docs/authoring/assets.md +29 -0
  8. package/docs/authoring/dates-and-calendars.md +37 -1
  9. package/docs/authoring/links-and-markup.md +112 -31
  10. package/docs/commands.md +30 -29
  11. package/docs/configuration.md +92 -19
  12. package/docs/content-format.md +1 -1
  13. package/docs/guides/book.md +5 -4
  14. package/docs/guides/module-project.md +2 -2
  15. package/docs/guides/site.md +41 -0
  16. package/docs/project-setup.md +4 -4
  17. package/docs/reference/format-details.md +323 -60
  18. package/docs/reference/note-types.md +9 -9
  19. package/docs/reference/theme-pages.md +496 -0
  20. package/engine/anchors.mjs +16 -10
  21. package/engine/art-fields.mjs +118 -0
  22. package/engine/art-slots.mjs +9 -2
  23. package/engine/asset-index.mjs +160 -10
  24. package/engine/asset-replacement.mjs +93 -0
  25. package/engine/asset-types.mjs +240 -0
  26. package/engine/base-compiler.mjs +54 -2
  27. package/engine/bundles.mjs +13 -1
  28. package/engine/content-blocks.mjs +4 -4
  29. package/engine/content-embeds.mjs +19 -0
  30. package/engine/content-figures.mjs +468 -0
  31. package/engine/content-footnotes.mjs +10 -2
  32. package/engine/content-format.yaml +1 -1
  33. package/engine/content-images.mjs +126 -25
  34. package/engine/content-index.mjs +36 -10
  35. package/engine/field-reference.mjs +31 -10
  36. package/engine/field-spec.mjs +10 -8
  37. package/engine/foreign-catalog.mjs +145 -19
  38. package/engine/frontmatter-lint.mjs +73 -1
  39. package/engine/heading-attributes.mjs +31 -18
  40. package/engine/helpers.mjs +102 -33
  41. package/engine/infobox-registry.mjs +4 -1
  42. package/engine/infobox.mjs +47 -8
  43. package/engine/itinerary-scenes.mjs +4 -10
  44. package/engine/journals.mjs +264 -39
  45. package/engine/lore-events.mjs +197 -0
  46. package/engine/markdown-expressions.mjs +257 -7
  47. package/engine/note-dates.mjs +305 -12
  48. package/engine/note-vocabulary.mjs +11 -5
  49. package/engine/pathnames.mjs +36 -0
  50. package/engine/pdf-build.mjs +357 -34
  51. package/engine/pdf-images.mjs +64 -0
  52. package/engine/pdf-render.mjs +160 -36
  53. package/engine/scenes.mjs +16 -4
  54. package/engine/site-build.mjs +127 -13
  55. package/engine/site-config.mjs +19 -24
  56. package/engine/site-index.mjs +72 -1
  57. package/engine/site-preview.mjs +2 -0
  58. package/engine/system-block.mjs +44 -16
  59. package/hm3/actors.mjs +2 -2
  60. package/hugo-theme/data/banners.yaml +69 -0
  61. package/hugo-theme/layouts/404.html +85 -0
  62. package/hugo-theme/layouts/_default/baseof.html +64 -0
  63. package/hugo-theme/layouts/_default/list.html +139 -0
  64. package/hugo-theme/layouts/_default/single.html +4 -0
  65. package/hugo-theme/layouts/blog/list.html +54 -0
  66. package/hugo-theme/layouts/blog-post/single.html +36 -0
  67. package/hugo-theme/layouts/index.html +75 -0
  68. package/hugo-theme/layouts/partials/banner-url.html +117 -0
  69. package/hugo-theme/layouts/partials/cdn-url.html +36 -0
  70. package/hugo-theme/layouts/partials/draft-notice.html +41 -0
  71. package/hugo-theme/layouts/partials/draft-state.html +26 -0
  72. package/hugo-theme/layouts/partials/footer.html +29 -0
  73. package/hugo-theme/layouts/partials/header.html +35 -0
  74. package/hugo-theme/layouts/partials/hero-banner.html +86 -0
  75. package/hugo-theme/layouts/partials/holdings/entry.html +21 -0
  76. package/hugo-theme/layouts/partials/holdings/kind-groups.html +71 -0
  77. package/hugo-theme/layouts/partials/holdings.html +61 -0
  78. package/hugo-theme/layouts/partials/infobox/entry.html +15 -0
  79. package/hugo-theme/layouts/partials/infobox/items.html +26 -0
  80. package/hugo-theme/layouts/partials/infobox/section.html +56 -0
  81. package/hugo-theme/layouts/partials/infobox/value.html +43 -0
  82. package/hugo-theme/layouts/partials/infobox.html +57 -0
  83. package/hugo-theme/layouts/partials/page-body.html +154 -0
  84. package/hugo-theme/layouts/partials/place-map.html +9 -0
  85. package/hugo-theme/layouts/partials/related.html +121 -0
  86. package/hugo-theme/layouts/partials/search.html +84 -0
  87. package/hugo-theme/layouts/partials/site-url.html +32 -0
  88. package/hugo-theme/layouts/partials/tags.html +30 -0
  89. package/hugo-theme/layouts/partials/toc.html +28 -0
  90. package/hugo-theme/layouts/partials/warn-missing-banner.html +3 -0
  91. package/hugo-theme/layouts/shortcodes/cartouche.html +64 -0
  92. package/hugo-theme/layouts/shortcodes/exturl.html +12 -0
  93. package/hugo-theme/layouts/shortcodes/glyph.html +43 -0
  94. package/hugo-theme/layouts/shortcodes/pagelist.html +63 -0
  95. package/hugo-theme/static/css/sohl-icons.css +57 -0
  96. package/hugo-theme/static/css/style.css +2953 -0
  97. package/hugo-theme/static/fonts/game-icons-kb.woff2 +0 -0
  98. package/hugo-theme/static/img/banners/takheperu-hieroglyphs.svg +1 -0
  99. package/hugo-theme/static/img/cartouches/takheperu.svg +1 -0
  100. package/hugo-theme/theme.toml +11 -0
  101. package/manifest.mjs +30 -24
  102. package/package.json +2 -1
  103. package/packagebuild-metadata.jsonl +13 -13
  104. package/sohl/item-fields.mjs +4 -3
  105. package/types/content-config.d.mts +48 -1
  106. package/types/engine/anchors.d.mts +9 -4
  107. package/types/engine/art-fields.d.mts +64 -0
  108. package/types/engine/art-slots.d.mts +2 -1
  109. package/types/engine/asset-index.d.mts +43 -0
  110. package/types/engine/asset-replacement.d.mts +22 -0
  111. package/types/engine/asset-types.d.mts +18 -0
  112. package/types/engine/base-compiler.d.mts +0 -17
  113. package/types/engine/content-embeds.d.mts +16 -0
  114. package/types/engine/content-figures.d.mts +101 -0
  115. package/types/engine/content-footnotes.d.mts +7 -0
  116. package/types/engine/content-images.d.mts +78 -12
  117. package/types/engine/content-index.d.mts +8 -2
  118. package/types/engine/field-reference.d.mts +21 -0
  119. package/types/engine/field-spec.d.mts +20 -16
  120. package/types/engine/foreign-catalog.d.mts +39 -0
  121. package/types/engine/heading-attributes.d.mts +13 -8
  122. package/types/engine/helpers.d.mts +28 -10
  123. package/types/engine/infobox-registry.d.mts +4 -1
  124. package/types/engine/infobox.d.mts +6 -0
  125. package/types/engine/journals.d.mts +31 -9
  126. package/types/engine/lore-events.d.mts +14 -0
  127. package/types/engine/markdown-expressions.d.mts +23 -2
  128. package/types/engine/note-dates.d.mts +64 -1
  129. package/types/engine/pathnames.d.mts +26 -0
  130. package/types/engine/pdf-build.d.mts +28 -6
  131. package/types/engine/pdf-images.d.mts +52 -0
  132. package/types/engine/pdf-render.d.mts +12 -0
  133. package/types/engine/site-build.d.mts +14 -3
  134. package/types/engine/site-config.d.mts +9 -9
  135. package/types/engine/site-index.d.mts +54 -0
  136. package/types/engine/system-block.d.mts +26 -2
  137. package/types/hm3/actors.d.mts +18 -0
  138. package/types/manifest.d.mts +19 -23
  139. package/engine/content-captions.mjs +0 -230
  140. package/types/engine/content-captions.d.mts +0 -58
@@ -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
  *
@@ -2,7 +2,8 @@
2
2
  * The four art slots, in the order the specification tabulates them.
3
3
  *
4
4
  * `banner` is the one that reaches no compiled document: it is the page's hero
5
- * image, read by the site and by the book's section plates and by nothing else.
5
+ * image, read by the site and by nothing else. The book's section plates read a
6
+ * banner of their own, from the book plan's `presentation.page.banner`.
6
7
  * That is what `document: false` states, and it is why the inert-art check
7
8
  * skips it — a key that is *meant* to reach no document is not an inert key.
8
9
  *
@@ -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
  */
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The replacement record for one asset address, or `null`.
3
+ *
4
+ * @param {string} address - A canonical asset address.
5
+ * @param {object} opts
6
+ * @param {string} opts.localPackage - The compiling package's own segment.
7
+ * @param {readonly string[]} opts.replacements - The package segments of
8
+ * every relationship declaring `assetReplacement: true`, in declaration
9
+ * order.
10
+ * @param {Map<string, object>} opts.index - The merged content index, keyed
11
+ * by canonical address.
12
+ * @returns {{record: object, package: string}|null} The first replacement
13
+ * carrying the address, or `null` when none does.
14
+ */
15
+ export function resolveAssetReplacement(address: string, { localPackage, replacements, index }: {
16
+ localPackage: string;
17
+ replacements: readonly string[];
18
+ index: Map<string, object>;
19
+ }): {
20
+ record: object;
21
+ package: string;
22
+ } | null;
@@ -27,6 +27,24 @@ export function assetTypeOfRoot(root: unknown): AssetType | undefined;
27
27
  * @returns {boolean} Whether it matches {@link ADDRESS_SEGMENT_PATTERN}.
28
28
  */
29
29
  export function isAssetShortcode(shortcode: string): boolean;
30
+ /**
31
+ * The pixel dimensions a raster image's own header states.
32
+ *
33
+ * Read from the file's bytes directly rather than through a decoder, so a
34
+ * corrupt or partial file fails closed — the same `undefined` answer as a
35
+ * format this carries no reader for — instead of throwing out of the asset
36
+ * walk. An SVG has no pixel dimensions to read; stating one would be a
37
+ * fabricated viewBox reading rather than a fact about the file.
38
+ *
39
+ * @param {string} absPath - The file's absolute path.
40
+ * @param {string} ext - Its extension, lowercase and dot-led.
41
+ * @returns {{width: number, height: number}|undefined} The declared size, or
42
+ * `undefined` when the format carries none, or the header cannot be read.
43
+ */
44
+ export function imageDimensions(absPath: string, ext: string): {
45
+ width: number;
46
+ height: number;
47
+ } | undefined;
30
48
  /**
31
49
  * The `<system>` segment every asset address carries.
32
50
  *
@@ -1,20 +1,3 @@
1
- /**
2
- * The tallies one pass accumulates while walking the tree.
3
- *
4
- * `declined` and `skippedOther` are deliberately separate numbers. A declined
5
- * note is one this build **refused** — it declares a retired frontmatter field
6
- * — and it is an error; a skipped one legitimately belongs to another pass, and
7
- * there are thousands of those. Folding the first into the second is what let a
8
- * whole tree be filtered out in silence.
9
- *
10
- * @typedef {object} PassStats
11
- * @property {number} compiled - Notes that became a document.
12
- * @property {number} skippedNoId - Notes with no `id`, where that is tolerated.
13
- * @property {number} skippedOther - Notes this pass does not claim.
14
- * @property {number} declined - Notes this pack **refused** — one declaring a
15
- * retired frontmatter field, or one routed to a system pack whose system it
16
- * says nothing about. Counted as errors, never as skips.
17
- */
18
1
  /**
19
2
  * The shared walk → filter → expand → convert → build → write → count loop.
20
3
  *
@@ -45,6 +45,22 @@ export function resolveEmbed(index: object, embed: ParsedEmbed): {
45
45
  target: string;
46
46
  type?: string;
47
47
  };
48
+ /**
49
+ * The role an address's asset declares, read through the same resolution an
50
+ * embed's address takes.
51
+ *
52
+ * This is the lookup a `:::figure` fence's `map` counter reaches through —
53
+ * see {@link module:engine/content-figures.scanFigures}'s `resolveRole` — for
54
+ * an address still in its authored form, before {@link resolveEmbeds} has
55
+ * rewritten it into the ordinary image every surface renders.
56
+ *
57
+ * @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex},
58
+ * or the equivalent the site and the book build.
59
+ * @param {string} address - The address as authored, inside `![[...]]`.
60
+ * @returns {string|undefined} The role, or `undefined` for a note's address,
61
+ * one that resolves nowhere, or a picture that declares none.
62
+ */
63
+ export function embedRole(index: object, address: string): string | undefined;
48
64
  /**
49
65
  * What is wrong with one embed's directive or placement, if anything.
50
66
  *
@@ -0,0 +1,101 @@
1
+ /** Number a sequence of figures with counters shared across documents. */
2
+ export function numberFigures(figures: any, counts: any): any;
3
+ /**
4
+ * Read every `:::figure` fence in a body.
5
+ *
6
+ * SQL fences are expanded before this pass, so their output is a table.
7
+ *
8
+ * @param {string} source - A note body with expanded tables.
9
+ * @param {object} [options] - Options.
10
+ * @param {(address: string) => string|undefined} [options.resolveRole] - From
11
+ * a picture's address to the role its asset declares, so a picture whose
12
+ * asset declares `role: map` counts as `map` rather than `figure`. Omitted,
13
+ * every picture counts as `figure` — the same reading a role other than
14
+ * `map` gets.
15
+ * @returns {{figures: Array<{id: string, slug: string, caption: string,
16
+ * hasCaption: boolean, classes: string[], kind: string, number: number,
17
+ * label: string, line: number, bodyStart: number, bodyEnd: number,
18
+ * captionStart: number, captionEnd: number, close: number}>,
19
+ * errors: Array<{line: number, column: number, message: string}>}}
20
+ */
21
+ export function scanFigures(source: string, { resolveRole }?: {
22
+ resolveRole?: ((address: string) => string | undefined) | undefined;
23
+ }): {
24
+ figures: Array<{
25
+ id: string;
26
+ slug: string;
27
+ caption: string;
28
+ hasCaption: boolean;
29
+ classes: string[];
30
+ kind: string;
31
+ number: number;
32
+ label: string;
33
+ line: number;
34
+ bodyStart: number;
35
+ bodyEnd: number;
36
+ captionStart: number;
37
+ captionEnd: number;
38
+ close: number;
39
+ }>;
40
+ errors: Array<{
41
+ line: number;
42
+ column: number;
43
+ message: string;
44
+ }>;
45
+ };
46
+ /**
47
+ * Render figure fences as HTML, leaving other Markdown untouched.
48
+ *
49
+ * A code, table or prose figure's contents are left as Markdown inside the
50
+ * wrapper, blank-line separated from its tags exactly as
51
+ * {@link module:engine/content-blocks.renderBlocks} leaves a named block's
52
+ * body — so the surrounding render sees it as part of its own document and a
53
+ * footnote reference inside it resolves against the note's own definitions.
54
+ *
55
+ * A figure's contents are rendered here rather than left for a later pass,
56
+ * because a picture-only fence has to become the surface's own `<figure>`
57
+ * markup directly inside the wrapper this function writes; the surrounding
58
+ * render never sees it as Markdown, so a footnote reference inside one is not
59
+ * resolved, the same gap a block's own title carries.
60
+ *
61
+ * @param {string} source - A note body with expanded tables.
62
+ * @param {(markdown: string) => string} [renderMarkdown] - How to render a
63
+ * figure's own contents.
64
+ * @param {Array<{id: string, label: string}>} [numbers] - Labels assigned by a
65
+ * surface that counts across documents, matched by id.
66
+ * @param {object} [options] - Options.
67
+ * @param {(address: string) => string|undefined} [options.resolveRole] - See
68
+ * {@link scanFigures}.
69
+ * @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>}}
70
+ */
71
+ export function renderFigureBlocks(source: string, renderMarkdown?: (markdown: string) => string, numbers?: Array<{
72
+ id: string;
73
+ label: string;
74
+ }>, { resolveRole }?: {
75
+ resolveRole?: ((address: string) => string | undefined) | undefined;
76
+ }): {
77
+ markdown: string;
78
+ errors: Array<{
79
+ line: number;
80
+ column: number;
81
+ message: string;
82
+ }>;
83
+ };
84
+ /** A figure's automatically determined kind, and the word that labels it. */
85
+ export const FIGURE_NAMES: Readonly<{
86
+ code: "Code";
87
+ table: "Table";
88
+ figure: "Figure";
89
+ map: "Map";
90
+ prose: "Prose";
91
+ }>;
92
+ /**
93
+ * The classes a figure takes.
94
+ *
95
+ * `.border` states that the captioned thing is drawn inside a border, and each
96
+ * medium owns what that means. The vocabulary is closed: a class absent from it
97
+ * is a finding naming the class.
98
+ *
99
+ * @type {readonly string[]}
100
+ */
101
+ export const FIGURE_CLASSES: readonly string[];
@@ -81,3 +81,10 @@ export function footnoteFindings(source: string): Array<{
81
81
  column: number;
82
82
  message: string;
83
83
  }>;
84
+ /**
85
+ * A footnote reference's shape, wherever it is written — excluding a
86
+ * definition's own `[^id]:` opener, which this never matches.
87
+ *
88
+ * @type {RegExp}
89
+ */
90
+ export const FOOTNOTE_REFERENCE: RegExp;