@heroiclands/package-build 23.1.1 → 23.2.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 (246) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/bin/content-commands.mjs +68 -149
  3. package/bin/package-build.mjs +56 -10
  4. package/ci/check-banners.mjs +42 -6
  5. package/ci/confirm-npm-version.mjs +135 -0
  6. package/ci/content-format-reference.mjs +14 -5
  7. package/config.mjs +1 -0
  8. package/container.mjs +185 -4
  9. package/content-config.mjs +333 -210
  10. package/deploy.mjs +116 -10
  11. package/docs/api.md +238 -246
  12. package/docs/authoring/assets.md +11 -5
  13. package/docs/authoring/frontmatter.md +1 -1
  14. package/docs/authoring/links-and-markup.md +87 -0
  15. package/docs/commands.md +138 -99
  16. package/docs/configuration.md +277 -125
  17. package/docs/content-format.md +2 -0
  18. package/docs/diagnostics.md +48 -13
  19. package/docs/guides/book.md +341 -21
  20. package/docs/guides/e2e.md +13 -2
  21. package/docs/guides/icons.md +14 -4
  22. package/docs/guides/module-project.md +18 -8
  23. package/docs/guides/release-and-deploy.md +32 -0
  24. package/docs/guides/site.md +5 -2
  25. package/docs/project-setup.md +30 -10
  26. package/docs/reference/format-details.md +373 -170
  27. package/docs/reference/note-types.md +128 -109
  28. package/docs/reference/theme-pages.md +31 -6
  29. package/e2e.mjs +140 -28
  30. package/engine/actor-compiler.mjs +22 -31
  31. package/engine/address-charset.mjs +4 -17
  32. package/engine/address-fields.mjs +1 -1
  33. package/engine/address-render.mjs +12 -1
  34. package/engine/address-values.mjs +6 -0
  35. package/engine/address.mjs +46 -33
  36. package/engine/art-fields.mjs +30 -5
  37. package/engine/art-slots.mjs +10 -1
  38. package/engine/asset-bindings.mjs +12 -1
  39. package/engine/base-compiler.mjs +34 -65
  40. package/engine/being-age.mjs +2 -7
  41. package/engine/being-measurements.mjs +12 -1
  42. package/engine/calendar-artifacts.mjs +6 -0
  43. package/engine/calendar-choice.mjs +12 -1
  44. package/engine/calendar-format.mjs +12 -1
  45. package/engine/calendar-human.mjs +12 -1
  46. package/engine/compile-corpus.mjs +3 -1
  47. package/engine/content-address.mjs +9 -12
  48. package/engine/content-alerts.mjs +13 -1
  49. package/engine/content-blocks.mjs +14 -3
  50. package/engine/content-embeds.mjs +11 -3
  51. package/engine/content-figures.mjs +9 -0
  52. package/engine/content-footnotes.mjs +12 -1
  53. package/engine/content-format-check.mjs +11 -262
  54. package/engine/content-format.yaml +5 -2
  55. package/engine/content-images.mjs +3 -0
  56. package/engine/content-index.mjs +443 -20
  57. package/engine/content-links.mjs +29 -35
  58. package/engine/content-lint.mjs +2 -3
  59. package/engine/content-poetry.mjs +12 -1
  60. package/engine/content-slug.mjs +2 -14
  61. package/engine/content-spans.mjs +13 -1
  62. package/engine/content-tables.mjs +12 -1
  63. package/engine/culture-choice.mjs +210 -13
  64. package/engine/data-keys.mjs +19 -16
  65. package/engine/date-conversion.mjs +12 -1
  66. package/engine/declaration-check.mjs +9 -0
  67. package/engine/derived-fields.mjs +6 -7
  68. package/engine/diagnostics.mjs +34 -4
  69. package/engine/docs-checks.mjs +9 -0
  70. package/engine/document-subtypes.mjs +47 -34
  71. package/engine/event-views.mjs +139 -14
  72. package/engine/exported-scene.mjs +92 -28
  73. package/engine/extension-attributes.mjs +12 -1
  74. package/engine/field-spec.mjs +11 -102
  75. package/engine/folder-notes.mjs +0 -64
  76. package/engine/foreign-catalog.mjs +2 -1
  77. package/engine/format-generated.mjs +9 -0
  78. package/engine/foundry-entries.mjs +10 -28
  79. package/engine/frontmatter-lint.mjs +83 -392
  80. package/engine/frontmatter.mjs +20 -70
  81. package/engine/generate.mjs +10 -15
  82. package/engine/heading-attributes.mjs +12 -1
  83. package/engine/helpers.mjs +41 -102
  84. package/engine/holdings.mjs +1 -1
  85. package/engine/homepage.mjs +3 -10
  86. package/engine/ids.mjs +1 -167
  87. package/engine/index-records.mjs +6 -4
  88. package/engine/index.mjs +0 -3
  89. package/engine/infobox-render.mjs +15 -3
  90. package/engine/infobox.mjs +13 -14
  91. package/engine/item-compiler.mjs +4 -10
  92. package/engine/item-docs.mjs +3 -5
  93. package/engine/item-registry.mjs +2 -10
  94. package/engine/journals.mjs +1 -5
  95. package/engine/literature-notes.mjs +4 -12
  96. package/engine/macros.mjs +1 -2
  97. package/engine/map-graphviz.mjs +12 -1
  98. package/engine/markdown-expressions.mjs +12 -1
  99. package/engine/markdownlint-run.mjs +567 -0
  100. package/engine/metadata-index.mjs +42 -12
  101. package/engine/note-addresses.mjs +143 -35
  102. package/engine/note-claims.mjs +14 -28
  103. package/engine/note-dates.mjs +3 -3
  104. package/engine/note-frontmatter.mjs +6 -0
  105. package/engine/note-ids.mjs +8 -14
  106. package/engine/note-package.mjs +8 -25
  107. package/engine/note-state.mjs +1 -3
  108. package/engine/note-vocabulary.mjs +148 -84
  109. package/engine/number-words.mjs +12 -1
  110. package/engine/pack-config.mjs +15 -25
  111. package/engine/pack-router.mjs +10 -16
  112. package/engine/pathnames.mjs +8 -22
  113. package/engine/pdf-build.mjs +773 -121
  114. package/engine/pdf-coverage.mjs +188 -0
  115. package/engine/pdf-index.mjs +54 -7
  116. package/engine/pdf-pages.mjs +247 -0
  117. package/engine/pdf-render.mjs +114 -4
  118. package/engine/pdf-toc.mjs +29 -32
  119. package/engine/project-init.mjs +46 -6
  120. package/engine/prose-config.mjs +1 -1
  121. package/engine/prose-lint.mjs +52 -88
  122. package/engine/readability-lint.mjs +9 -0
  123. package/engine/reckoning-markers.mjs +12 -1
  124. package/engine/reference-targets.mjs +7 -0
  125. package/engine/runtime-only-fields.mjs +5 -8
  126. package/engine/scene-levels.mjs +2 -2
  127. package/engine/scenes.mjs +1 -1
  128. package/engine/shortcode-references.mjs +7 -0
  129. package/engine/site-build.mjs +30 -16
  130. package/engine/site-index.mjs +10 -16
  131. package/engine/site-preview.mjs +6 -0
  132. package/engine/site-root.mjs +8 -1
  133. package/engine/social-tie-terms.mjs +12 -1
  134. package/engine/social-ties.mjs +3 -0
  135. package/engine/sql-tables.mjs +56 -20
  136. package/engine/standing-terms.mjs +11 -1
  137. package/engine/standings.mjs +11 -27
  138. package/engine/system-block.mjs +39 -141
  139. package/engine/system-vocabulary.mjs +6 -0
  140. package/engine/walk-files.mjs +69 -0
  141. package/engine/web-wikilinks.mjs +23 -3
  142. package/engine/wikilink-syntax.mjs +32 -21
  143. package/engine/wikilinks.mjs +27 -16
  144. package/githooks/attribution.mjs +8 -0
  145. package/githooks/commit-message.mjs +9 -0
  146. package/hm3/actors.mjs +9 -48
  147. package/hm3/default-item-art.mjs +9 -9
  148. package/hm3/item-fields.mjs +2 -9
  149. package/hm3/template-priority.mjs +6 -7
  150. package/hugo-theme/data/banner-kinds.yaml +78 -0
  151. package/hugo-theme/data/banners.yaml +2 -3
  152. package/hugo-theme/layouts/404.html +3 -3
  153. package/hugo-theme/layouts/partials/banner-url.html +31 -7
  154. package/hugo-theme/layouts/partials/hero-banner.html +13 -14
  155. package/hugo-theme/layouts/partials/related.html +1 -1
  156. package/hugo-theme/layouts/partials/warn-missing-banner.html +1 -1
  157. package/hugo-theme/static/css/style.css +1 -1
  158. package/manifest.mjs +11 -4
  159. package/markdownlint-config.mjs +1 -1
  160. package/package.json +6 -3
  161. package/release.mjs +21 -14
  162. package/sohl/actors.mjs +16 -27
  163. package/sohl/default-item-art.mjs +3 -3
  164. package/sohl/document-subtypes.mjs +1 -16
  165. package/sohl/infobox.mjs +2 -3
  166. package/sohl/item-builders.mjs +5 -8
  167. package/sohl/item-fields.mjs +5 -35
  168. package/sohl/items.mjs +0 -4
  169. package/sohl/kb-passes.mjs +3 -3
  170. package/types/container.d.mts +116 -5
  171. package/types/content-config.d.mts +83 -35
  172. package/types/deploy.d.mts +42 -6
  173. package/types/e2e.d.mts +35 -3
  174. package/types/engine/address-charset.d.mts +4 -17
  175. package/types/engine/address.d.mts +17 -8
  176. package/types/engine/art-fields.d.mts +7 -4
  177. package/types/engine/art-slots.d.mts +6 -0
  178. package/types/engine/base-compiler.d.mts +15 -17
  179. package/types/engine/content-address.d.mts +9 -12
  180. package/types/engine/content-embeds.d.mts +4 -2
  181. package/types/engine/content-format-check.d.mts +0 -64
  182. package/types/engine/content-index.d.mts +82 -3
  183. package/types/engine/content-links.d.mts +2 -4
  184. package/types/engine/culture-choice.d.mts +51 -8
  185. package/types/engine/data-keys.d.mts +2 -2
  186. package/types/engine/derived-fields.d.mts +1 -2
  187. package/types/engine/diagnostics.d.mts +27 -4
  188. package/types/engine/document-subtypes.d.mts +12 -31
  189. package/types/engine/event-views.d.mts +14 -2
  190. package/types/engine/exported-scene.d.mts +3 -2
  191. package/types/engine/field-spec.d.mts +13 -122
  192. package/types/engine/folder-notes.d.mts +0 -39
  193. package/types/engine/foreign-catalog.d.mts +2 -1
  194. package/types/engine/foundry-entries.d.mts +4 -5
  195. package/types/engine/frontmatter-lint.d.mts +2 -2
  196. package/types/engine/frontmatter.d.mts +19 -45
  197. package/types/engine/generate.d.mts +1 -1
  198. package/types/engine/helpers.d.mts +25 -42
  199. package/types/engine/homepage.d.mts +3 -10
  200. package/types/engine/ids.d.mts +0 -114
  201. package/types/engine/index-records.d.mts +6 -4
  202. package/types/engine/index.d.mts +0 -1
  203. package/types/engine/infobox-render.d.mts +7 -1
  204. package/types/engine/infobox.d.mts +3 -2
  205. package/types/engine/item-docs.d.mts +1 -1
  206. package/types/engine/item-registry.d.mts +2 -4
  207. package/types/engine/literature-notes.d.mts +1 -1
  208. package/types/engine/markdownlint-run.d.mts +40 -0
  209. package/types/engine/metadata-index.d.mts +9 -1
  210. package/types/engine/note-addresses.d.mts +27 -2
  211. package/types/engine/note-claims.d.mts +3 -5
  212. package/types/engine/note-ids.d.mts +2 -4
  213. package/types/engine/note-package.d.mts +1 -1
  214. package/types/engine/note-state.d.mts +1 -3
  215. package/types/engine/note-vocabulary.d.mts +28 -21
  216. package/types/engine/pack-config.d.mts +2 -6
  217. package/types/engine/pack-router.d.mts +4 -4
  218. package/types/engine/pathnames.d.mts +3 -3
  219. package/types/engine/pdf-build.d.mts +98 -12
  220. package/types/engine/pdf-coverage.d.mts +60 -0
  221. package/types/engine/pdf-index.d.mts +34 -5
  222. package/types/engine/pdf-pages.d.mts +143 -0
  223. package/types/engine/pdf-render.d.mts +45 -1
  224. package/types/engine/pdf-toc.d.mts +12 -14
  225. package/types/engine/prose-config.d.mts +1 -1
  226. package/types/engine/prose-lint.d.mts +11 -10
  227. package/types/engine/runtime-only-fields.d.mts +2 -2
  228. package/types/engine/site-build.d.mts +9 -5
  229. package/types/engine/site-index.d.mts +4 -5
  230. package/types/engine/site-root.d.mts +1 -1
  231. package/types/engine/sql-tables.d.mts +31 -2
  232. package/types/engine/standing-terms.d.mts +2 -1
  233. package/types/engine/standings.d.mts +7 -9
  234. package/types/engine/system-block.d.mts +10 -44
  235. package/types/engine/walk-files.d.mts +15 -0
  236. package/types/engine/wikilink-syntax.d.mts +18 -19
  237. package/types/hm3/actors.d.mts +1 -2
  238. package/types/hm3/item-fields.d.mts +2 -4
  239. package/types/hm3/template-priority.d.mts +6 -7
  240. package/types/manifest.d.mts +3 -2
  241. package/types/release.d.mts +8 -7
  242. package/types/sohl/actors.d.mts +2 -1
  243. package/types/sohl/document-subtypes.d.mts +1 -2
  244. package/types/sohl/item-fields.d.mts +3 -7
  245. package/engine/retired-fields.mjs +0 -631
  246. package/types/engine/retired-fields.d.mts +0 -354
@@ -36,7 +36,6 @@ import { readCanonicalKey } from "./content-address.mjs";
36
36
  // is stated there and read here — the anchor and the label are this module's,
37
37
  // and the tuple inside them is not.
38
38
  import {
39
- ITEM_DOC_PREFIX,
40
39
  readQualifier,
41
40
  resolveItemDocType,
42
41
  parseAddress,
@@ -197,16 +196,7 @@ export function buildWikilinkIndex(
197
196
  for (const v of foreignByKey.values()) {
198
197
  if (v.type) foreignTypes.push(norm(v.type));
199
198
  }
200
- // A legacy index may contain `doc<type>` records. Keep the authored type
201
- // vocabulary separate so the input alias can resolve to its base type.
202
- for (const t of foreignTypes) {
203
- if (!t.startsWith(ITEM_DOC_PREFIX)) types.add(t);
204
- }
205
- for (const t of foreignTypes) {
206
- if (!t.startsWith(ITEM_DOC_PREFIX)) continue;
207
- if (types.has(t.slice(ITEM_DOC_PREFIX.length))) continue;
208
- types.add(t);
209
- }
199
+ for (const t of foreignTypes) types.add(t);
210
200
 
211
201
  // Every package an address may name: this one, plus every package a
212
202
  // vendored manifest speaks for. What lets `thalorna-creature-grkrahk` be
@@ -274,7 +264,7 @@ function foreignHits(index, read) {
274
264
  if (!read || read.reason || !index.foreign?.size) return [];
275
265
  const tuple = completeAddress(read, { package: index.contentPackage, system: NOTE_SYSTEM });
276
266
  const wanted = norm(tuple.type);
277
- const shortcode = norm(read.shortcode);
267
+ const shortcode = read.shortcode;
278
268
 
279
269
  // **An omitted package means this package**, so a short form
280
270
  // addresses nothing foreign and never reaches a dependency's index. A link
@@ -300,6 +290,26 @@ function foreignHits(index, read) {
300
290
  return hits;
301
291
  }
302
292
 
293
+ /**
294
+ * What a `shortcode-not-lowercase` finding carries: the shortcode as written,
295
+ * and the lowercase spelling when a local note or a fetched dependency declares
296
+ * it.
297
+ *
298
+ * @param {object} index - From {@link buildWikilinkIndex}.
299
+ * @param {{shortcode: string, lowercase: object}} problem - The reader's verdict.
300
+ * @returns {{shortcode: string, declared?: string}} The finding's fields.
301
+ */
302
+ function shortcodeCase(index, problem) {
303
+ const read = problem.lowercase;
304
+ const local =
305
+ (read.package ?? index.contentPackage) === index.contentPackage &&
306
+ index.byShortcode?.has(
307
+ `${resolveItemDocType(read.type, index.types) ?? read.type}/${read.shortcode}`,
308
+ );
309
+ const declared = local || foreignHits(index, read).length > 0;
310
+ return { shortcode: problem.shortcode, ...(declared ? { declared: read.shortcode } : {}) };
311
+ }
312
+
303
313
  /**
304
314
  * How an **unresolved** link renders.
305
315
  *
@@ -335,8 +345,7 @@ function unresolvedLink(text, target) {
335
345
  * **The wrapper carries the cue and nothing else.** The link itself is
336
346
  * untouched — Foundry enriches inside HTML, so the `@UUID` still becomes a live
337
347
  * content link, and the note is in the packs, the manifest and the index
338
- * exactly as any other. Nothing here resembles the retired `draft:` field,
339
- * which moved a note from published to unresolvable without saying so.
348
+ * exactly as any other.
340
349
  *
341
350
  * The appearance lives in `scss/components/_draft-link.scss`, beside the
342
351
  * unresolved-link partial, not here.
@@ -409,8 +418,7 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index, cap
409
418
  const slug = parsed.anchor || null;
410
419
 
411
420
  // **Every link carries a label**. Without one there is nothing
412
- // to resolve against: the alias namespace a bare `[[Text]]` was looked
413
- // up in is retired, and a shortcode is an address rather than prose, so
421
+ // to resolve against: a shortcode is an address rather than prose, so
414
422
  // the link has neither a target this build can find nor text to show.
415
423
  // Reported before anything else, including the same-page form, because
416
424
  // it is a statement about how the link is *written* — `[[#slug]]` needs
@@ -452,6 +460,9 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index, cap
452
460
  target,
453
461
  offset,
454
462
  reason: qualified?.reason ?? "not-an-address",
463
+ ...(qualified?.reason === "shortcode-not-lowercase" ?
464
+ shortcodeCase(index, qualified)
465
+ : {}),
455
466
  addressed: true,
456
467
  });
457
468
  return unresolvedLink(text || target, target);
@@ -1,5 +1,13 @@
1
1
  /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
2
3
  * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
3
11
  * SPDX-License-Identifier: GPL-3.0-or-later
4
12
  */
5
13
 
@@ -1,7 +1,16 @@
1
1
  /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
2
3
  * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
3
11
  * SPDX-License-Identifier: GPL-3.0-or-later
4
12
  */
13
+
5
14
  import { readFileSync } from "node:fs";
6
15
  import { relative } from "node:path";
7
16
  import { attribution } from "./attribution.mjs";
package/hm3/actors.mjs CHANGED
@@ -46,20 +46,13 @@
46
46
  */
47
47
 
48
48
  import { resolveName } from "../engine/helpers.mjs";
49
- import { buildFromFields, retiredTopLevelKey, STRING } from "../engine/field-spec.mjs";
49
+ import { buildFromFields, STRING } from "../engine/field-spec.mjs";
50
50
  import { SystemActorCompiler } from "../engine/actor-compiler.mjs";
51
51
  import { renderSection } from "../engine/anchored-sections.mjs";
52
52
  import { withDraftNotice } from "../engine/draft-notice.mjs";
53
53
  import { documentSubtype } from "../engine/document-subtypes.mjs";
54
54
  import { HM3_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
55
55
  import { templateFlags } from "./template-priority.mjs";
56
- // The retirement window's reports, shared with the frontmatter lint so the
57
- // two cannot say different things about the same key.
58
- import {
59
- legacyKeyMessage,
60
- locateFrontmatterKey,
61
- retiredTopLevelMessage,
62
- } from "../engine/retired-fields.mjs";
63
56
  // The note-level `hm3:` block: `hm3.system` onto the document's `system`
64
57
  // verbatim, and `hm3.img` / `hm3.items` / `hm3.effects` / `hm3.flags`
65
58
  // overriding their shared top-level forms for this system alone.
@@ -92,15 +85,13 @@ export const ACTOR_FIELDS = Object.freeze([
92
85
  /**
93
86
  * The two `data:` facts HM3 declares on a `character` and not on a `creature`.
94
87
  *
95
- * Both name their shared source and their legacy in-block key, for the reason
96
- * {@link ACTOR_FIELDS} does.
88
+ * Both name a shared source in `data:`, which is the only place they are authored.
97
89
  *
98
90
  * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
99
91
  */
100
92
  export const CHARACTER_FIELDS = Object.freeze([
101
93
  {
102
94
  name: "data.gender",
103
- legacyKey: "gender",
104
95
  to: "gender",
105
96
  ...STRING,
106
97
  default: "",
@@ -108,7 +99,6 @@ export const CHARACTER_FIELDS = Object.freeze([
108
99
  },
109
100
  {
110
101
  name: "data.occupation",
111
- legacyKey: "occupation",
112
102
  to: "occupation",
113
103
  ...STRING,
114
104
  default: "",
@@ -126,8 +116,8 @@ export const CHARACTER_FIELDS = Object.freeze([
126
116
  * @type {Readonly<Record<string, string>>}
127
117
  */
128
118
  const DEFAULT_IMG = Object.freeze({
129
- character: "systems/hm3/images/svg/knight-silhouette.svg",
130
- creature: "systems/hm3/images/svg/monster-silhouette.svg",
119
+ character: "systems/hm3/assets/images/knightsilhouette.svg",
120
+ creature: "systems/hm3/assets/images/monstersilhouette.svg",
131
121
  });
132
122
 
133
123
  /**
@@ -176,9 +166,9 @@ function defaultActorImg(subType) {
176
166
  * @type {Readonly<Record<string, string>>}
177
167
  */
178
168
  const EMBEDDED_ITEM_ART = Object.freeze({
179
- invocation: "systems/hm3/images/icons/svg/circle.svg",
180
- psionic: "systems/hm3/images/icons/svg/psionics.svg",
181
- spell: "systems/hm3/images/icons/svg/pentacle.svg",
169
+ invocation: "systems/hm3/assets/icons/circle.svg",
170
+ psionic: "systems/hm3/assets/icons/psionics.svg",
171
+ spell: "systems/hm3/assets/icons/pentacle.svg",
182
172
  });
183
173
 
184
174
  /**
@@ -297,38 +287,9 @@ export class Hm3Actors extends SystemActorCompiler {
297
287
 
298
288
  const items = this.buildEmbeddedItems(itemsMap, id, fm, ctx);
299
289
 
300
- // The sweep's progress signal, reported where an author meets it
301
- // soonest — every consumer runs the compile, and not every one runs the
302
- // lint. A warning: the note compiles to the correct document
303
- // either way, and refusing the position comes once no tree writes it.
304
- const onLegacyKey = (field) =>
305
- this.noteWarn(
306
- legacyKeyMessage(block, field),
307
- locateFrontmatterKey(this.currentNote?.absPath, field.legacyKey),
308
- );
309
-
310
- // The shared level's own retiring position — the top-level key `data:`
311
- // gathered the field off. Same signal, same severity, separate
312
- // callback: a note may have moved one position and not the other.
313
- // Anchored at column 1, because the two spellings coincide — `portrait`
314
- // names a block key and a top-level one — and a locator that took the
315
- // first match would point at the wrong line.
316
- const onRetiredTopLevel = (field) =>
317
- this.noteWarn(
318
- retiredTopLevelMessage(field),
319
- locateFrontmatterKey(
320
- this.currentNote?.absPath,
321
- retiredTopLevelKey(field),
322
- undefined,
323
- { topLevel: true },
324
- ),
325
- );
326
- const reports = { block, onLegacyKey, onRetiredTopLevel };
290
+ const reports = { block };
327
291
 
328
- // One spelling, as everywhere else: `packFolder` names a folder note
329
- // by its address. This pass once read only the Foundry id, so an HM3
330
- // tree could not file an actor by address at all — which its own sweep
331
- // needs; the id spelling is retired outright.
292
+ // `packFolder` names a folder note by its address.
332
293
  const folder = this.folderResolver(blockDataProperty(fm, block, "packFolder", null), {
333
294
  isAddress: true,
334
295
  });
@@ -41,15 +41,15 @@
41
41
  * @type {Readonly<Record<string, string>>}
42
42
  */
43
43
  export const HM3_DEFAULT_ITEM_ART = Object.freeze({
44
- armorgear: "hm3/assets/images/icons/svg/armor.svg",
45
- armorlocation: "hm3/assets/images/icons/svg/anatomy.svg",
46
- containergear: "hm3/assets/images/icons/svg/sack.svg",
47
- miscgear: "hm3/assets/images/icons/svg/miscgear.svg",
48
- mysticalability: "hm3/assets/images/icons/svg/psionics.svg",
49
- projectilegear: "hm3/assets/images/icons/svg/arrow.svg",
50
- skill: "hm3/assets/images/icons/svg/skills.svg",
51
- trauma: "hm3/assets/images/icons/svg/injury.svg",
52
- weapongear: "hm3/assets/images/icons/svg/sword.svg",
44
+ armorgear: "hm3/assets/icons/armor.svg",
45
+ armorlocation: "hm3/assets/icons/anatomy.svg",
46
+ containergear: "hm3/assets/icons/sack.svg",
47
+ miscgear: "hm3/assets/icons/miscgear.svg",
48
+ mysticalability: "hm3/assets/icons/psionics.svg",
49
+ projectilegear: "hm3/assets/icons/arrow.svg",
50
+ skill: "hm3/assets/icons/skills.svg",
51
+ trauma: "hm3/assets/icons/injury.svg",
52
+ weapongear: "hm3/assets/icons/sword.svg",
53
53
  });
54
54
 
55
55
  /**
@@ -77,7 +77,6 @@ import { AS_AUTHORED, NUMBER } from "../engine/field-spec.mjs";
77
77
  const GEAR_COMMON = Object.freeze([
78
78
  {
79
79
  name: "data.weight",
80
- legacyKey: "weight",
81
80
  to: "weight",
82
81
  ...AS_AUTHORED,
83
82
  kind: "number",
@@ -86,7 +85,6 @@ const GEAR_COMMON = Object.freeze([
86
85
  },
87
86
  {
88
87
  name: "data.value",
89
- legacyKey: "value",
90
88
  to: "value",
91
89
  ...AS_AUTHORED,
92
90
  kind: "number",
@@ -107,7 +105,6 @@ const GEAR_COMMON = Object.freeze([
107
105
  */
108
106
  const QUANTITY = Object.freeze({
109
107
  name: "data.quantity",
110
- legacyKey: "quantity",
111
108
  to: "quantity",
112
109
  ...NUMBER,
113
110
  default: 1,
@@ -127,10 +124,8 @@ const QUANTITY = Object.freeze({
127
124
  * a registry is addressed by what a note calls itself. What the document is
128
125
  * called is the map's answer, and only the map's.
129
126
  *
130
- * Each field names both of its authored positions, as
131
- * {@link module:hm3/actors} does and for the same reason: `name` is the shared
132
- * source the content format's mapping tables state, and `legacyKey` is the key
133
- * the `hm3:` block still carries.
127
+ * A field whose `name` is a path into `data:` is authored there and nowhere
128
+ * else: `name` is the shared source the content format's mapping tables state.
134
129
  *
135
130
  * @type {Readonly<Record<string, readonly import("../engine/field-spec.mjs").FieldSpec[]>>}
136
131
  */
@@ -148,7 +143,6 @@ export const HM3_ITEM_FIELDS = Object.freeze({
148
143
  ...GEAR_COMMON,
149
144
  {
150
145
  name: "data.capacity",
151
- legacyKey: "capacity",
152
146
  to: "capacity.max",
153
147
  ...NUMBER,
154
148
  default: 0,
@@ -171,7 +165,6 @@ export const HM3_ITEM_FIELDS = Object.freeze({
171
165
  skill: Object.freeze([
172
166
  {
173
167
  name: "data.masteryLevel",
174
- legacyKey: "masteryLevel",
175
168
  to: "masteryLevel",
176
169
  ...NUMBER,
177
170
  default: 0,
@@ -49,13 +49,12 @@ import { blockProperty } from "../engine/system-block.mjs";
49
49
  * than a `null` nothing reads.
50
50
  *
51
51
  * **Read through the shared resolver, not a field declaration**. A
52
- * `FieldSpec`'s shared source is a single position, and this value has five:
53
- * `data:`, this block, the top level, and the retiring `archetype` spelling in
54
- * the latter two. The resolver is the single implementation of what a note said,
55
- * so the two systems cannot disagree about it. It is read against **this**
56
- * block: the `sohl:` block is not a source for an HM3 document, so a tree that
57
- * still states the priority there (`harn-ensemble`, on 2,502 notes) writes no
58
- * HM3 flag until it sweeps to `data:`.
52
+ * `FieldSpec`'s shared source is a single position, and this value has three:
53
+ * `data:`, this block, and the top level. The resolver is the single
54
+ * implementation of what a note said, so the two systems cannot disagree about
55
+ * it. It is read against **this** block: the `sohl:` block is not a source for
56
+ * an HM3 document, so a note that states the priority only there writes no HM3
57
+ * flag.
59
58
  *
60
59
  * @param {object} fm - The note's frontmatter.
61
60
  * @param {string} block - This pass's system block.
@@ -0,0 +1,78 @@
1
+ # Which banner a page of each kind opens with.
2
+ #
3
+ # `partials/banner-url.html` reads a page's note `type` and `subType` here and
4
+ # resolves the result against `banners.yaml`. A type maps to one banner name for
5
+ # every subType, or to a table keyed by every subType the note vocabulary
6
+ # declares for it. `default` is a deliberate choice of the default banner; a
7
+ # kind absent from this file resolves to its own name and warns when that name
8
+ # is not declared.
9
+ #
10
+ # `npm run lint:banners` and the test suite fail on a mapped name that
11
+ # `banners.yaml` does not declare, and on a type or subType missing here.
12
+
13
+ # Types whose banner is named by the page's `category`.
14
+ categoryTypes:
15
+ - doc
16
+
17
+ kinds:
18
+ affiliation:
19
+ arcanetradition: organization
20
+ criminal: organization
21
+ fellowship: organization
22
+ faithtradition: faith
23
+ governmental: organization
24
+ guild: organization
25
+ lineage: organization
26
+ order: organization
27
+ polity: organization
28
+ spirittradition: organization
29
+ venture: organization
30
+ affliction: affliction
31
+ armorgear: armor-clothing
32
+ armorlocation: default
33
+ attribute: skills-attributes
34
+ being:
35
+ character: character
36
+ creature: creature
37
+ npc: character
38
+ bundle: default
39
+ concoctiongear: default
40
+ containergear: containers
41
+ folder: default
42
+ homepage: default
43
+ lore:
44
+ arcana: default
45
+ bestiary: default
46
+ calendar: default
47
+ cosmology: default
48
+ culture: people
49
+ custom: default
50
+ deity: default
51
+ economy: default
52
+ folk: default
53
+ gathering: default
54
+ history: default
55
+ law: default
56
+ literature: default
57
+ material: default
58
+ spirit: default
59
+ theology: default
60
+ macro: default
61
+ map: default
62
+ miscgear: misc-gear
63
+ mystery: default
64
+ mysticalability: mystical-abilities
65
+ place:
66
+ celestial: default
67
+ feature: default
68
+ region: region
69
+ settlement: settlement
70
+ site: default
71
+ structure: default
72
+ world: default
73
+ projectilegear: projectiles
74
+ scenario: default
75
+ skill: skills-attributes
76
+ trauma: trauma
77
+ vehicle: default
78
+ weapongear: weapons
@@ -4,10 +4,9 @@
4
4
  # `images/banners/`, and that path is emitted into a `background-image` with no
5
5
  # way to test it: the artwork is served from the consuming site's
6
6
  # `params.cdnBaseURL`, and Hugo cannot ask a remote host whether a URL exists.
7
- # So a banner that was never drawn used to 404 in silence — the band rendered
7
+ # So a banner that was never drawn would 404 in silence — the band renders
8
8
  # with its title, palette and gradient intact and a dead image URL behind it,
9
- # and nothing failed: not the build, not Hugo, not the deploy guard
10
- # (issue #36).
9
+ # and nothing fails: not the build, not Hugo, not the deploy guard.
11
10
  #
12
11
  # This file is the declaration that replaces that missing test. A name listed
13
12
  # here is asserted to exist; a resolved banner whose name is absent falls back
@@ -6,7 +6,7 @@
6
6
  the deployment does not publish. Without it, Pages falls back to the site
7
7
  root, so an unknown path answers 200 with the landing page: a soft-404 that
8
8
  makes every "does this URL resolve?" check report success, and lets search
9
- engines index retired content as live (issue #1416).
9
+ engines index withdrawn content as live.
10
10
 
11
11
  Every consumer needs exactly this, so it lives here rather than being copied
12
12
  into each one (issue #1454). The theme carries layout, not addresses, so the
@@ -41,8 +41,8 @@
41
41
  <div class="single-content">
42
42
  <div class="single-body">
43
43
  <p>
44
- The page may have been renamed, moved to another part of the
45
- {{ $noun }}, or retired along with the concept it described. Links
44
+ The page may have been moved to another part of the
45
+ {{ $noun }}, or withdrawn along with the concept it described. Links
46
46
  from older discussions, issues, and search results can outlive the
47
47
  pages they point at.
48
48
  </p>
@@ -13,8 +13,12 @@
13
13
  2. `banner` — a hand-authored page's value. `none` (or `false`) declines an
14
14
  image and returns "". A full URL is used as-is; anything else is a
15
15
  fragment under `images/`.
16
- 3. `images/banners/{subtype}.webp`, when neither is set and a `subtype` is
17
- supplied. A caller that passes no subtype gets "" instead, which is
16
+ 3. `images/banners/{name}.webp`, when neither is set. The name is the
17
+ `subtype` supplied, or, when the caller passes the page's `type`, the
18
+ banner `data/banner-kinds.yaml` maps that `type` and `subType` to — a
19
+ `category` for the types it lists as taking theirs from there. A type
20
+ absent from the mapping keeps its own name. A caller that supplies
21
+ neither gets "" instead, which is
18
22
  what a card wants: a section with no banner of its own renders as the
19
23
  text card it rendered before.
20
24
  4. `images/banners/{fallback}` — whenever the name picked by 2 or 3 is not
@@ -44,8 +48,13 @@
44
48
  the content build resolved. Omit for a page that carries none.
45
49
  banner — the raw `banner:` value; may be unset, `none`/`false`, a
46
50
  fragment or an absolute URL.
47
- subtype — optional. The name to default to when `banner` is unset. Omit
48
- for "no banner means no image".
51
+ subtype — optional. The name to default to when `banner` is unset and no
52
+ `type` is given. Omit for "no banner means no image".
53
+ type — optional. The page's note `type`; with `subType` and `category`
54
+ it selects the default name through `data/banner-kinds.yaml`.
55
+ subType — optional. The page's note `subType`.
56
+ category — optional. The page's `category`, for the types that take their
57
+ banner name from it.
49
58
  context — optional. A page address, named in the missing-banner warning so
50
59
  the report says where the want came from.
51
60
  */ -}}
@@ -54,6 +63,22 @@
54
63
  {{- $context := .context | default "" -}}
55
64
  {{- $data := .data | default dict -}}
56
65
 
66
+ {{- /* The default name for a page that states its `type`. A type maps to one
67
+ name or to a table keyed by subType; one that the mapping does not know
68
+ keeps its own name, so the missing-banner warning below still reports
69
+ it. */ -}}
70
+ {{- with .type -}}
71
+ {{- $kinds := index (hugo.Data | default dict) "banner-kinds" | default dict -}}
72
+ {{- $entry := index ($kinds.kinds | default dict) . -}}
73
+ {{- if in ($kinds.categoryTypes | default slice) . -}}
74
+ {{- $subtype = $.category | default "default" -}}
75
+ {{- else if reflect.IsMap $entry -}}
76
+ {{- $subtype = (index $entry (lower ($.subType | default ""))) | default . -}}
77
+ {{- else -}}
78
+ {{- $subtype = $entry | default . -}}
79
+ {{- end -}}
80
+ {{- end -}}
81
+
57
82
  {{- $banners := hugo.Data.banners | default dict -}}
58
83
  {{- $dir := $banners.dir | default "images/banners" -}}
59
84
  {{- $fallback := $banners.fallback | default "default.webp" -}}
@@ -68,11 +93,10 @@
68
93
  {{- $declined := or (eq (lower $raw) "none") (eq $raw "false") -}}
69
94
 
70
95
  {{- /* A note's resolved banner, formatted for the same reason the decline
71
- above is: an unset key and a deliberate blank are both falsy, and only
96
+ above is: an unset key and an empty string are both falsy, and only
72
97
  formatting tells them apart. `<nil>` covers both an absent key and an
73
98
  explicit `null`, which the note format reads as *unset* — so the
74
- resolution carries on to the subtype default. `""` is the format's
75
- deliberate blank: a note naming no hero image, which declines one. */ -}}
99
+ resolution carries on to the subtype default. */ -}}
76
100
  {{- $resolved := printf "%v" (index $data "banner") -}}
77
101
  {{- $fromBuild := ne $resolved "<nil>" -}}
78
102
 
@@ -23,9 +23,10 @@
23
23
  see "Declining" below. A full URL is used
24
24
  as-is; anything else is treated as a
25
25
  fragment under images/.
26
- 3. images/banners/{subtype}.webp — subtype-specific default, where
27
- subtype = category for type=doc pages,
28
- otherwise type
26
+ 3. images/banners/{name}.webp — the default for the page's kind: the
27
+ banner data/banner-kinds.yaml maps its
28
+ type and subType to, or its category for
29
+ type=doc pages
29
30
  4. images/banners/default.webp — used when the page has no type, *and*
30
31
  whenever the name picked by 2 or 3 is not
31
32
  in the declared inventory (see below).
@@ -58,22 +59,20 @@
58
59
  considered answer was "none", so declining has to be sayable.
59
60
  */ -}}
60
61
 
61
- {{- /* For narrative content (type=doc) the meaningful kind of page lives in
62
- `category` — continent, region, polity, faith, lore, etc. — so prefer
63
- that when picking a default banner. */ -}}
62
+ {{- /* The order above, the `none` decision, the type-to-banner mapping in
63
+ `data/banner-kinds.yaml` and the declared-existence check all live in
64
+ `banner-url.html`, which a derived landing card calls too. A hero band
65
+ always wants the default for its page's kind; a card never does. A page
66
+ with no type gets `default`. */ -}}
64
67
  {{- $type := .Params.type | default "" -}}
65
- {{- $subtype := cond (eq $type "doc") (.Params.category | default "") $type -}}
66
- {{- if not $subtype }}{{ $subtype = "default" }}{{ end -}}
67
-
68
- {{- /* The order above, the `none` decision and the declared-existence check
69
- all live in `banner-url.html`, which a derived landing card calls too
70
- (issue #41). A hero band always wants the subtype default; a card never
71
- does. */ -}}
72
68
  {{- $bannerURL := partial "banner-url.html" (dict
73
69
  "site" .Site
74
70
  "data" .Params.data
75
71
  "banner" .Params.banner
76
- "subtype" $subtype
72
+ "type" $type
73
+ "subType" .Params.subType
74
+ "category" .Params.category
75
+ "subtype" (cond (eq $type "") "default" "")
77
76
  "context" .RelPermalink) -}}
78
77
 
79
78
  <section class="hero hero-with-image"{{ with $bannerURL }} style="background-image: url('{{ . }}');"{{ end }}>
@@ -25,7 +25,7 @@
25
25
  computed first and the <section> is rendered only when there is something
26
26
  to put in it.
27
27
 
28
- `return` cannot be used to bail out early here. It sets the partial's
28
+ `return` cannot bail out early here. It sets the partial's
29
29
  return value; it does not discard template text the partial has already
30
30
  emitted, so a guard written that way still prints the empty shell. Hence
31
31
  the emptiness test wraps the markup instead of preceding it.
@@ -1,3 +1,3 @@
1
1
  {{- warnidf (printf "hero-banner-missing-%s" .name)
2
- "hero-banner: no banner named %q in the shared artwork set; falling back to %s/%s. Draw it and add %q to data/banners.yaml, or name a hero image on the page — `data.banner` on a note, `banner:` on a hand-authored page. A blank `data.banner`, or `banner: none`, declines a hero image. First seen on %s."
2
+ "hero-banner: no banner named %q in the shared artwork set; falling back to %s/%s. Draw it and add %q to data/banners.yaml, or name a hero image with `data.banner`. Write `data.banner: none` to decline a hero image. First seen on %s."
3
3
  .name .dir .fallback .name .context -}}
@@ -771,7 +771,7 @@ img.door-image {
771
771
  margin: 0;
772
772
  }
773
773
 
774
- /* One card per row once two no longer fit beside each other; the fixed width
774
+ /* One card per row once two do not fit beside each other; the fixed width
775
775
  would otherwise overflow a narrow viewport. */
776
776
  @media (max-width: 400px) {
777
777
  .door {
package/manifest.mjs CHANGED
@@ -31,8 +31,8 @@
31
31
  * `descriptionHtml` is the one exception, folded into the derived
32
32
  * `description` below rather than surviving under its own name.
33
33
  * - **Derived** — the identity, the description (from `descriptionHtml`), the
34
- * version, the release addresses, the compatibility ranges and the pack
35
- * list. Declaring `description` directly is an error rather than an
34
+ * version, the authors, the release addresses, the compatibility ranges and
35
+ * the pack list. Declaring `description` directly is an error rather than an
36
36
  * override: the authored copy would be silently overwritten.
37
37
  * - **Computed** — namespaced `flags` a repository works out for itself.
38
38
  *
@@ -478,8 +478,9 @@ function requireManifestTitle(config) {
478
478
  * a key Foundry adds later needs no release of this package. The one
479
479
  * exception is `descriptionHtml`, folded into the description below rather
480
480
  * than surviving under its own name.
481
- * - **Derived** — the identity, the description, the release addresses, the
482
- * version, the Foundry and system compatibility ranges, and the pack list.
481
+ * - **Derived** — the identity, the description, the authors (the top-level
482
+ * `authors` list without its roles), the release addresses, the version, the
483
+ * Foundry and system compatibility ranges, and the pack list.
483
484
  * These are refused if also declared (`description` directly; `descriptionHtml`
484
485
  * is how it is authored) and the two would disagree with nothing to say so.
485
486
  * - **Computed** — namespaced `flags` a repository works out for itself, merged
@@ -519,6 +520,12 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
519
520
  // in it. Set only when the repository actually declares one.
520
521
  if (descriptionHtml !== undefined) derived.description = descriptionHtml;
521
522
  if (config.compatibility) derived.compatibility = config.compatibility;
523
+ // Foundry's author schema is name, email, url, discord and flags; a role
524
+ // is the book's, printed on its title page, and the manifest ships what
525
+ // it is given.
526
+ if (config.authors?.length) {
527
+ derived.authors = config.authors.map(({ role: _role, ...person }) => ({ ...person }));
528
+ }
522
529
 
523
530
  // `requiresSystem` is the gate half of the declare/require split. It
524
531
  // emits the `relationships.systems` entry Foundry's `supportsSystem` reads,
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  /**
15
- * The shared markdownlint rules, as a markdownlint-cli2 options module.
15
+ * The shared markdownlint rules, as a `.markdownlint-cli2.mjs` options module.
16
16
  *
17
17
  * `package-build markdown` applies these without a consumer declaring
18
18
  * anything; this is for an editor's markdownlint extension, and for a consumer