@heroiclands/package-build 22.21.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 (152) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/assets/styles/content-base.css +162 -0
  3. package/bin/content-commands.mjs +77 -17
  4. package/bin/package-build.mjs +15 -2
  5. package/ci/content-format-reference.mjs +68 -1
  6. package/config.mjs +26 -0
  7. package/content-config.mjs +153 -6
  8. package/docs/api.md +120 -89
  9. package/docs/authoring/assets.md +14 -0
  10. package/docs/authoring/dates-and-calendars.md +41 -3
  11. package/docs/authoring/frontmatter.md +2 -2
  12. package/docs/authoring/links-and-markup.md +325 -35
  13. package/docs/commands.md +12 -10
  14. package/docs/configuration.md +117 -14
  15. package/docs/content-format.md +4 -2
  16. package/docs/guides/book.md +5 -4
  17. package/docs/guides/reference-pages.md +15 -0
  18. package/docs/reference/format-details.md +545 -104
  19. package/docs/reference/note-types.md +30 -13
  20. package/engine/actor-compiler.mjs +23 -0
  21. package/engine/anchored-sections.mjs +35 -4
  22. package/engine/anchors.mjs +26 -30
  23. package/engine/art-fields.mjs +118 -0
  24. package/engine/art-slots.mjs +7 -1
  25. package/engine/asset-index.mjs +160 -10
  26. package/engine/asset-replacement.mjs +93 -0
  27. package/engine/asset-types.mjs +240 -0
  28. package/engine/base-compiler.mjs +139 -22
  29. package/engine/base-styles.mjs +149 -0
  30. package/engine/bundles.mjs +13 -2
  31. package/engine/compendiums.mjs +8 -1
  32. package/engine/content-blocks.mjs +410 -0
  33. package/engine/content-embeds.mjs +19 -0
  34. package/engine/content-figures.mjs +468 -0
  35. package/engine/content-footnotes.mjs +197 -0
  36. package/engine/content-format-check.mjs +15 -12
  37. package/engine/content-format.yaml +1 -1
  38. package/engine/content-images.mjs +126 -25
  39. package/engine/content-index.mjs +38 -12
  40. package/engine/content-links.mjs +14 -6
  41. package/engine/content-tables.mjs +45 -4
  42. package/engine/content-tasklists.mjs +189 -0
  43. package/engine/docs-checks.mjs +4 -4
  44. package/engine/extension-attributes.mjs +48 -2
  45. package/engine/field-reference.mjs +31 -10
  46. package/engine/field-spec.mjs +10 -8
  47. package/engine/foreign-catalog.mjs +143 -18
  48. package/engine/frontmatter-lint.mjs +120 -65
  49. package/engine/frontmatter.mjs +3 -3
  50. package/engine/heading-attributes.mjs +321 -0
  51. package/engine/helpers.mjs +109 -41
  52. package/engine/homepage.mjs +8 -8
  53. package/engine/index.mjs +3 -0
  54. package/engine/infobox-registry.mjs +4 -1
  55. package/engine/infobox-render.mjs +15 -3
  56. package/engine/infobox.mjs +129 -15
  57. package/engine/itinerary-scenes.mjs +4 -10
  58. package/engine/journals.mjs +294 -50
  59. package/engine/lore-events.mjs +197 -0
  60. package/engine/macros.mjs +1 -1
  61. package/engine/markdown-expressions.mjs +434 -59
  62. package/engine/note-dates.mjs +354 -12
  63. package/engine/note-frontmatter.mjs +7 -16
  64. package/engine/note-vocabulary.mjs +183 -42
  65. package/engine/page-lists.mjs +358 -0
  66. package/engine/pathnames.mjs +36 -0
  67. package/engine/pdf-build.mjs +431 -74
  68. package/engine/pdf-images.mjs +64 -0
  69. package/engine/pdf-render.mjs +563 -66
  70. package/engine/rank-ladder.mjs +198 -0
  71. package/engine/reckoning-markers.mjs +1 -1
  72. package/engine/retired-fields.mjs +8 -13
  73. package/engine/scenes.mjs +26 -7
  74. package/engine/site-build.mjs +166 -23
  75. package/engine/site-config.mjs +10 -1
  76. package/engine/site-index.mjs +90 -3
  77. package/engine/site-preview.mjs +20 -7
  78. package/engine/social-ties.mjs +52 -14
  79. package/engine/sql-tables.mjs +66 -9
  80. package/engine/standings.mjs +89 -6
  81. package/engine/system-block.mjs +56 -30
  82. package/engine/web-wikilinks.mjs +48 -1
  83. package/hm3/actors.mjs +8 -5
  84. package/manifest.mjs +43 -24
  85. package/package.json +1 -1
  86. package/packagebuild-metadata.jsonl +13 -13
  87. package/sohl/actors.mjs +22 -99
  88. package/sohl/item-fields.mjs +5 -5
  89. package/sohl/items.mjs +1 -1
  90. package/sohl/note-schemas.mjs +11 -21
  91. package/types/config.d.mts +9 -0
  92. package/types/content-config.d.mts +48 -1
  93. package/types/engine/actor-compiler.d.mts +11 -0
  94. package/types/engine/anchored-sections.d.mts +24 -0
  95. package/types/engine/anchors.d.mts +9 -4
  96. package/types/engine/art-fields.d.mts +64 -0
  97. package/types/engine/asset-index.d.mts +43 -0
  98. package/types/engine/asset-replacement.d.mts +22 -0
  99. package/types/engine/asset-types.d.mts +18 -0
  100. package/types/engine/base-compiler.d.mts +19 -17
  101. package/types/engine/base-styles.d.mts +93 -0
  102. package/types/engine/content-blocks.d.mts +87 -0
  103. package/types/engine/content-embeds.d.mts +16 -0
  104. package/types/engine/content-figures.d.mts +101 -0
  105. package/types/engine/content-footnotes.d.mts +80 -0
  106. package/types/engine/content-format-check.d.mts +8 -6
  107. package/types/engine/content-images.d.mts +78 -12
  108. package/types/engine/content-index.d.mts +8 -2
  109. package/types/engine/content-tables.d.mts +12 -4
  110. package/types/engine/content-tasklists.d.mts +72 -0
  111. package/types/engine/extension-attributes.d.mts +22 -1
  112. package/types/engine/field-reference.d.mts +21 -0
  113. package/types/engine/field-spec.d.mts +20 -16
  114. package/types/engine/foreign-catalog.d.mts +39 -0
  115. package/types/engine/frontmatter.d.mts +1 -1
  116. package/types/engine/heading-attributes.d.mts +179 -0
  117. package/types/engine/helpers.d.mts +29 -10
  118. package/types/engine/homepage.d.mts +7 -7
  119. package/types/engine/index.d.mts +1 -0
  120. package/types/engine/infobox-registry.d.mts +4 -1
  121. package/types/engine/infobox-render.d.mts +6 -1
  122. package/types/engine/infobox.d.mts +6 -0
  123. package/types/engine/journals.d.mts +41 -12
  124. package/types/engine/lore-events.d.mts +14 -0
  125. package/types/engine/markdown-expressions.d.mts +46 -2
  126. package/types/engine/note-dates.d.mts +64 -1
  127. package/types/engine/note-frontmatter.d.mts +1 -4
  128. package/types/engine/note-vocabulary.d.mts +82 -6
  129. package/types/engine/page-lists.d.mts +126 -0
  130. package/types/engine/pathnames.d.mts +26 -0
  131. package/types/engine/pdf-build.d.mts +28 -6
  132. package/types/engine/pdf-images.d.mts +52 -0
  133. package/types/engine/pdf-render.d.mts +84 -2
  134. package/types/engine/rank-ladder.d.mts +32 -0
  135. package/types/engine/retired-fields.d.mts +4 -6
  136. package/types/engine/site-build.d.mts +27 -4
  137. package/types/engine/site-config.d.mts +9 -0
  138. package/types/engine/site-index.d.mts +67 -1
  139. package/types/engine/site-preview.d.mts +1 -1
  140. package/types/engine/sql-tables.d.mts +31 -7
  141. package/types/engine/standings.d.mts +21 -3
  142. package/types/engine/system-block.d.mts +37 -14
  143. package/types/engine/web-wikilinks.d.mts +3 -1
  144. package/types/hm3/actors.d.mts +21 -3
  145. package/types/manifest.d.mts +19 -23
  146. package/types/sohl/actors.d.mts +6 -7
  147. package/engine/content-admonitions.mjs +0 -114
  148. package/engine/content-captions.mjs +0 -181
  149. package/engine/content-secrets.mjs +0 -112
  150. package/types/engine/content-admonitions.d.mts +0 -24
  151. package/types/engine/content-captions.d.mts +0 -45
  152. package/types/engine/content-secrets.d.mts +0 -17
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",
@@ -227,9 +227,9 @@ export class Hm3Actors extends SystemActorCompiler {
227
227
  /**
228
228
  * Build every embedded item an HM3 actor carries, from `hm3.items`.
229
229
  *
230
- * One list, not two: HM3 keeps a character's abilities in `system.abilities`
231
- * rather than as embedded documents, so there is no attributes map to
232
- * expand the way SoHL's pass expands `sohl.attributes`.
230
+ * HM3 keeps a character's abilities in `system.abilities` rather than as
231
+ * embedded documents, so an ability reaches the document through the
232
+ * `hm3.system` passthrough and never through this list.
233
233
  *
234
234
  * @param {Map<string, object>} itemsMap - The predefined items, by address.
235
235
  * @param {string} actorId - The owning actor's id, seeding embedded ids.
@@ -333,6 +333,9 @@ export class Hm3Actors extends SystemActorCompiler {
333
333
  isAddress: true,
334
334
  });
335
335
 
336
+ this.checkAnchoredSection(body || "", "appearance", ctx);
337
+ this.checkAnchoredSection(body || "", "dossier", ctx);
338
+
336
339
  const system = {
337
340
  // `system.bioImage` is **not** written here, for the reason SoHL's
338
341
  // `system.portrait` is not: a being's portrait is the lead image of
package/manifest.mjs CHANGED
@@ -48,10 +48,12 @@
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";
54
55
 
56
+ import { withBaseStyle } from "./engine/base-styles.mjs";
55
57
  import { emitDiagnostic, formatDiagnostic, positionOfYamlPath } from "./engine/diagnostics.mjs";
56
58
  import { metadataFileName } from "./engine/metadata-index.mjs";
57
59
 
@@ -389,33 +391,38 @@ export function packFolderFindings({ packFolders, packs = [] }) {
389
391
  }
390
392
 
391
393
  /**
392
- * Relationship keys that direct the **build**, rather than describe the
393
- * package.
394
- *
395
- * `relationships` is the one manifest block with a second reader.
396
- * `@heroiclands/package-build` consumes it too, and v1.8.0 added
397
- * `itemCatalog: true` as an opt-in on a declared dependency: it selects that
398
- * package's Item packs as a resolution
399
- * source for the actors pass. That is an instruction to the build, not a fact
400
- * about the shipped package — Foundry's relationship schema does not define
401
- * it, and someone reading a published manifest cannot tell a build directive
402
- * from a declaration about what the package needs.
403
- *
404
- * So the block is filtered rather than copied whole. The rule is the
405
- * distinction, not the name: a key listed here answers *how is this built?*,
406
- * and every key that survives answers *what does this package depend on?*.
407
- * `itemCatalog` is the first build-time key to land on a relationship and is
408
- * unlikely to be the last.
409
- *
410
- * A list is enough, and needs no prefix agreed between the two packages,
411
- * because the input is already closed: package-build normalises a relationship
412
- * to `id`, `type`, `manifest`, `compatibility` and its own build keys, and
413
- * rejects anything else at configuration time. A key that reaches here is one
414
- * 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.
415
420
  *
416
421
  * @type {readonly string[]}
417
422
  */
418
- 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
+ );
419
426
 
420
427
  /**
421
428
  * The `relationships` block as published — every declared dependency, with the
@@ -548,6 +555,18 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
548
555
 
549
556
  const merged = { ...declared, ...derived };
550
557
 
558
+ // The shared base stylesheet, named ahead of whatever the package declares.
559
+ // Derived rather than declared, because every package needs it and four of
560
+ // the five that do have no stylesheet of their own to name it from. See
561
+ // `engine/base-styles.mjs` for the layer it loads into and why that is what
562
+ // lets a package override it.
563
+ const styles = withBaseStyle({
564
+ declared: declared.styles,
565
+ artifact: /** @type {"system"|"module"} */ (artifact),
566
+ enabled: config.packageBuild?.baseStyles !== false,
567
+ });
568
+ if (styles !== undefined) merged.styles = styles;
569
+
551
570
  // The index every consumer resolves this package's addresses through.
552
571
  // Written unconditionally, because a package that publishes no
553
572
  // index is one nothing can link into — and the failure of an absent one is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "22.21.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"}
package/sohl/actors.mjs CHANGED
@@ -18,10 +18,9 @@
18
18
  * reference translation through the system's map, embedding and its stable ids,
19
19
  * the anchored prose sections — lives in
20
20
  * {@link module:engine/actor-compiler}, where a second system reaches it.
21
- * What is left here is SoHL's data model: the body structure and its
22
- * movement profiles, the attributes-and-items frontmatter that becomes embedded
23
- * documents, the opening mastery level a skill is baked with, and the `system`
24
- * block itself.
21
+ * What is left here is SoHL's data model: the `sohl.items` frontmatter that
22
+ * becomes embedded documents, the opening mastery level a skill is baked with,
23
+ * and the `system` block itself.
25
24
  *
26
25
  * One content type today, and the actor subtype it produces is **declared**
27
26
  * rather than assumed to be the same word: `sohl/document-subtypes.mjs` maps
@@ -29,10 +28,16 @@
29
28
  * `ACTOR_VAULT_TYPE = "being"` here and emitted `type: "being"` several hundred
30
29
  * lines below, which made the two vocabularies agree by coincidence.
31
30
  *
32
- * Attributes (`sohl.attributes` map) become embedded attribute items with
33
- * `scoreBase` set from the map value. Each entry in `sohl.items` is similarly
34
- * resolved by `(type, shortcode)` and deep-merged with the entry's other
35
- * properties. `sohl.skills` is ignored.
31
+ * Each entry in `sohl.items` is resolved by `(type, shortcode)` and deep-merged
32
+ * with the entry's other properties, so an attribute carries its `scoreBase`
33
+ * and a skill its `masteryLevelBase` as that entry's own `system` overlay.
34
+ *
35
+ * **A being's own document fields are authored under `sohl.system`**, which
36
+ * {@link module:engine/system-block.mergeSystemData} writes at the DataModel's
37
+ * own paths: a being's body is `sohl.system.body`, with
38
+ * `sohl.system.currentMoveMedium` and `sohl.system.movementProfiles` beside it.
39
+ * So this pass writes no body itself, and an incorporeal being authors none and
40
+ * keeps the schema's empty one.
36
41
  *
37
42
  * **A membership is derived, never authored here.** `data.affiliations` names
38
43
  * the bodies a being belongs to and the standing it holds in each, and each
@@ -105,60 +110,12 @@ function defaultActorImg(subType) {
105
110
  return img;
106
111
  }
107
112
 
108
- /**
109
- * Normalize a being's persisted `system.body` from a `sohl.body` block. The
110
- * authoring frontmatter mirrors the schema field-for-field: `sohl.body` nests
111
- * `structure` / `weight` / `reachBase` / `bodyScaleBase` / `personalFatigue`,
112
- * exactly like `system.body`.
113
- */
114
- function normalizeBody(bodyObj) {
115
- const b = bodyObj && typeof bodyObj === "object" ? bodyObj : {};
116
- const weight = b.weight || {};
117
- return {
118
- structure: b.structure ?? { parts: [], adjacent: [] },
119
- weight: {
120
- base: weight.base == null ? null : Number(weight.base),
121
- calc: String(weight.calc ?? "0"),
122
- },
123
- reachBase: Number(b.reachBase ?? 0) || 0,
124
- bodyScaleBase: Number(b.bodyScaleBase ?? 1) || 1,
125
- personalFatigue: String(b.personalFatigue ?? "enc"),
126
- };
127
- }
128
-
129
- /** Normalize per-medium movement profiles from a `sohl.movementProfiles` list. */
130
- function normalizeMovementProfiles(list) {
131
- return (Array.isArray(list) ? list : []).map((p) => ({
132
- medium: String(p.medium ?? "terrestrial"),
133
- feetPerRound: Number(p.feetPerRound ?? 0) || 0,
134
- leaguesPerWatch: Number(p.leaguesPerWatch ?? 0) || 0,
135
- encumbrance: String(p.encumbrance ?? "0"),
136
- strMod: String(p.strMod ?? "0"),
137
- disabled: Boolean(p.disabled ?? false),
138
- }));
139
- }
140
-
141
- /**
142
- * Extract a being's body (+ its movement) from a `sohl` block that mirrors the
143
- * schema: `sohl.body` (nested → `system.body`) and the flat
144
- * `sohl.currentMoveMedium` / `sohl.movementProfiles` (→ the base-actor movement
145
- * fields; movement is a universal actor capability, not part of the body).
146
- */
147
- function extractBodyAndMovement(fm) {
148
- return {
149
- body: normalizeBody(sohlField(fm, "body", {})),
150
- currentMoveMedium: String(sohlField(fm, "currentMoveMedium", "none")),
151
- movementProfiles: normalizeMovementProfiles(sohlField(fm, "movementProfiles", [])),
152
- };
153
- }
154
-
155
113
  /**
156
114
  * SoHL's Actor compile pass.
157
115
  *
158
116
  * Declares SoHL's note-type → document-subtype map and builds a `being` note's
159
- * document: the body structure and its movement profiles, the embedded
160
- * attribute and item documents the frontmatter names, and the `system` block.
161
- * Everything else is {@link module:engine/actor-compiler}'s.
117
+ * document: the embedded item documents the frontmatter names, and the `system`
118
+ * block. Everything else is {@link module:engine/actor-compiler}'s.
162
119
  */
163
120
  export class Actors extends SystemActorCompiler {
164
121
  /**
@@ -184,30 +141,12 @@ export class Actors extends SystemActorCompiler {
184
141
  }
185
142
 
186
143
  /**
187
- * Build all embedded items for an actor: one per `sohl.attributes`
188
- * entry plus one per `sohl.items` entry. `sohl.skills` is ignored.
144
+ * Build all embedded items for an actor: one per `sohl.items` entry, plus
145
+ * one per body the being belongs to.
189
146
  */
190
147
  buildEmbeddedItems(itemsMap, actorId, fm, ctx) {
191
148
  const items = [];
192
149
 
193
- const attributes = sohlField(fm, "attributes", null);
194
- if (attributes && typeof attributes === "object") {
195
- for (const [shortcode, value] of Object.entries(attributes)) {
196
- const overlay = { system: { scoreBase: Number(value) || 0 } };
197
- const embedded = this.resolveEmbedded(
198
- itemsMap,
199
- actorId,
200
- "attribute",
201
- shortcode,
202
- overlay,
203
- `attr:${shortcode}`,
204
- ctx,
205
- { fmKey: "attributes" },
206
- );
207
- if (embedded) items.push(embedded);
208
- }
209
- }
210
-
211
150
  const sohlItems = sohlField(fm, "items", null);
212
151
  if (Array.isArray(sohlItems)) {
213
152
  sohlItems.forEach((entry, index) => {
@@ -333,8 +272,8 @@ export class Actors extends SystemActorCompiler {
333
272
  *
334
273
  * **The scores used are the ones just written.** `SkillLogic` resolves
335
274
  * `attr.<code>` to an attribute's *effective* score, after active effects;
336
- * all this pass has is the `scoreBase` it set from `sohl.attributes`. For a
337
- * compiled being carrying no attribute-altering effects the two agree,
275
+ * all this pass has is the `scoreBase` on each embedded attribute item. For
276
+ * a compiled being carrying no attribute-altering effects the two agree,
338
277
  * which is every being in content today. One that did carry such an effect
339
278
  * would bake a Skill Base its client then disagrees with — that is the
340
279
  * limit of doing this at build time, and the point to revisit if it bites.
@@ -387,6 +326,9 @@ export class Actors extends SystemActorCompiler {
387
326
  const { value: authoredFolder, isAddress } = folderField(fm);
388
327
  const folder = this.folderResolver(authoredFolder, { isAddress });
389
328
 
329
+ this.checkAnchoredSection(body || "", "appearance", ctx);
330
+ this.checkAnchoredSection(body || "", "dossier", ctx);
331
+
390
332
  const system = {
391
333
  // The frontmatter shortcode is the actor's stable `(type, shortcode)`
392
334
  // key — and, for a being that is an archetype, its archetype
@@ -411,25 +353,6 @@ export class Actors extends SystemActorCompiler {
411
353
  dossier: withDraftNotice(fm, renderSection(body || "", "dossier")),
412
354
  };
413
355
 
414
- // Fill `system.body` (+ the base-actor movement fields) from the being's
415
- // frontmatter, rather than embedding a corpus item. The `sohl`
416
- // block mirrors `system` field-for-field: `sohl.body` nests the body
417
- // (`structure` / `weight` / …), with `currentMoveMedium` /
418
- // `movementProfiles` flat alongside it. An **incorporeal** being omits
419
- // `sohl.body` and keeps the schema's empty body.
420
- const bodyField = sohlField(fm, "body", null);
421
- if (bodyField && typeof bodyField === "object") {
422
- const bodyData = extractBodyAndMovement(fm);
423
- system.body = bodyData.body;
424
- system.currentMoveMedium = bodyData.currentMoveMedium;
425
- system.movementProfiles = bodyData.movementProfiles;
426
- } else if (bodyField != null) {
427
- this.noteError(
428
- `${ctx}: sohl.body must be an inline object (structure/weight/…), got ${typeof bodyField}`,
429
- );
430
- this.errorCount++;
431
- }
432
-
433
356
  // Being-only combat grouping (mirrors `system.defaultCombatGroup`).
434
357
  const defaultCombatGroup = sohlField(fm, "defaultCombatGroup", undefined);
435
358
  if (defaultCombatGroup !== undefined) {
@@ -391,11 +391,11 @@ export const ITEM_FIELDS = Object.freeze({
391
391
  ...STRING,
392
392
  default: "",
393
393
  topLevelMeans:
394
- "the note's own title — the heading its page is published under, " +
395
- "which has nothing to do with the style of address an office " +
396
- "carries. Author this on the membership instead: `sohl.system.title` " +
397
- "on the item, or the `system.title` of the entry in a being's " +
398
- "`sohl.items`.",
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 " +
398
+ "`system.title` of the entry in a being's `sohl.items`.",
399
399
  describe: "The style of address the office carries.",
400
400
  },
401
401
  {
package/sohl/items.mjs CHANGED
@@ -93,7 +93,7 @@ export class Items extends SystemItemCompiler {
93
93
  // it — an undeclared `system` key is discarded at construction
94
94
  // without a warning.
95
95
  templatePriority: systemTemplatePriority(fm, label),
96
- actionDefs: Array.isArray(fm.actionDefs) ? fm.actionDefs : [],
96
+ actionDefs: [],
97
97
  notes: "",
98
98
  docHtml: description || "",
99
99
  };
@@ -99,37 +99,27 @@ const MACRO_FIELDS = Object.freeze([
99
99
  const VEHICLE_FIELDS = Object.freeze([]);
100
100
 
101
101
  /**
102
- * A `being` note — an actor, with its body, movement and embedded items.
102
+ * A `being` note — an actor, with its embedded items and its combat grouping.
103
+ *
104
+ * **A being's own document fields are authored under `sohl.system`**, which the
105
+ * passthrough writes at the DataModel's own paths — a body and the movement that
106
+ * goes with it are `sohl.system.body`, `sohl.system.currentMoveMedium` and
107
+ * `sohl.system.movementProfiles`, written together — so this declaration states
108
+ * none of them. **Attributes and skills are embedded items**, each named by the
109
+ * catalogue entry it copies, so both are `sohl.items` entries with the score or
110
+ * the mastery level as that entry's own `system` overlay.
111
+ *
112
+ * What is left is the two keys the pass reads off the block itself.
103
113
  *
104
114
  * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
105
115
  */
106
116
  const BEING_FIELDS = Object.freeze([
107
- {
108
- name: "body",
109
- ...MAP,
110
- describe: "The being's body structure, by part shortcode.",
111
- },
112
- {
113
- name: "attributes",
114
- ...MAP,
115
- describe: "Attribute scores, by attribute shortcode.",
116
- },
117
117
  {
118
118
  name: "items",
119
119
  ...LIST,
120
120
  describe:
121
121
  "Items embedded on the being, each addressed by (type, shortcode) or carrying enough fields to stand alone.",
122
122
  },
123
- {
124
- name: "currentMoveMedium",
125
- ...STRING,
126
- describe: "Which medium the being is currently moving through.",
127
- },
128
- {
129
- name: "movementProfiles",
130
- ...LIST,
131
- describe: "Movement rates, one profile per medium.",
132
- },
133
123
  {
134
124
  name: "defaultCombatGroup",
135
125
  ...AS_AUTHORED,
@@ -102,6 +102,9 @@ export const DERIVED_MANIFEST_KEYS: Readonly<Record<string, string>>;
102
102
  * @property {string} stageDir The staged package root, relative to
103
103
  * `rootDir`. Every asset `to:` lands under it.
104
104
  * @property {readonly Readonly<AssetSpec>[]} assets
105
+ * @property {boolean} baseStyles Whether the package takes the shared base
106
+ * stylesheet, staged into `stageDir` and
107
+ * named first in the manifest's `styles`.
105
108
  * @property {Readonly<Record<string, unknown>>} manifest The manifest
106
109
  * specification, emitted as declared.
107
110
  * @property {string|null} manifestFlags Module to load a `flags` function
@@ -235,6 +238,12 @@ export type PackageBuildConfig = {
235
238
  */
236
239
  stageDir: string;
237
240
  assets: readonly Readonly<AssetSpec>[];
241
+ /**
242
+ * Whether the package takes the shared base
243
+ * stylesheet, staged into `stageDir` and
244
+ * named first in the manifest's `styles`.
245
+ */
246
+ baseStyles: boolean;
238
247
  /**
239
248
  * The manifest
240
249
  * specification, emitted as declared.
@@ -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
@@ -248,6 +248,17 @@ export class SystemActorCompiler extends BasePackCompiler {
248
248
  img: string;
249
249
  token: string;
250
250
  };
251
+ /**
252
+ * Report an `{#appearance}` or `{#dossier}` anchor on a heading
253
+ * {@link extractAnchorSection} cannot read, rather than letting the field
254
+ * it feeds compile blank with nothing to say why.
255
+ *
256
+ * @param {string} body - The note body.
257
+ * @param {string} anchorId - `"appearance"` or `"dossier"`.
258
+ * @param {string} ctx - The actor's label, for the message.
259
+ * @returns {void}
260
+ */
261
+ checkAnchoredSection(body: string, anchorId: string, ctx: string): void;
251
262
  /** @type {readonly string[]} */
252
263
  itemsSourceDirs: readonly string[];
253
264
  foreignSourceDirs: any;
@@ -6,6 +6,11 @@
6
6
  * heading exists. Fenced code blocks are respected so `# foo` inside
7
7
  * ``` blocks does not trigger a match.
8
8
  *
9
+ * An anchor declared on a lower heading is found by {@link collectAnchors}
10
+ * but not by the `#\s+` match below, so it extracts as absent rather than as
11
+ * wrong — {@link misplacedAnchorSection} is the check a caller runs first to
12
+ * tell the two apart.
13
+ *
9
14
  * @param {string} body - The note body.
10
15
  * @param {string} anchorId - The anchor to find.
11
16
  * @returns {string} The section's markdown, or "".
@@ -19,3 +24,22 @@ export function extractAnchorSection(body: string, anchorId: string): string;
19
24
  * @returns {string} The rendered HTML, or "".
20
25
  */
21
26
  export function renderSection(body: string, anchorId: string): string;
27
+ /**
28
+ * Where an `{#appearance}` or `{#dossier}` anchor sits on a heading below the
29
+ * top level, which {@link extractAnchorSection} cannot read.
30
+ *
31
+ * {@link module:engine/anchors.collectAnchors} finds the anchor at any
32
+ * level, because a lower heading legitimately starts a journal page. It does
33
+ * not feed an actor field: an H1-only extractor reads nothing from one, and a
34
+ * caller that trusts the empty result as "no such section" rather than
35
+ * "found, but unreadable" compiles a blank field with nothing to say why.
36
+ *
37
+ * @param {string} body - The note body.
38
+ * @param {string} anchorId - The anchor to check, case-insensitively.
39
+ * @param {number} [bodyLine=1] - The 1-based file line the body starts on.
40
+ * @returns {{line: number}|null} The misplaced anchor's line, or `null` when
41
+ * it is absent or already on an H1.
42
+ */
43
+ export function misplacedAnchorSection(body: string, anchorId: string, bodyLine?: number): {
44
+ line: number;
45
+ } | null;
@@ -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;